[AWS] CDKで、API Gateway + Lambda + DynamoDBなサンプルを作成してみる

# はじめに

以前投稿した「[[AWS] Serverless Application Model (SAM) でAPI Gateway + Lambda + DynamoDBなサンプルを作成してみる](https://qiita.com/herohit-tool/items/5b0fe520f6f28fb5b4bc)」のCDK版だと思ってください。

# CDKとは？
AWS Cloud Development Kitで。クラウドアプリケーションリソースをモデル化、およびプロビジョニングするためのフレームワークです。
SAMは、Serverless Applicationに重きを置いたフレームワークだったのに対し、CDKはIaC（Infrastructure As Code）の要素が強い開発フレームワークということになります。
さらに特徴的なのは、それをYAMLやJsonではなく、複数の言語で記述できる、ということです。

## 対応言語
以下の言語に対応しています。

- JavaScript
- TypeScript
- Python
- Java
- C#

## 前提条件
CDKを導入するうえで、まず`npm`を事前に使用できる状態にしておいてください。
また、今回は言語にPythonを使用するので`pip`も事前に使用できる状態にしておいてください。
あと、言わずもがなですが、AWSアカウントのご用意も忘れずに。

## インストール
```bash
$ npm install -g aws-cdk
/Users/******/.nodebrew/node/v12.13.1/bin/cdk -> /Users/******/.nodebrew/node/v12.13.1/lib/node_modules/aws-cdk/bin/cdk
+ aws-cdk@1.56.0
added 24 packages from 10 contributors, removed 12 packages and updated 22 packages in 4.342s
```

確認

```bash
$ cdk --version
1.56.0 (build c1c174d)
```

# サンプルコード作成
## プロジェクト作成

まず、適当な作業ディレクトリを作成し、そのディレクトリに移動します。

```bash
$ mkdir cdk
$ cd cdk
```

次に、プロジェクトの作成ですが、今回はpythonでプロジェクトを作成してみます。

```bash
$ cdk init --language=python
Applying project template app for python

# Welcome to your CDK Python project!

This is a blank project for Python development with CDK.

The 'cdk.json' file tells the CDK Toolkit how to execute your app.

This project is set up like a standard Python project.  The initialization
process also creates a virtualenv within this project, stored under the .env
directory.  To create the virtualenv it assumes that there is a 'python3'
(or 'python' for Windows) executable in your path with access to the 'venv'
package. If for any reason the automatic creation of the virtualenv fails,
you can create the virtualenv manually.

To manually create a virtualenv on MacOS and Linux:

'''
$ python3 -m venv .env
'''

After the init process completes and the virtualenv is created, you can use the following
step to activate your virtualenv.

'''
$ source .env/bin/activate
'''

If you are a Windows platform, you would activate the virtualenv like this:

'''
% .env\Scripts\activate.bat
'''

Once the virtualenv is activated, you can install the required dependencies.

'''
$ pip install -r requirements.txt
'''

At this point you can now synthesize the CloudFormation template for this code.

'''
$ cdk synth
'''

To add additional dependencies, for example other CDK libraries, just add
them to your 'setup.py' file and rerun the 'pip install -r requirements.txt'
command.

## Useful commands

 * 'cdk ls'          list all stacks in the app
 * 'cdk synth'       emits the synthesized CloudFormation template
 * 'cdk deploy'      deploy this stack to your default AWS account/region
 * 'cdk diff'        compare deployed stack with current state
 * 'cdk docs'        open CDK documentation

Enjoy!

Initializing a new git repository...
Please run python3 -m venv .env'!
Executing Creating virtualenv...
✅ All done!
```

一応、コマンド実行後に、上記のように手順が紹介されますが、一つずつ実行してみましょう。

まずは、Pythonの実行環境を仮想化します。

```bash
$ python3 -m venv .env
```

そして、シェルに環境変数を適用させます。

```bash
$ source .env/bin/activate
```

## モジュールの追加
続いて、`setup.py`に必要なモジュールを追加します。

```python:変更前
    install_requires=[
        "aws-cdk.core==1.56.0",
    ],
```

```python:変更後
    install_requires=[
        "aws-cdk.core==1.56.0",
        "aws-cdk.aws-apigateway",
        "aws-cdk.aws-lambda",
        "aws-cdk.aws-dynamodb"
    ],
```

この状態で、依存関係を追加します。

```bash
$ pip install -r requirements.txt
Obtaining file:///Users/******/cdk (from -r requirements.txt (line 1))
Collecting aws-cdk.core==1.56.0
  Downloading aws_cdk.core-1.56.0-py3-none-any.whl (988 kB)
     |████████████████████████████████| 988 kB 985 kB/s
Collecting aws-cdk.aws-apigateway
  Downloading aws_cdk.aws_apigateway-1.56.0-py3-none-any.whl (675 kB)
     |████████████████████████████████| 675 kB 3.0 MB/s
Collecting aws-cdk.aws-lambda
  Downloading aws_cdk.aws_lambda-1.56.0-py3-none-any.whl (370 kB)
     |████████████████████████████████| 370 kB 5.1 MB/s
Collecting aws_cdk.aws_dynamodb
  Downloading aws_cdk.aws_dynamodb-1.56.0-py3-none-any.whl (167 kB)
     |████████████████████████████████| 167 kB 5.6 MB/s
Collecting constructs<4.0.0,>=3.0.2
  Downloading constructs-3.0.4-py3-none-any.whl (76 kB)
     |████████████████████████████████| 76 kB 3.5 MB/s
Collecting jsii<2.0.0,>=1.9.0
  Downloading jsii-1.9.0-py3-none-any.whl (265 kB)
     |████████████████████████████████| 265 kB 3.3 MB/s
Collecting publication>=0.0.3
  Downloading publication-0.0.3-py2.py3-none-any.whl (7.7 kB)
Collecting aws-cdk.cloud-assembly-schema==1.56.0
  Downloading aws_cdk.cloud_assembly_schema-1.56.0-py3-none-any.whl (116 kB)
     |████████████████████████████████| 116 kB 3.3 MB/s
Collecting aws-cdk.cx-api==1.56.0
  Downloading aws_cdk.cx_api-1.56.0-py3-none-any.whl (117 kB)
     |████████████████████████████████| 117 kB 5.2 MB/s
Collecting aws-cdk.aws-certificatemanager==1.56.0
  Downloading aws_cdk.aws_certificatemanager-1.56.0-py3-none-any.whl (248 kB)
     |████████████████████████████████| 248 kB 4.0 MB/s
Collecting aws-cdk.aws-s3-assets==1.56.0
  Downloading aws_cdk.aws_s3_assets-1.56.0-py3-none-any.whl (50 kB)
     |████████████████████████████████| 50 kB 4.3 MB/s
Collecting aws-cdk.aws-s3==1.56.0
  Downloading aws_cdk.aws_s3-1.56.0-py3-none-any.whl (272 kB)
     |████████████████████████████████| 272 kB 3.5 MB/s
Collecting aws-cdk.assets==1.56.0
  Downloading aws_cdk.assets-1.56.0-py3-none-any.whl (22 kB)
Collecting aws-cdk.aws-iam==1.56.0
  Downloading aws_cdk.aws_iam-1.56.0-py3-none-any.whl (345 kB)
     |████████████████████████████████| 345 kB 6.6 MB/s
Collecting aws-cdk.aws-logs==1.56.0
  Downloading aws_cdk.aws_logs-1.56.0-py3-none-any.whl (111 kB)
     |████████████████████████████████| 111 kB 7.5 MB/s
Collecting aws-cdk.aws-ec2==1.56.0
  Downloading aws_cdk.aws_ec2-1.56.0-py3-none-any.whl (926 kB)
     |████████████████████████████████| 926 kB 4.1 MB/s
Collecting aws-cdk.aws-elasticloadbalancingv2==1.56.0
  Downloading aws_cdk.aws_elasticloadbalancingv2-1.56.0-py3-none-any.whl (379 kB)
     |████████████████████████████████| 379 kB 5.3 MB/s
Collecting aws-cdk.aws-efs==1.56.0
  Downloading aws_cdk.aws_efs-1.56.0-py3-none-any.whl (76 kB)
     |████████████████████████████████| 76 kB 8.2 MB/s
Collecting aws-cdk.aws-events==1.56.0
  Downloading aws_cdk.aws_events-1.56.0-py3-none-any.whl (143 kB)
     |████████████████████████████████| 143 kB 5.9 MB/s
Collecting aws-cdk.aws-codeguruprofiler==1.56.0
  Downloading aws_cdk.aws_codeguruprofiler-1.56.0-py3-none-any.whl (35 kB)
Collecting aws-cdk.aws-sqs==1.56.0
  Downloading aws_cdk.aws_sqs-1.56.0-py3-none-any.whl (77 kB)
     |████████████████████████████████| 77 kB 8.6 MB/s
Collecting aws-cdk.aws-cloudwatch==1.56.0
  Downloading aws_cdk.aws_cloudwatch-1.56.0-py3-none-any.whl (230 kB)
     |████████████████████████████████| 230 kB 2.9 MB/s
Collecting aws-cdk.aws-kms==1.56.0
  Downloading aws_cdk.aws_kms-1.56.0-py3-none-any.whl (78 kB)
     |████████████████████████████████| 78 kB 3.8 MB/s
Collecting aws-cdk.aws-applicationautoscaling==1.56.0
  Downloading aws_cdk.aws_applicationautoscaling-1.56.0-py3-none-any.whl (122 kB)
     |████████████████████████████████| 122 kB 8.5 MB/s
Collecting aws-cdk.custom-resources==1.56.0
  Downloading aws_cdk.custom_resources-1.56.0-py3-none-any.whl (151 kB)
     |████████████████████████████████| 151 kB 11.6 MB/s
Collecting typing-extensions~=3.7.4
  Downloading typing_extensions-3.7.4.2-py3-none-any.whl (22 kB)
Collecting python-dateutil
  Using cached python_dateutil-2.8.1-py2.py3-none-any.whl (227 kB)
Collecting attrs~=19.3.0
  Using cached attrs-19.3.0-py2.py3-none-any.whl (39 kB)
Collecting cattrs~=1.0.0
  Downloading cattrs-1.0.0-py2.py3-none-any.whl (14 kB)
Collecting aws-cdk.aws-route53==1.56.0
  Downloading aws_cdk.aws_route53-1.56.0-py3-none-any.whl (122 kB)
     |████████████████████████████████| 122 kB 6.2 MB/s
Collecting aws-cdk.region-info==1.56.0
  Downloading aws_cdk.region_info-1.56.0-py3-none-any.whl (59 kB)
     |████████████████████████████████| 59 kB 4.4 MB/s
Collecting aws-cdk.aws-ssm==1.56.0
  Downloading aws_cdk.aws_ssm-1.56.0-py3-none-any.whl (151 kB)
     |████████████████████████████████| 151 kB 6.1 MB/s
Collecting aws-cdk.aws-autoscaling-common==1.56.0
  Downloading aws_cdk.aws_autoscaling_common-1.56.0-py3-none-any.whl (30 kB)
Collecting aws-cdk.aws-sns==1.56.0
  Downloading aws_cdk.aws_sns-1.56.0-py3-none-any.whl (84 kB)
     |████████████████████████████████| 84 kB 4.9 MB/s
Collecting aws-cdk.aws-cloudformation==1.56.0
  Downloading aws_cdk.aws_cloudformation-1.56.0-py3-none-any.whl (112 kB)
     |████████████████████████████████| 112 kB 6.2 MB/s
Collecting six>=1.5
  Using cached six-1.15.0-py2.py3-none-any.whl (10 kB)
Installing collected packages: publication, typing-extensions, six, python-dateutil, attrs, cattrs, jsii, constructs, aws-cdk.cloud-assembly-schema, aws-cdk.cx-api, aws-cdk.core, aws-cdk.region-info, aws-cdk.aws-iam, aws-cdk.aws-cloudwatch, aws-cdk.aws-logs, aws-cdk.aws-kms, aws-cdk.aws-events, aws-cdk.aws-s3, aws-cdk.aws-ssm, aws-cdk.aws-ec2, aws-cdk.aws-route53, aws-cdk.assets, aws-cdk.aws-s3-assets, aws-cdk.aws-efs, aws-cdk.aws-codeguruprofiler, aws-cdk.aws-sqs, aws-cdk.aws-lambda, aws-cdk.aws-certificatemanager, aws-cdk.aws-elasticloadbalancingv2, aws-cdk.aws-apigateway, aws-cdk.aws-autoscaling-common, aws-cdk.aws-applicationautoscaling, aws-cdk.aws-sns, aws-cdk.aws-cloudformation, aws-cdk.custom-resources, aws-cdk.aws-dynamodb, cdk
  Running setup.py develop for cdk
Successfully installed attrs-19.3.0 aws-cdk.assets-1.56.0 aws-cdk.aws-apigateway-1.56.0 aws-cdk.aws-applicationautoscaling-1.56.0 aws-cdk.aws-autoscaling-common-1.56.0 aws-cdk.aws-certificatemanager-1.56.0 aws-cdk.aws-cloudformation-1.56.0 aws-cdk.aws-cloudwatch-1.56.0 aws-cdk.aws-codeguruprofiler-1.56.0 aws-cdk.aws-dynamodb-1.56.0 aws-cdk.aws-ec2-1.56.0 aws-cdk.aws-efs-1.56.0 aws-cdk.aws-elasticloadbalancingv2-1.56.0 aws-cdk.aws-events-1.56.0 aws-cdk.aws-iam-1.56.0 aws-cdk.aws-kms-1.56.0 aws-cdk.aws-lambda-1.56.0 aws-cdk.aws-logs-1.56.0 aws-cdk.aws-route53-1.56.0 aws-cdk.aws-s3-1.56.0 aws-cdk.aws-s3-assets-1.56.0 aws-cdk.aws-sns-1.56.0 aws-cdk.aws-sqs-1.56.0 aws-cdk.aws-ssm-1.56.0 aws-cdk.cloud-assembly-schema-1.56.0 aws-cdk.core-1.56.0 aws-cdk.custom-resources-1.56.0 aws-cdk.cx-api-1.56.0 aws-cdk.region-info-1.56.0 cattrs-1.0.0 cdk constructs-3.0.4 jsii-1.9.0 publication-0.0.3 python-dateutil-2.8.1 six-1.15.0 typing-extensions-3.7.4.2
```

## Lambdaコード作成
まず、コード用のディレクトリを作成して、その下に以下コードを作成してみましょう。

```bash
$ mkdir lambda
```

```python:lambda/app.py
import json
import boto3
import os
from datetime import datetime

def lambda_handler(event, context):
    try:
        event_body = json.loads(event["body"])
        if "local" in event_body and event_body["local"] == True:
            dynamodb = boto3.resource("dynamodb", endpoint_url="http://dynamodb:8000")
        else:
            dynamodb = boto3.resource("dynamodb")

        table = dynamodb.Table("Demo")
        table.put_item(
            Item={
                "Key": event_body["key"],
                "CreateDate": datetime.utcnow().isoformat()
            }
        )

        return {
            "statusCode": 200,
            "body": json.dumps({
                "message": "succeeded",
            }),
        }
    except Exception as e:
        return {
            "statusCode": 500,
            "body": json.dumps({
                "message": e.args
            }),
    }
```

このコードは「[[AWS] Serverless Application Model (SAM) でAPI Gateway + Lambda + DynamoDBなサンプルを作成してみる](https://qiita.com/herohit-tool/items/5b0fe520f6f28fb5b4bc)」と同じものにしてあります。

## デプロイとプロビジョニングコードの作成
プロジェクト直下にある`app.py`を以下のように修正してください。

```python:app.py
#!/usr/bin/env python3

from aws_cdk import (
    aws_apigateway,
    aws_lambda,
    aws_dynamodb,
    core
)

from aws_cdk.aws_dynamodb import (
    Table,
    Attribute,
    AttributeType
)

class LambdaSampleStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        handler = aws_lambda.Function(
            self, "backend",
            runtime=aws_lambda.Runtime.PYTHON_3_8,
            handler="app.lambda_handler",
            code=aws_lambda.AssetCode(path="./lambda"))

        api = aws_apigateway.LambdaRestApi(self, "SampleLambda", handler=handler, proxy=False)
        api.root.add_resource("ddb").add_method("POST")

        table = Table(
            self, "ItemsTable",
            table_name="Demo",
            partition_key=Attribute(
                name="Key",
                type=AttributeType.STRING
            ),
            sort_key=Attribute(
                name="CreateDate",
                type=AttributeType.STRING
            )
        )
        table.grant_write_data(handler)

app = core.App()
LambdaSampleStack(app, "LambdaSampleStack")

app.synth()
```

この中で一つ重要な点があります。それは

```python
table.grant_write_data(handler)
```

です。ここで、Lambda関数に、このテーブルへの書き込みの権限を与える宣言を行っています。
他にも読み出しや一覧など、権限がありますが、同様にLambda関数に適用してあげないと、Lambda関数からDynamoDBにアクセスすることができないので、ご注意を。

## デプロイの準備

まず、デプロイ用のS3バケットを作成するために、以下のコマンドを実行してください。

```bash
$ cdk bootstrap
 ⏳  Bootstrapping environment aws://************/ap-northeast-1...
CDKToolkit: creating CloudFormation changeset...



 ✅  Environment aws://************/ap-northeast-1 bootstrapped.
```

## デプロイ

ここまできたら、デプロイを実行してみます。

```bash
$ cdk deploy
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

IAM Statement Changes
┌───┬───────────────┬────────┬───────────────┬───────────────┬────────────────┐
│   │ Resource      │ Effect │ Action        │ Principal     │ Condition      │
├───┼───────────────┼────────┼───────────────┼───────────────┼────────────────┤
│ + │ ${ItemsTable. │ Allow  │ dynamodb:Batc │ AWS:${backend │                │
│   │ Arn}          │        │ hWriteItem    │ /ServiceRole} │                │
│   │               │        │ dynamodb:Dele │               │                │
│   │               │        │ teItem        │               │                │
│   │               │        │ dynamodb:PutI │               │                │
│   │               │        │ tem           │               │                │
│   │               │        │ dynamodb:Upda │               │                │
│   │               │        │ teItem        │               │                │
├───┼───────────────┼────────┼───────────────┼───────────────┼────────────────┤
│ + │ ${SampleLambd │ Allow  │ sts:AssumeRol │ Service:apiga │                │
│   │ a/CloudWatchR │        │ e             │ teway.amazona │                │
│   │ ole.Arn}      │        │               │ ws.com        │                │
├───┼───────────────┼────────┼───────────────┼───────────────┼────────────────┤
│ + │ ${backend.Arn │ Allow  │ lambda:Invoke │ Service:apiga │ "ArnLike": {   │
│   │ }             │        │ Function      │ teway.amazona │   "AWS:SourceA │
│   │               │        │               │ ws.com        │ rn": "arn:${AW │
│   │               │        │               │               │ S::Partition}: │
│   │               │        │               │               │ execute-api:${ │
│   │               │        │               │               │ AWS::Region}:$ │
│   │               │        │               │               │ {AWS::AccountI │
│   │               │        │               │               │ d}:${SampleLam │
│   │               │        │               │               │ bdaB2FF4FA1}/$ │
│   │               │        │               │               │ {SampleLambda/ │
│   │               │        │               │               │ DeploymentStag │
│   │               │        │               │               │ e.prod}/POST/d │
│   │               │        │               │               │ db"            │
│   │               │        │               │               │ }              │
│ + │ ${backend.Arn │ Allow  │ lambda:Invoke │ Service:apiga │ "ArnLike": {   │
│   │ }             │        │ Function      │ teway.amazona │   "AWS:SourceA │
│   │               │        │               │ ws.com        │ rn": "arn:${AW │
│   │               │        │               │               │ S::Partition}: │
│   │               │        │               │               │ execute-api:${ │
│   │               │        │               │               │ AWS::Region}:$ │
│   │               │        │               │               │ {AWS::AccountI │
│   │               │        │               │               │ d}:${SampleLam │
│   │               │        │               │               │ bdaB2FF4FA1}/t │
│   │               │        │               │               │ est-invoke-sta │
│   │               │        │               │               │ ge/POST/ddb"   │
│   │               │        │               │               │ }              │
├───┼───────────────┼────────┼───────────────┼───────────────┼────────────────┤
│ + │ ${backend/Ser │ Allow  │ sts:AssumeRol │ Service:lambd │                │
│   │ viceRole.Arn} │        │ e             │ a.amazonaws.c │                │
│   │               │        │               │ om            │                │
└───┴───────────────┴────────┴───────────────┴───────────────┴────────────────┘
IAM Policy Changes
┌───┬────────────────────────────────────┬────────────────────────────────────┐
│   │ Resource                           │ Managed Policy ARN                 │
├───┼────────────────────────────────────┼────────────────────────────────────┤
│ + │ ${SampleLambda/CloudWatchRole}     │ arn:${AWS::Partition}:iam::aws:pol │
│   │                                    │ icy/service-role/AmazonAPIGatewayP │
│   │                                    │ ushToCloudWatchLogs                │
├───┼────────────────────────────────────┼────────────────────────────────────┤
│ + │ ${backend/ServiceRole}             │ arn:${AWS::Partition}:iam::aws:pol │
│   │                                    │ icy/service-role/AWSLambdaBasicExe │
│   │                                    │ cutionRole                         │
└───┴────────────────────────────────────┴────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Do you wish to deploy these changes (y/n)? y
LambdaSampleStack: deploying...
[0%] start: Publishing b9d168908db593714198e01540eb08c015540d19881e7e2b8f58191486447226:current
[100%] success: Published b9d168908db593714198e01540eb08c015540d19881e7e2b8f58191486447226:current
LambdaSampleStack: creating CloudFormation changeset...










 ✅  LambdaSampleStack

Outputs:
LambdaSampleStack.SampleLambdaEndpoint9FAA5D96 = https://q63ljmbsf4.execute-api.ap-northeast-1.amazonaws.com/prod/

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:************:stack/LambdaSampleStack/5e354230-d722-11ea-b846-0aec1eafc950
```

DynamoDBのテーブルアクセスに関するRoleが作成されていることが確認できると思います。

## テスト

デプロイ時に、API Gatewayのエンドポイントが出力されるので、それを使ってURLを発行してみます。

```bash
$ curl -X POST -H "Content-Type: application/json" -d '{"key": "demo-data"}' https://q63ljmbsf4.execute-api.ap-northeast-1.amazonaws.com/prod/ddb
{"message": "succeeded"}
```

きた！！

<img width="1410" alt="ddb.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427998/c881686b-9ca2-16c9-99da-8a6866097614.png">

完璧！！

# まとめ
個人的には、「Infrastructure As Code」という観点だと、SAMよりCDKです。
ただ、LambdaへのDynamoDBの書き込み権限の設定など、若干癖がありますね。
これは、うまい具合にハイブリッドで使用した方がいいかもしれないです。

どちらも一長一短あると思うので、ケースに応じてどちらを使うかをチョイスしていく必要がありますね。
今回は、CDKでもSAM同様のことがある程度できた、ということが検証できました。

# サンプルコードリポジトリ

https://github.com/hito-psv/cdk-demo-001