Lambdaカクテル

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

(更新あり)AWS LambdaでCommon Lispを動かす

[追記]IPv6を無効化することによりうまく動作させることができました.詳細は記事下方を参照してください.

いやー来ましたね.AWS Lambdaで新言語・・・あれ??

aws.amazon.com

ナンダテメッコラー!!Lambdaの名前を冠しておきながらAWS LambdaでCommon Lispが動かないとはどういうことなんだ. 幸いにもLambdaにはCustom Runtimeという仕組みがあり,自由な言語を動かすことができるようになりました. というわけでAWS Lambda上で動くCommon Lispプログラムを実装してみました. 俺だってLambdaを冠したブログやってるからな.責任がありますよ責任. Let Lambda over Lambda over Lambdaじゃん.

docs.aws.amazon.com

というわけで後学のためにもLambdaでCommon Lispを動かす実験を記録することにします.

TL;DR

結論から言うと微妙なところで動いたり動かなかったりする状態になった動いたぞ!!!.コードは以下にある. roswellがあればビルド可能だ.makeって叩いたらout.zipができるので,これをLambdaに入れてやればOKだ. ビルド系をDockerfileにしたらよかったけど,眠くてあきらめたDocker化したぞ!!!

github.com

予習: Lambdaで動かすために必要なもの

bootstrapという実行ファイル(動けばよいのでシェルスクリプトでもよい)を入れたzipファイルが必要.これさえあれば動く.

bootstrapを作る

bootstrapでやらなければならない仕事は以下の通り.

  • 環境変数を受け取る
    • _HANDLER
      • Lambdaのユーザが指定したハンドラ名が入っている.呼びたい関数名だと思ってもらえればさしつかえない
    • LAMBDA_TASK_ROOT
      • ここにchdirする必要がある,理由は知らん
    • AWS_LAMBDA_RUNTIME_API
      • ここを叩いて外界とやりとりする
  • イベントループを回す
    • _HANDLERをハンドルするなら,これを使って呼び出すべき処理を決定しておく
      • 決め打ちでもよい
    • AWS_LAMBDA_RUNTIME_API以下の特定のエンドポイントにGETすると処理するべきコンテンツがもらえるので,これを処理する
    • 処理ができたらAWS_LAMBDA_RUNTIME_API以下の特定のエンドポイントにPOSTする
      • これで処理ができたことになる

bootstrap.rosを用意して,これをビルドしてbootstrapを生成する作戦で行きましょう.以下のコマンドでbootstrap.rosを生成する.

ros init bootstrap

bootstrap.rosの内容はこんな感じ.

#!/bin/sh
#|-*- mode:lisp -*-|#
#|
exec ros -Q -- $0 "$@"
|#
(progn ;;init forms
  (ros:ensure-asdf)
;;; ライブラリをインストールする
;; dexadorはHTTP  ライブラリ
;; uiopはOSまわりを扱う汎用ユーティリティ
  #+quicklisp (ql:quickload '(:dexador :uiop) :silent t)
  )

(defpackage :ros.script.bootstrap.3758360166
  (:use :cl))
(in-package :ros.script.bootstrap.3758360166)

;;; 環境変数を入れる大域変数を用意しておく
(defvar *handler*)
(defvar *lambda-task-root*)
(defvar *aws-lambda-runtime-api*)

;; 環境変数ロードする君
(defun load-envvar ()
  (setf *handler*
        (uiop:getenv "_HANDLER"))
  (setf *lambda-task-root*
        (uiop:getenv "LAMBDA_TASK_ROOT"))
  (setf *aws-lambda-runtime-api*
        (uiop:getenv "AWS_LAMBDA_RUNTIME_API")))

;; メインループ.
(defun main-loop (handler)
  (declare (ignorable handler))
  (let ((next-endpoint
;; ここを叩くとLambda呼び出し時のコンテンツがもらえる
          (format nil "http://~A/2018-06-01/runtime/invocation/next" *aws-lambda-runtime-api*)))
;; 実際にdex:getで叩く
    (loop for (body status headers . nil) = (multiple-value-list (dex:get next-endpoint))
          as request-id = (gethash "Lambda-Runtime-Aws-Request-Id" headers)
          as response = "Hello, Lambda World from Yet Another Lambda World!"
;; ここを叩いてリクエスト完了とする
          as response-endpoint = (format nil "http://~A/2018-06-01/runtime/invocation/~A/response" *aws-lambda-runtime-api* request-id)
;; 実際に叩く
          do (dex:post response-endpoint :content response))))

