NRIネットコム Blog

NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

Go言語のコードをコンテナイメージに埋め込み、ローカルのLambda関数で実行する

本記事は  【コンテナウィーク】  5日目の記事です。
💻  4日目  ▶▶ 本記事 

はじめに

こんにちは、Go言語を勉強中の新谷です。 AWS Lambda用にコンテナイメージを作成し、ローカルで実行する方法についてまとめたいと思います。

概要

私は最近プロジェクト管理の一環で行なっていた手作業をGo言語で一部自動化しました。できあがったコードをAWS Lambdaで動かすための方法を考えていましたが、Lambdaのマネージドランタイムのサポートが終了するため、コンテナイメージ化することにしました。

Amazon Linux AMI のメンテナンスサポートが 2023 年 12 月 31 日に終了するまで、Lambda は引き続き Go 1.x マネージドランタイムをサポートします。

docs.aws.amazon.com

下記のドキュメント(以下、参考ドキュメント)に手順が記載されており、ローカルPCで開発環境を構築することができます。

docs.aws.amazon.com

基本的には手順通りでよいのですが、環境の差異によりすんなりいかなかったところもあるので、変更を加えたところも含め、まとめたいと思います。

前提条件

前提条件にしたがって、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

docs.aws.amazon.com

実行時の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関数の作成とローカルでのテスト方法が気になったので試してみました。

ドキュメントにしたがって作業してみます。

docs.aws.amazon.com

初期化

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を使う方が手間がかからず、手軽に実行できるかと思います。

執筆者:新谷。AWSを業務で使うインフラエンジニアですが、Go言語を勉強中です。