Skip to content

Instantly share code, notes, and snippets.

@noid11
Created August 4, 2020 19:26
Show Gist options
  • Save noid11/8359cdfbd2c109406a998a28ca88dd30 to your computer and use it in GitHub Desktop.
Save noid11/8359cdfbd2c109406a998a28ca88dd30 to your computer and use it in GitHub Desktop.
CDK Workshop の TypeScript 編やったメモ

TOC

これは何?

AWS CDK 公式の Workshop (TypeScript) で手を動かしたメモ

TypeScript Workshop :: AWS Cloud Development Kit (AWS CDK) Workshop
https://cdkworkshop.com/20-typescript.html

New Project

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!

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

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)"
    ]
}
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>"
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

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 から良い感じに使い回せる

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

Clean up :: AWS Cloud Development Kit (AWS CDK) Workshop
https://cdkworkshop.com/20-typescript/60-cleanups.html

cdk destroy

Advanced Topics

Advanced Topics :: AWS Cloud Development Kit (AWS CDK) Workshop
https://cdkworkshop.com/20-typescript/70-advanced-topics.html

Constructs をテストしたい

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
expect(stack).to(haveResource('AWS::CertificateManager::Certificate', {
    DomainName: 'test.example.com',
    // Note: some properties omitted here

    ShouldNotExist: ABSENT
}));

Assertion Tests

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

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment