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言語を勉強中です。