RubyでServerless(FaaS)するには
この記事はServerless Advent Calendar 2016の8日目の記事になります。
さて。唐突ですが先日のre:Invent速報記事、「[速報]AWS LambdaでC#のサポートを発表。オープンソースの.NET Coreを採用。AWS re:Invent 2016」のはてなブックマーク人気コメントを見てましょう。
(http://b.hatena.ne.jp/entry/www.publickey1.jp/blog/16/aws_lambdacnet_coreaws_reinvent_2016.html)
あー。まあ、Rubyistのみなさんの思うことはだいたいそんなところですよね。
re:Invent 2016でAWS Lambdaのサポート言語にC#が加わり、JavaScript、Python、Javaと合わせて4つの言語が使えることになったようですが、Rubyは今のところサポートされておりません。
ちなみにMicrosoft Azureでは、Bash、Batch、C#、F#、Node、PHP、PowerShell、Python、TypeScriptといった辺りがサポートされている気配がありますが、ここでもRubyは含まれておりません。またGoogle Cloud FunctionはJavaScriptのみの対応のようです。
そのため、本稿はあくまで「Rubyに対応してない環境でRubyを使う」ものになります。ので、正直あまり筋が良くないというか、実戦投入はおすすめできません。それでもいいから試してみたい、という方向けの記事になりますので、その旨ご了承ください。
なお、この記事ではAWS Lambdaを例にしていますが、他のサーバレス環境でも参考になるところがあるかもしれません。
AWS LambdaでRubyを動かすための3つの方法
JRubyを使う
Rubyがサポートされていない環境でRubyを使おうとする場合、まず最初に思いつくのはJRubyかもしれません。Javaが動くならJRubyも動くでしょ? という発想はいかにも正しいです。
ところが、AWS Lambdaの場合、JVM言語はちょっと起動時間が厳しい感があります。おまけにサイズも大きくなりますし。速度の問題であれば定期的にpollingしてでもずっと動かしておけばいいのかもしれませんが、やはりちょっと無理している感覚が否めません。
ちなみにJRubyを使っている例としては下記のものがありました(未検証)。
Traveling Rubyを使う
次に思いつくのは、CRuby(MRI)を使うためにCRubyの実行ファイルを用意して突っ込む方法です。残念ながらAWS Lambdaの環境には、標準ではRubyがインストールされていないようですが、どうせ中身はLinuxなんだから、素朴にRubyのバイナリを同梱して使えばいいんじゃね? という発想ですね。
そんなあなたに知っていただきたいのがTraveling Rubyです。
Travelling Rubyは、Phusion PassengerでおなじみのPhusionが提供している、コンパイル済みのRuby同梱のアプリケーションパッケージを作るためのプロジェクトです。これを使えば、Windows・Linux・macOSそれぞれのパッケージが作れるという超便利な代物なのです。
実際に、これを使ってAWS LambdaでRubyを使うサンプルがすでにいくつかあります。
- https://github.com/lorennorman/ruby-on-lambda
- https://github.com/adomokos/aws-lambda-ruby
- https://github.com/hoshinotsuyoshi/serverless-traveling-ruby-starterkit
とはいえ、これで問題は解決かというと、あんまりそういうわけでもないんですよね…。
ここでも気になるのは起動時間です。例えばJavaScriptの場合、単純なHello Worldであれば数msecかそれ以下の短時間でさくさく動かすことができます。ところがRubyを使った場合、外部ライブラリに依存しないHello World的なものでも500msec程度、RubyGemsを使ったりすると3000msecくらいかかったりします。3桁以上違ってくるというのはちょっときついですね…。
もっとも、これは今回作ったサンプルの作りが、Rubyスクリプトを実行する際に、
+----+ +--------------+ +----------------+
| JS | -> | Shell Script | -> | Ruby + Bundler |
+----+ +--------------+ +----------------+
というくらいに間にいろいろ挟むからです(間のshell scriptは環境変数などの設定用です)。JSまでは一度起動すると再利用されるそうなのですが、Rubyを動かすにはその都度プロセスのforkから始めなければなりません。この辺りをショートカットして、Bundlerも使わなければもうちょっと早くなったりはします(そしてRuby起動時に--disable=gems
オプションを渡してやるとgemも使われなくなり、より高速化されます)。しかしBundlerも(gemも)使わないというのも開発する側からすると厳しいように思われますし、落とし所が難しそうです。
Opalを使う
実はLambdaでRubyを使うためのもう一つの方法があります。それはOpalを使う方法です。
OpalはJavaScript上で動くRuby処理系で、RubyスクリプトとOpal本体(Ruby処理系)の必要な部分をいったんJSのコードに変換して、それをJavaScript処理系上で動かすことができるようになります。これを使えばAWS LambdaのNodeの上でRubyのコードが動くはずです。そう思って作ってみたGitHubリポジトリが以下です。
実行するRubyスクリプトは以下のような感じです。
require 'json'
class Hello
def self.handler(event)
response = {
statusCode: 200,
body: JSON.generate(
message: 'Go Serverless v1.0! Your function executed successfully!',
input: event
)
}
response
end
end
とりあえずRackっぽいインターフェースで、Hello.handler(event)
を実行してJSONっぽいハッシュを返すようになっています。このサンプルはAPI Gatewayを使う版なので、いかにもRackを思わせますね。eventはJSON化されたものが渡される想定です。
このRubyスクリプトを呼び出すようにしたNodeのコードは以下になります。
'use strict';
const Opal = require('./handler.rb.js').Opal;
module.exports.hello = (event, context, callback) => {
const env = Opal.JSON.$from_object(event);
const resOpal = Opal.Hello.$handler(env).$to_n();
const response = {
statusCode: resOpal.statusCode,
body: resOpal.body
};
callback(null, response);
};
handler.rb.js
はhandler.rb
とOpal本体を一個のJavaScriptファイルに変換したものです。ここで得られたOpal
を使って、JSのオブジェクトをOpalのJSONに変換するのと、OpalのHello.handler
メソッドを呼び出しつつ、その結果をJSのオブジェクトに再変換することを行っています。あとはソースを見ればだいたい雰囲気はわかると思います(Opalのメソッドの頭には$
がつくことがちょっと変わっています)。
このリポジトリはServerless Frameworkを使っているので、そのままsls deploy
でデプロイできます。Lambdaのダッシュボードでテストしてみると、コンテナのキャッシュが効いているとおぼしきときには1msec以下の速度で動きます。これならばっちりですね。
ところが、OpalはOpalで問題があります。Opalを使う上での問題は、CRubyで書かれた資産がそのままでは使えないことです。これにはいくつかの課題に分割されます。
- CRubyのgemがOpalではそのまま使えない: 両方で動くgemの作り方もあるのですが、これはgemの作者の方で対応する必要があるもので、ほとんど全てのgemは現状Opalに対応しようとしていません
- C拡張が動かない: JSなので…
- Rubyの標準ライブラリの互換性が低かったり、そもそもOpalにはないものもあったりする: 例えばCGIがありません。ふつうOpalではCGIとしては使わないよね…と思っても、CGIライブラリに依存しているライブラリは普通にあったりします。つらい。
例えば最初の難関は「aws-sdkが動かない」ことですね。さすがに他のAWSのサービスが(頑張らないと)使えないとなると、Lambdaだけ動いてもうれしいことはほとんどありません。
Opalのライブラリの作り方としては、「JavaScriptのライブラリのラッパーを書く」という方法もあるのですが、これはこれでそれなりに労力が必要そうな上、Rubyの普通のコードとJSの普通のコードに違いがあり(特に非同期周りが厳しい)、どのように対応するかが問題になります。Rubyっぽく使えるライブラリにしようとすると手間がかかりますし、開き直ってJSっぽく使えるライブラリにするのであれば、そもそもJSで書いた方が早いのでは……? という根源的な疑問が湧き上がってきます。悩ましい限りです。
まとめ
AWSでもAzureでも(Googleは期待できなさそう)、早くRubyに対応してほしいですね!