👋

AWS CDK 入門

2024/09/08に公開

※1年以上前に書いた内容なので古くなっています。

AWS CDK (Cloud Development Kit)とは

AWS CDKは、コードでクラウドの設定を簡単に作成・管理するためのツールです。
AWSでクラウドサービスを構築するには、AWS マネジメントコンソールを使ってGUI画面を見ながらでポチポチクリックして作るか、AWS CLIを利用してコマンドラインからコマンドを打つという方法があります。
手順を書いたり仕様を作るとなると前者は画面変更があると厳しく、後者には学習コストとオペレーションの手間があります。

メリットとしては

  1. 再現性が高く、別のプロジェクト、リソースに展開し易い
  2. TypeScript(VS Codeを利用)で型情報やプロパティ情報が明確になるため設定方法に迷わない。
  3. コードで管理するので、例えばリソース間の情報共有を変数で設定できる
  4. 状態管理をCloudFormationのスタックで行われるので手離れがよく、構築中のロールバックは自動でおこなわれます。

デメリットとしては

  1. 学習コストが高い
  2. ちょっとした変更をマネジメントコンソールで加えると気が付かない(常にコードを更新しないといけない)
  3. 同じIaCのTeraformやServerless Frameworkと異なりAWSのリソースに特化している
  4. CloudFormationが遅いと感じることもある
CloudFormationとは

個人的に嬉しいところとしてはLambdaをTypeScriptで構築し易いのとTestを管理し易いといのが挙げられます。
特に小中規模の開発が多い弊社に向いている管理手法だと思っています。

今回のVersion
node v18.16.9
cdk 2.85.0

AWS CDK HelloWorld

前提条件

基本的には以下の前提条件を踏みます。
必要なものは以下のとおりです。
https://catalog.us-east-1.prod.workshops.aws/workshops/99731164-1d19-4d2e-9319-727a130e2d57/ja-JP

  • AWSアカウント
  • AWS CLI
  • Node.js
  • AWS CDK ツールキット
npm install -g aws-cdk

cdk init app

空のhello-cdkディレクトリを作成します。

mkdir hello-cdk && cd hello-cdk

アプリを作成します。

cdk を TypeScriptでinitします。

cdk init app --language typescript

以下のような文字列が出力されます。

Applying project template app for typescript
# Welcome to your CDK TypeScript project

This is a blank project for CDK development with TypeScript.

The `cdk.json` file tells the CDK Toolkit how to execute your app.

## Useful commands

* `npm run build`   compile typescript to js
* `npm run watch`   watch for changes and compile
* `npm run test`    perform the jest unit tests
* `cdk deploy`      deploy this stack to your default AWS account/region
* `cdk diff`        compare deployed stack with current state
* `cdk synth`       emits the synthesized CloudFormation template

スタックを作成します。

cdk deploy

以下のような出力がされ、CloudFormationにスタックが作成されます。
CDKはAWS CloudFormationに変換されます。

✨  Synthesis time: 5.94s

HelloCdkStack:  start: Building 
hogehoge:current_account-current_region
HelloCdkStack:  success: Built 
hogehoge:current_account-current_region
HelloCdkStack:  start: Publishing 
hogehoge:current_account-current_region
HelloCdkStack:  success: Published 
hogehoge:current_account-current_region
HelloCdkStack: deploying... [1/1]
HelloCdkStack: creating CloudFormation changeset...

 ✅  HelloCdkStack

✨  Deployment time: 12.17s

これだけでは何も出来ていないので、S3バケットを追加してみます。

S3バケットを追加

追加は2行
import文と new s3.Backetです。

lib/hello-cdk-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3'; // <-追加
export class HelloCdkStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    new s3.Bucket(this, 'MyFirstBucketFromCDK'); // <-追加
  }

保存出来たらcdk diffで差分を確認します。

cdk diff
Stack HelloCdkStack
Resources
[+] AWS::S3::Bucket MyFirstBucketFromCDK MyFirstBucketFromCDKhogehoge 

新しくバケットを追加することになっていることが確認できます。
それではdeployします。

cdk deploy

s3バケットが作成されました。
hellocdkstack-myfirstbucketfromcdk

タグの追加

タグとは

タグの追加は以下のように変更してください。

コンストラクトをconstで受け取り、Tags.ofに渡します。

lib/hello-cdk-stack.ts
+     const myFirstBucketFromCDK = new s3.Bucket(this, 'MyFirstBucketFromCDK'); 
+    cdk.Tags.of(myFirstBucketFromCDK).add('MyKey', 'HelloCDK');
-    new s3.Bucket(this, 'MyFirstBucketFromCDK');

ソースを変更したら忘れずにdeployします。

cdk deploy

