AWS CDKで定義したリソースをローカル環境で実行してみた

2019.10.25

こんにちは。プロダクトグループのさかいゅです。
CDK(Cloud Development Kit)で実装したリソースをローカル環境で実行できるように、ローカル開発環境について調査しました。 公式ドキュメントにSAM CLIを利用してローカル環境で実行する例を参考にローカル開発環境を構築してみましたので、紹介させていただきます。

バージョン情報

  • AWS CLI 1.16.220
  • CDK 1.13.1
  • SAM CLI 0.22.0
  • Docker 19.03.2
  • Visual Studio Code 1.39.2

概要

CDKで実装した、API Gateway + Lambda + DynamoDBでサーバーレスな構成のWebAPIをSAM CLIDynamoDB Localを利用してローカル開発環境を作成します。

プロジェクトの作成

CDKのプロジェクトを作成し、必要なパッケージをインストールします。

# プロジェクトを作成
cdk init --language typescript

# 必要なパッケージをインストール
npm install --save @aws-cdk/aws-lambda @aws-cdk/aws-apigateway @aws-cdk/aws-dynamodb
npm install --save-dev @types/node

リソースの定義

今回利用するリソースとDynamoDBにアクセスする単純なLambda関数を実装します。

lib/sample_project-stack.ts

import cdk = require("@aws-cdk/core");
import lambda = require("@aws-cdk/aws-lambda");
import apigateway = require("@aws-cdk/aws-apigateway");
import dynamodb = require("@aws-cdk/aws-dynamodb");

export class SampleProjectStack extends cdk.Stack {
    constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);
        const env = process.env.ENV || "local";

        // DynamoDBテーブル
        const sampleTable = new dynamodb.Table(this, "sampleTable", {
            tableName: "samples",
            billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
            partitionKey: { name: "id", type: dynamodb.AttributeType.STRING }
        });

        // Lambda関数
        const sampleLambda = new lambda.Function(this, "sampleLambda", {
            runtime: lambda.Runtime.NODEJS_8_10,
            handler: "index.handler",
            code: lambda.Code.asset("src/lambda/sample"),
            timeout: cdk.Duration.seconds(60),
            environment: {
                ENV: env
            }
        });
        sampleTable.grantReadWriteData(sampleLambda);

        // API Gateway
        const sampleApi = new apigateway.RestApi(this, "sampleApi", {
            restApiName: "sampleApi"
        });

        const samplesResource = sampleApi.root.addResource("samples");
        const getSamplesIntegration = new apigateway.LambdaIntegration(sampleLambda);
        samplesResource.addMethod("GET", getSamplesIntegration);
    }
}

src/lambda/sample/index.js

const AWS = require("aws-sdk");

exports.handler = async event => {
    var dynamodbOption = {};
    if (process.env.ENV === "local") {
        dynamodbOption = {
            endpoint: "http://dynamodb:8000",
            region: "local",
            accessKeyId: "local",
            secretAccessKey: "local"
        };
    }
    var docClient = new AWS.DynamoDB.DocumentClient(dynamodbOption);
    var params = {
        TableName: "samples"
    };
    var scanResult = await docClient.scan(params).promise();

    var responseBody = {
        samples: scanResult.Items
    };

    const response = {
        statusCode: 200,
        body: JSON.stringify(responseBody)
    };
    return response;
};

デプロイ

TypeScriptをコンパイルして、デプロイコマンドを実行することで、AWSにデプロイすることができます。Lambda関数内で、ENVを参照しlocalの場合、DynamoDB Localのエンドポイントを指定するように実装していますので、AWSにデプロイする時は、local以外の値を指定します。

npm run build
ENV=dev cdk deploy

ローカル実行

ここまでで、リソースをAWSにデプロイすることができました。ここから、ローカル環境で実行できるように設定していきます。

Dockerのネットワーク設定

SAM CLI、DynamoDB Localのいずれも、Dockerコンテナで実行されます。Dockerコンテナで実行しているLambdaからDockerコンテナで実行しているDynamoDB Localに接続するために、同じネットワーク設定を利用する必要があります。今回はsam-cliという名前で作成しました。(任意の名前でOKです)

docker network create sam-cli

DynamoDB Local

DynamoDB Localは、公式のDockerイメージがありますので、Dockerを使って構築します。デフォルトで起動するとInMemoryで起動されるためコンテナを停止すると、テーブル定義やデータが消えてしまいます。毎回テーブルの作成やデータを入れ直すのは効率が悪いので、今回はデータを永続化するように設定しました。

docker-compose.yml

version: "3.7"
services:
    dynamodb:
        image: amazon/dynamodb-local
        container_name: dynamodb
        ports:
            - 8000:8000
        command: -jar DynamoDBLocal.jar -dbPath /data
        volumes:
            - $PWD/dynamodb/data:/data
        networks:
            - sam-cli
