📝

AWS CDKで作成したlambdaをAWS SAMでローカル実行する

2024/08/13に公開

背景

serverlessがv4から有償化になるという話を耳にしました。
https://www.serverless.com/pricing
代替ツールを検討する中でcdkとsamを使った開発が便利そうだったので記事として残しておきます。

この記事で行うこと

aws cdkで作成したlambdaをsamを用いてローカルで実行します。
lambdaは関数URLを発行する形で作成します。
cdk、sam、goなどの詳しい解説はこの記事では行いません。公式を参照してください。

今回の内容のgithubレポジトリはこちらです
https://github.com/OdagakiHiroki/cdk-sam-sample

対象者/所要時間

この記事はcdk、sam、goをこれから扱う方がローカル実行まで到達できることを目指しています。
aws cliやgoを少し触ったことがある方は所要時間は1h程度かと思います。
お気軽に初めてみてください🙂

前提

  1. aws cli、aws cdk cliがインストールされていること
    https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/getting_started.html
  2. aws sam cliがインストールされていること
    https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/install-sam-cli.html

構成

  • hello-worldディレクトリはGoで実装したlambdaのディレクトリです。
  • cdk.outはこちらcdk synthで生成されます。
    上記以外はこちらcdk initで生成されます。
.
└── backend
    ├── README.md
    ├── bin
    ├── cdk.json
    ├── cdk.out
    ├── hello-world
    ├── jest.config.js
    ├── lib
    ├── node_modules
    ├── package-lock.json
    ├── package.json
    ├── test
    └── tsconfig.json

cdkプロジェクトのセットアップ

backendディレクトリで下記を実行しcdkプロジェクトをセットアップします。

cdk init app --language typescript

backendのstackを修正

backendのstackに関数URLを発行するlambdaを追加します。

handlerにはGoで作成したmain.goをビルドした実行ファイル名を記載しています。
codeには実行ファイルが配置してあるパスを記載しています。

import * as path from "node:path";
import * as cdk from 'aws-cdk-lib';
import * as lambda from "aws-cdk-lib/aws-lambda";
import { Construct } from 'constructs';

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

+    const helloWorld = new lambda.Function(this, "helloWorld", {
+      runtime: lambda.Runtime.PROVIDED_AL2023,
+      handler: "bootstrap",
+      code: lambda.Code.fromAsset(path.join(__dirname, "../hello-world"))
+    });

+    helloWorld.addFunctionUrl({
+      authType: lambda.FunctionUrlAuthType.NONE,
+      cors: {
+        allowedOrigins: ["*"],
+        allowedHeaders: ["*"],
+        allowedMethods: [lambda.HttpMethod.GET],
+      }
+    })
  }
}

lambdaの処理を実装

hello-worldディレクトリを作成し、Goプロジェクトを初期化します。

go mod init main

main.goを作成し下記を実装します。

package main

import (
	"context"
	"encoding/json"
	"fmt"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)

type Response struct {
	Message string `json:"string"`
}

func handler(ctx context.Context, event events.LambdaFunctionURLRequest) (events.LambdaFunctionURLResponse, error) {
	msg := "Hello world"
	fmt.Println(msg)
	responseBody, err := json.Marshal(Response{Message: msg})
	return events.LambdaFunctionURLResponse{StatusCode: 200, Body: string(responseBody)}, err
}

func main() {
	lambda.Start(handler)
}

moduleの依存関係を解決します。

go mod tidy

lambdaで実行する処理をbuild

hello-worldディレクトリで、作成したmain.goをビルドします。

GOOS=linux GOARCH=amd64 go build -tags lambda.norpc -o bootstrap main.go

lamdaで実行する環境に合わせてGOOS=linux GOARCH=amd64を指定しています。

  • GOOSの指定が抜けていた😖
    私はintel macで開発していたため、GOOSを指定しなければ、GOOSが異なるためローカル実行の際に下記のエラーが出力されました。

    START RequestId: 93808cb6-cbb5-45cc-9e15-a12c200a0004 Version: $LATEST
    12 Aug 2024 14:20:38,283 [ERROR] (rapid) Init failed InvokeID= error=fork/exec /var/task/bootstrap: exec format error
    12 Aug 2024 14:20:38,288 [ERROR] (rapid) Invoke failed error=fork/exec /var/task/bootstrap: exec format error InvokeID=6779a19e-3060-4b90-a449-76a935ad05ea
    12 Aug 2024 14:20:38,288 [ERROR] (rapid) Invoke DONE failed: Runtime.InvalidEntrypoint
    
  • ビルドしたファイル名がbootstrapじゃないといけなかった😖
    runtimeの制約のようです。
    https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/golang-handler.html#golang-handler-naming

    異なる名前にすると下記のようなエラーが出力されました。

    START RequestId: 1c996e00-0d0d-4d56-bc01-06ffd24b7944 Version: $LATEST
    12 Aug 2024 14:22:31,987 [ERROR] (rapid) Init failed error=fork/exec /var/task/bootstrap: exec format error InvokeID=
    12 Aug 2024 14:22:31,995 [ERROR] (rapid) Invoke failed error=fork/exec /var/task/bootstrap: exec format error InvokeID=764531af-4c9b-4690-a7c2-5a48bc69078d
    12 Aug 2024 14:22:31,995 [ERROR] (rapid) Invoke DONE failed: Runtime.InvalidEntrypoint
    

templateを作成

backendディレクトリで、templateを作成します

cdk synth --no-staging

samを使ってlocalで実行するためであれば、cdk synth --no-stagingでOKです。
--no-stagingをつけると、cdk.out/asset.XXXXが生成されなくなります。
もちろんcdk synthでも同様にローカル実行は可能です。

ローカルで実行

backendディレクトリで実行します。

lambdaを一度だけ実行する

sam local invoke helloWorld -t ./cdk.out/BackendStack.template.json

helloWorldはstackで指定したlambda関数名です。
-tはtemplate.jsonへのパスです。

リクエストを待ち受ける

コマンドを実行した際に関数URLのlmabdaを実行するのではなく、ローカルでリクエストを待ち受けたままの状態にしたいこともありますよね。
そのような場合は下記でリクエストを待ち受ける状態にできます。

sam local start-lambda -t ./cdk.out/BackendStack.template.json

リクエストを待ち受けた状態で、curl等でリクエストを送ることができます。

curl "http://localhost:3001/2015-03-31/functions/helloWorld/invocations" -d '{}'

awsコマンドでも可能です。

aws lambda invoke --function-name "helloWorld" --endpoint-url "http://127.0.0.1:3001" --no-verify-ssl out.txt --profile ${profile}

上記のコマンドを実行するとレスポンス結果が書き込まれたout.txtが生成されます。

お疲れ様でした!🎉🎉🎉

所感

stackの作成やローカル実行が今の所スムーズに実行できたので導入のハードルは低く感じています。
実際にどのようなstackが作成されるかもcdk.outのtemplateに出力されるのでデプロイ前などに事前に確認することもできそうです。
AWSのサービスを作成する時には良さそうです。
このまま導入してみてデプロイ周りも含め、もう少し使用感を確かめたいと思います。

Discussion