ローカルのAWS Lambda開発環境を整える(hot reload)

2024/05/17に公開

ローカルでLambdaの開発をやるために環境を整備したのでやったことを残しておきます。

コードはここ
https://github.com/imishinist/lambda-sample

できること

  1. ローカルに立ち上げたDockerでAWS Lambdaを動かす
  2. 手元からcurlでLambdaを呼び出す
  3. ビルド時に自動でLambdaを更新する(hot-reload)

動かし方

Goのコード(Lambdaのハンドラ)をビルドしたいときは以下の make を実行する。

make build-lambda

Lambdaを立ち上げたいときは docker-compose で起動する。

docker compose up -d --build

仕組み

AWS Lambda を Docker 上で動かす

Docker上で ハンドラを動かすためにコードをビルドする。

Makefile
.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

Dockerfile
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 に対して、ビルドしたコードを渡すことでリクエストを処理してもらう。

/entrypoint.sh
#!/bin/bash

if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then
  exec /usr/local/bin/aws-lambda-rie "$@"
else
  exec "$@"
fi
docker-compose.yaml
services:
  lambda-docker:
    build:
      context: .
      dockerfile: ./Dockerfile
    ports:
      - 8080:8080
    volumes:
      - ./build:/app
    environment:
      - PORT=8080
起動
docker compose up -d --build
Lambda実行
$ 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のイメージに含めるようにしておく。

Dockerfile
# 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 を実行するとファイルが置き換わるためではないかと思う)

/watch.sh
#!/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 オプションと、対象をディレクトリにする

Discussion