スタックDestory時にリソースが削除されるように変更

CDKで作成したS3バケットはスタック削除時にそのまま残るようになっています。同時に削除する場合はremovalPolicyを設定します。
new s3.Backetを更新することに不安を覚えますが大丈夫です。バケットの中身に影響はありません。

lib/hello-cdk-stack.ts
+    const myFirstBucketFromCDK = new s3.Bucket(this, 'MyFirstBucketFromCDK',{
+      removalPolicy: cdk.RemovalPolicy.DESTROY,
+      autoDeleteObjects: true,
+    });
-    const myFirstBucketFromCDK = new s3.Bucket(this, 'MyFirstBucketFromCDK');

ソースを変更したら忘れずにdeployします。

cdk deploy

テストスイートを使いリソースの確認を行う

複雑なシステムを構築するには、テストの存在が不可欠だと思います。CDKは環境構築が正しく行われているか確認することが出来ます。
ここでは簡単にS3バケットが作られているかのテストを行います。

test/hello-cdk.test.ts
import * as cdk from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import * as HelloCdk from '../lib/hello-cdk-stack';

test('S3 Bucket Created', () => {
  const app = new cdk.App();
    // WHEN
  const stack = new HelloCdk.HelloCdkStack(app, 'MyTestStack');
    // THEN
  const template = Template.fromStack(stack);

  template.resourceCountIs('AWS::S3::Bucket', 1);
  
});

テストを実行します。

npm run test
> hello-cdk@0.1.0 test
> jest

 PASS  test/hello-cdk.test.ts (7.075 s)
  ✓ S3 Bucket Created (42 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        7.393 s
Ran all test suites.

Lambda関数を実装します

Lambda関数のTypeScript型情報とesbuildをインストールします。

npm install --save-dev @types/aws-lambda esbuild@0

プロジェクトのルートにlambdaディレクトリを作成し、hello.tsを作っていきます。

hello-cdk/
  +-- bin/
  +-- lambda/  ... ラムダディレクトリ
  |    +-- hello.ts
  +-- lib/
  +-- test/

hello.tsにHelloLambdaを実装します。

lambda/hello.ts
import { Handler } from 'aws-lambda';

export const handler: Handler = async () => {
  console.log('Hello Lambda!');
};

スタックにLambdaを追加します。
Node.jsに特化したNodejsFunctionを使うとTypeScriptのままLambdaをコーディングすることが出来ます。

lib/hello-cdk-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
export class HelloCdkStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    //S3
    const myFirstBucketFromCDK = new s3.Bucket(this, 'MyFirstBucketFromCDK', {
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
    });
    cdk.Tags.of(myFirstBucketFromCDK).add('MyKey', 'HelloCDK');
    //Lambda
    new NodejsFunction(this, 'MyFirstLambda', {
      entry: 'lambda/hello.ts',
    });
  }
}

cdk diffを実行するとわかりますが、Lambdaに必要なIAMロールを自動作成してくれます。
deployします。

cdk deploy

ついでにテストにLambda作成確認を加えます。

test/hello-cdk.test.ts
test('Lambda Created', () => {
  const app = new cdk.App();
    // WHEN
  const stack = new HelloCdk.HelloCdkStack(app, 'MyTestStack');
    // THEN
  const template = Template.fromStack(stack);

  template.resourceCountIs('AWS::Lambda::Function', 1);
  
});

Lambda用のユニットテストを作成します

lambdaディレクトリ内に__test__ディレクトリを作成し、Jestが動くかを確認します。

hello-cdk/
  +-- lambda/
  |    +-- __test__/  ... testディレクトリ
  |        +-- check.test.ts
lambda/__test__/check.test.ts
test('check', () => {
  console.log('OK');
});

ルートのjest.config.jsを書き換えて*.test.tsが動くようにします。

jest.config.js
module.exports = {
  testEnvironment: 'node',
  roots: ['<rootDir>'],
  testMatch: ['**/*.test.ts'],
  transform: {
    '^.+\\.tsx?$': 'ts-jest'
  }
};

npm run testを実行し、結果を確認します。

npm run test

リソースの確認と同じタイミングでテストが動くことが確認出来たと思います。

S3への参照権限とLambdaにイベントを通知する

S3バケットにオブジェクトが追加されると呼び出すLambdaを作成します。
S3からは受け取ったイベントは一度に複数のことがあるため、一つずつ処理します。

lambda/CreateNotification.ts
import { S3Handler, S3Event } from 'aws-lambda';
export const handler: S3Handler = async (event: S3Event) => {
  for (const record of event.Records) {
    console.log(record.s3.object.key);
  }
  return;
};

