AWS LambdaのCustom RuntimeでサーバーレスDartしてみた

最近Dartに興味を持ってきたので今回はDartの話をします。

![dart-logo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215879/afb9d019-a457-1bc7-347d-ea0f6f71d20d.png)

今までDartはkotlinやscalaと比べてイケてないという評判を聞いて食わず嫌いしていたのですが、Flutterに採用されてから盛り返してるっぽいのを見てお盆休みで時間もあるし~~ロゴもかっこいいし~~ちょっと触って見ようかなと思ったのでLambdaで動くようにしてみました。

最終的な成果物は[こちら](https://github.com/limit7412/lambda_dart_sls)においてあります。

## デプロイしてみる

ではとりあえずこの自作ランタイムを使うときの流れを見ていきます。
デプロイに必要な物は以下の通りです。

  - Dart
  - Serverless Framework
  - docker


まず`serverless.yml`です。

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

custom:
  defaultStage: dev
  api_version: v0

provider:
  name: aws
  runtime: provided
  region: ap-northeast-1
  stage: ${opt:stage, self:custom.defaultStage}

functions:
  hello:
    handler: hello
    events:
      - http:
          path: hello
          method: get
          integration: lambda
  world:
    handler: world
    events:
      - http:
          path: world
          method: post
          integration: lambda
```

Serverless Frameworkがわかる方にはほとんど説明不要だとは思います。
使ったことが無い方へ説明をすると`functions`がLambdaを定義している箇所で、それぞれ`hello`をハンドラーとしてgetリクエストを`<url>/hello`で受け付けるLambdaファンクション`hello`と`world`をハンドラーとしてpostリクエストを`<url>/world`で受け付けるLambdaファンクション`world`が定義されています。

次にここでハンドラーとして指定されている`hello`と`world`を見ていきます。
`src/handler`の中にそれぞれ`hello.dart`と`world.dart`というファイルを用意しています。

```dart:hello.dart
import '../../runtime.dart';

dynamic hello(dynamic event) {
  return {'msg': '新たな光に会いに行こう。'};
}

void main() {
  lambdaHandler(hello);
}
```

getに対して適当なメッセージを返却するコードです。
`lambdaHandler`が今回自作したランタイムでlambdが呼び出されたときにhelloを実行するようにしています。

```dart:world.dart
import '../../runtime.dart';

dynamic world(dynamic event) {
  final body = event['body'];
  return body;
}

void main() {
  lambdaHandler(world);
}
```

こちらもほぼ同じです。
postで入ってきたbodyをそのまま返しています。

それではこれらをデプロイするための簡単なスクリプトを用意してあるので実行してみましょう。
問題無くデプロイされれば以下のような画面が出てくるはずです。

```console
$ ./deploy.sh
Password: # 一部コマンドのためにsudoが必要
Resolving dependencies... 
Got dependencies!
（略）
Serverless: Stack update finished...
Service Information
service: serverless-dart-sls
stage: dev
region: ap-northeast-1
stack: serverless-dart-sls-dev
resources: 16
api keys:
  None
endpoints:
  GET - https://hoge.execute-api.ap-northeast-1.amazonaws.com/dev/hello
  POST - https://hoge.execute-api.ap-northeast-1.amazonaws.com/dev/world
functions:
  hello: serverless-dart-sls-dev-hello
  world: serverless-dart-sls-dev-world
layers:
  None
（略）
```

それぞれ正しくデプロイできていれば以下のようなレスポンスが帰ってくるはずです。

```console
$ curl https://hoge.execute-api.ap-northeast-1.amazonaws.com/dev/hello
{"msg":"新たな光に会いに行こう。"}

$ curl -X POST -H "Content-Type: application/json" -d '[{"name": "島村卯月"}, {"name": "渋谷凛"}, {"name": "本田未央"}]' \
https://hoge.execute-api.ap-northeast-1.amazonaws.com/dev/world
[{"name":"島村卯月"},{"name":"渋谷凛"},{"name":"本田未央"}]
```


## 中身を見てみよう
Lambdaで動かすためのランタイムの中身をもう少し詳しく見ていきましょう。
まずcustom runtimeで任意のLambdaを動かすのに必要なことは大きく2つです。

  - 所定のapiから必要な情報を取得し得た情報を利用して何かをした後また別のapiに対して結果を送りつける無限ループの実装
  - custom runtimeに設定したときに起動時に最初に実行されるファイル`bootstrap`の用意

まず無限ループの方から見ていきます。
今回はルートディレクトリに`runtime.dart`というファイルを用意しています。

```dart:runtime.dart
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:io' show Platform;

void lambdaHandler(Function callback) async {
  final api = Platform.environment['AWS_LAMBDA_RUNTIME_API'];

  while (true) {
    final response =
        await http.get('http://${api}/2018-06-01/runtime/invocation/next');

    final event_data = json.decode(utf8.decode(response.bodyBytes));
    final request_id = response.headers['lambda-runtime-aws-request-id'];

    final result = await callback(event_data);

    http.post(
        'http://${api}/2018-06-01/runtime/invocation/${request_id}/response',
        body: json.encode(result));
  }
}
```

あまり難しいことはしていませんが一通り見ていきます。
`http://${Platform.environment['AWS_LAMBDA_RUNTIME_API']}/2018-06-01/runtime/invocation/next`を叩くことでLambdaへのリクエストの各種情報とリクエストIDが取得できます。
これをこの関数の引数のcallbackに処理をさせ、その結果をそのまま`http://${Platform.environment['AWS_LAMBDA_RUNTIME_API']}/2018-06-01/runtime/invocation/${request_id}/response`に引き渡すことで結果を返却することができます。

次に`bootstrap`です。もし先程デプロイしたままであればルートディレクトリにこのようなファイルがあるはずです。

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

bin_dir="$LAMBDA_TASK_ROOT/./.aot"
$bin_dir/dartaotruntime $bin_dir/$_HANDLER.dart.aot
```

`.aot`というディレクトリにaotのランタイムとコンパイルした結果を設置してありlambdaが起動したときにそれをそのまま実行するようになっています。
この`bootstrap`や`.aot`の中身を用意しているので先程使用した`deploy.sh`なのですが、この中身はこんな感じになってます。

```shell:deploy.sh
#!/bin/bash

stg=$1
[ "$stg" = "" ] && stg="dev"

handler_dir="./src/handler"
bin_dir="./.aot"

rm -rf ./bootstrap $bin_dir
mkdir $bin_dir

cat <<EOF > ./bootstrap
#!/bin/sh

bin_dir="\$LAMBDA_TASK_ROOT/$bin_dir"
\$bin_dir/dartaotruntime \$bin_dir/\$_HANDLER.dart.aot
EOF
chmod +x ./bootstrap || exit 1

docker run --rm -v $(pwd):/work -w /work google/dart cp /usr/lib/dart/bin/dartaotruntime ./.aot
sudo chmod +x $bin_dir/dartaotruntime || exit 1

pub get || exit 1

cat ./serverless.yml |
grep 'handler'       |
awk '{print $2}'     |
while read line; do
    dart2aot $handler_dir/$line.dart $bin_dir/$line.dart.aot || exit 1
done &&
sls deploy -s $stg
```

一つずつ見ていきましょう。

```shell
handler_dir="./src/handler"
bin_dir="./.aot"

rm -rf ./bootstrap $bin_dir
mkdir $bin_dir
```

まずコンパイル結果を置く場所や元のハンドラー場所を設定し、前回のデプロイ時作ったファイル類を掃除してます。

```shell
cat <<EOF > ./bootstrap
#!/bin/sh

bin_dir="\$LAMBDA_TASK_ROOT/$bin_dir"
\$bin_dir/dartaotruntime \$bin_dir/\$_HANDLER.dart.aot
EOF
chmod +x ./bootstrap || exit 1
```
実行に必要な`bootstrap`を生成しています。
別にbootstrapはわざわざ毎回生成する必要は本来無いのですが、既存のプロジェクトへの組み込みやすさ等を考えて今回はここで生成するようにしてしまいました。

```shell
docker run --rm -v $(pwd):/work -w /work google/dart cp /usr/lib/dart/bin/dartaotruntime ./.aot
sudo chmod +x $bin_dir/dartaotruntime || exit 1

pub get || exit 1
```

ここでdart側の事前準備をしています。
dartのdokcerイメージからlinux向けのaotの実行ランタイムをとって来ていますがこれはmacを考慮してそうしているだけでlinuxな環境ならローカルにある物をそのまま持ってきても問題無く動作します。
正直ここはもっと色々な物を一緒に詰めてやる必要があるかな？っと思ってたのですがlayerとかで頑張って用意する必要もなくあっさり動いてしまいました。

```shell
cat ./serverless.yml |
grep 'handler'       |
awk '{print $2}'     |
while read line; do
    dart2aot $handler_dir/$line.dart $bin_dir/$line.dart.aot || exit 1
done &&
sls deploy -s $stg
```

最後に各ハンドラーをコンパイルしてデプロイしています。
grepやawkでガチャガチャやってますがこれでハンドラーに指定している名前を引っ張りだしてコンパイルしています。

```console
$ cat ./serverless.yml | grep 'handler' | awk '{print $2}'
hello
world
```

またデプロイ時にステージを選択するオプションに引数を渡していますがこれはこのスクリプトに渡す引数をそのまま横流し、無いなら`dev`になるようにしています。

```shell
stg=$1
[ "$stg" = "" ] && stg="dev"
```

## Q.仕組みとかよくわかんねぇけどとりあえず既存のプロジェクトに組み込みてぇんだけどどうすればいいのさ？
そういった声にお答えしてこんな感じにやればいいんじゃねって感じの手順を用意しました。
ただし筆者はFlutter等の実践的なフロントやアプリのDartの経験があるわけでは無く、動作は保証しかねるのであしからず。

##### 1. pubspec.yamlにhttp追記
```yaml:pubspec.yaml
dependencies:
  http: ^0.12.0+2
```

##### 2. pubspec.yaml同じ階層にserverless.ymlとdeploy.shを設置
おそらく同じ階層にあればいけるはず

##### 3. お好きなところにruntime.dartとハンドラーを置いておくディレクトリを用意
`runtime.dart`はハンドラーから読めれば良いのでそれぞれのプロジェクトにとって適切な場所に置いておいてください。
ハンドラーを置くディレクトリもデフォルトでは`src/handler`としていますが簡単に変更出来るのでこれも適切な場所に用意してください。
ただしこのデプロイスクリプトではハンドラーのディレクトリに更を階層分けるような構造には対応していないので注意してください。

##### 4. deploy.shの修正
もし3でハンドラー置き場等を変更していたりする場合にはここの変数を修正します。

```shell
handler_dir="./src/handler"
bin_dir="./.aot"
```
`handler_dir`がハンドラー置き場なので好きに変更しましょう。
またコンパイル結果を置く場所もここで変更できます。

##### 5. デプロイ実行
これで実行すればデプロイされるはずです。

```console
$ ./deploy.sh
```

## 完走した感想
ということでDart on Lambdaでした。
Dartはシングルバイナリを吐くタイプの言語では無いので[Crystal](https://qiita.com/qazx7412/items/5054331183f8655f541c)などに比べて正直もっと苦戦すると思ってたのですが思いの外あっさり動きましたね。
あと今回始めてDartを触ってみて思ったのですが、vscodeの拡張の出来がいいのか思ってたより開発しやすかったです。
ロゴが好きなので触っただけだったので実績だけ作って終わりでもいいかなって感じで始めたけどこれならしばらくは個人で開発するLambdaはDartにしてもいいかもしれないですね。


## 2020/1/6追記
dart2native対応版出しました。
https://qiita.com/qazx7412/items/f25781b3b60847e86482