9
2

CFnテンプレートの修正って大変よね

CFn(CloudFormation)を触っててこんなことありませんか?

  • json/yamlの文法ミスってた...
  • プロパティ足りてなくてエラーになった...
  • 他にもIAMとか付随するサービスの定義も必要だった...
  • テンプレートだと自由度が低くて痒い所に手が届かない...

そんなときこそCDKです!

CDKとは?

「CFnテンプレートをコードで書ける」って代物です!
だいぶざっくりとした説明なので詳しくって方は公式docをご参照ください。
https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/home.html

さっそく使ってみるゾ

WorkShopを参考にしつつ触ってみたいと思います。
https://catalog.workshops.aws/typescript-and-cdk-for-beginner/ja-JP/40-cdk-introduction

お試し利用なので、デプロイリソースは単一のLambda関数にします。

環境セットアップ

作業前の環境は以下で、cli操作はGit Bash、cdkの実装はtypescriptにて行っていきます。

  • OS:Win11
  • AWS CLI:2.13.37
  • npm:10.2.4
  • git:2.41.0.windows.3

追加でCDK開発に必要なパッケージをインストールしておきます。

$ npm install -g aws-cdk
$ npm install -g typescript ts-node

CDKプロジェクトの立ち上げ

適当にプロジェクト用のディレクトリを作ります。

$ mkdir cdk-workshop
$ cd cdk-workshop

新規でプロジェクトをセットアップします。
ディレクトリの雛形ができるので、後は実装していくだけです。

$ cdk init sample-app --language typescript

いろいろとファイルはあるのですが、重要なのは以下の2つ(bin, lib)です。

cdk-workshop/
├── bin/
│   └── cdk-workshop.ts
├── lib/
│   └── cdk-workshop-stack.ts

bin配下のファイル(cdk-workshop.ts)は実際にデプロイする際のエントリーポイントとなっており、このファイルからlib配下のファイルを呼び出します。
実際にデプロイするリソース(スタック)の定義はlib配下で記述します。

実際にcdk-workshop.tsを見ると、CdkWorkshopStackcdk-workshop-stack.ts)をimportし、インスタンスを生成しているのがわかるかと思います。

[cdk-workshop.ts]

import * as cdk from 'aws-cdk-lib';
import { CdkWorkshopStack } from '../lib/cdk-workshop-stack';

const app = new cdk.App();
new CdkWorkshopStack(app, 'CdkWorkshopStack');

lambdaを定義してみる

cdk-workshop-stack.tsを修正し、lambdaの定義を記述してみます。
リソース毎に記述方法は異なるので、基本的には公式リファレンスを参考に定義を記述していきます。
https://docs.aws.amazon.com/cdk/api/v2/docs/aws-construct-library.html

今回はお試しなので適当なコードをインラインコードとして記述しちゃいました。

[cdk-workshop-stack.ts]

import { Stack, StackProps } from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';

export class CdkWorkshopStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const lambdaFunction = new lambda.Function(this, 'LambdaFunction', {
      runtime: lambda.Runtime.NODEJS_20_X,
      handler: 'index.handler',
      code: new lambda.InlineCode(`
        exports.handler = async (event) => {
          return {
            statusCode: 200,
            body: 'Hello, CDK Lambda!'
          };
        };
      `)
    });
  }
}

デプロイしてみる

cdkをデプロイする前に、ブートストラップスタックのデプロイが必要になります。
この作業は1つのAWSアカウントで1度だけ実施すればOKの作業となります。

$ cdk bootstrap

cdkコマンド実行時に以下のエラーが出る場合は、プロファイルの設定が必要となるため、適宜実施してください。

Unable to resolve AWS account to use. It must be either configured when you define your CDK Stack, or through the environment

参考までに自分のプロファイル設定を記載します。(旧AWS SSO環境です)
マスキングしておりますが、~/.aws/configは以下のような記述となっています。

~/.aws/config

[profile sso-profile]
sso_start_url = https://xxxxxxxx/start#/ ※SSOログインのURL
sso_region = [ログインするリージョン]
sso_account_id = [デプロイ先のアカウントID]
sso_role_name = [SSOログインする際のロール]

[profile deploy-profile]
role_arn = [ログイン後にスイッチするロール(実際にデプロイで使うロール)]
region = [ログインするリージョン]
source_profile = sso-profile

configファイルを用意した上で、cliからSSOログインします。
ブラウザで許可を求められるので許可します。

$ aws sso login --profile sso-profile

いちいち、プロファイル名を指定するのは面倒なのでスイッチするロール(デプロイに使うロール)のプロファイルを環境変数に登録しときます。

$ export AWS_PROFILE=deploy-profile

ちょっと逸れましたが、ブートストラップが完了したら、いよいよデプロイです。

$ cdk deploy

デプロイ対象にIAMが含まれていたりすると、続行してよいかを聞かれるので、yで続行します。
煩わしいなって場合は、デプロイ時に--require-approval neverのオプションを付けてあげるとスルーしてくれます。

Do you wish to deploy these changes (y/n)?

コンソールの出力でも確認できますが、CFn側でもstackが作成されていることが確認できます。
image.png

これにて、デプロイ完です!
どうです?GUIでポチポチするよりはマシですよね?

なお、リソースを削除する場合は、CFnでスタックを削除するか、以下コマンドを実行します。

$ cdk destroy

複数Stackの場合は?

lib配下でexportするクラスがスタックとなるため、1ファイル内で複数クラス定義するか、1つのクラス内で複数クラスをexportするかの選択肢があります。

今回は、新たにcdk-another-stack.tslib配下に作成し、以下構成にします。

