AWS CDK の開発環境をDevContainersで用意して、SAM とか LocalStackを使って開発する

2024/01/28に公開

以前下記の記事で Python の開発環境のテンプレートを dev conatiner 作ったので、AWS CDK でも同じように作ってみました。

今回の記事では、 dev container を使って AWS CDK や SAM 、LocalStack などを使って開発できるようにし、実際に SAM や LocalStack を使って AWS CDK による開発を実践しています。

今回試した環境

  • MacBook Air M1 (Darwin Kernel Version 23.2.0)
  • orbstack: 1.3.0_16556
  • docker: Docker version 24.0.7, build afdd53b
  • vs code: 1.85.1
  • Dev Containers: v0.327.0

先に結論

こちらのリポジトリに作った AWS CDK 開発のための dev container 環境のサンプルを用意しました。

Apple Silicon ということで、自分の勉強がてら arm64 で dev container を使えるようにしています。

AWS CDK の開発に必要な機能を設定する

以前の記事で、dev container をどのように設定していくか少し話しています。

なので、設定の仕方は端折って、どんな設定をしておけば AWS CDK が開発しやすいか設定した理由を書いてます。

devcontainer.json
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
{
  "name": "AWS CDK & SAM & LocalStack",
  "build": {
    "dockerfile": "Dockerfile"
  },
  "mounts": [
    {"source": "${localEnv:HOME}/.aws", "target": "/home/node/.aws","type": "bind"}
  ],
  "runArgs": ["--platform=linux/amd64"],
  "features": {
    "ghcr.io/devcontainers/features/aws-cli:1": {},
    "ghcr.io/devcontainers-contrib/features/amplify-cli:2": {},
    "ghcr.io/devcontainers-contrib/features/aws-cdk:2": {},
    "ghcr.io/devcontainers-contrib/features/aws-eb-cli:1": {},
    "ghcr.io/devcontainers-contrib/features/localstack:2": {},
    "ghcr.io/eitsupi/devcontainer-features/jq-likes:2": {},
    "ghcr.io/customink/codespaces-features/sam-cli:1": {}
  },
  "postCreateCommand": "pip install awscli-local git-remote-codecommit; npm install -g aws-cdk-local"
}

開発環境の docker image について

AWS CDK は TypeScript で開発するのが良いと思っているので、mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye の image を使います。

通常は、.devcontainer.json に利用する image を直接記述してしまって問題ないのですが、自分は Apple Silicon の環境なので、--platform の設定をしています。

Dockerfile
FROM --platform=linux/amd64 mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye
devcontainer.json
{
  "runArgs": ["--platform=linux/amd64"],
}

Features による機能追加

以前の記事でも少し触れていますが、こちらにある通り、簡単に dev container に機能を追加できます。

ので、 AWS CDK の開発にこれもあったほうがいいんじゃない?みたいな機能を追加します。

  • ghcr.io/devcontainers/features/aws-cli:1
    • 概要: AWS CLI を使えるようにする
    • 理由: AWS CDK で作ったリソースをちゃんとデプロイできるようにしておきたいから
  • ghcr.io/devcontainers/features/docker-outside-of-docker:1
    • 概要: dev container で Docker out side of Docker を使う
    • 理由: SAM を使うとき裏で Docker が動くので (LocalStack も自動的に Docker mode になる)
  • ghcr.io/devcontainers-contrib/features/amplify-cli:2
    • 概要: Amplify を使えるようにする
    • 理由: Amplify で AppSync の GraphQL API を作って AWS CDK 側で管理できるようにしておきたいから
  • ghcr.io/devcontainers-contrib/features/aws-cdk:2
    • 概要: CDK を使えるようにする (npx でもいいんですけどね。。。)
    • 理由: 当然
  • ghcr.io/devcontainers-contrib/features/localstack:2
    • 概要: LocalStack を使えるようにする
    • 理由: CDK を実際の AWS 環境を使わずに開発したいから
  • ghcr.io/eitsupi/devcontainer-features/jq-likes:2
    • 概要: jq,yq を使えるようにする
    • 理由: AWS CLI とか CDK で export した value を整形して使う場面があるから
  • ghcr.io/customink/codespaces-features/sam-cli:1
    • 概要: SAM CLI を使えるようにする
    • 理由: lambda とか簡単にテンプレートを作ったりして AWS CDK 側で管理できるようにしておきたいから
  • ghcr.io/devcontainers-contrib/features/aws-eb-cli:1
    • 概要: elastic beanstalk の CLI
    • 理由: 使う。。かも?(いや本当に使うか?)

その他の設定