;; entrypoint
;; roswellはmain関数をエントリポイントとしてバイナリを作るので,ここから実行される
(defun main ()
  (load-envvar)
  (uiop:chdir *lambda-task-root*)
  (main-loop *handler*))
;;; vim: set ft=lisp lisp:

bootstrap.rosができたのでビルドする.

ros build bootstrap.ros

ZIPを固める

基本的にはさっきのbootstrapをzipで固めれば完成だ.

このほか,HTTPライブラリのdexadorがいくつか共有ライブラリ(libcrypto, libssl)を要求するので,いっしょに同梱しておく.

この手順は面倒なのでMakefileにした.

.PHONY: default

default: out.zip

libcrypto.so.1.0.0: /lib64/libcrypto.so.1.0.0
    cp /lib64/libcrypto.so.1.0.0 .

libssl.so.1.0.0: /lib64/libssl.so.1.0.0
    cp /lib64/libssl.so.1.0.0 .

out.zip: bootstrap libcrypto.so.1.0.0 libssl.so.1.0.0
    zip out.zip bootstrap libcrypto.so.1.0.0 libssl.so.1.0.0

bootstrap: bootstrap.ros
    ros build bootstrap.ros

これでmake一発でout.zipを生成できるようになった.

make

実行

AWS Lambdaのコンソールで, カスタムランタイムとしてout.zipをアップロード(10MBくらいになった)して試しにtest実行してみた結果がこれ.

START RequestId: cae88257-a707-49ee-8cd0-8e28ae8b92a7 Version: $LATEST
Unhandled SIMPLE-ERROR in thread #<SB-THREAD:THREAD "main thread" RUNNING
                                    {1002D5CFA3}>:
  Syscall poll(2) failed: Operation not permitted

