この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
AWS CDK とは
AWS Cloud Development Kit (AWS CDK)は AWS のリソースを Typescript や Python 等のコードで定義するフレームワークです。コードで定義したリソースは CloudFormation テンプレートに変換され、デプロイされます。
CDK の対応言語は以下の5つです。(2020 年 5 月 現在)
- Typescript
- JavaScript
- Python
- Java
- C#
AWS CDK のメリットとデメリット
CDK を使うメリットとして主に上げられるのは以下です。
- CloudFormation で書くよりもはるかに少量のコードの記述で済む
- 使い慣れたプログラミング言語が使える
- if 文やループなどのプログラミングロジックが使える
- ライブラリとして切り出し、共有できる
- テストが書ける
デメリットとしては以下が挙げられます。
- プログラミングに慣れていない場合の高い学習コスト
作るもの
AWS Lambda + API Gateway + DynamoDB の構成 で DB からデータを取得する API を AWS CDK で作成してデプロイしてみます。
言語は Typescript を使います。
tsc --v
Version 3.7.4
インストール
AWS CDK を利用するにはNode.js >= 10.3.0
が必要です。
npm install -g aws-cdk
cdk --version
1.32.2
下準備
新しくフォルダを作成します。
mkdir hello-cdk
cd hello-cdk
言語を指定して雛形を作成します。
cdk init --language typescript
今回作成するリソースのモジュールもインストールしておきましょう。
$ npm install @aws-cdk/aws-apigateway
$ npm install @aws-cdk/aws-lambda
$ npm install @aws-cdk/aws-dynamodb
数が多くなると package.json に記述してnpm install
する方が楽かもしれません。
package.json 記述例
package.json
"dependencies": {
"@aws-cdk/aws-apigateway": "_",
"@aws-cdk/aws-dynamodb": "_",
"@aws-cdk/aws-lambda": "\*",
// more dependencies
}
cdk bootstrap
cdk bootstrap
AWS アカウント、リージョン単位で一度だけ実行するコマンドです。 CDKで利用するリソースを置いておく S3 バケットを作成してくれます。
Watch Mode
npm run watch
Starting compilation in watch mode...
Found 0 errors. Watching for file changes.
...
上記コマンドで Watch モードを起動します。これで Typescript のトランスパイラが随時 Typescript コードを Javascript にコンパイルしてくれます。オンにしておくことでコードを変更するごとにエラーチェックできるので便利ですし、デプロイする際にコンパイルし忘れるのも防げます。
これで準備は完了です。
早速lib/hello-cdk-stack.ts
に リソース定義を記述してゆきます。
Dynamo DB
まずはじめに DynamoDB のテーブルを定義します。
lib/hello-cdk-stack.ts
import \* as cdk from "@aws-cdk/core";
import {Table, AttributeType, } as dynamodb from "@aws-cdk/aws-dynamodb";
export class HelloCdkStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
new Table(this, "items", {
partitionKey: {
name: "itemId",
type: AttributeType.STRING,
},
tableName: "items",
removalPolicy: cdk.RemovalPolicy.DESTROY, // NOT recommended for production code
});
}
}
const app = new cdk.App();
new HelloCdkStack(app, "HelloCdkStack");
app.synth();
試しにこの状態で cdk deploy
してみると、以下の結果がかえります。
cdk deploy
HelloCdkStack: deploying...
HelloCdkStack: creating CloudFormation changeset...
0/3 | 2:24:38 AM | CREATE_IN_PROGRESS | AWS::CDK::Metadata | CDKMetadata
0/3 | 2:24:38 AM | CREATE_IN_PROGRESS | AWS::DynamoDB::Table | items (items07D08F4B)
0/3 | 2:24:39 AM | CREATE_IN_PROGRESS | AWS::DynamoDB::Table | items (items07D08F4B) Resource creation Initiated
0/3 | 2:24:39 AM | CREATE_IN_PROGRESS | AWS::CDK::Metadata | CDKMetadata Resource creation Initiated
1/3 | 2:24:39 AM | CREATE_COMPLETE | AWS::CDK::Metadata | CDKMetadata
2/3 | 2:25:09 AM | CREATE_COMPLETE | AWS::DynamoDB::Table | items (items07D08F4B)
3/3 | 2:25:11 AM | CREATE_COMPLETE | AWS::CloudFormation::Stack | HelloCdkStack
マネジメントコンソールを覗くとitems
テーブルが作成されています。
DynamoDB removalPolicy について
DynamoDB テーブルにデータが入っている状態でスタックを削除しようとした場合、以下のオプションで挙動が変わります。
- RETAIN: テーブルにデータが入っている状態で
cdk destroy
が実行された時、テーブルを削除しない - DESTROY: テーブルにデータが入っている状態で
cdk destroy
が実行された時、データごとテーブルを削除する
テーブル内のデータを意図せずに削除することがないよう本番環境では removalPolicy をRETAIN
に設定することが推奨されています。
Lambda
次に DynamoDB からデータを取得する Lambda Function を定義します。
Lambda のソースコードは/lambda/
というフォルダを新規に作成し、そこに記述することにします。
get-item.ts
に DB から Item を一つ取得する処理を記述します。
lib/lambda/get-item.ts
const AWS = require("aws-sdk");
const db = new AWS.DynamoDB.DocumentClient();
const TABLE_NAME = process.env.TABLE_NAME || "";
const PRIMARY_KEY = process.env.PRIMARY_KEY || "";
export const handler = async (event: any = {}): Promise<any> => {
const requestedItemId = event.pathParameters.id;
if (!requestedItemId) {
return {
statusCode: 400,
body: `Error: You are missing the path parameter id`,
};
}
const params = {
TableName: TABLE_NAME,
Key: {
[PRIMARY_KEY]: requestedItemId,
},
};
try {
const response = await db.get(params).promise();
return { statusCode: 200, body: JSON.stringify(response.Item) };
} catch (dbError) {
return { statusCode: 500, body: JSON.stringify(dbError) };
}
};
lib/hello-cdk-stack.ts
に Lambda を追加します。
Lambda から DynamoDB へアクセスするため、IAM Role も作成します。
lib/hello-cdk-stack.ts
import * as cdk from "@aws-cdk/core";
import { Table, AttributeType } from "@aws-cdk/aws-dynamodb";
import { AssetCode, Function, Runtime } from "@aws-cdk/aws-lambda";
export class HelloCdkStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const dynamoTable = new Table(this, "items", {
partitionKey: {
name: "itemId",
type: AttributeType.STRING,
},
tableName: "items",
removalPolicy: cdk.RemovalPolicy.DESTROY, // NOT recommended for production code
});
const getItemLambda = new Function(this, "getOneItemFunction", {
code: new AssetCode("lib/lambda"),
handler: "get-item.handler",
runtime: Runtime.NODEJS_10_X,
environment: {
TABLE_NAME: dynamoTable.tableName,
PRIMARY_KEY: "itemId",
},
});
// dynamodb読み取り権限をLambdaに付与
dynamoTable.grantReadData(getItemLambda);
const api = new RestApi(this, "itemsApi", {
restApiName: "Items Service",
});
}
}
const app = new cdk.App();
new HelloCdkStack(app, "HelloCdkStack");
app.synth();
cdk diff
ここまで記述した時点でcdk diff
を実行してみると、既存の Stack との差分をみてくれます。
Lambda、Lambda ServiceRole が表示されることをデプロイする前に一度確認してみましょう。
API Gateway
最後に API Gateway を定義します。
lib/hello-cdk-stack.ts
import \* as cdk from "@aws-cdk/core";
import { Table, AttributeType } from "@aws-cdk/aws-dynamodb";
import { AssetCode, Function, Runtime } from "@aws-cdk/aws-lambda";
import {
RestApi,
LambdaIntegration,
IResource,
MockIntegration,
PassthroughBehavior,
} from "@aws-cdk/aws-apigateway";
export class HelloCdkStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const dynamoTable = new Table(this, "items", {
partitionKey: {
name: "itemId",
type: AttributeType.STRING,
},
tableName: "items",
removalPolicy: cdk.RemovalPolicy.DESTROY, // NOT recommended for production code
});
const getItemLambda = new Function(this, "getOneItemFunction", {
code: new AssetCode("lib/lambda"),
handler: "get-item.handler",
runtime: Runtime.NODEJS_10_X,
environment: {
TABLE_NAME: dynamoTable.tableName,
PRIMARY_KEY: "itemId",
},
});
// dynamodb読み取り権限をLambdaに付与
dynamoTable.grantReadData(getItemLambda);
// ApiGateway
const api = new RestApi(this, "sampleApi", {
restApiName: "Sample API",
});
const items = api.root.addResource("items");
const singleItem = items.addResource("{id}");
const getItemIntegration = new LambdaIntegration(getItemLambda);
singleItem.addMethod("GET", getItemIntegration);
addCorsOptions(items);
}
}
export function addCorsOptions(apiResource: IResource) {
apiResource.addMethod(
"OPTIONS",
new MockIntegration({
integrationResponses: [
{
statusCode: "200",
responseParameters: {
"method.response.header.Access-Control-Allow-Headers":
"'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'",
"method.response.header.Access-Control-Allow-Origin": "'*'",
"method.response.header.Access-Control-Allow-Credentials":
"'false'",
"method.response.header.Access-Control-Allow-Methods":
"'OPTIONS,GET,PUT,POST,DELETE'",
},
},
],
passthroughBehavior: PassthroughBehavior.NEVER,
requestTemplates: {
"application/json": '{"statusCode": 200}',
},
}),
{
methodResponses: [
{
statusCode: "200",
responseParameters: {
"method.response.header.Access-Control-Allow-Headers": true,
"method.response.header.Access-Control-Allow-Methods": true,
"method.response.header.Access-Control-Allow-Credentials": true,
"method.response.header.Access-Control-Allow-Origin": true,
},
},
],
}
);
}
const app = new cdk.App();
new HelloCdkStack(app, "HelloCdkStack");
app.synth();
API Gateway CORS(Cross Origin Resource Sharing)対応
API Gatewayに対して CORS を有効にするには、以下の2点を設定する必要があります。
- Lambda のレスポンスに
headers: {"Access-Control-Allow-Origin": "<アクセスを許可したいオリジン>"}
を含める必要がある。 - Options メソッドを追加する
デプロイ&動作確認
お疲れ様でした。ここまで記述ができたらもう一度cdk deploy
を実行してリソースを作成しましょう。
Stackが作成されたことを確認できたら動作をみてみましょう。
DynamoDB へ適当なデータを入れて APIGateway へリクエストを投げてみます。
curl -v https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/items/123
* Trying xx.xxx.xxx.xx...
...
< HTTP/2 200
< content-type: application/json
< content-length: 39
< date: Tue, 26 May 2020 12:00:31 GMT
< x-amzn-requestid: daab105f-f66e-44c6-a4ec-9cc9d80d8bd9
< x-amz-apigw-id: NI2y7GssNjMFpBw=
< x-amzn-trace-id: Root=1-5ecd04df-c068ca1ce325b22cc88a43f6;Sampled=0
< x-cache: Miss from cloudfront
< via: 1.1 xxxxxxxxx.cloudfront.net (CloudFront)
< x-amz-cf-pop: NRT51-C1
< x-amz-cf-id: nahVrJ04K6zfUzuoXTQcBhH6ywmhjTlyLdKw6cAyUczEW8nG_VfpcQ==
<
* Connection #0 to host xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com left intact
{"itemId":"123","itemName":"すあま"}
テンプレート全文
ここまで記述したCDKテンプレートはこちらです。
lib/hello-cdk-stack.ts
import * as cdk from "@aws-cdk/core";
import { Table, AttributeType } from "@aws-cdk/aws-dynamodb";
import { AssetCode, Function, Runtime } from "@aws-cdk/aws-lambda";
import {
RestApi,
LambdaIntegration,
IResource,
MockIntegration,
PassthroughBehavior,
} from "@aws-cdk/aws-apigateway";
export class HelloCdkStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// DynamoDB定義
const dynamoTable = new Table(this, "items", {
partitionKey: {
name: "itemId",
type: AttributeType.STRING,
},
tableName: "items",
removalPolicy: cdk.RemovalPolicy.DESTROY, // NOT recommended for production code
});
// Lambda 関数定義
const getItemLambda = new Function(this, "getOneItemFunction", {
code: new AssetCode("lib/lambda"),
handler: "get-item.handler",
runtime: Runtime.NODEJS_10_X,
environment: {
TABLE_NAME: dynamoTable.tableName,
PRIMARY_KEY: "itemId",
},
});
// dynamodb読み取り権限をLambdaに付与
dynamoTable.grantReadData(getItemLambda);
// ApiGateway
const api = new RestApi(this, "sampleApi", {
restApiName: "Sample API",
});
const items = api.root.addResource("items");
const singleItem = items.addResource("{id}");
const getItemIntegration = new LambdaIntegration(getItemLambda);
singleItem.addMethod("GET", getItemIntegration);
addCorsOptions(items);
}
}
// Options Method を作成
export function addCorsOptions(apiResource: IResource) {
apiResource.addMethod(
"OPTIONS",
new MockIntegration({
integrationResponses: [
{
statusCode: "200",
responseParameters: {
"method.response.header.Access-Control-Allow-Headers":
"'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'",
"method.response.header.Access-Control-Allow-Origin": "'*'",
"method.response.header.Access-Control-Allow-Credentials":
"'false'",
"method.response.header.Access-Control-Allow-Methods":
"'OPTIONS,GET,PUT,POST,DELETE'",
},
},
],
passthroughBehavior: PassthroughBehavior.NEVER,
requestTemplates: {
"application/json": '{"statusCode": 200}',
},
}),
{
methodResponses: [
{
statusCode: "200",
responseParameters: {
"method.response.header.Access-Control-Allow-Headers": true,
"method.response.header.Access-Control-Allow-Methods": true,
"method.response.header.Access-Control-Allow-Credentials": true,
"method.response.header.Access-Control-Allow-Origin": true,
},
},
],
}
);
}
const app = new cdk.App();
new HelloCdkStack(app, "HelloCdkStack");
app.synth();
後片付け
不要なリソースは忘れずに削除しましょう。
cdk destroy
Are you sure you want to delete: HelloCdkStack (y/n)? y
HelloCdkStack: destroying...
✅ HelloCdkStack: destroyed
CDK コマンド早見表
最後にCDKで実行できるコマンドの一覧をおさらいしてみましょう。
cdk init --language <LANGUAGE> |
初期化 |
cdk bootstrap |
最初に一回だけ実行するおまじない |
cdk ls |
Stack のリストを表示する |
cdk diff |
デプロイ済の Stack との差異を表示する |
cdk synth |
生成された Cfn テンプレートを出力する |
cdk deploy |
Stack を AWS 環境へデプロイする |
cdk destroy <STACK NAME> |
Stack を破棄する |