Features による機能追加以外にも、入れておくべき便利ツールがあるので、そちらについては dev container のビルド時にインストールしています。

また、ローカル側の AWS Credential をマウントしてデプロイできるようにしておきます。

devcontainer.json
{
  "mounts": [
    {"source": "${localEnv:HOME}/.aws", "target": "/home/node/.aws","type": "bind"}
  ],
  "postCreateCommand": "pip install awscli-local git-remote-codecommit; npm install -g aws-cdk-local"
}

実際に使えるかユースケースで試してみる

この dev container の環境を作っただけで終わりたくないので、ちゃんと使えるようねということを入れたツールを使いつつ実践してみます。

お題

dev container でどんなものを作るか、そのお題は下記のとおりです。

S3 に配置しているデータを読み取って内容を DynamoDB に入れる Lambda を作成して、API Gateway から叩く。

というのを試します。

開発環境としては下記の機能を使って実践します。

  • SAM CLI
    • Lambda の作成や Event をテストする
  • LocalStack
    • 今回のお題の環境が CDK でデプロイできるか LocalStack で動作確認する
  • AWS CLI
    • 実際に dev container から作り上げた CDK プロジェクトがデプロイできるか確認する

SAM で Lambdaを作る

いつか SAM と CDK が本当の意味で共存できる日を夢見ていますが、今回は SAM を Lambda を作ったりテストしたりするためだけに使います。

sam init --runtime python3.11 で SAM による Lambda のプロジェクトテンプレートを作ります。最初の選択と最後の Project Name 以外はすべてデフォルトで作っています。

node ➜ /workspaces/aws-cdk-devcontainer-sample (main) $ sam init --runtime python3.11
Which template source would you like to use?
        1 - AWS Quick Start Templates
        2 - Custom Template Location
Choice: 1

Choose an AWS Quick Start application template
        1 - Hello World Example
        2 - Hello World Example With Powertools for AWS Lambda
        3 - Infrastructure event management
        4 - Multi-step workflow
        5 - Lambda EFS example
        6 - Serverless Connector Hello World Example
        7 - Multi-step workflow with Connectors
Template: 1
.
.
Project name [sam-app]: sam-lambda-dynamodb

プロジェクトが出来上がっているので、 sam-lambda-dynamodb に移動して sam local invoke で実行できるか確認します。

node ➜ /workspaces/aws-cdk-devcontainer-sample/sam-lambda-dynamodb (main) $ sam local invoke
Invoking app.lambda_handler (python3.11)
.
.
START RequestId: 822b15c5-1f5e-4509-92e5-2cdf6b0f179b Version: $LATEST
END RequestId: a7a12b44-2afd-4baa-9fdd-969c771b2ac3
REPORT RequestId: a7a12b44-2afd-4baa-9fdd-969c771b2ac3  Init Duration: 0.49 ms  Duration: 192.31 ms     Billed Duration: 193 ms Memory Size: 128 MB     Max Memory Used: 128 MB
{"statusCode": 200, "body": "{\"message\": \"hello world\"}"}

ということで、 Lambda の開発が簡単に進められるようになりました。

S3 と Lambda と DynamoDB を CDK て定義する

リソースを定義しておきます。

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

    const bucket = new s3.Bucket(this, 'DataBucket');

    const putDynamoDBFunc = new PythonFunction(this, 'PutDynamoDBFunc', {
      entry: 'sam-lambda-dynamodb/hello_world',
      runtime: lambda.Runtime.PYTHON_3_11,
      index: 'app.py',
      handler: 'lambda_handler',
    });

    bucket.grantRead(putDynamoDBFunc);

    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    const testApiGW = new apigw.LambdaRestApi(this, 'TestApiGW', {
      handler: putDynamoDBFunc,
    });

    const testTable = new dynamodb.TableV2(this, 'TestTable', {
      partitionKey: {
        name: 'MainTestKey',
        type: dynamodb.AttributeType.STRING,
      },
    });

    testTable.grantWriteData(putDynamoDBFunc);
    putDynamoDBFunc.addEnvironment('BUCKET_NAME', bucket.bucketName);
    putDynamoDBFunc.addEnvironment('DYNAMODB_TABLE_NAME', testTable.tableName);
    putDynamoDBFunc.addEnvironment('READ_DATA_KEY', 'test.dat');
  }
}

SAM を使って Lambda を作り込んでいく

再び、 sam-lambda-dynamodb のディレクトリでの作業になります。

SAM のテンプレートでは API Gateway で Lambda が invoke されるようになっているので、S3 trigger + DynamoDB が SAM で開発できるようにします。