cdk-workshop/
├── bin/
│   └── cdk-workshop.ts
├── lib/
│   └── cdk-workshop-stack.ts
│   └── cdk-another-stack.ts

lambda.Functionの第二引数にあたるIDをCdkWorkshopStackとは違う値としていますが、コンストラクト内で一意であればよいため、同じにしても大丈夫です。

[cdk-another-stack.ts]

import { Stack, StackProps } from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';

export class CdkAnotherStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const lambdaFunction = new lambda.Function(this, 'LambdaAnotherFunction', {
      runtime: lambda.Runtime.NODEJS_20_X,
      handler: 'index.handler',
      code: new lambda.InlineCode(`
        exports.handler = async (event) => {
          return {
            statusCode: 200,
            body: 'Hello, CDK Lambda!'
          };
        };
      `)
    });
  }
}

cdk-workshop.tsから呼び出す必要があるので、こちらも呼び出しを追加しておきます。
ここでは、第二引数のIDは異なるものを指定しないとダメです。

[cdk-workshop.ts]

import * as cdk from 'aws-cdk-lib';
import { CdkWorkshopStack } from '../lib/cdk-workshop-stack';
import { CdkAnotherStack } from '../lib/cdk-another-stack';

const app = new cdk.App();
new CdkWorkshopStack(app, 'CdkWorkshopStack');
// スタックを追加
new CdkAnotherStack(app, 'CdkAnotherStack');

デプロイパターンはいくつかあるので、状況に応じて使い分けするとよいかと思います。

  • 全て直列でデプロイ
    CdkWorkshopStackCdkAnotherStackの順でデプロイする場合は以下で実施します。
$ cdk deploy --all
  • 並列でデプロイ
    concurrencyで指定した数の並列数でCdkWorkshopStack, CdkAnotherStackを並列デプロイします。
    直列に比べ、早くデプロイすることが可能です。
    承認が必要なスタックがあると失敗するので、スルーするようオプションを付けときます。
$ cdk deploy --all --concurrency [並列可能数] --require-approval never
(例)
$ cdk deploy --all --concurrency 2 --require-approval never
  • 個別デプロイ
    CdkWorkshopStack or CdkAnotherStackを指定してデプロイします。
$ cdk deploy [スタックのクラス名]
(例)
$ cdk deploy CdkWorkshopStack

CFnテンプレートとの比較

ここまででもテンプレートよりCDK!って気持ちになってるかもしれないですが、最後に実際のCFnテンプレートとの比較を行ってみます。

直接CFnのGUIからデプロイ後のテンプレートも確認できますが、事前にどのようなテンプレートが使われるのかを確認することも可能です。

$ cdk synth

synthを実行することで標準出力にテンプレートが出力され、~/cdk.outにもテンプレートファイルが置かれます。

[CdkWorkshopStack.template.json(IAMとLambda部分を抜粋)]


{
 "Resources": {
  "LambdaFunctionServiceRoleC555A460": {
   "Type": "AWS::IAM::Role",
   "Properties": {
    "AssumeRolePolicyDocument": {
     "Statement": [
      {
       "Action": "sts:AssumeRole",
       "Effect": "Allow",
       "Principal": {
        "Service": "lambda.amazonaws.com"
       }
      }
     ],
     "Version": "2012-10-17"
    },
    "ManagedPolicyArns": [
     {
      "Fn::Join": [
       "",
       [
        "arn:",
        {
         "Ref": "AWS::Partition"
        },
        ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
       ]
      ]
     }
    ]
   },
   "Metadata": {
    "aws:cdk:path": "CdkWorkshopStack/LambdaFunction/ServiceRole/Resource"
   }
  },
  "LambdaFunctionBF21E41F": {
   "Type": "AWS::Lambda::Function",
   "Properties": {
    "Code": {
     "ZipFile": "\n        exports.handler = async (event) => {\n          return {\n            statusCode: 200,\n            body: 'Hello, CDK Lambda!'\n          };\n        };\n      "
    },
    "Handler": "index.handler",
    "Role": {
     "Fn::GetAtt": [
      "LambdaFunctionServiceRoleC555A460",
      "Arn"
     ]
    },
    "Runtime": "nodejs20.x"
   },
   "DependsOn": [
    "LambdaFunctionServiceRoleC555A460"
   ],
   "Metadata": {
    "aws:cdk:path": "CdkWorkshopStack/LambdaFunction/Resource"
   }
  },

実際にCdkWorkshopStack.template.jsonを確認するとAWS::IAM::Roleの定義が存在しているのを確認できるかと思います。

ですが、cdk側での実装はたった数行で済むんです!楽ですねぇ。

(CDK Pipelinesにも触れていこうと思ったんですが長くなるのでまたの機会に)

おまけ(インラインでLambdaコードなんて書かねぇよ)

今回はお試しなので簡易にインラインコードを書きましたが、実際にそんなことはしないと思います。
自分のプロジェクトではCDKスクリプトとLambdaスクリプトを同じレポジトリで管理し、以下のようなディレクトリ構成を取っています。

project-root/
├── cdk/ ・・・cdkスクリプト
│   └── bin
│   └── lib
│
├── lambda/ ・・・lambdaスクリプト

その上で、stack側では以下のようにlambda構築を実装しています。

const sampleFunc = new NodejsFunction(this, 'sampleFunc', {
    runtime: Runtime.NODEJS_20_X,
    entry: path.join(__dirname, [lambdaハンドラースクリプトまでの相対パス]),
    handler: 'handle',
    functionName: 'sampleFunc',
    timeout: Duration.seconds(900),
    memorySize: 512,
});

レポジトリ分けるとめんどくさいので、Lambda+CDKの場合は基本的にこの構成を取ることになるんでは、と思ってます。

9
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
2