最近Dartに興味を持ってきたので今回はDartの話をします。
今までDartはkotlinやscalaと比べてイケてないという評判を聞いて食わず嫌いしていたのですが、Flutterに採用されてから盛り返してるっぽいのを見てお盆休みで時間もあるしロゴもかっこいいしちょっと触って見ようかなと思ったのでLambdaで動くようにしてみました。
最終的な成果物はこちらにおいてあります。
デプロイしてみる
ではとりあえずこの自作ランタイムを使うときの流れを見ていきます。
デプロイに必要な物は以下の通りです。
- Dart
- Serverless Framework
- docker
まず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
というファイルを用意しています。
import '../../runtime.dart';
dynamic hello(dynamic event) {
return {'msg': '新たな光に会いに行こう。'};
}
void main() {
lambdaHandler(hello);
}
getに対して適当なメッセージを返却するコードです。
lambdaHandler
が今回自作したランタイムでlambdが呼び出されたときにhelloを実行するようにしています。
import '../../runtime.dart';
dynamic world(dynamic event) {
final body = event['body'];
return body;
}
void main() {
lambdaHandler(world);
}
こちらもほぼ同じです。
postで入ってきたbodyをそのまま返しています。
それではこれらをデプロイするための簡単なスクリプトを用意してあるので実行してみましょう。
問題無くデプロイされれば以下のような画面が出てくるはずです。
$ ./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
(略)
それぞれ正しくデプロイできていれば以下のようなレスポンスが帰ってくるはずです。
$ 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
というファイルを用意しています。
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
です。もし先程デプロイしたままであればルートディレクトリにこのようなファイルがあるはずです。
#!/bin/sh
bin_dir="$LAMBDA_TASK_ROOT/./.aot"
$bin_dir/dartaotruntime $bin_dir/$_HANDLER.dart.aot
.aot
というディレクトリにaotのランタイムとコンパイルした結果を設置してありlambdaが起動したときにそれをそのまま実行するようになっています。
このbootstrap
や.aot
の中身を用意しているので先程使用した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
一つずつ見ていきましょう。
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
実行に必要なbootstrap
を生成しています。
別にbootstrapはわざわざ毎回生成する必要は本来無いのですが、既存のプロジェクトへの組み込みやすさ等を考えて今回はここで生成するようにしてしまいました。
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とかで頑張って用意する必要もなくあっさり動いてしまいました。
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でガチャガチャやってますがこれでハンドラーに指定している名前を引っ張りだしてコンパイルしています。
$ cat ./serverless.yml | grep 'handler' | awk '{print $2}'
hello
world
またデプロイ時にステージを選択するオプションに引数を渡していますがこれはこのスクリプトに渡す引数をそのまま横流し、無いならdev
になるようにしています。
stg=$1
[ "$stg" = "" ] && stg="dev"
Q.仕組みとかよくわかんねぇけどとりあえず既存のプロジェクトに組み込みてぇんだけどどうすればいいのさ?
そういった声にお答えしてこんな感じにやればいいんじゃねって感じの手順を用意しました。
ただし筆者はFlutter等の実践的なフロントやアプリのDartの経験があるわけでは無く、動作は保証しかねるのであしからず。
1. pubspec.yamlにhttp追記
dependencies:
http: ^0.12.0+2
2. pubspec.yaml同じ階層にserverless.ymlとdeploy.shを設置
おそらく同じ階層にあればいけるはず
3. お好きなところにruntime.dartとハンドラーを置いておくディレクトリを用意
runtime.dart
はハンドラーから読めれば良いのでそれぞれのプロジェクトにとって適切な場所に置いておいてください。
ハンドラーを置くディレクトリもデフォルトではsrc/handler
としていますが簡単に変更出来るのでこれも適切な場所に用意してください。
ただしこのデプロイスクリプトではハンドラーのディレクトリに更を階層分けるような構造には対応していないので注意してください。
4. deploy.shの修正
もし3でハンドラー置き場等を変更していたりする場合にはここの変数を修正します。
handler_dir="./src/handler"
bin_dir="./.aot"
handler_dir
がハンドラー置き場なので好きに変更しましょう。
またコンパイル結果を置く場所もここで変更できます。
5. デプロイ実行
これで実行すればデプロイされるはずです。
$ ./deploy.sh
完走した感想
ということでDart on Lambdaでした。
Dartはシングルバイナリを吐くタイプの言語では無いのでCrystalなどに比べて正直もっと苦戦すると思ってたのですが思いの外あっさり動きましたね。
あと今回始めてDartを触ってみて思ったのですが、vscodeの拡張の出来がいいのか思ってたより開発しやすかったです。
ロゴが好きなので触っただけだったので実績だけ作って終わりでもいいかなって感じで始めたけどこれならしばらくは個人で開発するLambdaはDartにしてもいいかもしれないですね。
2020/1/6追記
dart2native対応版出しました。
https://qiita.com/qazx7412/items/f25781b3b60847e86482