スタックにLambdaの箇所を変更します。
s3-notificationsをimportします。
bucketのメソッドで設定していくのがプログラミングっぽくて良いです。

lib/hello-cdk-stack.ts
//中略
import * as s3n from 'aws-cdk-lib/aws-s3-notifications';
export class HelloCdkStack extends cdk.Stack {
//中略
    //Lambda
    const createNotificationLambda = new NodejsFunction(this, 'CreateNotification', {
      entry: 'lambda/CreateNotification.ts',
    });
    //権限付与と通知
    myFirstBucketFromCDK.grantRead(createNotificationLambda);
    myFirstBucketFromCDK.addEventNotification(
      s3.EventType.OBJECT_CREATED_PUT,
      new s3n.LambdaDestination(createNotificationLambda),
      //{ prefix: 'hoge/' },
    );
  }
}

deployします。
先程のMyFirstLambdaはdeployすると削除されます。

cdk deploy

s3になにかファイルを入れてみて、CloudWatchのログを見ると以下のようになっていれば成功です。

2023-08-25T08:46:38.858+09:00	INIT_START Runtime Version: nodejs:16.v18 Runtime Version ARN: arn:aws:lambda:ap-northeast-1::runtime:165d07d9ae2c13dc08ec93bc4385873611e86994008c14b85ece98a865a9f127

2023-08-25T08:46:39.011+09:00	START RequestId: 9838aba3-d35e-496c-b9f5-a901e68cb2e1 Version: $LATEST

2023-08-25T08:46:39.017+09:00	2023-08-24T23:46:39.017Z 9838aba3-d35e-496c-b9f5-a901e68cb2e1 INFO hoge/hoge.txt

2023-08-25T08:46:39.037+09:00	END RequestId: 9838aba3-d35e-496c-b9f5-a901e68cb2e1

2023-08-25T08:46:39.037+09:00	REPORT RequestId: 9838aba3-d35e-496c-b9f5-a901e68cb2e1 Duration: 25.92 ms Billed Duration: 26 ms Memory Size: 128 MB Max Memory Used: 57 MB Init Duration: 152.96 ms

バケットからコピーするLambda

コピーだけならLambdaを使わなくても出来ますが、画像処理などの加工を加えることを想定してLambdaで行います。

コピー先のバケット作成

コピー先のS3バケットを作成します。またLambdaに読み書き権限を付与します。

lib/hello-cdk-stack.ts
export class HelloCdkStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    //S3
    const myFirstBucketFromCDK = new s3.Bucket(this, 'MyFirstBucketFromCDK', {
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
    });
    cdk.Tags.of(myFirstBucketFromCDK).add('MyKey', 'HelloCDK');
    const destBucket = new s3.Bucket(this, 'DestBucket', {
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
    });
    //Lambda
    const createNotificationLambda = new NodejsFunction(this, 'CreateNotification', {
      entry: 'lambda/CreateNotification.ts',
      environment: {
        OUTPUT_BUCKET: destBucket.bucketName,
      },
    });
    //権限付与と通知
    myFirstBucketFromCDK.grantRead(createNotificationLambda);
    myFirstBucketFromCDK.addEventNotification(
      s3.EventType.OBJECT_CREATED_PUT,
      new s3n.LambdaDestination(createNotificationLambda),
      //{ prefix: 'hoge/' },
    );
    destBucket.grantReadWrite(createNotificationLambda);
  }
}

CopyObjectを追加

s3を操作するライブラリをインストールします。

npm install @aws-sdk/client-s3 

Lambdaにコピー関数を追加します。

lambda/CreateNotification.ts
import { S3Handler, S3Event } from 'aws-lambda';
import { S3Client, CopyObjectCommand } from '@aws-sdk/client-s3';
export const handler: S3Handler = async (event: S3Event) => {
  //宛先バケットを取得
  const destBucket = process.env.OUTPUT_BUCKET;
  if (!destBucket) {
    console.log('Not Found DestBucket1');
    return;
  }
  //S3クライアント作成
  const client = new S3Client({});
  for (const record of event.Records) {
    const { name } = record.s3.bucket;
    const { key } = record.s3.object;
    const command = new CopyObjectCommand({
      Bucket: destBucket,
      Key: key,
      CopySource: `${name}/${key}`,
    });
    await client.send(command);
  }
  return;
};

deployします。

cdk deploy

cdk destroy

Lambdaは使わられなければコストは発生しませんが、S3は少額ですが保管料がかかります。
DynamoDBをオンデマンドに設定していない場合もコストが発生しますので、テストのCDKはdestroyします。

cdk destroy

終わりに

自分の防備録として簡単な事例を追加していこうと思っています。
LambdaをTypeScritptで直接書く人が増えることを祈ります。

Discussion