AWS Cloud Development Kit (AWS CDK) を素振りしたメモ

AWS Serverless Application Model (AWS SAM)を素振りしたので次は AWS CDK を試してみました。

チュートリアル

下記のチュートリアルを見ながら試してみます。

typescript ts-node aws-cdk をグローバルに入れます。

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

「app」テンプレートでプロジェクトを初期化します。これは空のディレクトリで実行する必要があります。

cdk init app --language typescript

プロジェクトのテンプレートが展開されたうえで npm install で依存パッケージがインストールされるのですが・・

du -sh node_modules/
#=> 159M    node_modules/

でかい・・グローバルに入れてるはずの typescript とか ts-node とか aws-cdk とかも含まれているようでした。

du -sh node_modules/typescript node_modules/ts-node node_modules/aws-cdk
#=> 49M     node_modules/typescript
#=> 324K    node_modules/ts-node
#=> 67M     node_modules/aws-cdk

この時点ではまだなにも無い空っぽの状態ですが、デプロイしてみると、

cdk deploy

下記のリソースが作成されました。

  • AWS::CDK::Metadata

これは CDK やライブラリのバージョンを AWS にレポートするためのもののようです。

不要なら cdk.json"versionReporting": false を指定すれば作成されなくなります。

{
  "app": "npx ts-node bin/app.ts",
  "versionReporting": false,
  "context": {
    "@aws-cdk/core:enableStackNameDuplicates": "true",
    "aws-cdk:enableDiffNoFail": "true"
  }
}

もう一度デプロイすると AWS::CDK::Metadata リソースは消えます。

cdk deploy

次にチュートリアルの通りに S3 バケットを追加してみます。定義するリソースに応じたパッケージを入れる必要があります。S3 の場合は @aws-cdk/aws-s3 です。

npm install @aws-cdk/aws-s3

lib/ の中の ts コードに下記を追加します。

    new s3.Bucket(this, 'MyFirstBucket', {
        versioned: true
    });

cdk deploy で S3 バケットが作成されたりします。

cdk deploy

チュートリアルではこのあと S3 バケットの属性をちょっと変えて差分を表示したり再デプロイしたりいろいろしているのですがドキュメントのまんまなので割愛します。

次は Lambda 関数をデプロイしてみます。

Lambda 関数をデプロイ

↑で cdk init で作ったプロジェクトはいろいろ余分なものが含まれていたので、必要最小限になるように整理しました。残骸はこちら。APIGareway とも絡ませてみようと思ったのですが怠そうだったのでやめておきました。

まず最初に Lambda のコードをデプロイするために cdk bootstrap を実行します。

cdk bootstrap

CloudFormation の CDKToolkit という名前のスタックで下記のリソースが作成されました。

  • AWS::S3::Bucket
  • AWS::S3::BucketPolicy

AWS SAM とかと同様で、このバケットに Lambda のコードがアップロードされ、CloudFormation でそれをソースとしてデプロイされます。

次に必要な npm パッケージをインストールします。

npm i -D \
    @aws-cdk/core \
    @aws-cdk/aws-sns \
    @aws-cdk/aws-sqs \
    @aws-cdk/aws-lambda \
    @aws-cdk/aws-logs \
    @aws-cdk/aws-events \
    @aws-cdk/aws-events-targets \
    @aws-cdk/aws-lambda-event-sources

ts のコードででリソースを定義します。

import * as core from '@aws-cdk/core';
import * as sns from '@aws-cdk/aws-sns';
import * as sqs from '@aws-cdk/aws-sqs';
import * as lambda from '@aws-cdk/aws-lambda';
import * as logs from '@aws-cdk/aws-logs';
import * as events from '@aws-cdk/aws-events';
import * as targets from '@aws-cdk/aws-events-targets';
import * as sources from '@aws-cdk/aws-lambda-event-sources';

