ServerlessFrameworkとLambdaでサーバーレスCrystalしてみた話改めCustomRuntimeでCrystal動かしてみた話

# まえがきのまえがき
この記事の執筆は[Custom Runtime](https://dev.classmethod.jp/cloud/aws/lambda-custom-runtime/)の公開前でした
なので基本re:Invent2018以前の前提で書かれてます
一応Custom Runtimeについても追記しておきましたので「うるせぇ結論だけ聞かせろ」って方は追記部分だけお読みください
あとなんか[こんなのが](https://qiita.com/advent-calendar/2018/lambda-custom-runtimes)とかいつの間にか出来てるっぽいのでちゃんとした話を聞きたい人はそっちに行けばいいと思います（投げやり）

# 以下本文
ハンズラボでエンジニアをしている回路([@qazx7412](https://twitter.com/qazx7412))です
普段は[postfor](https://post-for.com)のバックエンドの担当としてServerlessFrameworkやAWSやPythonと格闘したり、臨時で他のチームのテストを手伝ったりしたりしてます

今回はAWSでのサーバーレスで活躍するLambdaに関する話~~です~~でした（過去形）

## tl;dr
  - nodeのexec等を使って叩いてやればLambdaで対応していない言語で強引にサーバーレスすることができる
  - [Apex/Up](https://up.docs.apex.sh)ではDocker上でLambdaで動くようにコンパイルしてCrystalに対応をしてる
  - 参考にしてServerlessFrameworkでデプロイしてみた
  - （追記）Custom Runtimeでも動かしてみました
  - （追記）ついでにServerlessFrameworkでそれっぽく動かして見ました

## ことの始まり
最近プライベートでの開発する量が増えました
それで考えるようになったことがインフラをどうやって無料で確保するかです
業務ならともかく個人なら財布のことを考えて無料か実質無料くらいのコストで済ませたいですね
実際自分の場合はGCPの無料枠のf1-microインスタンスや、みんな大好きherokuの無料枠や、さくらインターネットのArukasの無料枠でコンテナが一つ無料で動かせるのとかを活用してます
しかし本音としてはは普段触り慣れていてるAWSを使いたいところです
そこで出てくるのがLambdaです
![lambda](https://qiita-image-store.s3.amazonaws.com/0/215879/828f61fe-c441-fd89-47d5-329878a27e1e.png)
*AWS Lambdaだよ*
*コード書いてをアップロードするだけで実行してくれるすごいやつだよ*

言わずとしれたAWSのFaaSです
関数のリクエスト数とメモリと実行時間による課金なので個人でちょっとしたリクエストが少ない感じの物を動かす程度ならばお財布に優しいサービスです
ですがこういったFaas（やPaaS等）の宿命として対応している言語しか使えないといった制約があります

実際執筆時現在使える言語はこんな感じ

```
  - C#（.NET Core 1.0, 2.0, 2.1）
  - Go 1.x
  - Java8
  - Node.js （6.10, 8.10）
  - Python（2.7, 3.6, 3.7）
  - Ruby 2.5 <- New!!
```

~~RubyとPHP以外の主要な言語にはおおよそ対応してる感じですかね？~~ Ruby来ちゃったんだよなぁ…

ですが私はプライベートではこれまでRubyを使って来ましたし最近Crystalを触り始めたのでこれで開発をしてみたい欲があります
ですがCrystalでサーバーレスをしようとしても ~~マイナー言語なので~~ Lambdaでは対応していないので使えません
でも個人で使うならもっと自分の好きに言語を選びたい…
なのでどうにかする方法を考えます

## Lambdaで非対応な言語を動かす
公式で対応していない言語をLambdaで動かすにはいくつかの方法があります
例えばJavaが動くということはJVM言語が動くはずなのでScalaやKotlinを動かしてみるとか、TypeScriptやCoffeeScript、ElmにOpalみたいなAltJSをクロスコンパイルしてやるとか~~今更わざわざCoffeeScriptとか使いたいかどうかはともかく~~、対応している言語から他の言語を呼び出す（例えば[Python3からrust](https://qiita.com/0ncorhynchus/items/9bc48b4e074bfd6fc689)を呼び出したり出来る）とか[WebAssembly](https://dev.classmethod.jp/cloud/aws/lambda-using-webassembly/)とかやりようはいろいろあります

そしてこれらの手段に対応していないような言語でも最終手段があります
先程Lambdaのことを「コードをアップロードするといい感じに実行してくれるすごいやつ」みたいなことを書きましたが厳密にはこれは正確ではありません
正確には「コードをアップロードすると実行するためのコンテナを用意してくれて実行してくれるすごいやつ」です
何当たり前のことを言ってるんだって感じですが実際にはLambdaはコンテナの上で動作しています
なのでLambdaに使いたい言語を動かすのに必要な物を全部固めてアップロードしてnodeのexecのようなコマンドを実行する機能で無理やり叩いて実行してやれば理論上どんな言語でも動作するはずです

例えばRubyを動かすことを考えるなら、[Traveling Ruby](http://phusion.github.io/traveling-Ruby/)を突っ込んで実行してやれば無理やりですがサーバーレスRubyを実現することができます
（サーバーレスRubyに関する話は[ここ](https://qiita.com/mii-chan/items/3a9d9f45b2c370372d45)や[ここ](https://dev.classmethod.jp/cloud/lambda-in-Ruby/)などにまとまっています）
具体例としてはこんな感じ

```js:handler.js
'use strict';

const exec = require('child_process').exec;

module.exports.hello_crystal = (event, context, callback) => {
  const child = exec('/path/to/ruby ./path/to/script.rb');

  child.stdout.on('data', (result) => {
    callback(null,result);
  });
  child.stderr.on('data', (result) => {
    callback(result);
  });
};
```

まとめると今動かしたいCrystalを動かすならLambdaで動くようにコンパイルしてやってアップロードし、それを叩いてやればサーバーレスCrystalできるんじゃないかということです
そのためには

  1. コンパイルするための環境を用意しコンパイルする
  2. コンパイルした実行ファイルを一緒にしてデプロイしてやる

ことが必要です
そして実はこのやり方でサーバーレスCrystalを実現しているデプロイツールが存在します


## Apex/Up
Apexと同じ作者によるサーバーレス用のデプロイツールが[Up](https://up.docs.apex.sh)です（[日本語での紹介記事](https://dev.classmethod.jp/cloud/aws/deploy-serverless-applications-to-aws-with-apex-up/)）
コンパイル用のコンテナ作るために思想錯誤している最中に偶然見つけました
なんと[Crystalに対応しています](https://up.docs.apex.sh/#runtimes.crystal)

このUpというツールはPaaSのように普通に作ったAPIやWebアプリをデプロイできるのが特徴らしいです
普段使っているServerlessFrameworkとかはどちらかというとCFnのFaaSに寄った形での強化版みたいなサムシングだったのでまるっきり思想が違いますね

デプロイしてみます
コードはサンプルほぼそのままです

```up.json
{
  "name": "up-crystal",
  "profile": "<プロファイル名>",
  "regions": [
    "ap-northeast-1"
  ]
}
```

```rb:src/main.cr
require "http/server"

port = ENV["PORT"].to_i

server = HTTP::Server.new(port) do |ctx|
  ctx.response.content_type = "text/plain"
  ctx.response.print "新たな光に会いに行こう"
end

server.listen
```

デプロイも簡単です

```
$ up

     build: 120 files, 8.3 MB (718ms)
     deploy: version 1 (9.454s)
     stack: complete (22.085s)

```

それでできたAPIGatewayのアドレスを叩いてみましょう

```
$ curl https://hagehage.execute-api.ap-northeast-1.amazonaws.com/production/
新たな光に会いに行こう
```

拍子抜けするほど簡単に動きました

とりあえずサーバーレスでCrystalするという当初の目的自体は達成できました
しかし個人的にはサーバーレスならyamlでいろいろなを定義したいなというお気持ちがあるのでこれをServerlessFrameworkで動かすことを考えていきたいと思います
これがlambda向けにコンパイルするときのコマンドのようですが、これを参考にすれば他のデプロイツールでも動かせるはずです

```
$ docker run --rm -v $(pwd):/src -w /src tjholowaychuk/up-crystal crystal build --link-flags -static -o server main.cr
```

## ServerlessFramework
ということで本命のServerlessFrameworkでCrystalを動かしてみます
今回は入ってきたJSONをそのまま帰すだけの簡単なAPIを作ってみます

```:ディレクトリ構成
.
├ buildfile/
│  └ main
├ src/
│  └ main.cr
├ deploy.sh
├ handler.js
└ serverless.yml
```

nodeのexecでコマンドを叩いてCrystalを呼び出します

```js:handler.js
'use strict';

const exec = require('child_process').exec;

module.exports.hello_crystal = (event, context, callback) => {
  const child = exec(`echo '${JSON.stringify(event)}' | ./buildfile/main`);

  child.stderr.on('data', (result) => {
    callback(JSON.parse(result));
  });
  child.stdout.on('data', (result) => {
    const stdout = result
      .replace(/\"{/g,'{')
      .replace(/}\"/g,'}')
      .replace(/\\\"/g,'"')
      .split('\n')[0];
    callback(null,JSON.parse(stdout));
  });
};
```

```rb:src/main.cr
require "json"

stdin = JSON.parse(STDIN.gets_to_end)

p stdin["body"].to_json
```

標準入力でLambdaに渡ってきたデータをCrystalにぶち込んで同じく標準出力から結果を返してもらっています 
~~いろいろ雑なのは許してほしい~~

あとはこれをコンテナでコンパイルしてServerlessFrameworkでデプロイしてやればいいですね
dockerの方はApex/Upと同じですがコンパイル元と先をそれぞれ`src/main.cr` と `buildfile/main` に変更しています
デプロイするのにいちいち2つのコマンドを両方打つのはだるいので今回は一つにまとめておきます

```sh:deploy.sh
docker run --rm -v $(pwd):/src -w /src \
           tjholowaychuk/up-crystal crystal build \
           --link-flags -static -o buildfile/main src/main.cr &&\
sls deploy
```

```serverless.yml
service: serverlesshellocrystal

custom:
  defaultStage: dev
  api_version: v0
  common: common

provider:
  name: aws
  runtime: nodejs8.10
  region: ap-northeast-1
  stage: ${opt:stage, self:custom.defaultStage}
  profile: <プロファイル名>

functions:
  hello_crystal:
    handler: handler.hello_crystal
    events:
      - http:
          path: test
          method: post
          integration: lambda
```

ということで早速デプロイしちゃいます

```
$ ./deploy.sh
/opt/crystal/embedded/lib/../lib/libevent.a(evutil.o): In function `test_for_getaddrinfo_hacks':
evutil.c:(.text+0x1488): warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/opt/crystal/embedded/lib/../lib/libevent.a(evutil.o): In function `evutil_unparse_protoname':
evutil.c:(.text+0xf0d): warning: Using 'getprotobynumber' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (1.96 MB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
..............
Serverless: Stack update finished...
Service Information
service: serverlesshellocrystal
stage: dev
region: ap-northeast-1
stack: serverlesshellocrystal-dev
api keys:
  None
endpoints:
  POST - https://hage.execute-api.ap-northeast-1.amazonaws.com/dev/test
functions:
  hello_crystal: serverlesshellocrystal-dev-hello_crystal
Serverless: Removing old service artifacts from S3...
```

あとはこのエンドポイントにpostでバシバシ叩いてやればこれで今度こそLambdaでCrystalが動くはずですね

<img width="1153" alt="スクリーンショット 2018-11-22 16.59.02.png" src="https://qiita-image-store.s3.amazonaws.com/0/215879/17a5d05c-7674-f6e8-6b59-b33b6a824ecf.png">

動きました

## 最後に
ということで無事LambdaでCrystalを動かしてサーバーレスCrystalすることに成功しました
~~でも正直冷静に考えるとこんなオーバーヘッドを気にしないやり方ならJVMで動くScalaとかいろいろ先人の知見がありそうなRustとかのほうがよかったのでは…？~~
まぁTraveling Rubyを入れて頑張るよりは良いとは思うしRubyな人がちょっとLambdaでなんか動かしたいとかなら使ってもいいのではって気はする
今回はたまたま自分が触ってたからCrystalを題材にしましたが（少なくともLambdaでは）他の非対応な言語でもサーバーレス出来るかもしれない可能性はあると思うので諦めきれない人は頑張ってみるといいと思います
~~でも本当は公式がherokuのビルドパックみたいな仕組みを作ってくれるのが一番なんじゃないかな（露骨な要求）~~はえーよホセ

# …という話だったのさ
というのがCustom Runtime発表前の話だったのです
でも来ちゃったんですよねぇ…悲しいなぁ
くよくよしててもしゃあないのでCustom RuntimeでもサーバーレスCrystalやってみます
最終的な物は[こちら](https://github.com/limit7412/lambda-crystal-prototype)に置いてあります

## Custom Runtime
まず[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/runtimes-walkthrough.html)を読む限りCustom RuntimeでLambdaを動かす場合は `bootstrap ` を起点にして実行されるようです

```:ドキュメントでのディレクトリ構成の例
.
├ bootstrap
└ function.sh
```

```sh:bootstrap
#!/bin/sh

set -euo pipefail

# Initialization - load function handler
source $LAMBDA_TASK_ROOT/"$(echo $_HANDLER | cut -d. -f1).sh"

# Processing
while true
do
  HEADERS="$(mktemp)"
  # Get an event
  EVENT_DATA=$(curl -sS -LD "$HEADERS" -X GET "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next")
  REQUEST_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id "$HEADERS" | tr -d '[:space:]' | cut -d: -f2)

  # Execute the handler function from the script
  RESPONSE=$($(echo "$_HANDLER" | cut -d. -f2) "$EVENT_DATA")

  # Send the response
  curl -X POST "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$REQUEST_ID/response"  -d "$RESPONSE"
done
```

このbootstrapはここではbashで書かれていますがどうやらこのファイルはbashである必要は無いようです
そしてこのコードを読む感じではループの中で `http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next` から入ってきたデータとリクエストIDを取得して `http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/<リクエストID>/response` に返却する値を再び放り込んで上げれば動くということのようです

とりあえずLambda向けにCrystalをコンパイルできてはいるので同じようなコードをCrystalで書いてやりましょう

```rb:src/main.cr
require "http/client"

while true
  response = HTTP::Client.get "http://#{ENV["AWS_LAMBDA_RUNTIME_API"]}/2018-06-01/runtime/invocation/next"
  event_data = response.body
  request_id = response.headers["Lambda-Runtime-Aws-Request-Id"]

  url : String = "http://#{ENV["AWS_LAMBDA_RUNTIME_API"]}/2018-06-01/runtime/invocation/#{request_id}/response"
  HTTP::Client.post url, body: event_data
end
```

そうしたらコンパイルしてzipで固めてやります

```
$ docker run --rm -v $(pwd):/src -w /src tjholowaychuk/up-crystal crystal build --link-flags -static -o bootstrap src/main.cr
$ zip lambda-crystal.zip bootstrap
```

出来たらコンソールからアップロードして実行してやりましょう
<img width="316" alt="スクリーンショット 2018-11-30 14.12.04.png" src="https://qiita-image-store.s3.amazonaws.com/0/215879/66dd1e4c-cf1f-cebe-5634-5f57586ec2e8.png">

無事動きました

## 真・ServerlessFramework
動かしたけど手でいちいちzipで上げるとか面倒くさくて死にそうになるのでServerlessFrameworkでもどうにかします
最終的な物は[ここ](https://github.com/limit7412/serverlesshellocrystal)においてあります
作るのにあたっては
というわけでまずbootstrap

```sh:bootstrap
#!/bin/sh

set -euo pipefail

EXEC="$LAMBDA_TASK_ROOT/buildfile/$_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
```

[こちら](https://www.m3tech.blog/entry/aws-lambda-custom-runtime)を参考にさせていただきました
内容は同じですので何も言うことなし次serverless.yml

```yaml:serverless.yml
service: serverless-crystal-sls

custom:
  defaultStage: dev
  api_version: v0
  common: common

provider:
  name: aws
  runtime: provided
  region: ap-northeast-1
  stage: ${opt:stage, self:custom.defaultStage}
  profile: <プロファイル名>

functions:
  hello:
    handler: hello
    events:
      - http:
          path: test
          method: post
          integration: lambda
```
今までの物とほぼ同じです
ただ今回はhandlerはディレクトリを指定しています
中に入っている`main.cr`が各Lambda関数という感じです

```:ディレクトリ構成
.
├ buildfile/
│  └ hello
├ src/
│  ├ hello
│  │  └ main.cr
│  └ runtime
│     └ handler.cr
├ bootstrap
├ build.sh
├ deploy.sh
└ serverless.yml
```

中身はいろいろイマイチですがなんとなくhandlerっぽくしました
~~context？知らない子ですね…~~

```rb:hello/main.cr
require "./../runtime/handler"

def hello(event)
  event
end

lambda_handler(hello)
```

それでこっちが呼び出しているhandlerの本体です
前の例のbootstrapとほぼ同じです

```rb:runtime/handler.cr
require "json"
require "http/client"

macro lambda_handler(func)
  module Lambda
    extend self

    def run
      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"]

        body = {{ func }} event["body"]

        url : String = "http://#{ENV["AWS_LAMBDA_RUNTIME_API"]}/2018-06-01/runtime/invocation/#{request_id}/response"
        HTTP::Client.post url, body: body.to_json
      end
    end
  end
end

Lambda.run()
```

あとは`main.cr`がある物だけをコンパイルします

```sh:deploy.sh
stg=$1
[ "$stg" = "" ] && stg="dev"

ls $(pwd)/src/ |
while read line; do
  $(pwd)/build.sh $line || exit 1
done &&
sls deploy -s $stg
```

```sh:build.sh
func=$1
[ -n $func ] || exit 1
# main.crが存在する物だけを対象にしたい
[ -f $(pwd)/src/$func/main.cr ] || exit 0

docker run --rm -v $(pwd):/src -w /src \
           tjholowaychuk/up-crystal crystal build \
           --link-flags -static -o buildfile/$func src/$func/main.cr && \
chmod +x src/$func
```

```
$  ./deploy.sh
/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../x86_64-linux-gnu/libcrypto.a(dso_dlfcn.o): In function `dlfcn_globallookup':
(.text+0x11): warning: Using 'dlopen' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
T-C-P-S-ocket.o: In function `initialize':
/opt/crystal/src/socket/tcp_socket.cr:98: warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/opt/crystal/embedded/lib/../lib/libevent.a(evutil.o): In function `evutil_unparse_protoname':
evutil.c:(.text+0xf0d): warning: Using 'getprotobynumber' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: WARNING: Function hello has timeout of 300 seconds, however, it's attached to API Gateway so it's automatically limited to 30 seconds.
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (2.04 MB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
..............
Serverless: Stack update finished...
Service Information
service: serverless-crystal-sls
stage: dev
region: ap-northeast-1
stack: serverless-crystal-sls-dev
api keys:
  None
endpoints:
  POST - https://hagehage.execute-api.ap-northeast-1.amazonaws.com/dev/test
functions:
  hello: serverless-crystal-sls-dev-hello
layers:
  None
Serverless: Removing old service artifacts from S3...
```

そしたらまた動くか試してみましょう
<img width="1139" alt="スクリーンショット 2018-12-04 17.05.42.png" src="https://qiita-image-store.s3.amazonaws.com/0/215879/06858337-1ced-9ca8-83f7-cceb6afe0595.png">

こちらでもちゃんと動きましたね
（あ、ちなみにこれはあくまで自分用兼単に動かして見たかった事による産物なのでちゃんとライブラリ化してみたいなことは考えてないです…そういうのはもっと出来る人がやってどうぞ

## まとめ
というわけでCustom RuntimeでもサーバーレスCrystal出来ました
待望の機能が来た歓喜と結構な時間を書けて書いた記事が無に帰した絶望が入り混じった複雑な気分です
正直Rubyが無い前提で書いたのに対応してしまったのが一番痛かった…
もうアドベントカレンダーなんて二度と書きません（断言）

### 2019/12/5追記
開発するうちに大きく作りを変えてしまったので更新版を書きました。
https://qiita.com/qazx7412/items/151b2e9375e63c7a400d