
RIE(Runtime Interface Emulator)とはじめるコンテナLambda
電通デジタルで SRE をしている神田です。この記事は、電通デジタルアドベントカレンダー2021 17日目の記事になります。
私は、BOT BOOSTaR® というチャットボットツールの開発・運用に携わっています。BOT BOOSTaR®ではサーバーレスアーキテクチャを採用しており、AWS Lambda を利用する機会が多いです。また、私が所属する開発部ではもともとバックエンドサービスの開発言語として Go を採用することが多かったので一部テスト的に Go を使い始めています。本日は、Go で AWS Lambda 関数を開発する際のローカル環境でのテストについて話します。
GoでAWS Lambda関数を実装するときの問題
AWS Lambda 関数を開発する場合、開発言語ごとに用意されているランタイム から適当なバージョンを選びます。Go で Lambda 関数を開発しようとした場合、ランタイムで利用できる Go のバージョンは Amazon Linux にインストールされている Go パッケージに依存します。(ランタイムの名前も Go 1.x となっています。)
そのため AWS が提供する Go のランタイムを使うと Go のバージョンを固定できません。Go のランタイム利用者は、Amazon Linux のアップデートによって Go のバージョンが変更になった場合、その変更を検知して対応しなければならないことを意味します。
この問題を回避するため、私たちは関数コードをコンテナイメージを使ってデプロイする方法をとりました。
以降では、公式ドキュメントのGo の AWS Lambda 関数ハンドラー のサンプルコードからカスタムコンテナイメージを作成し、ローカルでテスト実行する手順を説明していきます。
GoでAWS Lambda関数を実装する
Go 1.17 が使えることを前提として説明します。
まず、作業用のディレクトリを作成し Go の開発ができる状態にします。
mkdir lambda-go-with-rie
cd lambda-go-with-rie
go mod init lambda-go-with-rie
次に以下のコードを main.go として保存します。このコードは公式ドキュメントの AWS Lambda 関数ハンドラーから抜き出したものです。
package main
import (
"fmt"
"context"
"github.com/aws/aws-lambda-go/lambda"
)
type MyEvent struct {
Name string `json:"name"`
}
func HandleRequest(ctx context.Context, name MyEvent) (string, error) {
return fmt.Sprintf("Hello %s!", name.Name ), nil
}
func main() {
lambda.Start(HandleRequest)
}
最後に以下のコマンドを実行して、go.mod と go.sum を更新します。
go mod tidy
以上で、AWS Lambda 関数として実行するアプリケーションの準備が整いました。
コンテナの作成
次にコンテナイメージを作成するための Dockerfile を説明します。Go で実装された Lambda 関数のコンテナイメージは普通のサーバーアプリケーションと変わらずビルド用コンテナでソースコードをコンパイルし、生成された実行ファイルを実行用イメージにコピーします。実行用イメージは debian:buster など最小構成のイメージをベースにします。
以下の例では debian:buster をベースイメージにしています。
FROM golang:1.17-buster as builder
WORKDIR /go/src/lambda
COPY go.mod go.sum ./
RUN go mod download
COPY ./ .
RUN go build -a -o /main .
FROM debian:buster as runner
WORKDIR /app/
COPY --from=builder /main /main
ENTRYPOINT [ "/main" ]
ここまではよくある Go のコンテナイメージの作り方と大きな違いはありません。以下のコマンドでコンテナイメージをビルドし、myfunction:latest というタグをつけます。
docker build -t myfunction:latest .
RIEを使ってローカルテスト
さきほど作成したコンテナイメージは、そのまま実行できません。実行しようとすると以下のようなエラーメッセージが表示されます。
docker run myfunction:latest
2021/12/05 11:21:05 expected AWS Lambda environment variables [_LAMBDA_SERVER_PORT AWS_LAMBDA_RUNTIME_API] are not defined
これは、Lambda 関数はランタイム API から呼び出されることが前提となっているためです。この問題へ対応するために、Runtime Interface Emulator(RIE)というツールが提供されています。
RIE はランタイム API の HTTP インタフェースを提供し、受け取った HTTP リクエストを変換し Lambda 関数を呼び出してくれます。
RIE の配置方法はいくつかのパターンがあり、公式ドキュメント では以下の 2 つのパターンが紹介されています。
・カスタムコンテナイメージに RIE を埋め込む方法
・実行時に ENTRYPOINT として RIE を差し込む方法
AWS が提供するランタイムベースイメージは前者の RIE を埋め込む方法で提供されています。
しかし、本番稼働させるイメージにテスト時にしか使わないスクリプトが含まれているのは、あまり好ましくありません。そのため、後者の ENTRYPOINT として RIE を差し込む方法について検討しました。
この方法は以下のステップに分解できます。
1. ローカル環境に RIE をダウンロードする
2. 1 でダウンロードしたパスを実行時にマウントする
3. 2 でマウントしたパスを ENTRYPOINT として設定する
具体的には以下のコマンドを実行します。
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 -d -v ~/.aws-lambda-rie:/aws-lambda \
--entrypoint /aws-lambda/aws-lambda-rie \
-p 9000:8080 \
myfunction:latest /main
このコマンドを実行すると、ランタイム API が 9000 ポートで起動した状態になります。以下のコマンドでランタイム API に HTTP リクエストを送信し、動作確認できます。アクセスする URL は Lambda 関数の実装に関わらず常に同じになります。
curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{ "name": "RIE" }'
"Hello RIE!"
以上で、カスタムコンテナイメージとして実装した Lambda 関数をローカルで実行できました。
マルチステージビルドを使ってテスト実行に必要なオプションを減らす
以上の方法でローカルテストは実施可能ですが、私は以下の点が気になりました。
・テストに必要なステップ数が多い
・ローカル環境に実行時にしか必要ないファイルを置く必要がある
・docker run のオプションが多くなる
これらの問題は Lambda 関数の実行イメージをベースにして、RIE をインストールしたテスト用ステージを追加することで解決できます。
さきほどビルドに使った Dockerfile へ以下のステージを追加します。
FROM runner as runner-with-rie
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
ENTRYPOINT [ "/usr/bin/aws-lambda-rie" ]
このイメージをビルド、実行するには以下のコマンドを実行します。
docker build --target runner-with-rie -t myfunction:latest .
docker run -d \
-p 9000:8080 \
myfunction:latest /main
以上でローカルテストを実施するための準備が整ったので、以下のコマンドで動作確認をします。
curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{ "name": "RIE" }'
"Hello RIE!"
テスト用のステージを追加した分、Dockerfile の記述量は増えてしまいましたがテスト実行時に必要なステップと実行に必要なオプションを減らすことができました。
docker-compose を使うとさらに簡潔にできます。
version: '3'
services:
backend:
build:
context: .
target: runner-with-rie
command: /main
ports:
- "9000:8080"
docker compose up -d
まとめ
コンテナ形式の AWS Lambda を使って任意のバージョンの Go を使う方法を紹介しました。また、マルチステージビルドを使って本番用イメージとローカルテスト用のイメージをわける方法を紹介しました。
これによって、ローカルテスト実施者が RIE を意識することなくテストを実施することが可能となりました。
今回紹介した Dockerfile は最終的に以下のようになります。
FROM golang:1.17-buster as builder
WORKDIR /go/src/lambda
COPY go.mod go.sum ./
RUN go mod download
COPY ./ .
RUN go build -a -o /main .
FROM debian:buster as runner
WORKDIR /app/
COPY --from=builder /main /main
ENTRYPOINT [ "/main" ]
FROM runner as runner-with-rie
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
ENTRYPOINT [ "/usr/bin/aws-lambda-rie" ]