コンテナイメージを利用してAWS Lambda上で動くGoの関数をデプロイしてみた

7 min読了の目安(約6300字TECH技術記事

2020 年の 12 月にAWS Lambda の新機能 – コンテナイメージのサポートがされていました。

Python ではすでに試していたのですが、デプロイされるもののサイズとか考えると Go の方がいいんじゃない? と思ったので Go でやってみることにしました。

参考にするドキュメント

AWS 公式にあるドキュメントを見ます。

おそらくこの 2 つだけ見ればまず動かすところまではできそうです。

作業したリポジトリ

雑ですが、ここにメモなどを残しつつやってみました。

https://github.com/yumechi/go-lambda-practice

やったこと

ディレクトリ構成

Go の AWS Lambda ハンドラーをまずは作成して、Go のディレクトリ構成っぽくしました[1]

 tree -a -I ".git|.idea"
.
├── .dockerignore
├── .gitignore
├── .tool-versions
├── LICENSE
├── README.md
├── build
│   ├── development
│   │   └── Dockerfile
│   └── production
│       └── Dockerfile
├── cmd
│   └── lambda_functions.go
├── entry.sh
├── go.mod
└── go.sum

4 directories, 11 files

ディレクトリ構成は Goにはディレクトリ構成のスタンダードがあるらしい。 - Qiita という記事を読みました。
project-layout/README_ja.md at master · golang-standards/project-layout を最終的に参考にしました。

役割考えてもメインの処理なのでたぶん cmd でよいでしょうという形でそこに lambda handler のコードを置きました。

参考にした「コンテナイメージを使用して Go Lambda 関数をデプロイする」のページでは、 Dockerfile を見てる限り直下に置くのかなと思いました。

しかし今回は Go のスタンダードっぽいディレクトリ構成に合わせて作業していくことにしました。

ソースコード

「構造化されたタイプを使用した Lambda 関数ハンドラー」を参考にしましたが、デバッグしてみて Key 名が自分のフィーリングに合わなかったので、下記のように変更しました。

type MyEvent struct {
	Name string `json:"Name"`
	Age  int    `json:"Age"`
}

type MyResponse struct {
	Message string `json:"Answer:"`
}

func HandleLambdaEvent(event MyEvent) (MyResponse, error) {
	return MyResponse{Message: fmt.Sprintf("%s is %d years old!", event.Name, event.Age)}, nil
}

DockerFile を書く

Dockerfile を書きます。
公式ページでは「provided.al2 ベースイメージを使用した Go のデプロイ」が先に紹介されていますが、この方法では容量が大きかったので(100MB だった気がする)、alpine にします。

alpine でやる方法は次のセクションの「代替ベースイメージを使用した Go のデプロイ」で紹介されています。
このあたりに注目します。

# cache dependencies
ADD go.mod go.sum ./
RUN go mod download GOPROXY=direct
# build
ADD . .
RUN go build -o /main

まずライブラリインストール時の解決のため、見てわかる通り go.mod , go.sum が必要です。
なので雑に go mod init example.com/m/v2 などをして、適切に書き換えておきます。

次にコードの build についてですが、 add してるのがそのディレクトリ全部といった感じなので、参照しているコードだけをデプロイするように書き換えます。
今、コードは cmd ディレクトリ配下に置いてあるので、私は下記に変更しました。

# build
ADD cmd/ cmd/
RUN go build -o /main cmd/lambda_functions.go

で、 docker build してみるとビルドできるはずです。

また lambda のコンソール上または Dockerfile 上で WORKDIR を選択する必要があります。
個人的にデフォルトの WORKDIR は決まっていた方がよさそう、と考えたので私は追加しました。

# copy artifacts to a clean image
FROM alpine:3.13.2
# if WORKDIR is not set, overwrite WORKDIR in the AWS Lambda console
WORKDIR /
COPY --from=build /main /main
ENTRYPOINT [ "/main" ]

ローカルのデバッグについて

ここまでで alpine を使って lambda で動くイメージができました。

一方で、ローカルで起動するには RIE(AWS Lambda Runtime Interface Emulatorのこと) を含めないと起動、デバッグが難しそうです。

なので、「画像に RIE を追加するには、」をみて解決します。ここ、image が画像と機械翻訳されていて変ですが、気にしないで進めます。

ただし、次の entry.sh を見た感じ、 aws-lambda-rie の実行ファイルを置く場所が Dockerfile 上の定義とずれています。
なので私は Dockerfile 側をentry.sh での定義に合わせました。よって下記のように書いています。

# 前の方は省略
# copy artifacts to a clean image
FROM alpine:latest
COPY --from=build /main /main
# if WORKDIR is not set, overwrite WORKDIR in the AWS Lambda console
WORKDIR /

# (Optional) Add Lambda Runtime Interface Emulator and use a script in the ENTRYPOINT for simpler local runs
ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie /usr/local/bin/aws-lambda-rie
RUN chmod 755 /usr/local/bin/aws-lambda-rie
COPY entry.sh /
RUN chmod 755 /entry.sh
ENTRYPOINT [ "/entry.sh" ]

これで docker build, docker run します。

❯ docker build -f ./build/development/Dockerfile -t hello-world-lambda-local . 
(省略)

❯ docker run -p 9000:8080 hello-world-lambda-local:latest /main
time="2021-02-20T07:48:19.973" level=info msg="exec '/main' (cwd=/, handler=)"

これに対してドキュメント記載の curl を投げてみると、こんな感じで返ってくるはず。

curl -s -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"name": "pekora", "age": 111}' | jq .
{
  "Answer:": "pekora is 111 years old!"
}

