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の概要や詳細
  • CDK本線のプラクティス
  • SAMのプラクティス
  • Lambda以外のinvoke

では、本編に進んでいきましょう。

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以外にもいくつかあります。

筆者は意外とSAMも利用します。例えば、CDKを用いて構築していない既存のインフラに対してLambdaを含むサーバレスなアプリケーションをシンプルに展開したい場合はSAMを使うことがあります。 ただし、Node.jsランタイムのLambdaを構築する場合は、NodejsFunctionという便利なコンストラクトがあるためCDKを使います。

docs.aws.amazon.com

他のツールと比べて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コンソールから実行する
  • nodets-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 synthcdk 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以外にremoteinvokeがあります*2。 これはその名の通り、remote=クラウド上にアップロードされたLambdaを起動させるためのコマンドです。

docs.aws.amazon.com

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を調べて指定するのはつらい

と、めんどくさがりなわたしが言っています。 もう少しいい方法がないものかと考えていたのですが、結論としてあまりいい方法はなかったです。

現状、取りうる代替策としては、次の方法ぐらいかなという状態です。

  1. jqyqコマンドでCloudFormationテンプレートから論理IDを引っこ抜く
  2. 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へのデータ送信も可能です。 少しでも画面移動のコンテキストスイッチを減らしつつ、高速開発を進めたい方は参考にしてみてください。

*1:ただし、全てのアプリコードがIaCのコードと結合しているべき、とは考えてはいません

*2:sam remote invokeはSAM CLI バージョン 1.88.0 以降で利用可能です