こんにちは、プログラミング言語が大好きなエムスリーエンジニアの園田です。
この記事は エムスリーアドベントカレンダー 3日目の記事となります。
先日、AWS 最大の年間イベントである re:Invent 2018 でラスベガスに渡航していました。
基調講演で発表された数々の新サービス、日本時間29日の基調講演で AI・機械学習、内部統制・コンプライアンス、ブロックチェーンと新サービスの発表が相次ぎました。
日本時間30日の基調講演ではサーバレス関連の新サービスや機能追加が続々と発表されました。アプリケーションエンジニアとしてはこちらのほうが胸アツでしたね。
その中でも個人的に特にインパクトが大きかったのが、 AWS Lambda Function の Ruby 対応 でした。
次に気になったのが、 AWS Lambda Custom Runtime です。要はどんな言語ランタイムでも Lambda で実行できるようになったということですね。
ローンチ時ですでに AWS から C++ と Rust の Custom Runtime が提供されており、パートナー企業からも以下のランタイムが提供されているということでした。
- Elixir
- Erlang
- N|Solid
- COBOL
- PHP
Elixir は再三このブログでも取り上げていますが、弊社でもプロダクション利用しており、「おっ」と思いました。
ところが、ないんです。あの言語が。私の推しラン(推しLanguage)がないのです。
そう、 Nim 言語 がないのです。
というわけで、 Nim の Lambda Runtime を実装してみました。
本記事は Nim アドベントカレンダー 3日目の記事でもあります。
Lambda Custom Runtime のインタフェースを理解する
公式ドキュメントはこちらです。
Custom Runtime では以下の順序で処理を実行するように実装します。
- 初期化処理
- イベントループ
シンプルですね。イベントループが従来の Lambda Handler にあたります。
エントリポイント
Custom Runtime では、インスタンス作成時に Lambda の実行ディレクトリ(環境変数 LAMBDA_TASK_ROOT
)の直下にある、実行可能な bootstrap
というファイル(ファイル名固定、拡張子なし)を実行します。
この bootstrap
の中に上記の初期化処理とイベントループが実装されていれば、どんな言語であれ Lambda 化が可能という仕組みです。チュートリアルではシェルでの実装例が示されています。
初期化処理
Lambda インスタンスの実行時に1回だけ実行される処理です。通常は環境変数の読み込みなどを行います。
Nim はコンパイルされてシングルバイナリになるため、初期化処理はシェルで実装し、バイナリを実行するだけとします。
イベントループ(Lambda Handler)
イベントループは Lambda の InvokeFunction を処理するための処理です。
Runtimes API に対して HTTP リクエストを実行することで、Lambda の実行環境からイベントデータの取得や結果のレスポンスを返すことを役割とします。
イベントデータの取得
Runtimes API の next エンドポイントに GET リクエストを実行することで、イベントキューのリクエストを取得することができます。
curl -sSL "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next"
AWS_LAMBDA_RUNTIME_API
は bootstrap
実行プロセスの環境変数に設定された Runtime ごと(?)に一意になるエンドポイントだと思われます。
この GET リクエストのレスポンスボディが InvokeFunction に渡されたイベントデータとなっています。
イベントデータの処理とエラー処理
取得したイベントデータを入力値としてビジネスロジックを実行します。正常に終了した場合は success エンドポイントにレスポンスを POST します。
curl -X POST "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$REQUEST_ID/response" -d "$RESPONSE"
REQUEST_ID
は、 next エンドポイント実行時のレスポンスヘッダに含まれています。
処理中にエラーが発生した場合は、 invocation error エンドポイントにエラーデータを POST します。
Nim での実装
エントリポイントの作成
エントリポイントとなる bootstrap
を実装します。こちらは Nim ではなく sh で実装します。チュートリアルにあるサンプルを参考に実装します。
set -euo pipefail
EXEC="$LAMBDA_TASK_ROOT/$_HANDLER"
if [ ! -x "$EXEC" ]; then
ERROR="{\"errorMessage\" : \"$_HANDLER is not found.\", \"errorType\" : \"HandlerNotFoundException\"}"
curl -X POST "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/init/error" -d "$ERROR"
exit 1
fi
$EXEC
忘れずに実行権限を付与しておきます。
chmod +x bootstrap
イベントループ(Lambda Handler)の実装
次に、イベントループを Nim で実装します。今回は単純な echo サーバを実装してみます。
import json, httpclient, os, strutils
let
runtimeApiEndpointPrefix = "http://" & getEnv("AWS_LAMBDA_RUNTIME_API") & "/2018-06-01/runtime/invocation/"
nextEndpoint = runtimeApiEndpointPrefix & "next"
client = newHttpClient()
type
InvocationContext* = object of RootObj
requestId*: string
deadline*: string
functionArn*: string
traceId*: string
eventData*: JsonNode
proc getInvocationContext(client: HttpClient): InvocationContext =
let nextResponse = client.request(nextEndpoint, httpMethod = HttpGet)
return InvocationContext(
requestId: nextResponse.headers["Lambda-Runtime-Aws-Request-Id"],
deadline: nextResponse.headers["Lambda-Runtime-Deadline-Ms"],
functionArn: nextResponse.headers["Lambda-Runtime-Invoked-Function-Arn"],
traceId: nextResponse.headers["Lambda-Runtime-Trace-Id"],
eventData: parseJson(nextResponse.bodyStream)
)
proc toJson(context: InvocationContext): string =
let jsonNode = %*{
"requestId": context.requestId,
"deadline": context.deadline.parseInt(),
"functionArn": context.functionArn,
"traceId": context.traceId,
"event": context.eventData
}
return $jsonNode
proc echoResponse(client: HttpClient, context: InvocationContext): void =
discard client.request(
runtimeApiEndpointPrefix & context.requestId & "/response",
httpMethod = HttpPost,
body = context.toJson())
proc handleInvocationError(client: HttpClient, context: InvocationContext, e:ref Exception, message: string): void =
let errorData = %*{ "errorMessage": message, "errorType": repr(e) }
discard client.request(
runtimeApiEndpointPrefix & context.requestId & "/error",
httpMethod = HttpPost,
body = $errorData)
while true:
let context = client.getInvocationContext()
try:
client.echoResponse(context)
except:
let
e = getCurrentException()
msg = getCurrentExceptionMsg()
handleInvocationError(client, context, e, msg)
上記を aws_lambda_nim_example.nim
というファイル名で保存します。
ビルド用のシェルを作成
build.sh
という名前で作成しました。nim をコンパイルして bootstrap と一緒に zip に固めているだけです。
MAIN=$1
~/.nimble/bin/nim c -d:release $MAIN.nim
zip $MAIN.zip bootstrap $MAIN
ビルド用 Dockerfile
現在のLambda実行環境であるAmazonLinux 2017.03.1.20170812 の Docker イメージでビルド環境を構築します。
FROM amazonlinux:2017.03.1.20170812
RUN yum install -y xz gcc
RUN curl https://nim-lang.org/choosenim/init.sh -sSf > init.sh \
&& chmod +x init.sh \
&& ./init.sh -y \
&& rm -f init.sh
RUN mkdir /work
WORKDIR /work
RUN yum install -y zip
COPY build.sh /build.sh
docker-compose.yml を作成します。
version: "3"
services:
nim:
build:
context: .
image: aws-lambda-nim-builder
command: /build.sh aws_lambda_nim_example
volumes:
- .:/work
最終的なファイル構成は以下のようになりました。
.
├── Dockerfile
├── aws_lambda_nim_example.nim
├── bootstrap
├── build.sh
└── docker-compose.yml
ビルド実行
docker-compose を利用して nim をコンパイルします。
docker-compose build
docker-compose up
コンパイル実行後のファイルは以下のようになりました。 aws_lambda_nim_example.zip
が Lambda にアップロードするファイルとなります。
.
├── Dockerfile
├── aws_lambda_nim_example
├── aws_lambda_nim_example.nim
├── aws_lambda_nim_example.zip
├── bootstrap
├── build.sh
└── docker-compose.yml
Lambda Function の作成
Lambda 実行用のロールはすでにあるものとします。 handler
にバイナリファイル名を指定して作成します。
aws lambda create-function \
--function-name "aws-lambda-nim-example" \
--zip-file "fileb://aws_lambda_nim_example.zip" \
--handler "aws_lambda_nim_example" \
--runtime provided \
--role arn:aws:iam::999999999999:role/lambda_basic_execution
Lambda 関数が作成されました。
{
"FunctionName": "aws-lambda-nim-example",
"FunctionArn": "arn:aws:lambda:ap-northeast-1:999999999999:function:aws-lambda-nim-example",
"Runtime": "provided",
"Role": "arn:aws:iam::999999999999:role/lambda_basic_execution",
"Handler": "aws_lambda_nim_example",
"CodeSize": 102317,
"Description": "",
"Timeout": 3,
"MemorySize": 128,
"LastModified": "2018-11-30T01:12:38.342+0000",
"CodeSha256": "oSjewil21oL7C1w+gSJ2NthQv2m6ONETSb5RtVtup4c=",
"Version": "$LATEST",
"TracingConfig": {
"Mode": "PassThrough"
},
"RevisionId": "ddbf1688-345f-460a-b7d0-e0662b9d0f2c"
}
実際に実行してみます。response.txt にレスポンスの中身を出力します。
aws lambda invoke \
--function-name "aws-lambda-nim-example" \
--payload '{"text":"Hello"}' \
response.txt
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
}
レスポンスの中身を cat response.txt | jq .
で見てみます。
{
"requestId": "3db41196-f442-11e8-a8b4-9d64ba70d9be",
"deadline": 1543542600555,
"functionArn": "arn:aws:lambda:ap-northeast-1:999999999999:function:aws-lambda-nim-example",
"traceId": "Root=1-5c009745-36997a167a83da36a3c93059;Parent=62cea21b272ebe72;Sampled=0",
"event": {
"text": "Hello"
}
}
event
にちゃんとリクエスト時のデータが格納されていますね!!
まとめ
AWS Lambda で Nim のようなマイナー 神 言語でも実行できることが確認できました!!
今回はシングルバイナリにコンパイルされる言語だったためシンプルでしたが、Runtime と実行コードが別になる言語(PHPなど)では Runtime だけを含めたレイヤ(新機能のLambda Layer)を作成するのがベストプラクティスとなります。
ただ、こういった足回りのコードを運用する必要があるため、よっぽどのことがない限りはサポートされている言語で実装することをおすすめします。
エンジニア募集!!
エムスリーでは、共に医療 × テクノロジーの未来を切り拓いてくれる仲間を募集中です!
AWS 以外にも GCP や Firebase などのクラウドも活用しています!興味がある方はカジュアル面談やTechtalkにおこしください!!
jobs.m3.com