- TOC
- これは何?
- New Project
- Hello, CDK!
- Writing constructs
- Using construct libraries
- Clean up
- Advanced Topics
AWS CDK 公式の Workshop (TypeScript) で手を動かしたメモ
TypeScript Workshop :: AWS Cloud Development Kit (AWS CDK) Workshop
https://cdkworkshop.com/20-typescript.html
New Project :: AWS Cloud Development Kit (AWS CDK) Workshop
https://cdkworkshop.com/20-typescript/20-create-project.html
# project のディレクトをリセットアップ
mkdir cdk-workshop && cd cdk-workshop
# cdk sample-app の初期化
cdk init sample-app --language typescript
# 変更のウォッチング: `tsc -w` が実行される
npm run watch
bin/cdk-workshop.ts
がアプリケーションのエントリポイント
#!/usr/bin/env node
import * as cdk from '@aws-cdk/core';
import { CdkWorkshopStack } from '../lib/cdk-workshop-stack';
const app = new cdk.App();
new CdkWorkshopStack(app, 'CdkWorkshopStack');
lib/cdk-workshop-stack.ts
メインとなる stack- SQS Queue, SNS Topic, SNS Subscription が定義されている
import * as sns from '@aws-cdk/aws-sns';
import * as subs from '@aws-cdk/aws-sns-subscriptions';
import * as sqs from '@aws-cdk/aws-sqs';
import * as cdk from '@aws-cdk/core';
export class CdkWorkshopStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const queue = new sqs.Queue(this, 'CdkWorkshopQueue', {
visibilityTimeout: cdk.Duration.seconds(300)
});
const topic = new sns.Topic(this, 'CdkWorkshopTopic');
topic.addSubscription(new subs.SqsSubscription(queue));
}
}
- シンセサイズする
- CloudFormation template を生成するということ
cdk synth
デプロイ
# environment の bootstrap
# environment は、 CDK App のデプロイ先となる AWS アカウントやリージョンといった環境という意味
cdk bootstrap
# デプロイ
cdk deploy
Hello, CDK! :: AWS Cloud Development Kit (AWS CDK) Workshop
https://cdkworkshop.com/20-typescript/30-hello-cdk.html
- SQS, SNS のサンプルアプリを API Gateway, Lambda のサーバーレスアプリに書き換えていく
- サンプルアプリのコードを削除
lib/cdk-workshop-stack.ts
import * as cdk from '@aws-cdk/core';
export class CdkWorkshopStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// nothing here!
}
}
- diff を確認する
npm run build # npm run watch してたら不要
cdk diff
- これでデプロイして、一旦リソースを削除する
cdk deploy
- まっさらな状態になったので API Gateway, Lambda の定義を作っていく
- まずは Lambda handler code を書いていく
mkdir lambda; touch lambda/hello.js
exports.handler = async function(event) {
console.log("request:", JSON.stringify(event, undefined, 2));
return {
statusCode: 200,
headers: { "Content-Type": "text/plain" },
body: `Hello, CDK! You've hit ${event.path}\n`
};
};
- CDK から Lambda を扱うため、 Lambda construct library をインストールする
npm install @aws-cdk/aws-lambda
- cdk stack に Lambda 関数を追加する
lib/cdk-workshop-stack.ts
- IAM Role は SAM のように勝手に作ってくれるようだ。便利
import * as cdk from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda';
export class CdkWorkshopStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// defines an AWS Lambda resource
const hello = new lambda.Function(this, 'HelloHandler', {
runtime: lambda.Runtime.NODEJS_10_X, // execution environment
code: lambda.Code.fromAsset('lambda'), // code loaded from "lambda" directory
handler: 'hello.handler' // file is "hello", function is "handler"
});
}
}
- diff 確認
npm run build
cdk diff
- デプロイ
cdk deploy
- Lambda 関数のテスト
- マネジメントコンソールから API Gateway イベントを作って動作させてみれば OK
- イベントテンプレートから Amazon API Gateway AWS Proxy を選択して使う
- API Gateway を追加していく
- API Gateway construct lib をインストール
npm install @aws-cdk/aws-apigateway
- LambdaRestApi construct を stack に追加
lib/cdk-workshop-stack.ts
import * as cdk from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda';
import * as apigw from '@aws-cdk/aws-apigateway';
export class CdkWorkshopStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// defines an AWS Lambda resource
const hello = new lambda.Function(this, 'HelloHandler', {
runtime: lambda.Runtime.NODEJS_10_X, // execution environment
code: lambda.Code.fromAsset('lambda'), // code loaded from "lambda" directory
handler: 'hello.handler' // file is "hello", function is "handler"
});
// defines an API Gateway REST API resource backed by our "hello" function.
new apigw.LambdaRestApi(this, 'Endpoint', {
handler: hello
});
}
}
- build して diff みる
npm run build
cdk diff
- デプロイと、アウトプットされるエンドポイント確認
cdk deploy
...
Outputs:
CdkWorkshopStack.Endpoint8024A810 = https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/
- test
- curl で HTTP リクエスト送ってみる
% curl https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/
Hello, CDK! You've hit /
Writing constructs :: AWS Cloud Development Kit (AWS CDK) Workshop
https://cdkworkshop.com/20-typescript/40-hit-counter.html
- API Gateway のバックエンドで Lambda 関数から DynamoDB Table に対する操作を行い、別の Lambda 関数を起動する構成を constructs を書いて実現していく
- Lambda 関数の定義を追加
touch lib/hitcounter.ts
import * as cdk from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda';
export interface HitCounterProps {
/** the function for which we want to count url hits **/
downstream: lambda.IFunction;
}
export class HitCounter extends cdk.Construct {
constructor(scope: cdk.Construct, id: string, props: HitCounterProps) {
super(scope, id);
// TODO
}
}
- 追加定義した Lambda 関数にデプロイするコードを書く
touch lambda/hitcounter.js
- DynamoDB table への書き込みと、別の Lambda 関数(downstream)の起動を行う
const { DynamoDB, Lambda } = require('aws-sdk');
exports.handler = async function(event) {
console.log("request:", JSON.stringify(event, undefined, 2));
// create AWS SDK clients
const dynamo = new DynamoDB();
const lambda = new Lambda();
// update dynamo entry for "path" with hits++
await dynamo.updateItem({
TableName: process.env.HITS_TABLE_NAME,
Key: { path: { S: event.path } },
UpdateExpression: 'ADD hits :incr',
ExpressionAttributeValues: { ':incr': { N: '1' } }
}).promise();
// call downstream function and capture response
const resp = await lambda.invoke({
FunctionName: process.env.DOWNSTREAM_FUNCTION_NAME,
Payload: JSON.stringify(event)
}).promise();
console.log('downstream response:', JSON.stringify(resp, undefined, 2));
// return response back to upstream caller
return JSON.parse(resp.Payload);
};
- DynamoDB construct lib をインストール
npm install @aws-cdk/aws-dynamodb
- DynamoDB 定義を
lib/hitcounter.ts
に追記
import * as cdk from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda';
import * as dynamodb from '@aws-cdk/aws-dynamodb';
export interface HitCounterProps {
/** the function for which we want to count url hits **/
downstream: lambda.IFunction;
}
export class HitCounter extends cdk.Construct {
/** allows accessing the counter function */
public readonly handler: lambda.Function;
constructor(scope: cdk.Construct, id: string, props: HitCounterProps) {
super(scope, id);
const table = new dynamodb.Table(this, 'Hits', {
partitionKey: { name: 'path', type: dynamodb.AttributeType.STRING }
});
this.handler = new lambda.Function(this, 'HitCounterHandler', {
runtime: lambda.Runtime.NODEJS_10_X,
handler: 'hitcounter.handler',
code: lambda.Code.fromAsset('lambda'),
environment: {
DOWNSTREAM_FUNCTION_NAME: props.downstream.functionName,
HITS_TABLE_NAME: table.tableName
}
});
}
}
- hit counter を stack に追加
lib/cdk-workshop-stack.ts
に追加
import * as cdk from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda';
import * as apigw from '@aws-cdk/aws-apigateway';
import { HitCounter } from './hitcounter';
export class CdkWorkshopStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const hello = new lambda.Function(this, 'HelloHandler', {
runtime: lambda.Runtime.NODEJS_10_X,
code: lambda.Code.fromAsset('lambda'),
handler: 'hello.handler'
});
const helloWithCounter = new HitCounter(this, 'HelloHitCounter', {
downstream: hello
});
// defines an API Gateway REST API resource backed by our "hello" function.
new apigw.LambdaRestApi(this, 'Endpoint', {
handler: helloWithCounter.handler
});
}
}
- デプロイ
npm run build
cdk deploy # テストするので Endpoint をメモっておく
...
CdkWorkshopStack.Endpoint8024A810 = https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/
- テスト
- Internal server error がレスポンスされるハズ
- これを次に fix していく
curl -i https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/
...
HTTP/2 502
...
{"message": "Internal server error"}
- 起動した Lambda 関数のログをマネジメントコンソール等で確認する
- Lambda 関数が DynamoDB に対するアクセス権限を持っていない的なエラーが記録されているハズ
{
"errorMessage": "User: arn:aws:sts::585695036304:assumed-role/CdkWorkshopStack-HelloHitCounterHitCounterHandlerS-TU5M09L1UBID/CdkWorkshopStack-HelloHitCounterHitCounterHandlerD-144HVUNEWRWEO is not authorized to perform: dynamodb:UpdateItem on resource: arn:aws:dynamodb:us-east-1:585695036304:table/CdkWorkshopStack-HelloHitCounterHits7AAEBF80-1DZVT3W84LJKB",
"errorType": "AccessDeniedException",
"stackTrace": [
"Request.extractError (/var/runtime/node_modules/aws-sdk/lib/protocol/json.js:48:27)",
"Request.callListeners (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:105:20)",
"Request.emit (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:77:10)",
"Request.emit (/var/runtime/node_modules/aws-sdk/lib/request.js:683:14)",
"Request.transition (/var/runtime/node_modules/aws-sdk/lib/request.js:22:10)",
"AcceptorStateMachine.runTo (/var/runtime/node_modules/aws-sdk/lib/state_machine.js:14:12)",
"/var/runtime/node_modules/aws-sdk/lib/state_machine.js:26:10",
"Request.<anonymous> (/var/runtime/node_modules/aws-sdk/lib/request.js:38:9)",
"Request.<anonymous> (/var/runtime/node_modules/aws-sdk/lib/request.js:685:12)",
"Request.callListeners (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:115:18)"
]
}
- Lambda 関数に DynamoDB Table に対するアクセスを付与する
import * as cdk from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda';
import * as dynamodb from '@aws-cdk/aws-dynamodb';
export interface HitCounterProps {
/** the function for which we want to count url hits **/
downstream: lambda.Function;
}
export class HitCounter extends cdk.Construct {
/** allows accessing the counter function */
public readonly handler: lambda.Function;
constructor(scope: cdk.Construct, id: string, props: HitCounterProps) {
super(scope, id);
const table = new dynamodb.Table(this, 'Hits', {
partitionKey: { name: 'path', type: dynamodb.AttributeType.STRING }
});
this.handler = new lambda.Function(this, 'HitCounterHandler', {
runtime: lambda.Runtime.NODEJS_10_X,
handler: 'hitcounter.handler',
code: lambda.Code.fromAsset('lambda'),
environment: {
DOWNSTREAM_FUNCTION_NAME: props.downstream.functionName,
HITS_TABLE_NAME: table.tableName
}
});
// grant the lambda role read/write permissions to our table
table.grantReadWriteData(this.handler);
}
}
- デプロイ
% npm run build
% cdk deploy
...
CdkWorkshopStack.Endpoint8024A810 = https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/
- test
% curl -i https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/
HTTP/2 502
...
{"message": "Internal server error"}
- まだエラーじゃん!ということで debug
- Lambda 関数のログを確認すると、以下のようなエラーが出力されていることを確認できる
User: <VERY-LONG-STRING> is not authorized to perform: lambda:InvokeFunction on resource: <VERY-LONG-STRING>"
- これは DynamoDB Table に書き込みはできたものの、 downstream となる Lambda 関数を invoke する権限がなく起動に失敗したため
- downstream Lambda 関数を起動する権限を追加する
lib/hitcounter.ts
- grantInvoke で IAM ポリシーに Lambda 関数の invoke を追加できる
import * as cdk from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda';
import * as dynamodb from '@aws-cdk/aws-dynamodb';
export interface HitCounterProps {
/** the function for which we want to count url hits **/
downstream: lambda.Function;
}
export class HitCounter extends cdk.Construct {
/** allows accessing the counter function */
public readonly handler: lambda.Function;
constructor(scope: cdk.Construct, id: string, props: HitCounterProps) {
super(scope, id);
const table = new dynamodb.Table(this, 'Hits', {
partitionKey: { name: 'path', type: dynamodb.AttributeType.STRING }
});
this.handler = new lambda.Function(this, 'HitCounterHandler', {
runtime: lambda.Runtime.NODEJS_10_X,
handler: 'hitcounter.handler',
code: lambda.Code.fromAsset('lambda'),
environment: {
DOWNSTREAM_FUNCTION_NAME: props.downstream.functionName,
HITS_TABLE_NAME: table.tableName
}
});
// grant the lambda role read/write permissions to our table
table.grantReadWriteData(this.handler);
// grant the lambda role invoke permissions to the downstream function
props.downstream.grantInvoke(this.handler);
}
}
- diff チェック
npm run build
cdk diff
- diff sample
- IAM Policy に Lambda の InvokeFunction が追加されることを確認できる
Resources
[~] AWS::IAM::Policy HelloHitCounter/HitCounterHandler/ServiceRole/DefaultPolicy HelloHitCounterHitCounterHandlerServiceRoleDefaultPolicy1487A60A
└─ [~] PolicyDocument
└─ [~] .Statement:
└─ @@ -19,5 +19,15 @@
[ ] "Arn"
[ ] ]
[ ] }
[+] },
[+] {
[+] "Action": "lambda:InvokeFunction",
[+] "Effect": "Allow",
[+] "Resource": {
[+] "Fn::GetAtt": [
[+] "HelloHandler2E4FBA4D",
[+] "Arn"
[+] ]
[+] }
[ ] }
[ ] ]
- deploy, test
% cdk deploy
...
Outputs:
CdkWorkshopStack.Endpoint8024A810 = https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/
...
% curl -i https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/
HTTP/2 200
...
Hello, CDK! You've hit /
- 200 が応答された!
- いくつか異なるパスでもリクエストしてみる
% curl https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/
Hello, CDK! You've hit /
% curl https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/
Hello, CDK! You've hit /
% curl https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/hello
Hello, CDK! You've hit /hello
% curl https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/hello/world
Hello, CDK! You've hit /hello/world
% curl https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/hello/world
Hello, CDK! You've hit /hello/world
- 上記リクエストを試したら、マネジメントコンソール等から DynamoDB Table に path と hit(アクセス回数) が記録されているか確認してみよう
Using construct libraries :: AWS Cloud Development Kit (AWS CDK) Workshop
https://cdkworkshop.com/20-typescript/50-table-viewer.html
- このチャプターでは cdk-dynamo-tabel-viewer という construct library をインポートして、 DynamoDB Table に使う
cdk-dynamo-table-viewer - npm
https://www.npmjs.com/package/cdk-dynamo-table-viewer
- install
npm install cdk-dynamodb-table-viewer
- stack に追加
lib/cdk-workshop-stack.ts
- dynamodb table 名を持ってくる必要があるが、これはそもそもの DynamoDB Table を定義している
lib/hitcounter.ts
から良い感じに使い回せる
- dynamodb table 名を持ってくる必要があるが、これはそもそもの DynamoDB Table を定義している
lib/hitcounter.ts
import * as cdk from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda';
import * as dynamodb from '@aws-cdk/aws-dynamodb';
export interface HitCounterProps {
/** the function for which we want to count url hits **/
downstream: lambda.Function;
}
export class HitCounter extends cdk.Construct {
/** allows accessing the counter function */
public readonly handler: lambda.Function;
/** the hit counter table */
public readonly table: dynamodb.Table;
constructor(scope: cdk.Construct, id: string, props: HitCounterProps) {
super(scope, id);
const table = new dynamodb.Table(this, "Hits", {
partitionKey: {
name: "path",
type: dynamodb.AttributeType.STRING
}
});
this.table = table;
this.handler = new lambda.Function(this, 'HitCounterHandler', {
runtime: lambda.Runtime.NODEJS_10_X,
handler: 'hitcounter.handler',
code: lambda.Code.fromAsset('lambda'),
environment: {
DOWNSTREAM_FUNCTION_NAME: props.downstream.functionName,
HITS_TABLE_NAME: table.tableName
}
});
// grant the lambda role read/write permissions to our table
table.grantReadWriteData(this.handler);
// grant the lambda role invoke permissions to the downstream function
props.downstream.grantInvoke(this.handler);
}
}
lib/cdk-workshop-stack.ts
import * as cdk from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda';
import * as apigw from '@aws-cdk/aws-apigateway';
import { HitCounter } from './hitcounter';
import { TableViewer } from 'cdk-dynamo-table-viewer';
export class CdkWorkshopStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const hello = new lambda.Function(this, 'HelloHandler', {
runtime: lambda.Runtime.NODEJS_10_X,
code: lambda.Code.fromAsset('lambda'),
handler: 'hello.handler'
});
const helloWithCounter = new HitCounter(this, 'HelloHitCounter', {
downstream: hello
});
// defines an API Gateway REST API resource backed by our "hello" function.
new apigw.LambdaRestApi(this, 'Endpoint', {
handler: helloWithCounter.handler
});
new TableViewer(this, 'ViewHitCounter', {
title: 'Hello Hits',
table: helloWithCounter.table
});
}
}
- build, diff, deploy
% npm run build
% cdk diff
% cdk deploy
...
CdkWorkshopStack.ViewHitCounterViewerEndpointCA1B1E4B = https://yyy.execute-api.ap-northeast-1.amazonaws.com/prod/
CdkWorkshopStack.Endpoint8024A810 = https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/
- ViewHitCounterViewerEndpointCA1B1E4B にブラウザでアクセスすると、 DynamoDB Table のアイテム状況が良い感じに見れる
Clean up :: AWS Cloud Development Kit (AWS CDK) Workshop
https://cdkworkshop.com/20-typescript/60-cleanups.html
cdk destroy
Advanced Topics :: AWS Cloud Development Kit (AWS CDK) Workshop
https://cdkworkshop.com/20-typescript/70-advanced-topics.html
Testing Constructs :: AWS Cloud Development Kit (AWS CDK) Workshop
https://cdkworkshop.com/20-typescript/70-advanced-topics/100-construct-testing.html
- testing lib をインストール
npm install --save-dev jest @types/jest @aws-cdk/assert
- CDK Asset Library
@aws-cdk/assert
haveResource
といったメソッドを使って、 Construct が特定リソースを保持しているのか assertion が可能ABSENT
を使って、特定のキーが設定されていないオブジェクトや undefined に設定されているという扱いにする magic value を assert できる- https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/assert/README.md
expect(stack).to(haveResource('AWS::CertificateManager::Certificate', {
DomainName: 'test.example.com',
// Note: some properties omitted here
ShouldNotExist: ABSENT
}));
Assertion Tests :: AWS Cloud Development Kit (AWS CDK) Workshop
https://cdkworkshop.com/20-typescript/70-advanced-topics/100-construct-testing/1000-assertion-test.html
- DynamoDB Table のテストを書いてみる
cdk init
時に生成されたテストコードは不要なので削除rm test/cdk-workshop.test.ts
touch test/hitcounter.test.ts
- test も ts で書くのかー
- workshop の sample code は
@aws-cdk/aws-lambda
の import が不足してる - HitCounter Construct が DynamoDB Table リソースを持っている事をテスト
import { expect as expectCDK, haveResource } from '@aws-cdk/assert';
import cdk = require('@aws-cdk/core');
import * as lambda from '@aws-cdk/aws-lambda';
import { HitCounter } from '../lib/hitcounter';
test('DynamoDB Table Created', () => {
const stack = new cdk.Stack();
// WHEN
new HitCounter(stack, 'MyTestConstruct', {
downstream: new lambda.Function(stack, 'TestFunction', {
runtime: lambda.Runtime.NODEJS_10_X,
handler: 'lambda.handler',
code: lambda.Code.inline('test')
})
});
// THEN
expectCDK(stack).to(haveResource("AWS::DynamoDB::Table"));
});
- build, test
% npm run build && npx jest
> cdk-workshop@0.1.0 build /Users/xxx/workspaces/cdk-workshop
> tsc
PASS test/hitcounter.test.ts
✓ DynamoDB Table Created (203ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 3.277s, estimated 9s
Ran all test suites.
- Lambda 関数のテストを書く
test/hitcounter.test.ts
に追記- Lambda 関数が想定した環境変数を持つのかテスト
test('Lambda Has Environment Variables', () => {
const stack = new cdk.Stack();
// WHEN
new HitCounter(stack, 'MyTestConstruct', {
downstream: new lambda.Function(stack, 'TestFunction', {
runtime: lambda.Runtime.NODEJS_10_X,
handler: 'lambda.handler',
code: lambda.Code.inline('test')
})
});
// THEN
expectCDK(stack).to(haveResource("AWS::Lambda::Function", {
Environment: {
Variables: {
DOWNSTREAM_FUNCTION_NAME: "TestFunction",
HITS_TABLE_NAME: "MyTestConstructHits"
}
}
}));
})
- build, test
- 以下のようにテストが失敗する
- これは環境変数の値が異なるため
% npm run build && npx jest
> cdk-workshop@0.1.0 build /Users/xxx/workspaces/cdk-workshop
> tsc
FAIL test/hitcounter.test.ts (5.362s)
✓ DynamoDB Table Created (314ms)
✕ Lambda Has Environment Variables (130ms)
● Lambda Has Environment Variables
None of 2 resources matches resource 'AWS::Lambda::Function' with {
"$objectLike": {
...
"Environment": {
"Variables": {
"DOWNSTREAM_FUNCTION_NAME": {
"Ref": "TestFunction22AD90FC"
},
"HITS_TABLE_NAME": {
"Ref": "MyTestConstructHits24A357F0"
}
}
}
...
29 | });
30 | // THEN
> 31 | expectCDK(stack).to(haveResource("AWS::Lambda::Function", {
| ^
32 | Environment: {
33 | Variables: {
34 | DOWNSTREAM_FUNCTION_NAME: "TestFunction",
at HaveResourceAssertion.assertOrThrow (node_modules/@aws-cdk/assert/lib/assertions/have-resource.ts:100:13)
at StackInspector._to (node_modules/@aws-cdk/assert/lib/inspector.ts:25:15)
at StackInspector.to (node_modules/@aws-cdk/assert/lib/inspector.ts:15:14)
at Object.<anonymous> (test/hitcounter.test.ts:31:20)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 passed, 2 total
Snapshots: 0 total
Time: 6.905s
- 実際に生成される CloudFormation template の一部が出力されるので、そこから実際の値を環境変数として指定する
- なんかスマートじゃない気もするが・・・
test('Lambda Has Environment Variables', () => {
const stack = new cdk.Stack();
// WHEN
new HitCounter(stack, 'MyTestConstruct', {
downstream: new lambda.Function(stack, 'TestFunction', {
runtime: lambda.Runtime.NODEJS_10_X,
handler: 'lambda.handler',
code: lambda.Code.inline('test')
})
});
// THEN
expectCDK(stack).to(haveResource("AWS::Lambda::Function", {
Environment: {
Variables: {
DOWNSTREAM_FUNCTION_NAME: {"Ref": "TestFunction22AD90FC"},
HITS_TABLE_NAME: {"Ref": "MyTestConstructHits24A357F0"}
}
}
}));
})
- build, test
- 無事テストが pass された。こんな感じで TDD な開発も可能
% npm run build && npx jest
> cdk-workshop@0.1.0 build /Users/xxx/workspaces/cdk-workshop
> tsc
PASS test/hitcounter.test.ts
✓ DynamoDB Table Created (221ms)
✓ Lambda Has Environment Variables (77ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 3.641s, estimated 5s
Ran all test suites.
- DynanmoDB Table にサーバーサイド暗号化を追加する
test/hitcounter.test.ts
test('DynamoDB Table Created With Encryption', () => {
const stack = new cdk.Stack();
// WHEN
new HitCounter(stack, 'MyTestConstruct', {
downstream: new lambda.Function(stack, 'TestFunction', {
runtime: lambda.Runtime.NODEJS_10_X,
handler: 'lambda.handler',
code: lambda.Code.inline('test')
})
});
// THEN
expectCDK(stack).to(haveResource('AWS::DynamoDB::Table', {
SSESpecification: {
SSEEnabled: true
}
}));
});
- テストを実行して失敗する
% npm run build && npx jest
> cdk-workshop@0.1.0 build /Users/xxx/workspaces/cdk-workshop
> tsc
FAIL test/hitcounter.test.ts (6.65s)
✓ DynamoDB Table Created (251ms)
✓ Lambda Has Environment Variables (80ms)
✕ DynamoDB Table Created With Encryption (68ms)
● DynamoDB Table Created With Encryption
None of 1 resources matches resource 'AWS::DynamoDB::Table' with {
"$objectLike": {
"SSESpecification": {
"SSEEnabled": true
}
}
}.
- Field SSESpecification missing in:
{
"Type": "AWS::DynamoDB::Table",
"Properties": {
"KeySchema": [
{
"AttributeName": "path",
"KeyType": "HASH"
}
],
"AttributeDefinitions": [
{
"AttributeName": "path",
"AttributeType": "S"
}
],
"ProvisionedThroughput": {
"ReadCapacityUnits": 5,
"WriteCapacityUnits": 5
}
},
"UpdateReplacePolicy": "Retain",
"DeletionPolicy": "Retain"
}
50 | });
51 | // THEN
> 52 | expectCDK(stack).to(haveResource('AWS::DynamoDB::Table', {
| ^
53 | SSESpecification: {
54 | SSEEnabled: true
55 | }
at HaveResourceAssertion.assertOrThrow (node_modules/@aws-cdk/assert/lib/assertions/have-resource.ts:100:13)
at StackInspector._to (node_modules/@aws-cdk/assert/lib/inspector.ts:25:15)
at StackInspector.to (node_modules/@aws-cdk/assert/lib/inspector.ts:15:14)
at Object.<anonymous> (test/hitcounter.test.ts:52:22)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 2 passed, 3 total
Snapshots: 0 total
Time: 8.12s
Ran all test suites.
- アプリ側でサーバーサイド暗号化を有効化する
lib/hitcounter.ts
const table = new dynamodb.Table(this, "Hits", {
partitionKey: {
name: "path",
type: dynamodb.AttributeType.STRING
},
serverSideEncryption: true
});
- 再度テストを実行して成功させる
% npm run build && npx jest
> cdk-workshop@0.1.0 build /Users/xxx/workspaces/cdk-workshop
> tsc
PASS test/hitcounter.test.ts
✓ DynamoDB Table Created (309ms)
✓ Lambda Has Environment Variables (87ms)
✓ DynamoDB Table Created With Encryption (98ms)
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 4.558s, estimated 7s
Ran all test suites.
Validation Tests :: AWS Cloud Development Kit (AWS CDK) Workshop
https://cdkworkshop.com/20-typescript/70-advanced-topics/100-construct-testing/2000-validation-tests.html
- 設定する値を validation したい場合・・・
- 例えば HitCounter で使用する DynamoDB Table リソースの readCapacity 範囲を限定したい場合、
interface
を使う
export interface HitCounterProps {
/** the function for which we want to count url hits **/
downstream: lambda.IFunction;
/**
* The read capacity units for the table
*
* Must be greater than 5 and less than 20
*
* @default 5
*/
readCapacity?: number;
}
- 実際に DynamoDB Table リソースを定義する際には、 readCapacity の設定を行う
const table = new dynamodb.Table(this, "Hits", {
partitionKey: {
name: "path",
type: dynamodb.AttributeType.STRING
},
serverSideEncryption: true,
readCapacity: props.readCapacity || 5
});
- constructor を呼び出す際に、具体的なレンジの validation ロジックを実装する
constructor(scope: cdk.Construct, id: string, props: HitCounterProps) {
if (props.readCapacity !== undefined && (props.readCapacity < 5 || props.readCapacity > 20)) {
throw new Error('readCapacity must be greater than 5 and less than 20');
}
super(scope, id);
const table = new dynamodb.Table(this, "Hits", {
partitionKey: {
name: "path",
type: dynamodb.AttributeType.STRING
},
serverSideEncryption: true,
readCapacity: props.readCapacity || 5
});
- validation ロジックが想定通り動作するかテストするコードを書く
test('read capacity can be configured', () => {
const stack = new cdk.Stack();
expect(() => {
new HitCounter(stack, 'MyTestConstruct', {
downstream: new lambda.Function(stack, 'TestFunction', {
runtime: lambda.Runtime.NODEJS_10_X,
handler: 'lambda.handler',
code: lambda.Code.inline('test')
}),
readCapacity: 3
});
}).toThrowError(/readCapacity must be greater than 5 and less than 20/);
});
- ビルドしてテスト実行
- テストが成功し、想定通りの validation が実装できていることを確認できた
% npm run build && npx jest
> cdk-workshop@0.1.0 build /Users/xxx/workspaces/cdk-workshop
> tsc
PASS test/hitcounter.test.ts
✓ DynamoDB Table Created (202ms)
✓ Lambda Has Environment Variables (75ms)
✓ DynamoDB Table Created With Encryption (71ms)
✓ read capacity can be configured (7ms)
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 6.299s
Ran all test suites.