Backtrace for: #<SB-THREAD:THREAD "main thread" RUNNING {1002D5CFA3}>
0: (SB-DEBUG::DEBUGGER-DISABLED-HOOK #<SIMPLE-ERROR "Syscall poll(2) failed: ~A" {1003108703}> #<unused argument>)
1: (SB-DEBUG::RUN-HOOK SB-EXT:*INVOKE-DEBUGGER-HOOK* #<SIMPLE-ERROR "Syscall poll(2) failed: ~A" {1003108703}>)
2: (INVOKE-DEBUGGER #<SIMPLE-ERROR "Syscall poll(2) failed: ~A" {1003108703}>)
3: (ERROR "Syscall poll(2) failed: ~A" "Operation not permitted")
4: (SB-UNIX:UNIX-SIMPLE-POLL 3 :INPUT -1)
5: (SB-SYS:WAIT-UNTIL-FD-USABLE 3 :INPUT NIL NIL)
6: (SB-IMPL::REFILL-INPUT-BUFFER #<SB-SYS:FD-STREAM for "socket 127.0.0.1:40116, peer: 127.0.0.1:9001" {1003106693}>)
7: (SB-IMPL::INPUT-UNSIGNED-8BIT-BYTE #<SB-SYS:FD-STREAM for "socket 127.0.0.1:40116, peer: 127.0.0.1:9001" {1003106693}> NIL NIL)
8: (READ-BYTE #<SB-SYS:FD-STREAM for "socket 127.0.0.1:40116, peer: 127.0.0.1:9001" {1003106693}> NIL NIL)
9: (DEXADOR.BACKEND.USOCKET::READ-UNTIL-CRLF*2 #<SB-SYS:FD-STREAM for "socket 127.0.0.1:40116, peer: 127.0.0.1:9001" {1003106693}>)
10: (DEXADOR.BACKEND.USOCKET::READ-RESPONSE #<SB-SYS:FD-STREAM for "socket 127.0.0.1:40116, peer: 127.0.0.1:9001" {1003106693}> T NIL T)
11: (DEXADOR.BACKEND.USOCKET:REQUEST #<unavailable argument> :METHOD :GET)
12: (MAIN-LOOP #<unused argument>)
13: (MAIN)
14: (ROSWELL::ENTRY "ROS.SCRIPT.BOOTSTRAP.3758360166::MAIN")
15: (ROSWELL:RUN ((:ENTRY "ROS.SCRIPT.BOOTSTRAP.3758360166::MAIN")))
16: ((FLET "WITHOUT-INTERRUPTS-BODY-26" :IN SB-EXT:SAVE-LISP-AND-DIE))
17: ((LABELS SB-IMPL::RESTART-LISP :IN SB-EXT:SAVE-LISP-AND-DIE))

unhandled condition in --disable-debugger mode, quitting
END RequestId: cae88257-a707-49ee-8cd0-8e28ae8b92a7
REPORT RequestId: cae88257-a707-49ee-8cd0-8e28ae8b92a7  Duration: 448.42 ms Billed Duration: 500 ms     Memory Size: 128 MB Max Memory Used: 80 MB  
RequestId: cae88257-a707-49ee-8cd0-8e28ae8b92a7 Error: Runtime exited with error: exit status 1
Runtime.ExitError

死んでんじゃねえか!ふざけんな!

とはいえCommon Lisp自体は動いているようだ!やったぜ.

しかしpoll(2)の実行に失敗しており,リクエストコンテンツの取得には至らなかったようだ.

これには同様の報告がいくつか存在しており,Lambdaのクセのようだ.

[http://y2q-actionman.hatenablog.com/entry/2018/12/06/AWS_Lambda%E3%81%AE_Custom_Runtime%E3%81%A8%E3%81%97%E3%81%A6_Common_Lisp(sbcl)%E3%82%92%E4%BD%BF%E3%81%86:title]

blog.marshallbrekka.com

後者については,Erlang処理系での記事なのでそのまま適用できるかは分からないが,IPv6サポートを切れば動くのではという示唆を与えてくれた.しかしIPv6サポートの切り方がわからなかったので諦めた.IPv6サポートを切ることができ,動作に成功したので後述する.

ひとまずCommon Lispが動いたのでめでたい.よかったですね.

まとめ

  • LambdaでCommon Lisp(SBCL, ClozureCL)は動作する
  • 動作するがソケットまわりの不具合で完動しない
  • もう一声なのでこれからに期待

参考にした記事

[http://y2q-actionman.hatenablog.com/entry/2018/12/06/AWS_Lambda%E3%81%AE_Custom_Runtime%E3%81%A8%E3%81%97%E3%81%A6_Common_Lisp(sbcl)%E3%82%92%E4%BD%BF%E3%81%86:title]

blog.marshallbrekka.com

www.m3tech.blog

qiita.com

追記 2019-02-07

この挙動はIPv6が正常に動作していないからではないかと推測していましたが,この推測はかなり当たっているようすです.同様のトラブルをまた見付けました.

github.com

さて,処理に問題が発生しているのはHTTPクライアントであるdexadorが使用している汎用ソケットライブラリusocketです.このコードを読み解いていると,以下のようなfeatureの切り替えコードがあるのを発見しました.

github.com

まず軽く*feature*について説明しましょう.*feature*とは特殊なスペシャル変数で,処理系が利用することができる機能がキーワードの形で格納されています. よくある利用方法としては処理系の判別に使われます.処理系が何であるかも,処理系が利用することができる機能として表しているのですね. 例えばsbclでのみ実行したい処理,あるいは実行したくない処理はリーダマクロ#+/#-を使って以下のように表現できます.

#+sbcl
(print "only sbcl")

#-sbcl
(print "all but sbcl")

これは*feature*:SBCLが格納されているかどうかで,読み取り時(実行時ではない)に式を評価するかどうか決定しているのです.

さて,ClozureCLという処理系ではIPv6をサポートするかどうかを:IPV6というfeatureで表現しています(featureにどういう名前を使うかは処理系依存で,可搬性はなさそうです).そしてusocketもこのフラグを利用してIPv6を使うかを判定しているようです. したがって,強制的に:IPV6*feature*から取り上げた後でライブラリを読み込ませれば,IPv6サポート無しのusocketをインストールできます.

IPv6サポートを外すために,処理系をClozureCLに変更し,スクリプトに手を入れます.

まずDockerfileを修正します.eshamsterさんのベースイメージをお借りします.

FROM eshamster/cl-base

ENV libs 'openssl-dev'
RUN apk update \
  && apk add ${libs}

# ClozureCLを使う
RUN ros install ccl-bin/1.11
RUN ros use ccl-bin/1.11

# Assuming whole application directory is mounted as /app
WORKDIR /app/

CMD /bin/sh

そしてroswellスクリプトを修正します. roswellスクリプトでライブラリがインストールされるタイミングは(ql:quickload)のタイミングなので,それより前に:IPV6を取り除きます.

(progn ;;init forms
  (ros:ensure-asdf)
  (setf *features* (delete :ipv6 *features*))
  #+quicklisp (ql:quickload '(:dexador :uiop) :silent nil)
  )

これでmakeします.

make

こうして得られたzipファイルをアップロードして動作を確認したところ………

やったぜ.SBCLでも同じことができれば同様の結果を得られそうです.