LoginSignup
11

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-08-16

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

dart-logo.png

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

最終的な成果物はこちらにおいてあります。

デプロイしてみる

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

  • Dart
  • Serverless Framework
  • docker

まずserverless.ymlです。

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ファンクションhelloworldをハンドラーとしてpostリクエストを<url>/worldで受け付けるLambdaファンクションworldが定義されています。

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

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

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

void main() {
  lambdaHandler(hello);
}

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

world.dart
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というファイルを用意しています。

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です。もし先程デプロイしたままであればルートディレクトリにこのようなファイルがあるはずです。

bootstrap
#!/bin/sh

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

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

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追記
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でハンドラー置き場等を変更していたりする場合にはここの変数を修正します。

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

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
11