ローカルでLambdaの開発をやるために環境を整備したのでやったことを残しておきます。
zenn にも全く同じ内容を書いています。
今回作成したサンプルの全体コードはこちらのリポジトリです。
できること
- ローカルに立ち上げたDockerでAWS Lambdaを動かす
- 手元からcurlでLambdaを呼び出す
- ビルド時に自動でLambdaを更新する(hot-reload)
動かし方
Goのコード(Lambdaのハンドラ)をビルドしたいときは以下の make
を実行する。
make build-lambda
Lambdaを立ち上げたいときは docker-compose
で起動する。
docker compose up -d --build
仕組み
AWS Lambda を Docker 上で動かす
Docker上で ハンドラを動かすためにコードをビルドする。
.PHONY: build-lambda
build-lambda:
cd src/lambda && \
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o ../../build/main
make build-lambda
Docker上で動かすため、 Linux 向けにクロスコンパイルするのを忘れないように注意
実行環境としては、AWSが公式で提供している、Dockerイメージを使用する
Dockerイメージ: public.ecr.aws/lambda/provided:al2
FROM public.ecr.aws/lambda/provided:al2
COPY --chmod=755 ./docker/lambda/entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["/app/main"]
/usr/local/bin/aws-lambda-rie
を使用して entrypoint.sh
に対して、ビルドしたコードを渡すことでリクエストを処理してもらう。
#!/bin/bash
if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then
exec /usr/local/bin/aws-lambda-rie "$@"
else
exec "$@"
fi
services:
lambda-docker:
build:
context: .
dockerfile: ./Dockerfile
ports:
- 8080:8080
volumes:
- ./build:/app
environment:
- PORT=8080
docker compose up -d --build
$ curl -i "http://localhost:8080/2015-03-31/functions/function/invocations" -d '{}'
HTTP/1.1 200 OK
Date: Fri, 17 May 2024 10:25:47 GMT
Content-Length: 4
Content-Type: text/plain; charset=utf-8
null
ここまでやれば、 「1. ローカルに立ち上げたDockerでAWS Lambdaを動かす」ことと「2. 手元からcurlでLambdaを呼び出す」 ことが実現できる。
hot reload を実現
このままだと、コードをビルドしても、Docker上のLambdaには反映されない。
ビルドしたら自動でLambdaに反映させるために、 watchexec を使用する。
./build/main
ファイルを /app/main
にマウントしているため、Docker内で watchexec
を適切に設定して起動すれば、ビルド時に自動でリロードしてくれるようになる。
Multi Stage Build を使って、watchexec のみを Lambdaのイメージに含めるようにしておく。
# syntax=docker/dockerfile:1
FROM ubuntu:latest AS builder
RUN apt update && apt install -y curl xz-utils
ENV WATCHEXEC_VERSION=2.1.1
RUN mkdir -p /tmp/dist && \
curl -sSL https://github.com/watchexec/watchexec/releases/download/v${WATCHEXEC_VERSION}/watchexec-${WATCHEXEC_VERSION}-$(uname -i)-unknown-linux-musl.tar.xz | tar -xJ -C /tmp/dist --strip-components 1
FROM public.ecr.aws/lambda/provided:al2
COPY --from=builder /tmp/dist/watchexec /usr/bin/watchexec
COPY --chmod=755 ./docker/lambda/entrypoint.sh /entrypoint.sh
COPY --chmod=755 ./docker/lambda/watch.sh /watch.sh
ENTRYPOINT ["/watch.sh"]
CMD ["/app/main"]
ここで、 --force-poll
オプションを有効にしているのは、 Mac での開発時にうまく検知してくれないことがあったため。不要な場合は削除しても良い。
あと watchexec
の仕様として、ウォッチする対象をファイルで指定していると、 ビルドした後に変更を検知してくれなくなるので、 /app/
ディレクトリを指定している。(多分検知対象のファイルをオープンしたときの ファイルディスクリプターを元に変更を検知していて、go build
を実行するとファイルが置き換わるためではないかと思う)
#!/bin/bash
watchexec -w /app/ --force-poll 100ms -r /entrypoint.sh "$@"
これで無事にホットリロードが実現できた。
(途中 null が返っちゃってるのは、ファイルが置き換わったときの遅延?よくわからない)
まとめ
- Goのコードをビルドするときは、Linux向けにクロスコンパイルする
-
public.ecr.aws/lambda/provided:al2
イメージを使う -
watchexec
でhot-reloadを実現するときは、--force-poll
オプションと、対象をディレクトリにする