Lambdaカクテル

京都在住Webエンジニアの日記です

任意のCommon Lisp(Roswell)スクリプトをAWS Lambda化するツール「Lambda-over-Lambda」の紹介

任意の.rosファイルを変換して,AWS Lambda Custom Runtimeが受け取る.zipに変換するツールができあがったので紹介と使い方のメモを兼ねてここに書いておく. 実際に動作するランタイムは,IPv6まわりのトラブルの影響でClozureCLを使っているが,今後余裕ができればSBCLでも動かせるようにしたい.

github.com

使い方

まずLambda化したいroswellスクリプトを用意する.任意のRoswellスクリプトを変換できると言ったもののmain関数は以下のフォーマットに従う必要があるので,適宜修正する*1

  • 第1引数としてhandlerを,第2引数としてlambda eventを受け取る
    • eventはJSOWNのオブジェクト
    • truetに変換される
    • false:fに変換される
    • null:nullに変換される
    • []nilに変換される
  • JSOWNのオブジェクトを返す必要がある

ちなみにJSOWNとはCommon Lisp用のJSONシリアライザ・デシリアライザであり,S式とJSONとを相互変換できる.

上記の要件を満たすmain関数は例えば次のような形になるだろう.

(defun main (handler event)
  `(:obj ("result" . "ok")
         ("handler" . ,handler)
         ("event" . ,event)))

このmain関数は,ハンドラとイベントを含んだ簡易なオブジェクトを生成し,それを返す.JSOWNオブジェクトを返すので,返り値は:objで始まる連想リストになっている.

準備は整った.lambda-over-lambda.rosを使って先程のスクリプトを変換してみよう.同様のスクリプトがリポジトリ内のyou-should-try-convert-this-script.rosに保存されているので,ぜひ試してみてほしい.

% git clone git@github.com:windymelt/lambda-over-lambda.git
% cd lambda-over-lambda/
% ./roswell/lambda-over-lambda.ros you-should-try-convert-this-script.ros
 => out.zip

このスクリプトをCustom Runtimeとして登録する.

f:id:Windymelt:20190216223346p:plain
出力されたzipファイルを関数パッケージとしてLambdaにアップロードする

アップロードに成功したなら,テストボタンを使ってテストイベントを発行してみよう.

f:id:Windymelt:20190216223454p:plain
テスト実行に成功した

Roswell scriptがLambdaに変換された.たぶんどんなスクリプトも動くはずなので,みんなで遊んでみてほしい.ネイティブコンパイルされているのでけっこう速いぞ.

あらすじ

先日こういった記事を書いていた.

blog.3qe.us

blog.3qe.us

動かすのには成功したので,これを押し進めていたところ,今回のスクリプトが完成した.

仕組み

Lambda-over-Lambda(以下lol)はいくつかのステップでソースの.rosファイルをCustom Runtimeに変換する.

ランタイムコードの合成

lolはターゲットコードにLambda用のラップコードを合成し,必要なライブラリとあわせてzipファイルに固める.

ふつうの関数は引数を受取り結果を返すものだが,AWS Lambda Custom RuntimeではこれをHTTPリクエストの送受信で実現している. Lambdaが用意している特定のエンドポイントにアクセス(GET)するとイベントを受け取ることができ,また別の特定のエンドポイントにアクセス(POST)するとLambdaファンクションとして結果を返すことができるのだ.

Lolはイベントの取り出しと結果の返却をラップするための追加のコードを生成し,ソースと合体させてターゲット.rosファイルを生成する.((今のところ適当にtemporary.rosといった名前で生成しているが,そのうちちゃんとしたい.))ラップコードはmain関数を乗っ取り,Lambdaからイベントを受け取るためのやりとりをすませると,オリジナルのmain関数を呼び出し,得られた結果をまた変換してLambdaに送り返す.

これにより,ソースroswellスクリプトは一般的な入出力方法しか使わずにLambdaとやりとりできるようになる.現在は以下のようなI/Oをサポートしている.

  • eventをJSOWNライブラリのobjectとしてargvで受け取り,JSOWNライブラリのobjectをmainの返り値とする

またこのほかにも,以下のようなI/Oが考えられる.まだ未実装だがこれから必要に応じて実装していきたい.

  • eventの特定フィールドを標準入力として受け取り,標準出力をキャプチャしてLambdaの返り値に(JSONの1フィールドにするなどして)変換する

大変だった点

限定的なIPv6サポート

LambdaのランタイムではIPv6が正しく動作しないようで,これに起因するソケットのエラー(ソケットをオープンしようとするとシステムコールに失敗する)に見舞われ,開発が停滞してしまった.

解決策としてはIPv6(正確にはソケットライブラリであるUSOCKETのIPv6サポート)をオフにするという手段をとった.詳細は以下のエントリにまとめている.

blog.3qe.us

OpenSSLのバージョン違い

LambdaのランタイムはAmazon Linuxで動作しているようで,インストールされているOpenSSLのバージョンもこれに準じているようだ.そのため自分で適当にLispバイナリをビルドしてアップロードしても,(ビルド時の情報をもとに共有ライブラリを読み込むようなので)共有ライブラリの読み込みに失敗して死んでしまう.こういうことはスクリプト型言語ではあまり陥ることのない穴なので面喰らってしまった.

Common Lisp,とくにネイティブコンパイラを備えたSBCLとClozureCLのような処理系では,スクリプト言語のような動的な一面とコンパイル型言語のような静的な一面をあわせもった挙動になるので注意してやる必要がある.例えばグローバルスコープで(defparameterなどで)宣言したスペシャル変数は,実行時ではなくrosスクリプトのビルド時に評価されるので,当然実行時に評価されるものと思ってコーディングすると痛い目を見ることになる.ファイルが一度読み込まれ,評価され,その状態がバイナリとして産み落され,あらためてmain 関数から実行される,というイメージを持つ必要がある.

ともかくOpenSSLの問題はDockerを使ってLambdaと互換な環境でバイナリを生成するという手段で乗りきった.amazonlinux:2017.03.1.20170812というベースイメージでroswellをコンパイルし,処理系をインストールすることでなんとかAPIの互換性を保つことができた.OpenSSLうまく動かすのつらすぎる・・・

まあ動いてよかった

AWSという最新のインフラにもついていける体力がCommon Lispにもあることを示していきたい.COBOLに負けないぞ.

*1:必要が生じればさまざまなフォーマットに対応させるつもりだ