また、SAM では pytest を使った unit test や integration test が可能なので、そちらを利用してなるべく SAM 上で Lambda 単体の開発後 CDK でのデプロイ作業に入ります(今回は unit test だけとします)。

SAM project の README.md にテストのやり方が書いてあるので試しに実行します。

Tests

Tests are defined in the tests folder in this project. Use PIP to install the test dependencies and run tests.

sam-lambda-dynamodb$ pip install -r tests/requirements.txt --user
# unit test
sam-lambda-dynamodb$ python -m pytest tests/unit -v

その後、 sam-lambda-dynamodb/hello_world/app.py について、S3 からデータを読み込んで DynamoDB Table へ Put するコードに修正します。

S3 に置かれる想定のデータは下記の感じです。このデータの key, value がそれぞれ DynamoDB の Table に Put され、成功の場合は Success!, 失敗の場合は Failed! が返却されるように実装します。

testkey1,testvalue1
testkey2,testvalue2

サンプルコードは下記のとおりで、詳細を知りたい方は リポジトリを参照してください。

app.py
def lambda_handler(event, context):
    try:
        bucket = os.environ["BUCKET_NAME"]
        key = os.environ["READ_DATA_KEY"]
        response = s3.get_object(Bucket=bucket, Key=key)
        body = response["Body"].read().decode()
        lines = body.split("\n")
        for line in lines:
            l = line.split(",")
            if len(l) ==  2:
                dynamodb.put_item(TableName=os.environ["DYNAMODB_TABLE_NAME"],
                    Item = {
                        "MainTestKey": {
                            "S": l[0]
                        },
                        "SubTestKey": {
                            "S": l[1]
                        }
                    }
                )
        return {
            "statusCode": 200,
            "body": json.dumps({
                "message": "Success!",
            }),
        }
    except:
        return {
            "statusCode": 404,
            "body": json.dumps({
                "message": "Failed!",
            }),
        }

テストについては moto を使った単体テストを実装していますが、記事が長くなってしまうので省略します。気力があれば別でまとめます。

README.md に従って pytest を実行します。

node ➜ /workspaces/aws-cdk-devcontainer-sample/sam-lambda-dynamodb (main) $ python -m pytest tests/unit -v
====================================== test session starts ======================================
platform linux -- Python 3.10.8, pytest-7.4.4, pluggy-1.4.0 -- /usr/local/python/current/bin/python
cachedir: .pytest_cache
rootdir: /workspaces/aws-cdk-devcontainer-sample/sam-lambda-dynamodb
plugins: mock-3.12.0
collected 2 items

tests/unit/test_handler.py::test_lambda_handler_success PASSED                            [ 50%]
tests/unit/test_handler.py::test_lambda_handler_failed PASSED                             [100%]

======================================= 2 passed in 0.81s ======================================

ひとまずこれで、 SAM を使った Lambda が完成したので、 LocalStack を使って動作確認を行っていきます。

LocalStack へのデプロイと動作確認

localstack start で LocalStack のコンテナを起動します。

node ➜ /workspaces/aws-cdk-devcontainer-sample/sam-lambda-dynamodb (main) $ localstack start

     __                     _______ __             __
    / /   ____  _________ _/ / ___// /_____ ______/ /__
   / /   / __ \/ ___/ __ `/ /\__ \/ __/ __ `/ ___/ //_/
  / /___/ /_/ / /__/ /_/ / /___/ / /_/ /_/ / /__/ ,<
 /_____/\____/\___/\__,_/_//____/\__/\__,_/\___/_/|_|

 💻 LocalStack CLI 3.0.2

[04:29:00] starting LocalStack in Docker mode 🐳         localstack.py:495
2024-01-27T04:29:32.667  WARN --- [-functhread2] l.u.c.container_client     : Host part of port mappings are ignored currently in additional flags
2024-01-27T04:29:32.668  WARN --- [-functhread2] l.u.c.container_client     : Host part of port mappings are ignored currently in additional flags

続いて、cdklocal bootstrap で CDK の環境が LocalStack で使えるようにします。

node ➜ /workspaces/aws-cdk-devcontainer-sample (main) $ cdklocal bootstrap
 ⏳  Bootstrapping environment aws://000000000000/ap-northeast-1...
Trusted accounts for deployment: (none)
Trusted accounts for lookup: (none)
Using default execution policy of 'arn:aws:iam::aws:policy/AdministratorAccess'. Pass '--cloudformation-execution-policies' to customize.
CDKToolkit: creating CloudFormation changeset...
 ✅  Environment aws://000000000000/ap-northeast-1 bootstrapped.

