CDKで作ったLambdaをremote-invokeでも動かす
みなさん、こんにちは。horsewinです。 最近というか2023年はBlogを全然書けていなかったことに愕然としています。
AWS CDK Advent Calendar 2023 10日目の記事となります。
AWS CDK(以降、CDK)で作成したLambdaやStep Functionsを自分のローカル環境で検証するにはいくつかの方法があります。そのうち、筆者が好きな方法は実際の動作と近い形で検証する方法です。具体的には、AWS Serverless Application Model(以降、SAM)のinvoke機能を利用します。 SAMのinvokeにはlocalとremoteがありますが、今回はremote(以降、remote-invoke)を上手く活用する方法について言及します。
CDK本線の機能ではないですが、CDKのコードと近い場所でアプリケーション(特にLambda)を作る際に動作検証はみんなどうやっているんだろう?と疑問になり、1つのプラクティスとして紹介しようと思った次第です。
今回触れること、触れないこと
次の内容について、本エントリで述べていきます。
触れること
- CDKとLambda開発の相性の良さについて
- Lambdaの動作検証方法について
触れないこと
では、本編に進んでいきましょう。
CDKにおけるLambda開発の手軽さ
CDKでは、インフラストラクチャを作るコード(以降、IaCコード)とLambdaなどのアプリケーションコードを次のように同居させます。
次の例はcdk init
で作成したディレクトリ構成にLambdaコードを同居させた例です。
通常のIaCコードの中にlambda
ディレクトリを作成して、その下にアプリケーションのコードを置きます。
❯ tree -L 2 -I node_modules . . ├── README.md ├── bin │ └── cdk2023advent.ts ├── cdk.json ├── cdk.out ├── jest.config.js ├── lambda │ └── hello.js ├── lib │ └── cdk2023advent-stack.ts ├── package-lock.json ├── package.json ├── test │ └── cdk2023advent.test.ts └── tsconfig.json
シンプルなコードの場合、ローカルで開発したLambdaのコードをそのままコピーして、AWSマネジメントコンソールのLambda上のGUIへ直接貼り付けて、コード更新も可能です。 一方、アプリケーションの中で外部モジュールを利用する場合、コードアーティファクトをZip圧縮してからアップロードするなど手間がかかります。 そのような手間があるため、Lambdaを開発/デプロイするフレームワークや手法がCDK以外にもいくつかあります。
- AWS CLIを用いて直接アップロードする
- SAMを利用してデプロイする
- Serverless Frameworkを利用してデプロイする
- Chaliceを利用してデプロイする(Pythonの場合)
筆者は意外とSAMも利用します。例えば、CDKを用いて構築していない既存のインフラに対してLambdaを含むサーバレスなアプリケーションをシンプルに展開したい場合はSAMを使うことがあります。 ただし、Node.jsランタイムのLambdaを構築する場合は、NodejsFunctionという便利なコンストラクトがあるためCDKを使います。
他のツールと比べてCDKにおけるLambda開発の手軽さはどこにあるでしょうか。大きなポイントは個人的には2つあります。
IaCコードとアプリケーションコードが近くにある
1点目は、IaCコードとアプリケーションコードを近くに配置することで、双方の状態をコンテキストスイッチを少なく閲覧できる点です。 Lambdaでアプリケーションを作るメリットは、書いたコードがすぐに動くところにあります。加えて、AWSのほかサービスとの統合が容易な点と考えています。 AWSのほかサービスとの統合をする際にIaC側で作成したリソースを活用するケースが多くあります。 IaCコードのリポジトリとアプリケーションコードのリポジトリが分断されていると、ワークスペースの行き来が発生することによるコンテキストスイッチが発生します。 これはコーディングする上で少しストレスがたまります。できるだけIaCコードとアプリケーションコードが近い場所にあることで双方が閲覧しやすいのがメリットです*1。 また、双方のコードが近くにあることでインフラ見ているメンバとアプリケーションを見ているメンバの会話がしやすくなるといったメリットもあります。
cdk deploy
によるシームレスなデプロイ
CDKプロジェクト下にあるlambdaはIaCのデプロイと同じライフサイクルでデプロイができます。 IaC側をデプロイして、次にアプリケーションコードをデプロイするという順番を踏まずともシームレスにデプロイができるのは開発者体験としては非常に良いです。
CDKプロジェクト下で開発したアプリケーションの動作検証
今回の本題です。アプリケーションを作成した後は動作検証をしなければいけません。書いたコードが一発で動くというのは幻想です(筆者の感想です)。 では、CDKプロジェクト下で書いたLambdaはどうやって動作検証するのでしょうか。
クラウド上のLambdaを更新してLambdaのコンソール上から実行をするのが一番シンプルではあります。
しかし、この方法の場合、Lambdaを更新(cdk deploy
)→ブラウザに切り替えて画面を更新→Lambda実行という流れになり、ガンガンコンテキストスイッチが入ります。修正が頻繁に発生するケースでは、この方法ではしんどいものがあります。
Lambdaの動作検証にはコンソール実行も含めて次のケースがあります(前後で連携するAWSサービスによっては選択できないものあり)。
- Lambdaコンソールから実行する
node
やts-node
によってLambdaハンドラを動かす- AWS CLIでLambda invokeコマンドを実行する
- SAMの
sam local invoke
で実行する(以降、local-invoke) - SAMの
sam remote invoke
で実行する - その他フレームワークでサポートするコマンド(serverlessであれば、
serverless invoke
)
数年前は1〜3ポチ目で実行をするケースが多かった気がします。 最近は、SAMのドキュメントにもCDKアプリケーションをローカルでテストするという内容が追加されており、SAMを利用する例が多くなっている感覚があります。 docs.aws.amazon.com
sam local invoke
名前の通り、AWS側にあるLambdaではなく、ローカルのコードを元にLambdaを擬似的に実行します。 仕組みとしては、Lambdaランタイムが動作するDockerコンテナの内部でビルドしたLambda関数を動作させています。図にすると次の通りです。
次のCDKのコードがあるとします。
import * as cdk from "aws-cdk-lib"; import { Construct } from "constructs"; import * as lambda from "aws-cdk-lib/aws-lambda"; export class Cdk2023AdventStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); new lambda.Function(this, "HelloHandler", { functionName: "cdk2023advent", runtime: lambda.Runtime.NODEJS_18_X, code: lambda.Code.fromAsset("lambda"), handler: "hello.handler" }); } }
Lambdaのコードは次のように非常にシンプルです。
exports.handler = async function (event) { console.log("request:", JSON.stringify(event, undefined, 2)); return { statusCode: 200, headers: { "Content-Type": "text/plain" }, body: `Hello 2023's Advent calendar!\n` }; };
CDK下のLambdaである場合、cdk synth
によって生成されたCloudFormationテンプレートをInputとして、Lambdaのどのランタイムを動作させるかを指定します。
そのため事前にcdk synth
やcdk deploy
によって、CloudFormationテンプレートを作成しておきます。
❯ cdk synth # -tにテンプレート名を指定、その後にCDKコードで与えたIDを指定 ❯ sam local invoke -t ./cdk.out/Cdk2023AdventStack.template.json HelloHandler Invoking hello.handler (nodejs18.x) Local image is up-to-date Using local image: public.ecr.aws/lambda/nodejs:18-rapid-x86_64. Mounting /Users/uma/xxx/cdk.out/asset.8d6cbc7038fd2b5b34d654a49fdd3f4936bbae6aeae4657a2a15212537ac1977 as /var/task:ro,delegated, inside runtime container START RequestId: 99e2c900-f883-4876-a021-355caa6246d4 Version: $LATEST 2023-12-09T14:15:54.108Z 99e2c900-f883-4876-a021-355caa6246d4 INFO request: {} END RequestId: 99e2c900-f883-4876-a021-355caa6246d4 REPORT RequestId: 99e2c900-f883-4876-a021-355caa6246d4 Init Duration: 1.17 ms Duration: 1274.96 ms Billed Duration: 1275 ms Memory Size: 128 MB Max Memory Used: 128 MB {"statusCode": 200, "headers": {"Content-Type": "text/plain"}, "body": "Hello 2023's Advent calendar!\n"}
sam remote invoke
Lambdaだけで閉じるコードであれば、で十分です。 一方、Lambdaだけで閉じるコードは意外と少なく、DynamoDBにデータを書き込み、SQSにデータを書き込みなど他のAWSサービスと連携させるケースが多々あります。 ローカル端末にAWSサービスへの操作権限を持つクレデンシャルを持っている場合、local-invokeでも他サービスと連携ができますが、厳密な動作検証とはなりません。 やはり、Lambdaに付与したIAMロールを用いてサービス間連携まで含めた動作検証をしたいところです。
SAMのCLIでは、local
以外にremote
のinvokeがあります*2。
これはその名の通り、remote=クラウド上にアップロードされたLambdaを起動させるためのコマンドです。
Dry-runや非同期呼び出しなど複数のモードもサポートしており、オプションも豊富です。 冒頭で述べた通り、筆者が好きな方法は「実際の動作と近い形で検証する方法」であるため、ちょっとした修正の場合はこちらの動作検証を使いたくなります。
一方、SAMではlocal-invokeと同じメンタルモデルでremote-invokeを利用できますが、CDKの場合は少し工夫が必要となります。 remote-invokeを試してみましょう。
❯ sam remote invoke --stack-name Cdk2023AdventStack Invoking Lambda Function HelloHandler2E4FBA4D {"statusCode":200,"headers":{"Content-Type":"text/plain"},"body":"Hello 2023's Advent calendar!\n"}START RequestId: 1ea7fc42-abab-463d-9684-62ba77771a02 Version: $LATEST 2023-12-09T14:40:01.099Z 1ea7fc42-abab-463d-9684-62ba77771a02 INFO request: {} END RequestId: 1ea7fc42-abab-463d-9684-62ba77771a02 REPORT RequestId: 1ea7fc42-abab-463d-9684-62ba77771a02 Duration: 18.66 ms Billed Duration: 19 ms Memory Size: 128 MB Max Memory Used: 67 MB
Lambda関数が1つしかない場合は普通に動きますね。 もう1つ、Lambda関数を追加してみましょう。
import * as cdk from "aws-cdk-lib"; import { Construct } from "constructs"; import * as lambda from "aws-cdk-lib/aws-lambda"; export class Cdk2023AdventStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); new lambda.Function(this, "HelloHandler", { functionName: "cdk2023advent", runtime: lambda.Runtime.NODEJS_18_X, code: lambda.Code.fromAsset("lambda"), handler: "hello.handler" }); + new lambda.Function(this, "HelloHandler2", { + functionName: "cdk2023advent2", + runtime: lambda.Runtime.NODEJS_18_X, + code: lambda.Code.fromAsset("lambda"), + handler: "hello.handler" }); } }
cdk deploy
後、再度remote-invokeを実行するとエラーとなります。
# 実行対象が見つけられずにエラーとなる ❯ sam remote invoke --stack-name Cdk2023AdventStack Error: Cdk2023AdventStack contains more than one resource that could be used with remote invoke, please provide resource_id argument to resolve ambiguity.
実行対象のLambdaを特定するためには、「Lambdaリソースの論理 ID またはリソース ARN」が必要となります。 SAMの場合、CloudFormationテンプレートに相当するSAMテンプレートは開発者自身が作成します。そのため、論理IDに直感的な名前(例えば、HelloHandler)をつけることで直感的に動かすことができます。
一方、CDKの場合はCloudFormationテンプレートはcdk synth
で自動生成され、論理IDはCDKにおまかせした値になります。今回の例では、1つ目のLambdaには「HelloHandler2E4FBA4D」という論理IDが付与されています。
この値を指定することで、remote-invokeを動かすことができます。
❯ sam remote invoke --stack-name Cdk2023AdventStack HelloHandler2E4FBA4D Invoking Lambda Function HelloHandler2E4FBA4D START RequestId: ba09d479-0991-4ca8-a918-053d77db9550 Version: $LATEST 2023-12-09T14:52:30.683Z ba09d479-0991-4ca8-a918-053d77db9550 INFO request: {} END RequestId: ba09d479-0991-4ca8-a918-053d77db9550 REPORT RequestId: ba09d479-0991-4ca8-a918-053d77db9550 Duration: 3.06 ms Billed Duration: 4 ms Memory Size: 128 MB Max Memory Used: 67 MB Init Duration: 166.06 ms {"statusCode":200,"headers":{"Content-Type":"text/plain"},"body":"Hello 2023's Advent calendar!\n"}%
論理IDを調べて指定するのはつらい
と、めんどくさがりなわたしが言っています。 もう少しいい方法がないものかと考えていたのですが、結論としてあまりいい方法はなかったです。
現状、取りうる代替策としては、次の方法ぐらいかなという状態です。
jq
やyq
コマンドでCloudFormationテンプレートから論理IDを引っこ抜くfunctionName
プロパティを元にARNを直接指定する
1はお世辞にも直感的なコマンドにはならないため、筆者は2の方法で試すことが多いです。ただしLambda関数を作成する際に関数名もCDKにおまかせしている場合は、1の手段が必要となります。
2の手段としては次のような使い道になります。
❯ FN=cdk2023advent; sam remote invoke arn:aws:lambda:${CDK_DEFAULT_REGION}:${CDK_DEFAULT_ACCOUNT}:function:${FN} Invoking Lambda Function arn:aws:lambda:ap-northeast-1:123456789012:function:cdk2023advent 2023-12-09T15:24:47.141Z acbdc675-3dea-46b7-9e7e-67187075b791 INFO request: {} START RequestId: acbdc675-3dea-46b7-9e7e-67187075b791 Version: $LATEST END RequestId: acbdc675-3dea-46b7-9e7e-67187075b791 REPORT RequestId: acbdc675-3dea-46b7-9e7e-67187075b791 Duration: 13.22 ms Billed Duration: 14 ms Memory Size: 128 MB Max Memory Used: 67 MB {"statusCode":200,"headers":{"Content-Type":"text/plain"},"body":"Hello 2023's Advent calendar!\n"}
ARNを直接指定しているため、--stack-name
の指定が不要となっています。
CDK_~
で始まる環境変数は事前に設定しておく必要があります。
local-invokeの場合はシンプルにLambdaのリソースID指定で動かせた分、少し煩雑に感じます。 現状はこの方法が落とし所にはなりますが、remote-invokeを使えることでAWSサービスと連携するLambda開発が捗ることを期待しています。
まとめ
以上、SAMではなくCDKで作ったLambdaをremote-invokeでも動かす方法について本記事では触れていきました。
remote-invokeではLambda以外にもStep Functions実行やSQS、Kinesisへのデータ送信も可能です。 少しでも画面移動のコンテキストスイッチを減らしつつ、高速開発を進めたい方は参考にしてみてください。