michimani.net

Go で実装した Lambda 関数をコンテナイメージとしてデプロイする

2021-01-22

昨年の re:Invent で、Lambda のデプロイパッケージとしてコンテナイメージがサポートされるようになったと発表がありました。今回は、 Go で実装した Lambda 関数をコンテナイメージとしてデプロイしてみます。

目次

概要

S3 バケット名リストを取得して返却する Lambda 関数を Go で実装し、コンテナイメージにして ECR に Push、そのイメージを Lambda にデプロイします。 サンプルコードは GitHub に置いているので、こっちを見てもらえばだいたい分かると思います。

michimani/go-lambda-sample: This is a sample that implements the AWS Lambda function in Go language and deploys it as a container image.

やってみる

手順としては下記の通りです。

  1. Go で実装
  2. Dockerfile 作成・ビルド
  3. ローカルで実行
  4. ECR Repository を作成・Push
  5. ECR のイメージから Lambda 関数を作成

今回はマネジメントコンソールを使わずに AWS CLI で諸々操作していきます。

手順については下記の公式ドキュメントを参考にしています。

Deploy Go Lambda functions with container images - AWS Lambda

1. Go で実装

まずは Go で Lmabda 関数の処理を実装します。SDK は v1 です。

package main

import (
	"errors"
	"fmt"
	"os"

	"github.com/aws/aws-sdk-go/aws"

	runtime "github.com/aws/aws-lambda-go/lambda"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
)

type Response struct {
	Message    string   `json:"message"`
	BucketList []string `json:"bucket_list"`
}

func listBuckets(c *s3.S3) ([]string, error) {
	out, err := c.ListBuckets(&s3.ListBucketsInput{})
	if err != nil {
		return nil, err
	}

	var list []string
	for _, b := range out.Buckets {
		list = append(list, aws.StringValue(b.Name))
	}

	return list, nil
}

func handleRequest() (Response, error) {
	if os.Getenv("AWS_DEFAULT_REGION") == "" {
		return Response{}, errors.New("'AWS_DEFAULT_REGION' is required.")
	}
	region := os.Getenv("AWS_DEFAULT_REGION")
	s3sess := session.Must(session.NewSession(&aws.Config{
		Region: aws.String(region),
	}))
	s3client := s3.New(s3sess)

	buckets, err := listBuckets(s3client)
	if err != nil {
		return Response{Message: fmt.Sprintf("An error occurred: %s", err.Error())}, nil
	}

	return Response{
		Message:    "Success!",
		BucketList: buckets,
	}, nil
}

func main() {
	runtime.Start(handleRequest)
}

Go で Lambda 関数を実装する場合、 handleRequest()main() 内で runtime.Start() に渡す形にします。1
handleRequest() では引数としてコンテキストとリクエストを受け取りますが、今回は特に何か受け取ることはないので引数なしにしてます。

2. Dockerfile 作成・ビルド

Dockerfile は下記のような内容で作成します。

FROM public.ecr.aws/lambda/provided:al2 as build
RUN yum install -y golang
RUN go env -w GOPROXY=direct
ADD go.mod go.sum ./
RUN go mod download
ADD . .
RUN go build -o /main
FROM public.ecr.aws/lambda/provided:al2
COPY --from=build /main /main
COPY entry.sh /
RUN chmod 755 /entry.sh
ENTRYPOINT [ "/entry.sh" ]

ENTRYPOINT として指定している entry.sh は下記です。

#!/bin/sh
if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then
  exec /usr/local/bin/aws-lambda-rie "$@"
else
  exec "$@"
fi

環境変数 AWS_LAMBDA_RUNTIME_API が設定されていない場合、つまりローカル実行時には RIE (Runtime Interface Emulator) を使って main を実行します。

上記の Dockerfile でビルドします。

$ docker build -t go-lambda-sample .

ちなみに、 provided:al2 ではなく alpine をベースイメージとする場合は、下記の内容になります。

FROM alpine as build
RUN apk add go git
RUN go env -w GOPROXY=direct
ADD go.mod go.sum ./
RUN go mod download
ADD . .
RUN go build -o /main
FROM alpine
COPY --from=build /main /main
ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie /usr/bin/aws-lambda-rie
RUN chmod 755 /usr/bin/aws-lambda-rie
COPY entry.sh /
RUN chmod 755 /entry.sh
ENTRYPOINT [ "/entry.sh" ] 

3. ローカルで実行

下記のコマンドでローカル実行します。

$ docker run \
--rm \
-p 9000:8080 \
-e AWS_DEFAULT_REGION="ap-northeast-1" \
-e AWS_ACCESS_KEY_ID="************" \
-e AWS_SECRET_ACCESS_KEY="************" \
go-lambda-sample-ed-1:latest /main

今回のサンプルコード内では Amazon S3 にアクセスしているので、必要な Role を持った Credential が必要になります。なので、環境変数 AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY に値を設定して実行しています。また、 AWS_DEFAULT_REGION も必要なのでこれも設定してます。

Lambda で実行する場合、これらは不要です。権限については Lambda にアタッチする IAM Role で、 デフォルトリージョンについては Lambda 関数を作成したリージョンが設定されます。

