サーバーレスCrystalのための自作Custom Runtimeを改良した話

みなさんCustom Runtimeしてますか？
確かLambdaの新機能としてCustom Runtimeが発表されてから大体一年くらい経ちましたね。
私は[去年のアドベントカレンダー](https://qiita.com/qazx7412/items/5054331183f8655f541c)で壮大に渾身のネタを潰されたのでよく覚えています。
今qiitaなどで[検索をかけてみる](https://qiita.com/search?utf8=✓&sort=created&q="custom+runtime")と直近ではphp勢が目立つ感じですかね？
まあRubyが対応してしまった今となっては人口の多くてLambdaを使いたいような言語のうち対応してないのはphpくらいでしょうしね。

あれから発表当時作ったCrystal用のCustom Runtimeでいくらか開発をしてきて私のLambdaやCrystalへの理解に合わせて更新していってもはや見る影もなく変わってしまったので更新版について適当に書いていこうと思います。
あ、基本的にServerless Frameworkを使うのを前提としますのであしからず。
最終的な成果物は[こちら](https://github.com/limit7412/lambda-crystal-sls)に置いてあります。

## 本文
### serverless.yml
とりあえず[前回](https://qiita.com/qazx7412/items/5054331183f8655f541c)と同じ様に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` です。

```rb:src/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へアラートを飛ばすという完全に自分用の処理を入れるためのものです。

```rb:src/runtime/error.cr
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
```

### イベントループ
さて次に本丸のイベントループの実装です。

```rb:src/runtime/handler.cr
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つにできるという点があります。

### デプロイスクリプト
では最後にデプロイ用のスクリプトを見ていきましょう。

```sh:deploy.sh
#!/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](https://qiita.com/advent-calendar/2019/mediado)、実は明日も担当は私なのでもう1日お付き合いいただければと思います。