CDK と LocalStack でローカルに Lambda と DynamoDB の実行環境を構築

AWS CDK (Cloud Development Kit) を使って、ローカル環境の LocalStack に Lambda 関数と DynamoDB のテーブルを構築してみました。

下記のようなツールを使用し、CDK によるスタックと Lambda 関数ハンドラーは TypeScript で実装しました。

今回のソースは http://github.com/fits/try_samples/tree/master/blog/20210524/

1. はじめに

今回は cdk init を使わずに CDK のコードを自前で構築する事にします。

まず、TypeScript で実装するために下記モジュールを

  • typescript
  • ts-node
  • @types/node

CDK を使って Lambda と DynamoDB を定義するために下記モジュールを

TypeScript のビルド用に下記モジュールを

  • esbuild

Lambda から DynamoDB へ接続するために下記モジュールを

そして、aws-cdk を使って LocalStack に対してデプロイ等を実施するために下記モジュールを

  • aws-cdk-local

これらをインストールして package.json は以下のようになりました。

package.json
{
  "name": "cdk_localstack_sample",
  "version": "1.0.0",
  "description": "",
  "devDependencies": {
    "@types/node": "^14.17.0",
    "aws-cdk": "^1.105.0",
    "aws-cdk-local": "^1.65.4",
    "esbuild": "^0.12.1",
    "ts-node": "^9.1.1",
    "typescript": "^4.2.4"
  },
  "dependencies": {
    "@aws-cdk/aws-dynamodb": "^1.105.0",
    "@aws-cdk/aws-lambda": "^1.105.0",
    "@aws-cdk/aws-lambda-nodejs": "^1.105.0",
    "@aws-sdk/client-dynamodb": "^3.16.0"
  }
}

CDK 用の設定ファイル cdk.json はシンプルに以下のようにしました。

cdk.json
{
    "app": "npx ts-node --prefer-ts-exts app.ts"
}

2. 実装

DynamoDB のテーブルと Lambda 関数を構築するだけの簡単なスタックを定義しました。

app.ts (スタック定義)
import { App, Construct, Stack, StackProps, CfnOutput } from '@aws-cdk/core'
import * as dynamodb from '@aws-cdk/aws-dynamodb'
import * as lambda from '@aws-cdk/aws-lambda'
import { NodejsFunction } from '@aws-cdk/aws-lambda-nodejs'

class SampleStack extends Stack {
    constructor(scope: Construct, id: string, props?: StackProps) {
        super(scope, id, props)

        const table = new dynamodb.Table(this, 'Items', {
            partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING}
        })

        const func = new NodejsFunction(this, 'SampleFunc', {
            runtime: lambda.Runtime.NODEJS_14_X,
            entry: './src/handler.ts',
            environment: {
                'TABLE_NAME': table.tableName
            }
        })

        // テーブルへの書き込み権限を Lambda に付与
        table.grantWriteData(func)

        // Lambda 関数名の出力
        new CfnOutput(this, 'functionName', {
            value: func.functionName
        })
    }
}

const app = new App()

new SampleStack(app, 'SampleStack')

続いて Lambda 関数ハンドラーです。

LocalStack で実行している場合に、接続先の DynamoDB を LocalStack のものに切り替える必要があります。

また、LocalStack では Node.js ランタイムの Lambda 関数ハンドラーを docker で実行するようになっており、その際に LocalStack のホスト名は LOCALSTACK_HOSTNAME 環境変数で、ポート番号は EDGE_PORT 環境変数でそれぞれ渡されるようになっていました。

そこで、今回はこれらの環境変数の値を利用して DynamoDB の接続先を変えるようにしてみました。

src/handler.ts(Lambda 関数ハンドラー)
import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb'

const tableName = process.env.TABLE_NAME

const config = {}

if (process.env.LOCALSTACK_HOSTNAME) {
    // LocalStack の DynamoDB へ接続するための設定
    config['endpoint'] = `http://${process.env.LOCALSTACK_HOSTNAME}:${process.env.EDGE_PORT}`
}

const client = new DynamoDBClient(config)

export interface Input {
    id: string
}

export const handler = async (event: Input) => {
    const res = await client.send(new PutItemCommand({
        TableName: tableName,
        Item: {
            id: { S: event.id }
        }
    }))

    console.log(`dynamodb put-item: ${JSON.stringify(res)}`)

    return {
        statusCode: 201,
        body: {
            id: event.id
        }
    }
}