networks:
    sam-cli:
        external: true
docker-compose up -d

テーブルの作成とデータの作成

サンプルテーブルと適当なデータをDynamoDB Localに作成します。

samples.json

{
    "AttributeDefinitions": [
        {
            "AttributeName": "Id",
            "AttributeType": "S"
        }
    ],
    "TableName": "samples",
    "KeySchema": [
        {
            "AttributeName": "Id",
            "KeyType": "HASH"
        }
    ],
    "ProvisionedThroughput": {
        "ReadCapacityUnits": 1,
        "WriteCapacityUnits": 1
    }
}

samples_data.json

{
    "samples": [
        {
            "PutRequest": {
                "Item": {
                    "Id": { "S": "sample1" },
                    "Name": { "S": "HogeHoge" }
                }
            }
        },
        {
            "PutRequest": {
                "Item": {
                    "Id": { "S": "sample2" },
                    "Name": { "S": "FooFoo" }
                }
            }
        }
    ]
}

DynamoDB Localは、ACCESS_KEY_IDやリージョン名を利用して、ファイルを保存するようなので、テーブルを作成する前に、環境変数を設定しておきます。(リージョン名はプログラムの指定と合っていれば何でもOKです)

export AWS_SECRET_ACCESS_KEY=local
export AWS_ACCESS_KEY_ID=local
export AWS_DEFAULT_REGION=local

aws dynamodb create-table --endpoint-url http://localhost:8000 --cli-input-json file://dynamodb/samples.json
aws dynamodb batch-write-item --endpoint-url http://localhost:8000 --request-items file://dynamodb/samples_data.json

SAM CLI

cdk synthコマンドでCloudFormationのテンプレートファイルを作成することができます。ここは、公式ドキュメントに記載されている通りに進めます。

cdk synth --no-staging > template.yaml

API Gatewayを実行

生成したCloudFormationのテンプレートファイルを利用して、APIを実行します。DynamoDB Localに接続する処理を実行する場合は、--docker-networkオプションを指定してください。

$ sam local start-api -t template.yaml --docker-network sam-cli
Mounting sampleLambdaXXXXX at http://127.0.0.1:3000/samples [GET]
You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
2019-10-25 00:31:58  * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)

もちろんブラウザでアクセスすることができます。

Lambda関数を実行

Lambda関数を直接実行することもできます。API Gateway実行時と同様に、必要に応じて--docker-networkオプションを指定してください。また、Lambdaの関数名はtemplate.yamlを参照して設定してください。

template.yaml

…
  sampleLambdaXXXXX:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket:
…
$ sam local invoke sampleLambdaXXXXX --no-event --docker-network sam-cli
Invoking index.handler (nodejs8.10)
2019-10-25 00:42:50 Found credentials in environment variables.

Fetching lambci/lambda:nodejs8.10 Docker container image......
Mounting /XXXX/XXXX/SampleProject/src/lambda/sample as /var/task:ro,delegated inside runtime container
START RequestId: XXXX-YYYY Version: $LATEST
END RequestId: XXXX-YYYY
REPORT RequestId: XXXX-YYYY  Duration: 297.76 ms     Billed Duration: 300 ms   Memory Size: 128 MB     Max Memory Used: 44 MB

{"statusCode":200,"body":"{\"samples\":[{\"Id\":\"sample1\",\"Name\":\"HogeHoge\"},{\"Id\":\"sample2\",\"Name\":\"FooFoo\"}]}"}

デバッグ

SAM CLIを利用してLambda関数をデバッグすることができます。今回はVisual Studio Codeを利用して、デバッグしてみます。
デバッグ設定を作成して、--debug-portを指定してLambda関数を実行します。

launch.json

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "CDK + SAM CLIでデバッグ",
            "type": "node",
            "request": "attach",
            "address": "localhost",
            "port": 5858,
            "localRoot": "${workspaceRoot}/src/lambda/sample",
            "remoteRoot": "/var/task",
            "protocol": "inspector",
            "stopOnEntry": false
        }
    ]
}
sam local invoke sampleLambdaXXXXX --no-event --docker-network sam-cli --debug-port 5858

ブレークポイントで止まりました。成功ですね。通常のデバッグと同様に変数の中身を確認したり、ステップ実行をすることができます。

注意事項

デバッグを実行する際に、ランタイムの設定がNode.jsの10系だとaws-sdkモジュールがImportModuleErrorとなり実行できませんでした。なんらかの回避策があるかもしれませんが、今回はNode.jsの8系で実行しています。

さいごに

テストはAWSにデプロイして実施するべきですが、デプロイする前の動作確認には十分使える環境だと思いました。 AWSリソースをローカルで実行する方法の1つとして参考になれば幸いです。