あとはそのまま cdklocal deploy でデプロイ完了です。デプロイ後に Output として表示される API Gateway の Endpoint URL を控えておきます。

 ✅  AwsCdkProjectTemplateStack

✨  Deployment time: 15.24s

Outputs:
AwsCdkProjectTemplateStack.TestApiGWEndpointF4D06F73 = https://i47kuuzeyx.execute-api.localhost.localstack.cloud:4566/prod/
Stack ARN:
arn:aws:cloudformation:ap-northeast-1:000000000000:stack/AwsCdkProjectTemplateStack/8e09e173

✨  Total time: 30.2s

それでは動作確認します。

デプロイされた S3 のバケットにデータを Put するために、バケット名を調べてテストデータを upload します。

node ➜ /workspaces/aws-cdk-devcontainer-sample (main) $ awslocal s3 ls
2024-01-23 13:46:34 cdk-hnb659fds-assets-000000000000-ap-northeast-1
2024-01-23 13:46:53 awscdkprojecttemplatestack-databuckete3889a50-10111fc7
node ➜ /workspaces/aws-cdk-devcontainer-sample (main) $ awslocal s3 cp ./sam-lambda-dynamodb/tests/unit/test.dat s3://awscdkprojecttemplatestack-databuckete3889a50-10111fc7
upload: sam-lambda-dynamodb/tests/unit/test.dat to s3://awscdkprojecttemplatestack-databuckete3889a50-10111fc7/test.dat

続いて DynamoDB のテーブルに何も登録されていないことを確認します。

node ➜ /workspaces/aws-cdk-devcontainer-sample (main) $ awslocal dynamodb list-tables
{
    "TableNames": [
        "AwsCdkProjectTemplateStack-TestTable5769773A-7f1f3e34",
        "AwsCdkProjectTemplateStack-TestTable5769773A-7f1f3e34"
    ]
}
node ➜ /workspaces/aws-cdk-devcontainer-sample (main) $ awslocal dynamodb scan --table-name AwsCdkProjectTemplateStack-TestTable5769773A-7f1f3e34
{
    "Items": [],
    "Count": 0,
    "ScannedCount": 0,
    "ConsumedCapacity": null
}

それでは、控えておいた Endpoint URL にリクエストを飛ばして Lambda を起動させましょう。

node ➜ /workspaces/aws-cdk-devcontainer-sample (main) $ curl https://i47kuuzeyx.execute-api.localhost.localstack.cloud:4566/prod/
{"message": "Success!"}

無事成功したので、最後に test.dat の内容が DynamoDB のテーブルに挿入されているか確認します。

node ➜ /workspaces/aws-cdk-devcontainer-sample (main) $ awslocal dynamodb scan --table-name AwsCdkProjectTemplateStack-TestTable5
769773A-7f1f3e34
{
    "Items": [
        {
            "SubTestKey": {
                "S": "testvalue2"
            },
            "MainTestKey": {
                "S": "testkey2"
            }
        },
        {
            "SubTestKey": {
                "S": "testvalue1"
            },
            "MainTestKey": {
                "S": "testkey1"
            }
        }
    ],
    "Count": 2,
    "ScannedCount": 2,
    "ConsumedCapacity": null
}

ということで、test.dat の内容が見事 LocalStack で反映されていました。

最後に実際にデプロイできるか確認

実際にここまで作ったものをデプロイして正しく動作できるかも確認します。

デプロイするアカウントの適切な Credential を設定して、npx cdk deploy して実際にデプロイされているか確認します。

適当に S3 見てみるとバケットが作成されているので、SAM 開発で使用した test.dat を配置します。

この状態で DynamoDB のテーブルにはデータがありません。

そして API に対して curl して DynamoDB のテーブルを確認すると、、、

見事 test.dat に記載のデータが AWS コンソールから確認でき、 dev container 上の SAM と LocalStack で AWS CDK 開発ができました。

npx cdk destroy での後片付けも忘れずに。

終わりに

dev container を使って AWS CDK の開発環境を用意して、実際に開発ができるかユースケースを通して確認しました。

実は元々 S3 Notification Trigger で Lambda を動かそうとしていたのですが、 LocalStack 側が free 版では対応していなかったので、今回はちょっとよくわからない実装になっています。

LocalStack や SAM を使っていますが、 CodeCommit なども使っての開発例とかも簡単だから試せばよかったかなと今更思います。

せっかく個人的には満足できる環境ができたので、アプリ開発も想定に含めた拡張機能を今後も追加できたらな〜。

最低限 Python の環境くらいは VS Code で開発できる環境を目指したいところです。

Discussion