Lambda は標準で Go や Python などが使えますが、これらの言語以外でも自分で runtime を作りさえすればあらゆる言語で使えるようになります。
Custom AWS Lambda runtimes
今まで runtime を自作するのは難しそうだなぁと思っていましたが、昨年12月のアップデートでコンテナイメージが使えるようになり、また AWS Lambda Runtime Interface Emulator もできたことで Lambda の挙動をローカルで実験するのが手軽になったので、試しに bash runtime を作ってみました。
Tutorial – Publishing a custom runtime - AWS Lambda
bash runtime
以下が Lambda で bash スクリプトを実行するための例です。簡単のためエラーハンドリングはしていません。
Dockerfile
- Lambda で使うイメージ用
- custom runtime 用の
public.ecr.aws/lambda/provided:al2
をもとに作成- al2 は amazonlinux2 のこと
- https://gallery.ecr.aws/lambda/provided
main.sh
- Lambda で実行するスクリプト(handler)
LAMBDA_TASK_ROOT
に配置する
bootstrap
- entrypoint から呼ばれるプログラム
LAMBDA_RUNTIME_DIR
に配置する- request_id, event を GET して、handler にそれらを渡して、handler の返り値を POST するということを永遠繰り返す
Dockerfile
FROM public.ecr.aws/lambda/provided:al2 COPY bootstrap $LAMBDA_RUNTIME_DIR/bootstrap COPY main.sh $LAMBDA_TASK_ROOT/ CMD ["main.sh"]
bootstrap
#!/bin/bash RUNTIME_URL="http://$AWS_LAMBDA_RUNTIME_API/2018-06-01" function post() { request_id=$1 response=$2 url="$RUNTIME_URL/runtime/invocation/$request_id/response" curl -X POST $url -d "$response" } while true do HEADERS="$(mktemp)" EVENT_DATA=$(curl -sS -LD "$HEADERS" -X GET "$RUNTIME_URL/runtime/invocation/next") RESPONSE=$(bash ${LAMBDA_TASK_ROOT}/${_HANDLER} $EVENT_DATA) REQUEST_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id "$HEADERS" | tr -d '[:space:]' | cut -d: -f2) post $REQUEST_ID $RESPONSE done
main.sh
#!/bin/bash EVENT=$1 echo "Hello:$EVENT"
AWS Lambda execution environment - AWS Lambda
実装したのはこの図の Runtime + Function
のところです。(extensions のところはまたいつか深ぼるかも)
bootstrap
からわかるようにやっていることは極めて単純で curl -X GET http://$AWS_LAMBDA_RUNTIME_API/2018-06-01/runtime/invocation/next
(next invocation と呼ばれる)で必要な情報とってきて、handler でコネコネして、curl -X POST "http://$AWS_LAMBDA_RUNTIME_API/2018-06-01/runtime/invocation/$request_id/response" -d $response
(Invocation response と呼ばれる)で所定の場所に結果を渡しているだけです。
この GET/POST さえしておけば Lambda としてとりあえず動きます。なので sokcet が使える普通の言語であればシュッとその言語用の runtime を作ることができます。
※ このスクリプトでは handler に event (body)しか渡していませんが、通常の Lambda では context として header も渡します。
$ aws lambda invoke --cli-binary-format raw-in-base64-out \ --function-name lambda_container \ --payload '"John"' /dev/stderr > /dev/null Hello:"John"
この bash runtime では next invocation, invocation response の2つの runtime API しか使っていませんが、他にエラー時に使用する Invocation error, Initialization error があります。 エラーハンドリングもちゃんとする場合はこれらの API も使います。 AWS Lambda runtime API
entrypoint からの動きを追ってみる
AWS の公式イメージを使って、所定のプログラムを所定の場所に置くと動くことがわかりましたが、ブラックボックス感が否めないので entrypoint から動きを追ってみます。
$ docker inspect public.ecr.aws/lambda/provided:al2 | jq .[].Config.Entrypoint[] "/lambda-entrypoint.sh"
デフォルトの entrypoint が /lambda-entrypoint.sh
とわかったので中を覗いていきます。
#!/bin/sh # Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. if [ $# -ne 1 ]; then echo "entrypoint requires the handler name to be the first argument" 1>&2 exit 142 fi export _HANDLER="$1" RUNTIME_ENTRYPOINT=/var/runtime/bootstrap if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then exec /usr/local/bin/aws-lambda-rie $RUNTIME_ENTRYPOINT else exec $RUNTIME_ENTRYPOINT fi
第1引数に handler 名をとり、/var/runtime/bootstrap
を実行しています。
本物の Lambda ではAWS_LAMBDA_RUNTIME_API
に値が入っているので /var/runtime/bootstrap
を実行し、値が入っていなかった場合(ローカルで実行した場合)は aws-lambda-rie (runtime interface emulator) を使ってエミュレーションしています。
/var/runtime/bootstrap
以降は書いたプログラムのままなので、どうやら内部的に複雑なことはしていないようです。
alpine base でイメージを作る
bootstrap
が動けばいいだけなら alpine linux をもとにしたイメージでも動くのでは?と思ったので試しに作ってみました。
Dockerfile
FROM alpine:3.13.0 RUN apk add curl ENV LAMBDA_RUNTIME_DIR=/var/runtime \ LAMBDA_TASK_ROOT=/var/task COPY bootstrap /$LAMBDA_RUNTIME_DIR/bootstrap COPY main.sh $LAMBDA_TASK_ROOT/ WORKDIR $LAMBDA_TASK_ROOT ENTRYPOINT ["/var/runtime/bootstrap"] CMD ["main.sh"]
main.sh
#!/bin/sh EVENT=$1 echo "HelloWorld:$EVENT"
bootstrap
#!/bin/sh _HANDLER=$1 RUNTIME_URL="http://$AWS_LAMBDA_RUNTIME_API/2018-06-01" function post() { request_id=$1 response=$2 url="$RUNTIME_URL/runtime/invocation/$request_id/response" echo post url: $url curl -X POST $url -d "$response" } while true do HEADERS="$(mktemp)" EVENT_DATA=$(curl -sS -LD "$HEADERS" -X GET "$RUNTIME_URL/runtime/invocation/next") RESPONSE=$(sh ${LAMBDA_TASK_ROOT}/${_HANDLER} $EVENT_DATA) REQUEST_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id "$HEADERS" | tr -d '[:space:]' | cut -d: -f2) post $REQUEST_ID $RESPONSE done
image サイズは公式のイメージをもとにした場合は 303 MB でしたが、alpine を使った場合は 9.69 MB まで減りました。
rie 使ってまずはローカルで動くか試してみます。
$ curl -Lo aws-lambda-rie https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie $ chmod +x aws-lambda-rie $ docker build -t lambda_alpine . $ docker run --rm -p 9000:8080 \ -v $(pwd)/aws-lambda-rie:/aws-lambda-rie \ --entrypoint="/aws-lambda-rie" \ lambda_alpine /var/runtime/bootstrap main.sh $ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '"John"' HelloWorld:John
ひとまず、ローカルで実験する限りは Lambda っぽく動いてくれました。
続いて、イメージを ECR にあげて、本物の Lambda で動かしてみます。
$ docker push ... $ aws lambda update-function-code ... $ aws lambda invoke --cli-binary-format raw-in-base64-out --function-name lambda_container --payload '"John"' /dev/stderr > /dev/null HelloWorld:"John"
無事にほしい結果が返ってきました。runtime API の扱いさえちゃんとしていれば Lambda 用にソフトウェア追加する必要はどうやらないようです。
これなら bootstrap
を一つ書けば既存の docker image を Lambda 用に編集するというのも容易にできそうですね。
Lambda の副作用問題
副作用のあるプログラムをLambda で動かすともろにその影響を受けるというのは AWS Lambda を使っている人には有名ですが、bootstrap
の中で同じ関数を繰り返し実行していることを考えれば至極当然だとわかりますね。
# main.py x = [] n = 0 def handler(event, context): global n x.append(n) n += 1 return str(x)
$ aws lambda invoke --function-name lambda_container /dev/stderr > /dev/null "[0]" $ aws lambda invoke --function-name lambda_container /dev/stderr > /dev/null "[0, 1]" $ aws lambda invoke --function-name lambda_container /dev/stderr > /dev/null "[0, 1, 2]" $ aws lambda invoke --function-name lambda_container /dev/stderr > /dev/null "[0, 1, 2, 3]"