以前の記事を先に読むと理解が進むかも(?) goropikari.hatenablog.com
AWS Lambda がコンテナイメージをサポートしたのと時を同じくして Runtime Interface Clients (RIC) と Runtime Interface Emulator (RIE) というツールが公開されました。
RIC / RIE は AWS が配布している各言語用のイメージの中にはすでに含まれており、また暗黙的に動いているので普通に Lambda を使う分にはこれらのツールについて意識することはおそらくありません。
ではどんなときに意識するとよいのかというと次のような場合が考えられると思います
- AmazonLinux 以外の OS イメージを使いたい
- Debian が使いたいー
- Alpine が使いたいー
- etc...
- 既存のコンテナイメージを Lambda 用に改造したい
- Lambda の挙動をローカルでテストしたい
これらのことを簡単に実現させますよというのが RIC とRIE です。
Lambda はどう動いているのか
RIC/RIE を説明する前に Lambda はどのような流れで実行されるのかを確認しておきます。
AWS Lambda execution environment
- API Gateway や S3 event などで Lambda が着火すると、まず Lambda Service は Execution Environment というものを作成します。
- その上で runtime などの初期化がなされます。
- その後 runtime は Runtime API を介して Lambda Service と通信し、event、context を取得、Function (Handler) にそれらを渡します
- Function の返り値を Runtime API を通して Lambda Service に渡します。
- Invoke がされなくなるまで 3~4 を繰り返します
- 一定時間の間 invoke がされなかったとき、Lambda Service は runtime をシャットダウンし、Execution Environment を削除します。
Runtime Interface Clients
先に説明したとおり runtime は Lambda Service と通信する必要があります。
ですが、世にあるコンテナイメージは Lambda Service と通信する機能を用意していません。
そんなときに Lambda Service とおしゃべりする仕組みを提供してくれるのが Runtime Interface Clients (RIC)です。
RIC を使わず Lambda Service と通信しようとすると前回の記事の bootstrap
のようなものを自分で書かねばならず面倒です。
RIC は各言語用にパッケージとして提供されており、現在提供されているのは Lambda が標準でサポートしている以下の6種類です。
- Node.js
- Python
- Java
- .NET
- Go
- Go の場合は
aws-lambda-go/lambda
の中に含む - https://godoc.org/github.com/aws/aws-lambda-go/lambda
- Go の場合は
- Ruby
ref: Runtime support for Lambda container images
それ以外の言語は公式には用意されていないので自分で作りましょう!
一例として RIC を使って素の Python イメージを Lambda 用に変えてみます。
Dockerfile
FROM python:3.9-slim WORKDIR /var/task COPY main.py . RUN pip install awslambdaric ENTRYPOINT ["/usr/local/bin/python", "-m", "awslambdaric"] CMD ["main.handler"]
main.py
def handler(event, context): return f"Hello {event}"
$ docker build ... $ docker push ... $ aws lambda update-funciton-code ... $ aws lambda invoke ... --payload '"John"' Hello John
前回の記事の ENTRYPOINT ["/var/runtime/bootstrap"]
の部分が ENTRYPOINT ["/usr/local/bin/python", "-m", "awslambdaric"]
に置き換わった感じです。
他の言語でも似たような感じで使うことができます。
既存のイメージを Lambda 用に改造したい場合は、handler 関数作って、RIC 入れて、ENTRYPOINT
, CMD
を上記のような感じで書けば Lambda で動かせるようになります。ね、簡単でしょ?
Runtime Interface Emulator
RIC によって Lambda Service と通信する手段を獲得しましたが、我々のローカル環境には Lambda Service はいないので作ったプログラムをテストできません。困りました。 そんなときに Lambda Service の代わりをしてくれるのが Runtime Interface Emulator (RIE) です。こちらは RIC と違い言語縛りはありません。現時点では Linux x86-64 用のバイナリのみ配布されています。とはいえ Go 製のツールなのでソースコードから自分でビルドすれば Mac でも動くと思います(試していませんが)。コンテナイメージ内で使われることを想定しているっぽいので今後も Linux 以外の OS はサポートしないと思います。
使い方は以下のように ENTRYPOINT
, CMD
で書いた内容をそのまま引数に取ります。
./aws-lambda-rie /usr/local/bin/python -m awslambdaric main.handler
このとき注意点としては python への PATH は絶対 PATH で書きます。
RIE を使うと 8080, 9001 port で Listen している HTTP server が立ち上がります。9001 が Runtime API 用で、8080 が Lambda 関数を invoke するための口です。
$ curl -XPOST "http://localhost:8080/2015-03-31/functions/function/invocations" -d '"John"' "Hello John"
POST すると実行時間などが出力されますが、本物の Lambda でやった場合と比べるとかけ離れた数値を返すので参考になりません。
START RequestId: 762d8fc3-3fe8-4d08-8690-212e727fbbcc Version: $LATEST END RequestId: 762d8fc3-3fe8-4d08-8690-212e727fbbcc REPORT RequestId: 762d8fc3-3fe8-4d08-8690-212e727fbbcc Init Duration: 0.28 ms Duration: 64.73 ms Billed Duration: 100 ms Memory Size: 3008 MB Max Memory Used: 3008 MB START RequestId: ccacb48a-a5e2-4200-a7b6-51559ea2332e Version: $LATEST END RequestId: ccacb48a-a5e2-4200-a7b6-51559ea2332e REPORT RequestId: ccacb48a-a5e2-4200-a7b6-51559ea2332e Duration: 1.31 ms Billed Duration: 100 ms Memory Size: 3008 MB Max Memory Used: 3008 MB
自作 Lambda Service
RIE の動きを見て、とりあえず HTTP server を2つ建てておけば良さそうな雰囲気を察したので雑にオレオレ Lambda Service を作ってみました。
server.py
# https://stackoverflow.com/a/60753 import json import re import uuid from http.server import BaseHTTPRequestHandler, HTTPServer from queue import Queue from socketserver import ThreadingMixIn from threading import Thread inputs = Queue() results = Queue() class Lambda(BaseHTTPRequestHandler): def do_POST(self): if self.path == '/2015-03-31/functions/function/invocations': content_length = int(self.headers['Content-Length']) post_data = self.rfile.read(content_length) inputs.put(post_data) self.send_response(200) self.send_header("Content-type", "application/json") self.end_headers() self.wfile.write(results.get()) class RuntimeAPI(BaseHTTPRequestHandler): def do_GET(self): if self.path == '/2018-06-01/runtime/invocation/next': self.send_response(200) self.send_header("Content-type", "application/json") self.send_header("Lambda-Runtime-Aws-Request-Id", str(uuid.uuid4())) self.end_headers() self.wfile.write(inputs.get()) def do_POST(self): if re.match(r"/2018-06-01/runtime/invocation/.*/response", self.path): self.send_response(200) self.end_headers() content_length = int(self.headers['Content-Length']) post_data = self.rfile.read(content_length) results.put(post_data) class ThreadingHTTPServer(ThreadingMixIn, HTTPServer): daemon_threads = True def serve_on_port(port, handler): server = ThreadingHTTPServer(("localhost", port), handler) server.serve_forever() if __name__ == '__main__': Thread(target=serve_on_port, args=[8080, Lambda]).start() serve_on_port(9001, RuntimeAPI)
RIC が AWS_LAMBDA_RUNTIME_API
という環境変数を使うのでそれだけセットして実行します。
# terminal 1 $ python server.py # terminal 2 $ export AWS_LAMBDA_RUNTIME_API=localhost:9001 $ /usr/local/bin/python -m awslambdaric main.handler # terminal 3 $ curl -XPOST "http://localhost:8080/2015-03-31/functions/function/invocations" -d '"John"' "Hello John"
無事にそれっぽい動きをしてくれました。
機能を絞ってしまえば、案外自分でも書けるものですね。
現時点で RIE は Logs API に対応しておらず Lambda extension の動作検証ができないので、案外早くオレオレ Lambda Service を使う日が来るかも(?)