本記事は
【コンテナウィーク】
5日目の記事です。
💻
4日目
▶▶ 本記事
はじめに
こんにちは、Go言語を勉強中の新谷です。 AWS Lambda用にコンテナイメージを作成し、ローカルで実行する方法についてまとめたいと思います。
概要
私は最近プロジェクト管理の一環で行なっていた手作業をGo言語で一部自動化しました。できあがったコードをAWS Lambdaで動かすための方法を考えていましたが、Lambdaのマネージドランタイムのサポートが終了するため、コンテナイメージ化することにしました。
Amazon Linux AMI のメンテナンスサポートが 2023 年 12 月 31 日に終了するまで、Lambda は引き続き Go 1.x マネージドランタイムをサポートします。
下記のドキュメント(以下、参考ドキュメント)に手順が記載されており、ローカルPCで開発環境を構築することができます。
基本的には手順通りでよいのですが、環境の差異によりすんなりいかなかったところもあるので、変更を加えたところも含め、まとめたいと思います。
前提条件
前提条件にしたがって、Go、Docker、AWS CLIをインストールします。
実行環境
- PC:MacBook Air、Apple M1、Ventura 13.5.2
- Go
$ go version go version go1.21.4 darwin/arm64
- DockerDesktop(一部略)
$ docker version Client: Version: 24.0.6 Server: Docker Desktop 4.25.1 (128006) Engine: Version: 24.0.6 containerd: Version: 1.6.22 runc: Version: 1.1.8 docker-init: Version: 0.19.0
Settings→Generalにある「Use Rosetta for x86/amd64 emulation on Apple Silicon」は有効化
- AWS CLI
$ aws --version aws-cli/2.13.32 Python/3.11.6 Darwin/22.6.0 exe/x86_64 prompt/off
初期作業
参考ドキュメントにしたがって、ディレクトリ作成からGoモジュールの初期化、Lambdaライブラリをモジュールの依存関係として追加まで実行します。
$ mkdir hello $ cd hello $ go mod init example.com/hello-world go: creating new go.mod: module example.com/hello-world $ go get github.com/aws/aws-lambda-go/lambda go: added github.com/aws/aws-lambda-go v1.41.0
Lambda関数用コード作成
main.goという名前のファイルを作成します。
$ touch main.go
エディタでmain.goを開き、参考ドキュメントのサンプルコードを貼り付けます。
package main import ( "context" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" ) func handler(ctx context.Context, event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { response := events.APIGatewayProxyResponse{ StatusCode: 200, Body: "\"Hello from Lambda!\"", } return response, nil } func main() { lambda.Start(handler) }
Dockerfile 作成
つづいてDockerfile を作成します。
$ touch Dockerfile
エディタで Dockerfile を開き、参考ドキュメントのサンプルコードを貼り付け、ローカル環境に合わせて Dockerfile で指定する Go のバージョンを変更します。
FROM golang:1.21.4-bookworm as build
この後ビルドしたいところですが、このままビルドし、実行すると私の環境ではうまく動作しなかったため、修正を加えます。
「CGO_ENABLED=0」の追加
参考ドキュメントの Dockerfile はマルチステージビルドを利用しているため、ビルド環境と最終的なイメージが異なり、実行時に下記のエラーがログに出力されていました。
START RequestId: c5b33971-1aaa-4959-a04c-effc71e4777d Version: $LATEST ./main: /lib64/libc.so.6: version `GLIBC_2.34' not found (required by ./main) ./main: /lib64/libc.so.6: version `GLIBC_2.32' not found (required by ./main) 14 Nov 2023 06:35:59,269 [WARNING] (rapid) First fatal error stored in appctx: Runtime.ExitError 14 Nov 2023 06:35:59,269 [WARNING] (rapid) Process runtime-1 exited: exit status 1 14 Nov 2023 06:35:59,269 [ERROR] (rapid) Init failed InvokeID= error=Runtime exited with error: exit status 1 14 Nov 2023 06:35:59,269 [INFO] (rapid) Starting runtime domain 14 Nov 2023 06:35:59,269 [WARNING] (rapid) Cannot list external agents error=open /opt/extensions: no such file or directory END RequestId: 729a550b-f3ea-4e36-b41f-2ad20e2aa98f
回避するために、go build 時に「CGO_ENABLED=0」を追加し、静的ビルドしてバイナリを生成するように修正しました。
RUN CGO_ENABLED=0 go build -tags lambda.norpc -o main main.go
修正後の Dockerfile
修正を加え、 Dockerfile は下記のようになりました。
FROM golang:1.21.4-bookworm as build WORKDIR /helloworld # Copy dependencies list COPY go.mod go.sum ./ # Build with optional lambda.norpc tag COPY main.go . RUN CGO_ENABLED=0 go build -tags lambda.norpc -o main main.go # Copy artifacts to a clean image FROM public.ecr.aws/lambda/provided:al2 COPY --from=build /helloworld/main ./main ENTRYPOINT [ "./main" ]
イメージをビルド
参考ドキュメントの手順に従ってビルドします。
$ docker build --platform linux/amd64 -t hello:test . (略) $ docker images hello REPOSITORY TAG IMAGE ID CREATED SIZE hello test a7c1eaed1319 8 seconds ago 317MB
余談になりますが、BuildKitを使用してビルドする場合はこちらになります。
$ docker buildx build --platform linux/amd64 -t hello:test .
ビルドしたイメージをローカルでテスト
ランタイムインターフェースエミュレーターの取得
ビルドしたイメージをローカルでテストするにはランタイムインターフェースエミュレーターを取得する必要があるので、取得しておきます。
$ mkdir -p ~/.aws-lambda-rie && curl -Lo ~/.aws-lambda-rie/aws-lambda-rie https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie-arm64 && chmod +x ~/.aws-lambda-rie/aws-lambda-rie
実行時のplatform指定
platform指定なしで実行したところ、WARNING が出力されたため、platform 指定して実行します。
$ docker run -d -v ~/.aws-lambda-rie:/aws-lambda -p 9000:8080 \ --entrypoint /aws-lambda/aws-lambda-rie \ hello:test \ WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested
テスト実行
$ docker run --platform=linux/amd64 -d -v ~/.aws-lambda-rie:/aws-lambda -p 9000:8080 \ --entrypoint /aws-lambda/aws-lambda-rie \ hello:test \ ./main 37f54e3723f2********* $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 37f54e3723f2 hello:test "/aws-lambda/aws-lam…" 8 seconds ago Up 7 seconds 0.0.0.0:9000->8080/tcp elated_einstein
curlコマンドによるテスト
curl コマンドを使用してLambda関数をテストします。
$ curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}' {"statusCode":200,"headers":null,"multiValueHeaders":null,"body":"\"Hello from Lambda!\""}
実行時のログが下記のように出力され、実行されていることが確認できました。
$ docker logs `docker ps -a -q | awk '{print $1}'` -f 14 Nov 2023 09:06:05,066 [INFO] (rapid) exec './main' (cwd=/var/task, handler=) START RequestId: d99cd82b-2b3c-4f92-8f4c-6cf2df9bd77b Version: $LATEST 14 Nov 2023 09:06:50,643 [INFO] (rapid) extensionsDisabledByLayer(/opt/disable-extensions-jwigqn8j) -> stat /opt/disable-extensions-jwigqn8j: no such file or directory 14 Nov 2023 09:06:50,643 [INFO] (rapid) Configuring and starting Operator Domain 14 Nov 2023 09:06:50,644 [INFO] (rapid) Starting runtime domain 14 Nov 2023 09:06:50,644 [WARNING] (rapid) Cannot list external agents error=open /opt/extensions: no such file or directory 14 Nov 2023 09:06:50,644 [INFO] (rapid) Starting runtime without AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN , Expected?: false END RequestId: a5edd70d-d163-41f3-9588-c1010731711a REPORT RequestId: a5edd70d-d163-41f3-9588-c1010731711a Init Duration: 0.49 ms Duration: 85.27 ms Billed Duration: 86 ms Memory Size: 3008 MB Max Memory Used: 3008 MB
SAM
手順を確認していく中で、Serverless Application Modelを使って同じようにLambda関数の作成とローカルでのテスト方法が気になったので試してみました。
ドキュメントにしたがって作業してみます。
初期化
sam initを使って初期化します。初期化時にアプリケーション名、パッケージタイプ、アーキテクチャを指定しています。
$ mkdir hello $ cd hello $ sam init --name hello --package-type Image --architecture x86_64 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 - Machine Learning Template: 1 Which runtime would you like to use? 1 - dotnet6 2 - go1.x 3 - go (provided.al2) 4 - go (provided.al2023) (略) Runtime: 4 Based on your selections, the only dependency manager available is mod. We will proceed copying the template using mod. Would you like to enable X-Ray tracing on the function(s) in your application? [y/N]: Would you like to enable monitoring using CloudWatch Application Insights? For more info, please view https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch-application-insights.html [y/N]: Cloning from https://github.com/aws/aws-sam-cli-app-templates (process may take a moment) ----------------------- Generating application: ----------------------- Name: hello Base Image: amazon/go-provided.al2023-base Architectures: x86_64 Dependency Manager: mod Output Directory: . Configuration file: hello/samconfig.toml Next steps can be found in the README file at hello/README.md Commands you can use next ========================= [*] Create pipeline: cd hello && sam pipeline init --bootstrap [*] Validate SAM template: cd hello && sam validate [*] Test Function in the Cloud: cd hello && sam sync --stack-name {stack-name} --watch
ディレクトリ構成
sam init実行後、ディレクトリ構成は下記のようになりました。
$ tree . └── hello ├── README.md ├── events │ └── event.json ├── hello-world │ ├── Dockerfile │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── main_test.go ├── samconfig.toml └── template.yaml 4 directories, 9 files
ローカルでのビルド
ディレクトリを移動し、sam build でアプリケーションをビルドします。
$ cd hello $ sam build Building codeuri: /**********/hello runtime: None metadata: {'DockerTag': 'provided.al2023-v1', 'DockerContext': '/**********/hello/hello-world', 'Dockerfile': 'Dockerfile'} architecture: x86_64 functions: HelloWorldFunction Building image for HelloWorldFunction function Setting DockerBuildArgs: {} for HelloWorldFunction function Step 1/7 : FROM public.ecr.aws/docker/library/golang:1.19 as build-image 1.19: Pulling from docker/library/golang 012c0b3e998c: Pull complete 00046d1e755e: Pull complete 9f13f5a53d11: Pull complete 190fa1651026: Pull complete 0808c6468790: Pull complete 5ec11cb68eac: Pull complete Status: Downloaded newer image for public.ecr.aws/docker/library/golang:1.19 ---> 80b76a6c918c Step 2/7 : WORKDIR /src ---> [Warning] The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested ---> Running in 8f98ef7f6eac Removing intermediate container 8f98ef7f6eac ---> ad9a15be23cc Step 3/7 : COPY go.mod go.sum main.go ./ ---> 21ee42887bce Step 4/7 : RUN go build -o lambda-handler ---> [Warning] The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested ---> Running in 766ac5ad10aa go: downloading github.com/aws/aws-lambda-go v1.36.1 Removing intermediate container 766ac5ad10aa ---> d580f33fbbb5 Step 5/7 : FROM public.ecr.aws/lambda/provided:al2023 al2023: Pulling from lambda/provided 1249de317c92: Pull complete 5ba847c139c4: Pull complete fb3885617ec9: Pull complete 79a77e7c1be9: Pull complete Status: Downloaded newer image for public.ecr.aws/lambda/provided:al2023 ---> e04005ad418f Step 6/7 : COPY --from=build-image /src/lambda-handler . ---> 0c4dbee0fbd3 Step 7/7 : ENTRYPOINT ./lambda-handler ---> [Warning] The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested ---> Running in 1f26b5911d61 Removing intermediate container 1f26b5911d61 ---> 52891f2c08fb Successfully built 52891f2c08fb Successfully tagged helloworldfunction:provided.al2023-v1 Build Succeeded Built Artifacts : .aws-sam/build Built Template : .aws-sam/build/template.yaml Commands you can use next ========================= [*] Validate SAM template: sam validate [*] Invoke Function: sam local invoke [*] Test Function in the Cloud: sam sync --stack-name {{stack-name}} --watch [*] Deploy: sam deploy --guided
ローカルでの実行
sam local invoke コマンドで、ローカル実行してみます。
$ sam local invoke Invoking Container created from helloworldfunction:provided.al2023-v1 Building image................. Using local image: helloworldfunction:rapid-x86_64. START RequestId: be2474c4-7142-426f-8808-1e76a516c119 Version: $LATEST END RequestId: be2474c4-7142-426f-8808-1e76a516c119 REPORT RequestId: be2474c4-7142-426f-8808-1e76a516c119 Init Duration: 0.48 ms Duration: 107.97 msBilled Duration: 108 ms Memory Size: 128 MB Max Memory Used: 128 MB {"statusCode": 200, "headers": null, "multiValueHeaders": null, "body": "Hello, world!\n"}
特に修正を加えることなく、ローカルでのテスト実行までできました。
途中でCPUアーキテクチャの違いにより、Warningが出力されているので、同様の対応をすれば出力されなくなると思います。
[Warning] The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested
まとめ
Go言語を使ってローカルで作成したコードを、ローカルのLambda関数でテスト実行する手順を整理しました。
ドキュメントとローカル環境の差異により修正が必要でしたが、なんとかテスト実行することができました。
SAMで同様のことができるか確認し、特に詰まることなくテスト実行することができましたので、要件を満たすことができるのであればSAMを使う方が手間がかからず、手軽に実行できるかと思います。