というわけで、ローカルでも実行ができました。

ECR に push する

公式手順の 6, 7 の通りにやるだけです。 aws cli は使えるようにしとくのが大事です。
あと東京リージョンの方が何かと都合いいかなと思ったので ap-northeast-1 にして push しました。

ここで ECR の話が出てきたので書いておくと、 ECR はストレージサイズに対して料金がかかってきます。
なのでイメージは小さいほうが良い、だから alpine にしたんですね…!

料金 - Amazon ECR | AWS

lambda上でやること

たぶん CloudFormation とか terraform をうまく使えばローカルから何かするだけでできそうな気がしますが、 aws console から今回はやります。

AWS Lambda の新機能 – コンテナイメージのサポート を見るとわかりやすいかもしれません。

AWS console で lambda を探して、「関数の作成」から、「コンテナイメージ」を選択します。
コンテナイメージ URI で先ほど push した ECR の URI を指します。「イメージを参照」から探すのが早そうです。
WOEKDIR や Option の設定については、「コンテナイメージの上書き」を開いて編集します。

lambda 関数ができたら、「テストイベントの設定」に行き、下記のような感じで設定します。

テストイベント

この状態でテストを実行して、リクエストが帰ってこれば OK です。

テスト実行

はまったところ

WORKDIR 設定起因のエラー

WORKDIR が設定されていないがためにずっとはまる。具体的には下記のエラーが出続ける(すでに解消してしまったのでキャプチャがないですが)。

IMAGE Launch error: the working directory '' is invalid, it needs to be an absolute path

そのあとに出てるエラーを見ていると WORKDIR が設定されてないことが原因でした。
もし動かないときは WORKDIR 設定を一度見てみるのがよさそうです。

aws-lambda-rie を使ってうまくデバッグできない

alpine を使っている場合はサンプルの aws-lambda-rie の配下位置が Dockerfile と entry.sh でずれているので、どちらかに合わせましょう。
あるいは AWS provided.al2 ベースイメージを使うのも手ですね。

デバッグ用の curl コマンド

一番最後に . が含まれているものがありますが、たぶんいらないです。

まとめ

サクッと動くでしょうと思って 30 分くらいでやろうとしていましたが、最終的に 2 時間くらいかかってしまったので、大変でした。

ですが、やっぱり image をそのまま lambda で動かせるというのはとても良いですね。
以前バイナリを置く? zip であげる?形式の時は、Mac 特有の何かにはまり、大変でした。
image ならそういう事故も防げそうですし、ローカルでも開発がしやすく思います。

Go の勉強がてら少し作って動かしてみようと思っているものがあるので、頑張ってそれを作って lambda 上で動かせるようにしたいです。

脚注
  1. ちなみに tree コマンドで複数ディレクトリを無視する方法を知らなかったのでtree: ignore directories with patternsを参考にしました。 ↩︎