AWS CDKを使ってDynamoDBとLambda環境を構築する


[前回](!https://qiita.com/chocomintkusoyaro/items/dda5054b5c7c64ee9bb5) の続きでLambdaとDynamoDBの環境を構築していきます。

## 作成する環境

以下のような環境を構築するコードとなっています。
IsolatedサブネットにLambda関数をデプロイし、VPC Gatewayを経由してDynamoDBテーブルのストリームをフェッチできるようにします。

![Untitled Diagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203061/a74bca05-2904-ed16-4e3d-ef07fb4b2e1f.png)



## AWS CDKのインストール

```
sudo npm install -g aws-cdk
```

```
cdk ---version
1.18.0 (build bc924bc)
```


```
mkdir mini-cdk
cd mini-cdk
cdk init app --language=typescript
```

```
npm install @aws-cdk/aws-lambda @aws-cdk/aws-dynamodb @aws-cdk/aws-ec2 @aws-cdk/aws-iam
```

```
# 作成するスタッックのファイル
touch lib/mini-vpc-stack.ts
touch lib/mini-lambda-stack.ts
touch lib/mini-dynamo-stack.ts

# 都合上削除
rm test/mini-cdk.test.ts
```

## Lambda関数の作成

`mini-cdk`プロジェクトの中に`lambda`ディレクトリを作成してそこにLambda関数をコーディングしていきます。

```
mkdir -p lambda/src
touch lambda/src/fetchStream.ts
cd lambda
tsc --init

npm init -y
npm install aws-sdk aws-lambda
npm install -D @types/aws-lambda
```

DynamoDBストリームをコンソールに出力するだけの簡単なLambda関数を作成します。

```lambda/src/fetchStream.ts
import { DynamoDBStreamHandler } from "aws-lambda";

export const handler: DynamoDBStreamHandler = (event, context) => {
  event.Records.forEach(record => {
    // TODO Complex Processing
    console.log(record.eventName);
    console.log(JSON.stringify(record, null, 2));
  });
};
```

## VPCスタックの作成

[前回](!https://qiita.com/chocomintkusoyaro/items/dda5054b5c7c64ee9bb5) のものを丸々同じです。再掲しておきます。

```lib/mini-vpc-stack.ts
import cdk = require("@aws-cdk/core");
import ec2 = require("@aws-cdk/aws-ec2");

export class MiniVpcStack extends cdk.Stack {
  public readonly vpc: ec2.Vpc;

  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // VPCを作成する
    this.vpc = this.createVpc();

    // GatewayEndpointをセットアップする
    this.setupGatewayEndpoint();

    // InterfaceEndpointをセットアップする
    this.setupInterfaceEndpoint();
  }

  private createVpc() {
    return new ec2.Vpc(this, "MiniVpc", {
      cidr: "172.17.0.0/16",
      natGateways: 0, // NatGatewayはお金がかかるので使わない
      maxAzs: 1, // Availability Zoneの数
      subnetConfiguration: [
        { name: "Public", cidrMask: 20, subnetType: ec2.SubnetType.PUBLIC },
        { name: "Isolated", cidrMask: 20, subnetType: ec2.SubnetType.ISOLATED }
      ]
    });
  }
  private setupGatewayEndpoint() {
    // IsolatedのサブネットからS3に直接アクセスできるようにVPCエンドポイントを利用する
    this.vpc.addGatewayEndpoint("S3EndpointForIsolatedSubnet", {
      service: ec2.GatewayVpcEndpointAwsService.S3,
      subnets: [{ subnetType: ec2.SubnetType.ISOLATED }]
    });

    // IsolatedのサブネットからDynamoDBに直接アクセスできるようにVPCエンドポイントを利用する
    this.vpc.addGatewayEndpoint("DynamoDbEndpointForIsolatedSubnet", {
      service: ec2.GatewayVpcEndpointAwsService.DYNAMODB,
      subnets: [{ subnetType: ec2.SubnetType.ISOLATED }]
    });
  }

  private setupInterfaceEndpoint() {
    // InterafceEndpoint用のセキュリティグループ
    const interfaceEndpointSecurityGroup = new ec2.SecurityGroup(
      this,
      "InterfaceEndpointSecurityGroup",
      {
        securityGroupName: "InterfaceEndpointSecurityGroup",
        vpc: this.vpc
      }
    );
    interfaceEndpointSecurityGroup.addIngressRule(
      ec2.Peer.anyIpv4(),
      ec2.Port.tcp(443)
    );

    // Isolated SubnetからECRに接続するためのエンドポイント
    this.vpc.addInterfaceEndpoint("ECRInterfaceEndpoint", {
      securityGroups: [interfaceEndpointSecurityGroup],
      service: ec2.InterfaceVpcEndpointAwsService.ECR,
      subnets: {
        subnetType: ec2.SubnetType.ISOLATED
      }
    });
    this.vpc.addInterfaceEndpoint("ECR_DockerInterfaceEndpoint", {
      securityGroups: [interfaceEndpointSecurityGroup],
      service: ec2.InterfaceVpcEndpointAwsService.ECR_DOCKER
    });
  }
}
```

## DynamoDBスタックの作成

```lib/mini-dynamo-stack.ts
import cdk = require("@aws-cdk/core");
import dynamodb = require("@aws-cdk/aws-dynamodb");

export class MiniDynamoDbStack extends cdk.Stack {
  public readonly todoTable: dynamodb.Table;

  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Setup Todo Table
    this.todoTable = this.setupTodoTable();
  }

  private setupTodoTable() {
    return new dynamodb.Table(this, "TodoTable", {
      tableName: "todos",
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      partitionKey: {
        name: "id",
        type: dynamodb.AttributeType.STRING
      },
      stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES
    });
  }
}
```

## Lambdaスタックの作成

```lib/mini-lambda-stack.ts
import cdk = require("@aws-cdk/core");
import lambda = require("@aws-cdk/aws-lambda");
import ec2 = require("@aws-cdk/aws-ec2");
import dynamodb = require("@aws-cdk/aws-dynamodb");
import iam = require("@aws-cdk/aws-iam");

interface MiniLambdaStackProps extends cdk.StackProps {
  vpc: ec2.Vpc;
  todoTable: dynamodb.Table;
}

export class MiniLambdaStack extends cdk.Stack {
  private readonly vpc: ec2.Vpc;
  private readonly todoTable: dynamodb.Table;

  constructor(scope: cdk.Construct, id: string, props: MiniLambdaStackProps) {
    super(scope, id, props);

    this.vpc = props.vpc;
    this.todoTable = props.todoTable;

    // DynamoDbテーブルのストリームをフェッチするLambda関数のセットアップ
    this.setupFetchTodoTableStreamFunction();
  }

  // DynamoのTodo TableストリームをフェッチするためLambdaの関数を定義する
  private setupFetchTodoTableStreamFunction() {
    const role = new iam.Role(this, "FetchTodoTableStreamRole", {
      roleName: "FetchTodoTableStreamRole",
      assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com")
    });

    // DynanmoDB Streamをフェッチするために必要なポリシー
    if (this.todoTable.tableStreamArn) {
      role.addToPolicy(
        new iam.PolicyStatement({
          actions: [
            "dynamodb:GetRecords",
            "dynamodb:GetShardIterator",
            "dynamodb:ListStream",
            "dynamodb:DescribeStream"
          ],
          resources: [this.todoTable.tableStreamArn]
        })
      );
    }

    // Isolated SubnetからInterface Endpointを経由してDynamoDBにアクセスするため、
    // ENIを作成するポリシーも必要になる
    // AWSのManagedPolicyを利用することによって、ENI作成ポリシーとロググループ関連のポリシーをアタッチすることができる
    role.addManagedPolicy(
      iam.ManagedPolicy.fromAwsManagedPolicyName(
        "service-role/AWSLambdaVPCAccessExecutionRole"
      )
    );

    const handler = new lambda.Function(this, "FetchTodoTableStreamFunction", {
      functionName: "FetchTodoTableStreamFunction",
      handler: "src/fetchStream.handler",
      code: lambda.Code.fromAsset("lambda"),
      runtime: lambda.Runtime.NODEJS_12_X,
      vpc: this.vpc,
      vpcSubnets: {
        subnetType: ec2.SubnetType.ISOLATED
      },
      role
    });

    // DynamoDBのStreamをSourceとするためのマッピング設定
    if (this.todoTable.tableStreamArn) {
      handler.addEventSourceMapping("FetchTodoTableStreamSourceMapping", {
        eventSourceArn: this.todoTable.tableStreamArn,
        batchSize: 10,
        startingPosition: lambda.StartingPosition.LATEST
      });
    }
  }
}
```

## エンドポイントの作成

ここまで作成したスタックのエンドポイントとなるファイルを作成します。


```bin/mini-cdk.ts
#!/usr/bin/env node
import "source-map-support/register";
import cdk = require("@aws-cdk/core");
import { MiniVpcStack } from "../lib/mini-vpc-stack";
import { MiniDynamoDbStack } from "../lib/mini-dynamo-stack";
import { MiniLambdaStack } from "../lib/mini-lambda-stack";

import util = require("util");
import childProcess = require("child_process");

main();

async function main() {
  // Lambdaソース(TypeScript)をビルドする
  await buildTypeScript();

  const app = new cdk.App();

  // VPCスタック
  const miniVpcStack = new MiniVpcStack(app, "MiniVpcStack");

  // DynamoDbスタック
  const miniDynamoDbStack = new MiniDynamoDbStack(app, "MiniDynamoDbStack");

  // Lambdaスタック
  new MiniLambdaStack(app, "MiniLambdaStack", {
    vpc: miniVpcStack.vpc,
    todoTable: miniDynamoDbStack.todoTable
  });

  app.synth();
}

async function buildTypeScript() {
  const exec = util.promisify(childProcess.exec);
  exec("cd lambda && tsc");
}

```



## ビルドとデプロイ

ビルドして`js`ファイルを生成します。

```
npm run build
```

ここまでで準備が整ったので、スタックをまとめてデプロイして環境を構築します。

```
export AWS_DEFAULT_REGION=ap-northeast-1
cdk -c keyName=mini-cdk deploy *Stack --require-approval never --profile dev
```

デプロイが完了したら、DynamoDBの`todos`テーブルを更新してCloudWatch Logsを確認してみてください。