3. デプロイと動作確認

それでは、LocalStack を実行してデプロイを実施してみます。

LocalStack 実行

docker コマンドを使って LocalStack を実行します。

Node.js ランタイムの Lambda 関数を実行するために LAMBDA_EXECUTOR 環境変数の値を docker にして、/var/run/docker.sockマッピングしています。

docker で LocalStack 実行
$ docker run --rm -it -p 4566:4566 -e LAMBDA_EXECUTOR=docker -v /var/run/docker.sock:/var/run/docker.sock localstack/localstack

デプロイ

aws-cdk-local モジュールには、cdklocal コマンドという LocalStack に対してデプロイ等を実施する cdk コマンドのラッパーが用意されているので、このコマンドを使います。

cdk コマンドと同様に初回時は bootstrap を実行します。

ブートストラップ処理
$ npx cdklocal bootstrap

 ⏳  Bootstrapping environment aws://000000000000/ap-northeast-1...
CDKToolkit: creating CloudFormation changeset...
7:38:43 AM | UPDATE_IN_PROGRESS   | AWS::CloudFormation::Stack | UsePublicAccessBlockConfiguration

 ✅  Environment aws://000000000000/ap-northeast-1 bootstrapped.

続いてデプロイを実施します。

デプロイ処理
$ npx cdklocal deploy --require-approval never

Bundling asset SampleStack/SampleFunc/Code/Stage...
  ・・・
SampleStack: deploying...
・・・
SampleStack: creating CloudFormation changeset...
・・・


 ✅  SampleStack

Outputs:
SampleStack.functionName = SampleStack-lambda-c75c6ee1

Stack ARN:
arn:aws:cloudformation:us-east-1:000000000000:stack/SampleStack/c83384b9

これで DynamoDB のテーブルと Lambda 関数が作られ、Lambda の関数名は SampleStack-lambda-c75c6ee1 となりました。

動作確認

ここからは AWS CLI v2 を使って動作確認してみます。

LocalStack へ接続するには --endpoint-urlhttp://localhost:4566 を設定して aws コマンドを使います。

それでは、lambda invoke を使って SampleStack-lambda-c75c6ee1 を実行してみます。

Lambda の実行
$ aws --endpoint-url=http://localhost:4566 lambda invoke --function-name SampleStack-lambda-c75c6ee1  --payload '{"id":"id1"}' --cli-binary-format raw-in-base64-out output.json

{
    "StatusCode": 200,
    "LogResult": "",
    "ExecutedVersion": "$LATEST"
}

output.json の内容は以下のようになり、正常に実行できているようです。

output.json
{"body":{"id":"id1"},"statusCode":201}

次に、この Lambda 関数が出力した CloudWatch のログを確認してみます。 こちらも特に問題は無さそうです。

CloudWatch のログ確認
$ aws --endpoint-url=http://localhost:4566 logs tail "/aws/lambda/SampleStack-lambda-c75c6ee1"

2021-05-23T22:42:30.401000+00:00 2021/05/23/[LATEST]24101d6b START RequestId: ced21627-9622-15e9-45cd-e1c1c93c39a9 Version: $LATEST
2021-05-23T22:42:30.419000+00:00 2021/05/23/[LATEST]24101d6b 
2021-05-23T22:42:30.437000+00:00 2021/05/23/[LATEST]24101d6b 2021-05-23T22:42:28.141Z   ced21627-9622-15e9-45cd-e1c1c93c39a9    INFO    dynamodb put-item: {"$metadata":{"httpStatusCode":200,"requestId":"0f5854a3-1110-42e7-bd95-3ff8bd8bb003","attempts":1,"totalRetryDelay":0},"ConsumedCapacity":{"CapacityUnits":1,"TableName":"SampleStack-Items5C12978B-b5cec818"}}
2021-05-23T22:42:30.473000+00:00 2021/05/23/[LATEST]24101d6b END RequestId: ced21627-9622-15e9-45cd-e1c1c93c39a9
・・・

最後に、SampleStack-Items5C12978B-b5cec818 テーブルの内容も出力してみました。

DynamoDB テーブルの検索結果
$ aws --endpoint-url=http://localhost:4566 dynamodb scan --table-name SampleStack-Items5C12978B-b5cec818

{
    "Items": [
        {
            "id": {
                "S": "id1"
            }
        }
    ],
    "Count": 1,
    "ScannedCount": 1,
    "ConsumedCapacity": null
}