michimani.net

AWS Lambda における Go の init() の振る舞い

2021-07-01

AWS Lambda にはコールドスタートとウォームスタートの 2 パターンありますが、それぞれのパターンで Go の init() 関数の振る舞いが異なるようなので実際に試してみました。

結論

AWS Lambda のウォームスタート時には Go の init() 関数は実行されません。

手元で確認されたい場合は、下記のリポジトリを clone してお試しください。

michimani/base-of-lambda-container-image | GitHub

試してみる

実際に AWS 上に作った Lambda のコールドスタート/ウォームスタートはこちらでは制御できないので、 Lambda のコンテナイメージをローカルで実行して試します。

試すために用意した Go のコードは下記です。

package main

import (
	"log"
	"net/http"

	runtime "github.com/aws/aws-lambda-go/lambda"
)

type Response struct {
	Message    string `json:"message"`
	StatusCode int    `json:"statusCode"`
}

var count int

func handleRequest() (Response, error) {
	log.Println("start handler")
	defer log.Println("end handler")

	count++
	message := "Hello AWS Lambda"
	for i := 0; i < count; i++ {
		message = message + "!"
	}

	return Response{
		Message:    message,
		StatusCode: http.StatusOK,
	}, nil
}

func init() {
	count = 0
	log.Println("init function called")
}

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

このコード、実行するとどんなログ出力、レスポンスになるでしょうか。

下記のような Dockerfile を用意してイメージをビルドします。

FROM public.ecr.aws/lambda/provided:al2 as build
# install compiler
RUN yum install -y golang
RUN go env -w GOPROXY=direct
# cache dependencies
ADD go.mod go.sum ./
RUN go mod download
# build
ADD . .
RUN go build -o /main
# copy artifacts to a clean image
FROM public.ecr.aws/lambda/provided:al2
COPY --from=build /main /main
COPY entry.sh /
RUN chmod 755 /entry.sh
ENTRYPOINT [ "/entry.sh" ]
docker build -t base-of-lambda-container-image .

コンテナを起動します。

docker run \
--rm \
-p 9000:8080 \
base-of-lambda-container-image:latest /main

別のターミナルで Lambda を invoke します。

curl "http://localhost:9000/2015-03-31/functions/function/invocations"

すると、コンテナを起動している方のターミナルでは下記のような出力が得られます。

START RequestId: 7910c8d7-1394-455a-b253-64f6322044bc Version: $LATEST
time="2021-07-01T14:34:23.324" level=info msg="extensionsDisabledByLayer(/opt/disable-extensions-jwigqn8j) -> stat /opt/disable-extensions-jwigqn8j: no such file or directory"
time="2021-07-01T14:34:23.324" level=warning msg="Cannot list external agents" error="open /opt/extensions: no such file or directory"
2021/07/01 14:34:23 init function called
2021/07/01 14:34:23 start handler
2021/07/01 14:34:23 end handler
END RequestId: 7910c8d7-1394-455a-b253-64f6322044bc
REPORT RequestId: 7910c8d7-1394-455a-b253-64f6322044bc  Init Duration: 0.18 ms  Duration: 6.83 ms   Billed Duration: 100 ms Memory Size: 3008 MB    Max Memory Used: 3008 MB

REPORT の中に Init Duration の情報が含まれているので、コールドスタートであることがわかります。 init() が実行されていることもわかります。

一方、 invoke した方のターミナルでは、レスポンスとして下記の出力が得られます。

{"message":"Hello AWS Lambda!","statusCode":200}

では、続けてもう一度実行するとどうなるでしょうか。
コンテナ側と invoke 側、それぞれの出力は下記のようになります。

START RequestId: 28c51c4b-cd7a-43b7-a2ae-fa8ef9017015 Version: $LATEST
2021/07/01 14:37:01 start handler
2021/07/01 14:37:01 end handler
END RequestId: 28c51c4b-cd7a-43b7-a2ae-fa8ef9017015
REPORT RequestId: 28c51c4b-cd7a-43b7-a2ae-fa8ef9017015  Duration: 1.17 ms       Billed Duration: 100 ms     Memory Size: 3008 MB    Max Memory Used: 3008 MB

REPORT の中に Init Duration の情報が含まれておらず、ウォームスタートであることがわかります。そして、 init function called のログがないため init() は実行されていないことがわかります。

{"message":"Hello AWS Lambda!!","statusCode":200}

message の語尾の ! が 2 つになっています。このようになるのは下記が原因です。

このまま連続で実行していくと ! の数は増えていきます。

$ for i in `seq 5`; do
curl "http://localhost:9000/2015-03-31/functions/function/invocations" && echo "\n"
done

{"message":"Hello AWS Lambda!!!","statusCode":200}

{"message":"Hello AWS Lambda!!!!","statusCode":200}

{"message":"Hello AWS Lambda!!!!!","statusCode":200}

{"message":"Hello AWS Lambda!!!!!!","statusCode":200}

{"message":"Hello AWS Lambda!!!!!!!","statusCode":200}

コンテナを一旦停止して再度起動すれば、最初の一回だけはコールドスタートで実行されます。

まとめ

AWS Lambda のウォームスタート時には Go の init() 関数は実行されません。
実行ごとに初期化したい変数がある場合は、グローバル変数で定義せずにハンドラ内で宣言・初期化する、もしくは beforeHandler() みたいな初期化用の関数を作って、ハンドラの最初でそれを実行するみたいなことをする必要がありそうです。

下記のリポジトリに今回の挙動を試すことができる一式を置いているので、手元で確認したい場合は clone して試してみてください。

michimani/base-of-lambda-container-image: This is a base of the AWS Lambda function in Go and deploys it as a container image.


comments powered by Disqus