みなさんCustom Runtimeしてますか?
確かLambdaの新機能としてCustom Runtimeが発表されてから大体一年くらい経ちましたね。
私は去年のアドベントカレンダーで壮大に渾身のネタを潰されたのでよく覚えています。
今qiitaなどで検索をかけてみると直近ではphp勢が目立つ感じですかね?
まあRubyが対応してしまった今となっては人口の多くてLambdaを使いたいような言語のうち対応してないのはphpくらいでしょうしね。
あれから発表当時作ったCrystal用のCustom Runtimeでいくらか開発をしてきて私のLambdaやCrystalへの理解に合わせて更新していってもはや見る影もなく変わってしまったので更新版について適当に書いていこうと思います。
あ、基本的にServerless Frameworkを使うのを前提としますのであしからず。
最終的な成果物はこちらに置いてあります。
本文
serverless.yml
とりあえず前回と同じ様にserverless.ymlから。
service: serverless-crystal-sls
custom:
defaultStage: dev
api_version: v0
provider:
name: aws
runtime: provided
timeout: 300
region: ap-northeast-1
stage: ${opt:stage, self:custom.defaultStage}
environment:
${file(./env.yml)}
functions:
hello:
handler: hello
events:
- http:
path: test
method: post
integration: lambda
ここはとくに代わり映えしないですね。
hello
というハンドラーが /test
にpostすると動きますよというだけです。
ディレクトリ構成
次にディレクトリ構成。
.
├ src/
│ ├ runtime/
│ │ ├ error.cr
│ │ └ handler.cr
│ └ main.cr
├ deploy.sh
├ env.yml
└ serverless.yml
今度は結構様変わりしてます。
bootstrapとかhandlerごとのディレクトリとか色々なくなってますね。
ハンドラー
次はCrystalの動作の起点となる main.cr
です。
require "./runtime/handler"
require "./runtime/error"
Lambda.handler "hello" do |event|
begin
event["body"]
rescue err
LambdaError.alert err
raise err
end
end
もう完全に前回とは別物ですね。
今回はSinatra系のWAFっぽい感じを目指しました。
引数がハンドラーとして設定せれている文字列と一致すれば、ブロック内の処理を実行します。
ちなみにこのサンプルではbeginでエラーを一度握りつぶしていますがこれは単にslackへアラートを飛ばすという完全に自分用の処理を入れるためのものです。
require "json"
module LambdaError
extend self
def alert(error)
post = {
fallback: ENV["FAILD_FALLBACK"],
pretext: "<@#{ENV["SLACK_ID"]}> #{ENV["FAILD_FALLBACK"]}",
title: error.message,
text: error.backtrace.join("\n"),
color: "#EB4646",
footer: "function-name",
}
body = {
attachments: [post],
}
HTTP::Client.post("#{ENV["FAILD_WEBHOOK_URL"]}",
body: body.to_json
)
end
end
イベントループ
さて次に本丸のイベントループの実装です。
require "json"
require "http/client"
module Lambda
extend self
def handler(name : String)
return if name != ENV["_HANDLER"]
ENV["SSL_CERT_FILE"] = "/etc/pki/tls/cert.pem"
while true
response = HTTP::Client.get "http://#{ENV["AWS_LAMBDA_RUNTIME_API"]}/2018-06-01/runtime/invocation/next"
event = JSON.parse(response.body)
request_id = response.headers["Lambda-Runtime-Aws-Request-Id"]
begin
body = yield event
header = nil
url = "http://#{ENV["AWS_LAMBDA_RUNTIME_API"]}/2018-06-01/runtime/invocation/#{request_id}/response"
rescue err
body = {
msg: "Internal Lambda Error",
err: err.message,
}
header = HTTP::Headers{"Lambda-Runtime-Function-Error-Type" => "Unhandled"}
url = "http://#{ENV["AWS_LAMBDA_RUNTIME_API"]}/2018-06-01/runtime/invocation/#{request_id}/error"
end
HTTP::Client.post url, headers: header, body: body.to_json
end
end
end
Custom Runtimeのイベントループが一体何者なのか、という話はすでに何回もされているものなので詳しくは解説しませんが、ざっと説明するとCustom Runtimeはループの中で用意されたhttpのエンドポイントから実際のリクエストを受け取り、成功時用と失敗時用のエンドポイントへそれぞれ結果を引き渡すという処理を繰り返しています。
前回からの変更点はいくつかあります。
まず引数としてハンドラーを指定する形になったので合わせて一致しない場合は早期returnするようにしてます。
次に環境変数 SSL_CERT_FILE
にパスを入れています。
これは外部とHTTPSでやり取りをするのにCrystalでは自力でSSL証明書を見つけられないのでこうやって指定してやる必要があるからです。
それからまたbeginが入っています。
これはエラー時にはエラーが起きたときのためのエンドポイントへ結果を流すようにするためです。
そしてこれが一番大きいですが、マクロを使うのをやめてyieldとブロックを利用するようにした点ですね。
最初にこのCustom Runtimeを作成したときはそもそもCrystal(とその元ネタとなったRuby)に関する知識が足りてなかったのでマクロを使わないとこういったcallback的な処理は解決できないものだと思いこんでいたのですが、見ての通りyieldとブロックを利用した処理のほうがむしろ綺麗ですね。
そしてそれ以上の利点として、マクロでバイナリごとにハンドラーを指定するのをやめたので、ビルドするバイナリを1つにできるという点があります。
デプロイスクリプト
では最後にデプロイ用のスクリプトを見ていきましょう。
#!/bin/bash
stg=$1
[ "$stg" = "" ] && stg="dev"
[ -e bootstrap ] && sudo rm bootstrap
sudo docker run --rm -v $(pwd):/src -w /src \
crystallang/crystal crystal build \
--link-flags -static -o bootstrap src/main.cr && \
sudo chmod +x bootstrap || exit 1
sls deploy -s $stg
前回の記事を読んでいない方のための説明として、Custom Runtimeではbootstrapというファイルが実行の起点になっています。
前回との大きな違いはビルドが1回になっていることですね。
bootstrapではわざわざハンドラーごとにどのバイナリを動かすか、という制御が不要になったのでビルドの時点で直接bootstrapを作るようにしています。
Crystalはビルドにとくに時間のかかる言語なのでなおさらですが、ほぼ同じソースに対して何回もビルドを行うのは非効率ですし容量も比較的かさまなくなったのではないでしょうか。
あとがき
ということで自作Custom Runtimeを改良した話をしました。
Media Do Advent Calendar 2019、実は明日も担当は私なのでもう1日お付き合いいただければと思います。