AWS LambdaのCustom RuntimeでCommon Lisp(ECL)を動かしてみた #reinvent
こちらはAWS Lambda Custom Runtimes芸人 Advent Calendar 2018の18日目の記事です。
はじめに
先日のre:Invent 2018で発表されたAWS Custom Lambda Runtimesですが、Lambdaといえば思い出すのはLISPです。すでにSchemeの記事が公開されていますが、今回はLISPを試してみることにします。
emacs-lispは普段使いしていますが、Common Lispに触れるのは20年振りくらいです。昨今のCommon Lisp事情をほとんど知らないので、少し調べてみました。こちらにまとまっていますが、処理系はいろいろ選べるようです。その中でもECLという実装が目を引きました。ECLは、Embeddable Common Lispの略称で、埋め込めるCommon Lispということのようです。ECLは、昔懐かしいKCL(Kyoto Common Lisp)の系譜にあるなんだそうです。
いつの時代にもLISPといえばイメージサイズが超巨大というのが課題でしたが、ECLはコンパクトに動作させることができるようです。特にコンパイル済みバイナリはとても小さくなるようです。
ECLを動かしてみる
まずは感触をつかむために、MacOSXで試してみました。ECL用のbrewのフォーミュラが提供されていますので、インストールは簡単です。
$ brew install ecl Updating Homebrew... ==> Installing dependencies for ecl: bdw-gc ==> Installing ecl dependency: bdw-gc ==> Downloading https://homebrew.bintray.com/bottles/bdw-gc-8.0.0.sierra.bottle.tar.gz ######################################################################## 100.0% ==> Pouring bdw-gc-8.0.0.sierra.bottle.tar.gz /usr/local/Cellar/bdw-gc/8.0.0: 65 files, 1.2MB ==> Installing ecl ==> Downloading https://homebrew.bintray.com/bottles/ecl-16.1.3_3.sierra.bottle.tar.gz ######################################################################## 100.0% ==> Pouring ecl-16.1.3_3.sierra.bottle.tar.gz /usr/local/Cellar/ecl/16.1.3_3: 304 files, 10.9MB
さっそく動かしてみます。
$ ecl ECL (Embeddable Common-Lisp) 16.1.3 (git:UNKNOWN) Copyright (C) 1984 Taiichi Yuasa and Masami Hagiya Copyright (C) 1993 Giuseppe Attardi Copyright (C) 2000 Juan J. Garcia-Ripoll Copyright (C) 2016 Daniel Kochmanski ECL is free software, and you are welcome to redistribute it under certain conditions; see file 'Copyright' for details. Type :h for Help. Top level in: #<process TOP-LEVEL>. > 'Hello HELLO
こちらを参考に、HelloWorldをコンパイルしてみます。 次のようなソースファイルを用意します(シンタックスハイライトがLisp非対応なのがちょっと残念w)。
(defun hello () (write-line "Hello, World!!")) (hello) (quit 0)
Hello Worldを表示する関数定義だけではなくて、その関数をトップレベルで呼び出し、そしてquitでシステムを抜けています。
これをECLでコンパイルします。.oファイルができるので、さらにリンクする必要があるようです。
$ ecl > (compile-file "hello.lisp" :system-p t) ;;; Loading #P"/usr/local/Cellar/ecl/16.1.3_3/lib/ecl-16.1.3/cmp.fas" ;;; ;;; Compiling hello.lisp. ;;; OPTIMIZE levels: Safety=2, Space=0, Speed=3, Debug=0 ;;; ;;; Compiling (DEFUN HELLO ...). ;;; End of Pass 1. ;;; Emitting code for HELLO. ;;; Finished compiling hello.lisp. ;;; #P"/Users/yourname/ecl-lambda/hello.o" NIL NIL > (c:build-program "a.out" :lisp-files '("hello.o")) > ^D
できたバイナリを動かしてみます。
$ ./a.out Hello, World!!
ちゃんと動きました!
AWS Lambda用の環境を作る
さて、ECLの簡単な使い方がわかったので、AWS Lambdaで動かすコンパイル環境を作成します。
AWS Lambdaは、「Amazon Linux 1」相当の環境とのことですので、Dockerで環境を作ることにします。
タグ名にamazonlinux:1
を指定するとAmazon Linux 1相当の環境を引っ張ってくることができます。ただこれにはツールが何も入っていないので、まずはyumでインストール、そしてECLのソースを引っ張ってきてビルドを行います。途中curlがエラーになったので、--http1.1オプションを足しています。
FROM amazonlinux:1 MAINTAINER TT RUN yum -y update && \ yum -y groupinstall "Development tools" && \ rm -rf /var/cache/yum/* && \ yum clean all WORKDIR /usr/local/src RUN curl --http1.1 -O https://common-lisp.net/project/ecl/static/files/release/ecl-16.1.3.tgz && \ tar xfz ecl-16.1.3.tgz && \ cd ecl-16.1.3 && \ ./configure --prefix=/usr/local && \ make && \ make install && \ make clean && \ cd .. && \ rm -rf ecl-16.1.3* WORKDIR /root CMD /usr/local/bin/ecl
上記のDockerfile
でイメージをビルドします。ビルドが完了するまでしばらく時間がかかります。
$ docker build -t ecl .
Docker上のECLでコンパイル
Dockerイメージがビルドできたら動かしてみます。ローカルのカレントディレクトリを/rootにマッピングしておきます。プロセスは使い捨てにするので--rmオプションを付けておきます。
デフォルトではECLが起動しますが、ここではbash経由で作業します。カレントディレクトリにはhello.lispが見えている前提です。bashからECLを立ち上げて、まずは先ほどと同じようにコンパイルします。実行ファイルはmain
という名称にしておきます。コンパイルが終わったら、ECLを抜けてmain
を実行してみます。
$ docker run -it -v $PWD:/root --rm ecl bash bash-4.2# ecl > (compile-file "hello.lisp" :system-p t) #P"/root/hello.o" NIL NIL > (c:build-program "main" :lisp-files '("hello.o")) #P"main" > ^D bash-4.2# ./main Hello, World!!
実行ファイルのサイズと、リンクされる共有ライブラリをチェックしておきます。
bash-4.2# ls -l main -rwxr-xr-x 1 root root 44224 Dec 17 18:15 main bash-4.2# ldd main linux-vdso.so.1 => (0x00007ffd86ff0000) libecl.so.16.1 => /usr/local/lib/libecl.so.16.1 (0x00007f7de8694000) libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f7de8478000) libdl.so.2 => /lib64/libdl.so.2 (0x00007f7de8274000) libm.so.6 => /lib64/libm.so.6 (0x00007f7de7f72000) libc.so.6 => /lib64/libc.so.6 (0x00007f7de7ba5000) /lib64/ld-linux-x86-64.so.2 (0x00007f7de8d1f000)
このうち/usr/local/lib/libecl.so.16.1
が追加で必要なECLのランタイムで、実行ファイルと一緒にAWS Lambdaにアップロードする必要があります。カレントディレクトリにコピーしておきます。ここでDocker上での作業はおしまいですので、Ctrl-Dで抜けます。
bash-4.2# cp /usr/local/lib/libecl.so.16.1 . bash-4.2# ^D
AWS Lambda関数を作成する
あとは、ホスト(OSX)側でzipでまとめます。コンパイル済みバイナリmain
と合わせて、シェルスクリプトbootstrap
と、先ほどコピーしたlibecl.so.16.1
を一緒のzipに入れます。
$ zip lambda.zip bootstrap main libecl.so.16.1 updating: bootstrap (deflated 36%) updating: main (deflated 57%) updating: libecl.so.16.1 (deflated 67%)
こちらのシェルスクリプトbootstrap
は、チュートリアルとまったく同じものです。chmod +xしておくのを忘れないようにします。
#!/bin/sh set -euo pipefail EXEC="$LAMBDA_TASK_ROOT/$_HANDLER" # Processing while true do HEADERS="$(mktemp)" # Get an event EVENT_DATA=$(curl -sS -LD "$HEADERS" -X GET "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next") REQUEST_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id "$HEADERS" | tr -d '[:space:]' | cut -d: -f2) # Execute the handler function from the script RESPONSE=$(echo "$EVENT_DATA" | $EXEC) # Send the response curl -X POST "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$REQUEST_ID/response" -d "$RESPONSE" done
そしてlambdaにアップロードします。
$ aws lambda create-function --function-name "lambda-ecl" \ --zip-file "fileb://lambda.zip" \ --handler "main" \ --runtime provided \ --role arn:aws:iam::XXXXXXXXXXXX:role/your-lambda-role
2回目以後でzipファイルを更新する場合は、update-functionを使います。
$ aws lambda update-function-code --function-name "lambda-ecl" --zip-file "fileb://lambda.zip"
さあ、AWS Lambda関数を実行してみましょう。マネージメントコンソールからTestボタンをクリックしてみます。
ちゃんと動いたようですね!
使ったファイル
以上の実験に使ったファイルはこちらに置いておきました。
まとめ
Common Lisp実装の一つであるECLをコンパイラとして使用し、できたバイナリをAWS Lambda環境で動作させることができました。
本当はもうちょっとLispらしいコードを動かしてみたかったのですが、実行環境を用意するので精一杯でした。インタープリタを直接動かす、またはECLのランタイムをLambda Layerにするなどしても面白いのではと思います。今後の宿題とさせてください。