class AppStack extends core.Stack {
    constructor(scope: core.App, id: string, props?: core.StackProps) {
        super(scope, id, props);

        const helloTopic = new sns.Topic(this, 'HelloTopic', {
            topicName: 'hello-cdk-topic',
        });

        const helloQueue1 = new sqs.Queue(this, 'HelloQueue1', {
            queueName: 'hello-cdk-queue1',
        });

        const helloQueue2 = new sqs.Queue(this, 'HelloQueue2', {
            queueName: 'hello-cdk-queue2',
        });

        const helloLambda = new lambda.Function(this, 'HelloLambda', {
            functionName: 'hello-cdk-lambda',
            handler: 'index.handler',
            runtime: lambda.Runtime.NODEJS_12_X,
            memorySize: 128,

            // コードのディレクトリを指定
            code: lambda.Code.asset('src/'),

            // イベントソースを指定する
            events: [new sources.SqsEventSource(helloQueue1)],

            // トピックの ARN を環境変数に入れる
            environment: {
                SNS_TOTIC_ARN: helloTopic.topicArn,
            },

            // ロググループの失効期間を設定する Lambda が作成される
            // ロググループ自体が管理されるわけではないので destroy してもロググループは残る
            logRetention: logs.RetentionDays.ONE_DAY,
        });

        // イベントソースを props ではなくメソッドで指定する
        helloLambda.addEventSource(new sources.SqsEventSource(helloQueue2));

        // Lambda 関数に SNS トピックにパブリッシュするポリシーを与える
        helloTopic.grantPublish(helloLambda);

        // スケジュール実行のためのルールを作成
        new events.Rule(this, 'hello-rule', {
            schedule: events.Schedule.expression('rate(1 minute)'),
            targets: [new targets.LambdaFunction(helloLambda)]
        });
    }
}

const app = new core.App();
new AppStack(app, 'AppStack');

cdk synth で CloudFormation のテンプレートが表示されます。合計で 262 行ありました。

cdk synth | wc -l
#=> 262

内容も非常にわかりにくく、これを手書きするのは辛そうですね・・CDK なら上記のように簡潔でわかりやすく、TypeScript なので入力補完もビシビシ効くので書きやすいです。

cdk diff で差分表示、cdk deploy でデプロイできます。

# 初っ端なので全部表示される
cdk diff

# CloudFormation の進捗状況も表示される
cdk deploy

要らなくなったら cdk destroy で削除できます。

cdk destroy

さいごに

CloudFormation のテンプレート、昔と比べれば YAML で書けるようになった分だいぶマシにはなっているのだと思いますが cdk synth の出力を見た感じ手書きは辛そうなので CloudFormation で構成管理するなら CDK 使っておけば良さそうです。

Serverless Framework や AWS SAM と比べると Lambda 周りのリソースの記述が煩雑そうです。AWS CDK で Lambda と APIGateway を絡めようとすると結構怠そうです。ただ Serverless Framework や AWS SAM は特化型なのに対して AWS CDK はそうではないのであたりまえですね。これらを比較する意味はなさそうです。

比較するなら Terraform ですね。Terraform と比べると汎用言語なので Terraform の count や for_each のようなことも簡単に記述できますし、TypeScript なら型推論で補完もビシバシ効くのでリファレンスをほとんど見なくてもサクサク書き進めることができます。

ただ Terraform ならマネジメントコンソールや AWS CLI で作成したリソースをインポートして構成管理に追加したり、最悪いざとなれば tfstate ファイルを直接いじればなんでもできる、と思いますが、CDK だと同じようなことが難しそうです。

素の CloudFormation なら既存のリソースのインポートなども出来るようですが、CDK 絡むと難しそう・・cdk synth で CloudFormation のテンプレートが出力できるのでそれを使ってリソースのインポートもできなくはないですかね。。Terraform と比べるとだいぶめんどくさそうです。

今後使うことがあるかどうかは・・うーん Terraform にもう慣れているので Terraform で出来ることならあえて CDK を使う必要はないようにも感じます。

強いて言えば検証用とかでサクッと環境を立ち上げたいときは CDK の方が素早く開始できて(慣れれば)、Terraform でいうところの tfstate の置き場所に困ることもなくて良さそうです。

また、CloudFormation を見ればどういう構成が作られているかを直ぐに把握できるのも大きいメリットだと感じます。Terraform で検証で色々作ったり消したりしていると、わりとちょくちょく消し忘れが残ってしまうことがあります。ちょっとした検証用だと思って tfstate をローカルにおいていて、検証が終わった後に terraform destroy し忘れてローカルのディレクトリを削除してしまうと、もう terraform destroy では消せなくなって非常にめんどくさいことになることがあります。

CDK ならローカルでコードを削除してしまってもマネジメントコンソールからスタックの削除ができるので、削除漏れが残ってしまうこともありません。