上記コマンドを実行し、ターミナルの別セッションで invoke 用のエンドポイントにアクセスすると、実装した Lambda 関数を実行できます。

$ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}' -o out.json

## レスポンス確認
$ cat out.json | jq .
{
  "message": "Success!",
  "bucket_list": [
    ...
  ]
}

また、 docker run したターミナルでは標準出力に Lambda 実行時のログが出力されます。

START RequestId: d0ac5bfb-10f6-4941-b7b4-dbd653eb9f2a Version: $LATEST
END RequestId: d0ac5bfb-10f6-4941-b7b4-dbd653eb9f2a
REPORT RequestId: d0ac5bfb-10f6-4941-b7b4-dbd653eb9f2a  Init Duration: 0.38 ms  Duration: 515.25 ms   Billed Duration: 600 ms  Memory Size: 3008 MB    Max Memory Used: 3008 MB
START RequestId: 8267a9ea-d234-4c04-a730-70049eefbdd8 Version: $LATEST
END RequestId: 8267a9ea-d234-4c04-a730-70049eefbdd8
REPORT RequestId: 8267a9ea-d234-4c04-a730-70049eefbdd8  Duration: 283.04 ms     Billed Duration: 300 msMemory Size: 3008 MB    Max Memory Used: 3008 MB

ちなみに、ベースイメージに alpine を使用して RIE をイメージに含めなかった場合は、ローカルマシンに RIE をインストールして docker run 時にエントリーポイントとしてローカルの RIE を指定することでも実行可能です。各自で RIE をインストールする必要はありますが、イメージサイズを小さくしたい場合はこの方法が良さそうです。

RIE を含めない場合の Dockerfile は下記のような内容で。

FROM alpine as build
RUN apk add go git
RUN go env -w GOPROXY=direct
ADD go.mod go.sum ./
RUN go mod download
ADD . .
RUN go build -o /main
FROM alpine
COPY --from=build /main /main
ENTRYPOINT [ "/main" ] 
## aws-lambda-rie のインストールとセットアップ
$ 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 \
&& chmod +x ~/.aws-lambda-rie/aws-lambda-rie

## ローカル実行
$ docker run \
--rm \
--entrypoint /aws-lambda/aws-lambda-rie \
-v ~/.aws-lambda-rie:/aws-lambda \
-p 9000:8080 \
-e AWS_DEFAULT_REGION="ap-northeast-1" \
-e AWS_ACCESS_KEY_ID="************" \
-e AWS_SECRET_ACCESS_KEY="************" \
go-lambda-sample:latest /main

4. ECR Repository を作成・Push

まずは ECR のリポジトリを作成します。

$ aws ecr create-repository \
--repository-name go-lambda-sample \
--region ap-northeast-1

続いて、ビルドしたイメージにタグを追加して、 ECR にログイン、 Push します。

$ AWS_ACCOUNT_ID=$( \
aws sts get-caller-identity \
--query 'Account' \
--output text ) \

## タグ追加
$ docker tag go-lambda-sample:latest "${AWS_ACCOUNT_ID}".dkr.ecr.ap-northeast-1.amazonaws.com/go-lambda-sample:latest

## ECR にログイン
$ aws ecr get-login-password --region ap-northeast-1 \
| docker login \
--username AWS \
--password-stdin "${AWS_ACCOUNT_ID}".dkr.ecr.ap-northeast-1.amazonaws.com

## Push
$ docker push "${AWS_ACCOUNT_ID}".dkr.ecr.ap-northeast-1.amazonaws.com/go-lambda-sample:latest

5. ECR のイメージから Lambda 関数を作成

$ IMAGE_URI="${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/go-lambda-sample:latest"
$ aws lambda create-function \
--function-name "go-lambda-sample" \
--package-type "Image" \
--code "ImageUri=${IMAGE_URI}" \
--timeout 30 \
--role "<iam-role-arn>" \
--region ap-northeast-1

<iam-role-arn> にはあらかじめ作成した Role の ARN を指定します。今回のサンプルであれば CloudWatchLogs に加えて S3 への Read 権限があれば十分です。

lambda create-function コマンドには --runtime オプションがありますが、コンテナイメージから作成する場合は指定は不要です。指定した場合、下記のエラーになります。

An error occurred (InvalidParameterValueException) when calling the CreateFunction operation: The Runtime parameter is not supported for functions created with container images.

まとめ

Go で実装した Lambda 関数をコンテナイメージとしてデプロイしてみた話でした。
関数をコンテナイメージで用意できて、 RIE を使ってローカルで Lambda 関数として実行できるが良さげです。今回はただローカルで実行しただけなので、他のサービス (DynamoDB とか) と合わせたローカルでのテストとかはやり方考えたいと思います。


  1. “github.com/aws/aws-lambda-go/lambda” にエイリアスを付けているのは、 “github.com/aws/aws-sdk-go/service/lambda” が存在するからです。まあ、今回は使ってないのでエイリアス付けなくてもいいんですが… ↩︎


comments powered by Disqus