AWS CDK が GA! さっそく TypeScript でサーバーレスアプリケーションを構築するぜ【 Cloud Development Kit 】
やってみた感想から。これが本当の Infrastructure as Code かなと思いました。アプリケーションの開発者がインフラの構築も一緒にやっていて、 インフラ用のテンプレートファイルと格闘している 場合、AWS CDKに移行すると恩恵を得られそうです。一方インフラ構築をメイン業務として、CloudFormation テンプレートに慣れている場合、現時点ではまだ移行コストが大きいと考えています。
AWS CDK (Cloud Development Kit) とは
AWS リソースを 構成要素(construct) としてプログラムで書き、それらを組み合わせて実行するとデプロイできるというツールキットです。開発者視点では、AWSのインフラを TypeScript などのプログラミング言語を使って定義・デプロイできます。裏では、CDKプログラムを実行することで CloudFormation テンプレートを生成、そのテンプレートを使ってデプロイすることになります。これにより、プログラミング言語で実装することによるIDEや型補完の恩恵と、 CloudFormation でデプロイすることによるバリデーションや ChangeSet レビューの恩恵をいいとこどりできます。
2019年7月11日 Generally Available
2018年から GitHub でなにやら開発していた様子はありましたが、ついに AWS CDK がGAとなりました。GA対象の実装言語は Python と TypeScript です。AWS CDK はいろいろと目的やコンセプトがありますが、私が使うとしたらこの理由が大きいです。
Personally I really like that by using the AWS CDK, you can build your application, including the infrastructure, in your IDE, using the same programming language and with the support of autocompletion and parameter suggestion that modern IDEs have built in, without having to do a mental switch between one tool, or technology, and another. The AWS CDK makes it really fun to quickly code up your AWS infrastructure, configure it, and tie it together with your application code!
要するに、アプリケーション開発と同じようにインフラも構築できるよ。IDEの恩恵受けられて楽だし、楽しそうだよね、 という話で、まったくそのとおりですね。私たちのようなアプリケーション開発をメインで請ける開発者は、インフラ構築作業を簡略化、高速化し、ロジックに集中したいものです。 AWS SAM や CloudFormation もインフラ構築の手助けをしてくれる点は CDK と同じですが、CDKで独特なのはインフラ構築作業をアプリケーション開発のコンテキストに組み込んだ点です。これにより、IDE、テストツール、差分検証、コードレビューなど、アプリケーション開発のエコシステムがそのままインフラのコードにも適用できます。
参考: CDK の利用例自体はすでにあります
特に、株式会社はてな様がヘビーユースしているようです。資料もありますね。
また弊社ブログでもすでにいくつか例があります。ユースケース別で参考にしてみてください。
- 【awslabs探訪】AWS Cloud Development Kit (AWS CDK)を使ってみた | DevelopersIO
- AWS Cloud Development Kit (AWS CDK)でECS環境を構築してみた | DevelopersIO
- [AWS CDK] AWS CDK Intro Workshop for Java #reinvent | DevelopersIO
API + Lambda + DynamoDB のサーバーレスアプリを構築
さっそく使ってみましょう。本記事では典型的なAWSのサーバーレスアプリケーションを構築してみます。AWS CDK を使わないのであれば、AWS SAM で作成するようなアプリケーションです。インフラをプログラムで組めるメリットを最大限活かすために、AWS CDK の言語を TypeScript にして型の恩恵にあずかりましょう。さらに、 Lambda Function の実装言語も TypeScript とし、統一して試します。
次の手順でやっていきます。
- CDK のインストール
- Lambda Function の準備
- インフラコードをCDKで書く
- デプロイ、実行
主にこれらのドキュメントを見ながら進めました。
- AWS Cloud Development Kit (CDK) – TypeScript and Python are Now Generally Available | AWS News Blog
- aws-cdk-examples/typescript/api-cors-lambda-crud-dynamodb at master · aws-samples/aws-cdk-examples
CDK のインストール
$ npm install -g aws-cdk $ mkdir hello-cdk $ cd hello-cdk $ cdk init app --language=typescript Applying project template app for typescript Initializing a new git repository... Executing npm install... npm notice created a lockfile as package-lock.json. You should commit this file. npm WARN hello-cdk@0.1.0 No repository field. npm WARN hello-cdk@0.1.0 No license field. # Useful commands * `npm run build` compile typescript to js * `npm run watch` watch for changes and compile * `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
$ tree -L 3 . ├── node_modeules ├── README.md ├── bin │ └── hello-cdk.ts ├── cdk.json ├── lib │ └── hello-cdk-stack.ts ├── package-lock.json ├── package.json └── tsconfig.json
インストールOKです。
Lambda Function の準備
ここに Lambda Function のソースコードを src/lambda/hello-cdk.ts
として追加します。まずは Lambda Function のコードを書きましょう。こちらも TypeScript で書き、tsc
でビルドする流れとします。
$ mkdir -p src/lambda $ touch src/lambda/hello-cdk.ts
import { UpdateItemInput } from 'aws-sdk/clients/dynamodb'; import * as AWS from 'aws-sdk'; const EnvironmentVariableSample = process.env.GREETING_TABLE_NAME!; const Region = process.env.REGION!; const DYNAMO = new AWS.DynamoDB( { apiVersion: '2012-08-10', region: Region } ); export async function handler(event: User): Promise<GreetingMessage> { return HelloWorldUseCase.hello(event); } export class HelloWorldUseCase { public static async hello(userInfo: User): Promise<GreetingMessage> { const message = HelloWorldUseCase.createMessage(userInfo); await DynamodbGreetingTable.greetingStore(message); return message; } private static createMessage(userInfo: User): GreetingMessage { return { title: `hello, ${userInfo.name}`, description: 'my first message.', } } } export class DynamodbGreetingTable { public static async greetingStore(greeting: GreetingMessage): Promise<void> { const params: UpdateItemInput = { TableName: EnvironmentVariableSample, Key: {greetingId: {S: 'hello-cdk-item'}}, UpdateExpression: [ 'set title = :title', 'description = :description' ].join(', '), ExpressionAttributeValues: { ':title': {S: greeting.title}, ':description': {S: greeting.description} } }; await DYNAMO.updateItem(params).promise() } } export interface User { name: string; } export interface GreetingMessage { title: string; description: string; }
API Gateway から受け取ったデータをもとに挨拶文を生成し、それを DynamoDB へ保存する動きになっています。これで Lambda Function の準備は終わりました。
インフラコードをCDKで書く
CDK でインフラのリソースを定義していくにあたり、気になる課題ポイントをあらかじめ出しておきます。
- あいさつアプリケーションのソースコードは
src/lambda
にある。Lambda Function にこのパスを教えるにはどうすればよいか - あいさつ Lambda Function は環境変数を利用している。 Lambda Function へ環境変数を設定するにはどうするか
- あいさつ Lambda Function は API Gateway から入力を受け取っている。APIへの入力を Lambda Function へ渡すにはどうするか
これらに注目しながら書いていきましょう。CDKで個別のリソースを作るために、必要な追加ライブラリをインストールします。
npm install --save @aws-cdk/aws-dynamodb @aws-cdk/aws-lambda @aws-cdk/aws-apigateway
cdk init app
で生成された lib/index.ts
を編集します。
import * as dynamodb from '@aws-cdk/aws-dynamodb'; import * as lambda from '@aws-cdk/aws-lambda'; import * as apigateway from '@aws-cdk/aws-apigateway'; import cdk = require('@aws-cdk/core'); import { Duration } from '@aws-cdk/core'; export class HelloCdkStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // (1) DynamoDB const greetingTable = new dynamodb.Table(this, 'greeting', { partitionKey: { name: 'greetingId', type: dynamodb.AttributeType.STRING }, tableName: 'greeting' }); // (2) Lambda Function const putGreetingItemLambda = new lambda.Function(this, 'putGreetingItemLambda', { code: lambda.Code.asset('src/lambda'), // (3) handler: 'hello-cdk.handler', runtime: lambda.Runtime.NODEJS_10_X, timeout: Duration.seconds(3), // (4) environment: { GREETING_TABLE_NAME: greetingTable.tableName, REGION: props ? props.env!.region : 'ap-northeast-1' } }); // (5) grant (maybe create iam role for lambda?) greetingTable.grantReadWriteData(putGreetingItemLambda); // (6) api gateway const api = new apigateway.RestApi(this, 'itemsApi', { restApiName: 'hello-cdk-greeting' }); const greetingResource = api.root.addResource('greeting'); // (7) request integration const putGreetingItemIntegration = new apigateway.LambdaIntegration( putGreetingItemLambda, { proxy: false, integrationResponses: [ { statusCode: '200', responseTemplates: { 'application/json': '$input.json("$")' } } ], passthroughBehavior: apigateway.PassthroughBehavior.WHEN_NO_MATCH, requestTemplates: { 'application/json': '$input.json("$")' }, } ); greetingResource.addMethod( 'POST', putGreetingItemIntegration, {methodResponses: [{statusCode: '200',}]}); } } const app = new cdk.App(); new HelloCdkStack(app, 'HelloCdkApp'); app.synth();
思ったよりもずっと短い。先ほどあげた課題含め、どんなことを書いているか見ていきましょう。
- (1) DynamoDBを定義: テーブル名やパーティションキーを指定することで DynamoDB を定義できます
- (2) Lambda Function を定義: Lambda Function を定義します。
- (3) ソースコードのパスを指定:
lambda.Code.asset()
で指定できるようです。アセット扱いなんですね。 - (4) 環境変数を指定: オプションの environment キーで設定できるようです。わかりやすいですね。
- (5) 権限設定: CloudFormation テンプレートとの違いその1。IAMリソースが個別であるのではなく、DynamoDBの構成要素が Lambda Function の構成要素に Grant するという書き方になります。
- (6) API Gateway: CloudFormation テンプレートとの違いその2。API定義をつらつらと書いていくのではなく、APIのルートに
addResource
するという考え方です。 - (7) 統合リクエストを定義: オプションでゴリゴリ定義することもよくあります。このあたりは CloudFormation をリスペクトしているのかもしれません。
型の恩恵は例えばつぎのように受けられます。
Lambda Function でサポートしているランタイムを調べるのに、ドキュメントを検索する必要はありません。定義された候補をIDEで調べれば確定できます。さらに、EOLなどの理由で非推奨となるバージョンはプログラム上のDeprecatedとして表現できます。NodeJS4.3は打ち消し線で非推奨なのだとわかります。
インフラコードはこれで準備OKです。デプロイします。
デプロイ・実行
Lambda Function のコードも、CDKのコードも TypeScript で書いたので JavaScript へ変換します。
$ npm run build # 実体は tsc です > hello-cdk@0.1.0 build /Users/wada.yusuke/Downloads/hello-cdk > tsc
これでどちらのコードもJavaScriptが生成されました。次にデプロイするにあたり、最終的には CloudFormation テンプレートとなることを考えると、S3バケットが必要になるはずです。これは手で作るのか…というとそんなことはなく、cdkのコマンドが用意されています。cdk bootstrap
を使います。スイッチロールを行い、デプロイ対象のAWSアカウントにアクセスできる状況で実施してください。私は fish shell の aws_swrole を使いました。
$ aws_swrole my-account $ cdk bootstrap ⏳ Bootstrapping environment aws://xxxxxxxxxxxxx/ap-northeast-1... CDKToolkit: creating CloudFormation changeset... 0/2 | 10:50:31 | CREATE_IN_PROGRESS | AWS::CloudFormation::Stack | CDKToolkit User Initiated 0/2 | 10:50:37 | CREATE_IN_PROGRESS | AWS::S3::Bucket | StagingBucket 0/2 | 10:50:38 | CREATE_IN_PROGRESS | AWS::S3::Bucket | StagingBucket Resource creation Initiated 1/2 | 10:51:00 | CREATE_COMPLETE | AWS::S3::Bucket | StagingBucket 2/2 | 10:51:02 | CREATE_COMPLETE | AWS::CloudFormation::Stack | CDKToolkit ✅ Environment aws://xxxxxxxxxxxxx/ap-northeast-1 bootstrapped.
デプロイ準備ができました。cdk deoploy
でデプロイします。
$ cdk deploy
すると変更内容が確認されます。テーブルフォーマットで整形してくれ、見やすいです。
OKであればそのままスタックの作成に入ります。
Do you wish to deploy these changes HelloCdkStack: deploying... Updated: asset.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx HelloCdkStack: creating CloudFormation changeset... 0/16 | 10:52:09 | CREATE_IN_PROGRESS | AWS::CloudFormation::Stack | HelloCdkStack User Initiated 0/16 | 10:52:38 | CREATE_IN_PROGRESS | AWS::CDK::Metadata | CDKMetadata 0/16 | 10:52:38 | CREATE_IN_PROGRESS | AWS::ApiGateway::RestApi | itemsApi 0/16 | 10:52:38 | CREATE_IN_PROGRESS | AWS::IAM::Role | itemsApi/CloudWatchRole 0/16 | 10:52:38 | CREATE_IN_PROGRESS | AWS::IAM::Role | putGreetingItemLambda/ServiceRole 0/16 | 10:52:38 | CREATE_IN_PROGRESS | AWS::DynamoDB::Table | greeting 0/16 | 10:52:39 | CREATE_IN_PROGRESS | AWS::ApiGateway::RestApi | itemsApi 0/16 | 10:52:39 | CREATE_IN_PROGRESS | AWS::DynamoDB::Table | greeting 0/16 | 10:52:39 | CREATE_IN_PROGRESS | AWS::IAM::Role | putGreetingItemLambda/ServiceRole 1/16 | 10:52:39 | CREATE_COMPLETE | AWS::ApiGateway::RestApi | itemsApi 1/16 | 10:52:39 | CREATE_IN_PROGRESS | AWS::IAM::Role | itemsApi/CloudWatchRole 1/16 | 10:52:41 | CREATE_IN_PROGRESS | AWS::CDK::Metadata | CDKMetadata Resource creation Initiated 2/16 | 10:52:41 | CREATE_COMPLETE | AWS::CDK::Metadata | CDKMetadata 2/16 | 10:52:43 | CREATE_IN_PROGRESS | AWS::ApiGateway::Resource | itemsApi/Default/greeting 2/16 | 10:52:43 | CREATE_IN_PROGRESS | AWS::ApiGateway::Resource | itemsApi/Default/greeting 3/16 | 10:52:43 | CREATE_COMPLETE | AWS::ApiGateway::Resource | itemsApi/Default/greeting 3/16 | 10:52:46 | CREATE_IN_PROGRESS | AWS::ApiGateway::Method | itemsApi/Default/greeting/OPTIONS 3/16 | 10:52:46 | CREATE_IN_PROGRESS | AWS::ApiGateway::Method | itemsApi/Default/greeting/OPTIONS 4/16 | 10:52:48 | CREATE_COMPLETE | AWS::ApiGateway::Method | itemsApi/Default/greeting/OPTIONS 5/16 | 10:52:58 | CREATE_COMPLETE | AWS::IAM::Role | itemsApi/CloudWatchRole 6/16 | 10:52:58 | CREATE_COMPLETE | AWS::IAM::Role | putGreetingItemLambda/ServiceRole 6/16 | 10:53:02 | CREATE_IN_PROGRESS | AWS::ApiGateway::Account | itemsApi/Account 6/16 | 10:53:03 | CREATE_IN_PROGRESS | AWS::ApiGateway::Account | itemsApi/Account 7/16 | 10:53:03 | CREATE_COMPLETE | AWS::ApiGateway::Account | itemsApi/Account 8/16 | 10:53:10 | CREATE_COMPLETE | AWS::DynamoDB::Table | greeting 8/16 | 10:53:13 | CREATE_IN_PROGRESS | AWS::IAM::Policy | putGreetingItemLambda/ServiceRole/DefaultPolicy 8/16 | 10:53:14 | CREATE_IN_PROGRESS | AWS::IAM::Policy | putGreetingItemLambda/ServiceRole/DefaultPolicy 9/16 | 10:53:23 | CREATE_COMPLETE | AWS::IAM::Policy | putGreetingItemLambda/ServiceRole/DefaultPolicy 9/16 | 10:53:26 | CREATE_IN_PROGRESS | AWS::Lambda::Function | putGreetingItemLambda 9/16 | 10:53:27 | CREATE_IN_PROGRESS | AWS::Lambda::Function | putGreetingItemLambda 10/16 | 10:53:27 | CREATE_COMPLETE | AWS::Lambda::Function | putGreetingItemLambda 10/16 | 10:53:30 | CREATE_IN_PROGRESS | AWS::Lambda::Permission | putGreetingItemLambda/ApiPermission.Test.POST..greeting 10/16 | 10:53:30 | CREATE_IN_PROGRESS | AWS::ApiGateway::Method | itemsApi/Default/greeting/POST 10/16 | 10:53:30 | CREATE_IN_PROGRESS | AWS::Lambda::Permission | putGreetingItemLambda/ApiPermission.Test.POST..greeting 10/16 | 10:53:30 | CREATE_IN_PROGRESS | AWS::ApiGateway::Method | itemsApi/Default/greeting/POST 11/16 | 10:53:31 | CREATE_COMPLETE | AWS::ApiGateway::Method | itemsApi/Default/greeting/POST 11/16 | 10:53:34 | CREATE_IN_PROGRESS | AWS::ApiGateway::Deployment | itemsApi/Deployment 11/16 | 10:53:35 | CREATE_IN_PROGRESS | AWS::ApiGateway::Deployment | itemsApi/Deployment 12/16 | 10:53:35 | CREATE_COMPLETE | AWS::ApiGateway::Deployment | itemsApi/Deployment 12/16 | 10:53:38 | CREATE_IN_PROGRESS | AWS::ApiGateway::Stage | itemsApi/DeploymentStage.prod 12/16 | 10:53:39 | CREATE_IN_PROGRESS | AWS::ApiGateway::Stage | itemsApi/DeploymentStage.prod 13/16 | 10:53:40 | CREATE_COMPLETE | AWS::ApiGateway::Stage | itemsApi/DeploymentStage.prod 14/16 | 10:53:40 | CREATE_COMPLETE | AWS::Lambda::Permission | putGreetingItemLambda/ApiPermission.Test.POST..greeting 14/16 | 10:53:43 | CREATE_IN_PROGRESS | AWS::Lambda::Permission | putGreetingItemLambda/ApiPermission.POST..greeting 14/16 | 10:53:44 | CREATE_IN_PROGRESS | AWS::Lambda::Permission | putGreetingItemLambda/ApiPermission.POST..greeting 15/16 | 10:53:54 | CREATE_COMPLETE | AWS::Lambda::Permission | putGreetingItemLambda/ApiPermission.POST..greeting ✅ HelloCdkStack Outputs: HelloCdkStack.itemsApiEndpointYYYYYY = https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/ Stack ARN: arn:aws:cloudformation:ap-northeast-1:XXXXXXXX:stack/HelloCdkStack/uuid-uuid-uuid-uuid-uuid
API Gateway の Endpoint まで出力されました。本当に動くのでしょうか。
$ curl -X POST -d '{"name":"wada"}' https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/greeting {"title":"hello, wada","description":"my first message."} $ aws dynamodb scan --table-name greeting { "Items": [ { "description": { "S": "my first message." }, "greetingId": { "S": "hello-cdk-item" }, "title": { "S": "hello, wada" } } ], "Count": 1, "ScannedCount": 1, "ConsumedCapacity": null }
動きました。DynamoDBにもたしかにデータが保存されています。ここまで一切、AWSコンソールにログインしていません。 いい時代になりました。
まとめ
典型的なサーバーレスアプリケーションのインフラ構築を AWS CDK でやってみました。 CloudFormation テンプレートよりも少ない記述量でやりたいことができました。また、デプロイ経過や結果の出力内容も丁寧で、AWSコンソールといったりきたりがありませんでした。私のようにアプリケーション開発に従事している者からするとメリットが大きいと感じます。一方、コードで表現することからテンプレートで書くときと多少は考え方を変える必要があり、慣れも必要です。
Pros:
アプリケーション開発の一環で作業でき、作業者のコンテキストスイッチが少ない。YAMLテンプレートのかわりにプログラミング言語でインフラを構築できる。これにより、IDEなどを使って、アプリケーション開発の一環で作業できる。
Cons:
CDK を使いこなすためには、ある程度 CloudFormation テンプレートで努力した実績が必要。また、「各リソースは構成要素であり、構成要素同士が連携する」という考え方をプログラムに落とし込んで考えられると効率的に理解できるが、そのためにはアプリケーション構築の経験がそれなりに必要。本記事ではDynamoDBへのGrantが最たる例。
// これでIAMが生成される greetingTable.grantReadWriteData(putGreetingItemLambda);
さらに実践的に使いこなすために
次のような内容についても検証していきます。
- 環境ごとにリソース名を分けたいというケースに対応できるか (
dev-greeting-table
など) - Lambda Function AWS SDK 以外のライブラリを使う場合は、JavaScript への変換だけでなくバンドルが必要になりそう
- これまで CloudFormation で書いていたものを、どこまでCDKで置き換えられるか
- => CloudFormation Layer というインタフェースが用意されていて、その気になればすべて置き換えられそうです。CDKで完結させるという強い意志を感じます。
- Escape Hatches - AWS Cloud Development Kit (AWS CDK)
AWS CDK はオープンソースです。たくさん使ってどんどんフィードバックしていきましょう。