AWS CDK 入門
※1年以上前に書いた内容なので古くなっています。
AWS CDK (Cloud Development Kit)とは
AWS CDKは、コードでクラウドの設定を簡単に作成・管理するためのツールです。
AWSでクラウドサービスを構築するには、AWS マネジメントコンソールを使ってGUI画面を見ながらでポチポチクリックして作るか、AWS CLIを利用してコマンドラインからコマンドを打つという方法があります。
手順を書いたり仕様を作るとなると前者は画面変更があると厳しく、後者には学習コストとオペレーションの手間があります。
メリットとしては
- 再現性が高く、別のプロジェクト、リソースに展開し易い
- TypeScript(VS Codeを利用)で型情報やプロパティ情報が明確になるため設定方法に迷わない。
- コードで管理するので、例えばリソース間の情報共有を変数で設定できる
- 状態管理をCloudFormationのスタックで行われるので手離れがよく、構築中のロールバックは自動でおこなわれます。
デメリットとしては
- 学習コストが高い
- ちょっとした変更をマネジメントコンソールで加えると気が付かない(常にコードを更新しないといけない)
- 同じIaCのTeraformやServerless Frameworkと異なりAWSのリソースに特化している
- CloudFormationが遅いと感じることもある
CloudFormationとは
個人的に嬉しいところとしてはLambdaをTypeScriptで構築し易いのとTestを管理し易いといのが挙げられます。
特に小中規模の開発が多い弊社に向いている管理手法だと思っています。
今回のVersion
node v18.16.9
cdk 2.85.0
AWS CDK HelloWorld
前提条件
基本的には以下の前提条件を踏みます。
必要なものは以下のとおりです。
- 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です。
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に渡します。
+ 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を更新することに不安を覚えますが大丈夫です。バケットの中身に影響はありません。
+ 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バケットが作られているかのテストを行います。
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を実装します。
import { Handler } from 'aws-lambda';
export const handler: Handler = async () => {
console.log('Hello Lambda!');
};
スタックにLambdaを追加します。
Node.jsに特化したNodejsFunctionを使うとTypeScriptのままLambdaをコーディングすることが出来ます。
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('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
test('check', () => {
console.log('OK');
});
ルートのjest.config.jsを書き換えて*.test.tsが動くようにします。
module.exports = {
testEnvironment: 'node',
roots: ['<rootDir>'],
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.tsx?$': 'ts-jest'
}
};
npm run testを実行し、結果を確認します。
npm run test
リソースの確認と同じタイミングでテストが動くことが確認出来たと思います。
S3への参照権限とLambdaにイベントを通知する
S3バケットにオブジェクトが追加されると呼び出すLambdaを作成します。
S3からは受け取ったイベントは一度に複数のことがあるため、一つずつ処理します。
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のメソッドで設定していくのがプログラミングっぽくて良いです。
//中略
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に読み書き権限を付与します。
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にコピー関数を追加します。
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