5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-04

みなさんCustom Runtimeしてますか?
確かLambdaの新機能としてCustom Runtimeが発表されてから大体一年くらい経ちましたね。
私は去年のアドベントカレンダーで壮大に渾身のネタを潰されたのでよく覚えています。
今qiitaなどで検索をかけてみると直近ではphp勢が目立つ感じですかね?
まあRubyが対応してしまった今となっては人口の多くてLambdaを使いたいような言語のうち対応してないのはphpくらいでしょうしね。

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

本文

serverless.yml

とりあえず前回と同じ様に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 です。

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

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

イベントループ

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

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つにできるという点があります。

デプロイスクリプト

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

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、実は明日も担当は私なのでもう1日お付き合いいただければと思います。

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?