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

どうも前に[こんな記事](https://qiita.com/qazx7412/items/8bda5d97614d272ed829)を書いた者です。
ちょっとNimやKotlin/Nativeを相手にしている合間に[dart2native](https://dart.dev/tools/dart2native)なるものができてDartからシングルバイナリが作れるようなっていました。
シングルバイナリが作れるようになったのですからcliを作りやすくなりますしサーバーサイドのデプロイも今までよりも楽になりますしこれはエラいことです。
Lambdaのような意地でも必要なものを全部詰め込まなければならない環境ならなおさらです。

ということでこの正月にdart2native対応したので変更点を中心に簡単にまとめ直します。
毎度のことですが最終的な成果物は[こちら](https://github.com/limit7412/lambda_dart_sls)においてあります。

## デプロイスクリプト
まずデプロイ用のシェルスクリプトから見てみましょう。
前回のスクリプトがシェルとしてはそれなりに複雑だったのに対してかなり単純な作りだと思います。
やっていることは単純です。
Custom RuntimeではLambdaの起動時に `./bootstrap` というファイルが実行されるようになっているのでdart2nativeで同名のシングルバイナリを生成しているだけです。
dockerを使っているのは今の所dart2nativeがクロスコンパイル未対応なのでlinux（すなわちLamdba）で動かせるバイナリを作成するために使っています。
ただライブラリ周りをうまいこと処理できなかったのでビルド前に `pub get` をコンテナの中で強引にすることで解決しています。
本当はもう少しスマートな解決方法もあるんじゃないかとは思いますがシングルバイナリが来たならどうせクロスコンパイルはいつか解決するんでしょうしここで頑張っても後で意味が無くなりそうだなって思ってしまったのでこれでお許しいただきたい…

```sh:deploy.sh
rm -rf ./bootstrap
pub get || exit 1

# TODO: change use cross compile
sudo docker run --rm -v $(pwd):/work -w /work google/dart bash -c "pub get && dart2native ./src/main.dart -o ./bootstrap" &&
sudo chmod 755 ./bootstrap || exit 1

sls deploy -s $stg &&
pub get
```

## ハンドラー
さてでは次はDartのコードの方を見ていきます。
前回は `./src/handler/hoge.dart` みたいなhandlerというディレクトリ内にあるファイルをそれぞれコンパイルするという方式をとっていましたが、デプロイスクリプトを見て頂いた通りシングルバイナリを1つだけ出力するように変更しましたのでそれに伴いハンドラーの構造も変更しました。

ではまずdart2nativeで指定している `./src/main.dart` とその中で記述されている各ハンドラーの振る舞いを定義する `serverless.yml` です。

```dart:main.dart
import 'dart:convert';
import './runtime.dart';

void main() {
  lambdaHandler("hello", (event) async {
    return {
      'statusCode': 200,
      'body': json.encode({'msg': '新たな光に会いに行こう。'}),
    };
  });

  lambdaHandler("world", (event) async {
    final body = event['body'];
    return {
      'statusCode': 200,
      'body': body,
    };
  });
}
```

```yml:serverless.yml
functions:
  hello:
    handler: hello
    events:
      - http:
          path: hello
          method: get
  world:
    handler: world
    events:
      - http:
          path: world
          method: post
```

serverless.ymlは前回とほぼ同じです。（ついでにプロキシ統合を使うように変更してたりしますが…）
`<url>/hello` へgetでリクエストをするとhelloが、`<url>/world` へpostでリクエストをするとworldが動作します。
一方でハンドラーにはcallbackの他に引数が1つ増えています。
前回は各ハンドラーでそれぞれ別のファイルをコンパイルしていたので不要でしたが今回は同じmain関数内にハンドラーが記述されることになるのでどのハンドラーが動くのか指定できるように増やしました。
これの仕組みは環境変数 `_HANDLER` にserverless.ymlで指定した文字列が入っているのでそれを利用して判定、違ったら早期returnをするという極めて単純なものです。

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

void lambdaHandler(String name, Function callback) async {
  if (name != Platform.environment['_HANDLER']) {
    return;
  }

  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'];

    try {
      final result = await callback(event_data);
      http.post(
          'http://${api}/2018-06-01/runtime/invocation/${request_id}/response',
          body: json.encode(result));
    } catch (e) {
      http.post(
          'http://${api}/2018-06-01/runtime/invocation/${request_id}/error',
          body: json.encode({
            'statusCode': 500,
            'body': json.encode({'msg': 'Internal Lambda Error'}),
          }));
    }
  }
}
```

デプロイしてみれば前回と同じ結果が帰ってくることが確認できるはずです。

```
$ ./deploy.sh
（略）
Service Information
service: serverless-dart-sls
stage: dev
region: ap-northeast-1
stack: serverless-dart-sls-dev
resources: 17
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
Serverless: Removing old service artifacts from S3...
Serverless: Run the "serverless" command to setup monitoring, troubleshooting and testing.
Resolving dependencies...
Got dependencies!

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

$ curl -X POST -H "Content-Type: application/json" -d '{"msg": "大石泉すき"}' \
  https://hoge.execute-api.ap-northeast-1.amazonaws.com/dev/world
{"msg": "大石泉すき"}
```

## まとめ
ということで駆け足でしたがdart2nativeでもサーバーレスできました。
dart2native、触ってみた感じやはりクロスコンパイルできないのはちょっとつらいですが悪くはないですね。
GoもそうですがGoogleの作るプログラミング言語って文法は保守的ですけど足回りはめちゃくちゃ良いみたいなところがありますね。
現在1番手のGoはともかくNimやcrystal、Kotlin/NativeみたいなGCありのシングルバイナリ出力言語的には手強いライバルだと思います。
（ところで[直近の記事](https://qiita.com/qazx7412/items/76c1dcd173bdfa07b570)でCustom Runtimeの記事を書く人は脱却したいとか言いつつ結局Custom Runtimeの記事書いてるな…）