これは Java Advent Calendar 2018 の 7 日目のエントリーです。
Java をラムダで動かす
Lambda SDK を使って、チュートリアルどおりに作成します。以上。
そうではないですね。
今回やりたいのは AWS Lambda のカスタムランタイムで Java のカスタムランタイムで動く関数を動かすことです。これを会社の同僚に言ったところ、「おまえは何を言っているんだ」というような顔をされました。
大事なことなのでもう一度言いますが、「 AWS のカスタムランタイムで Java のカスタムランタイムを動かしたい!」、これが今回のエントリーの目標です。
Java 9 のモジュールシステムと jlink
によって、アプリケーションが利用する必要最低限のモジュールだけを選別したカスタムランタイムイメージが作れるようになりました。カスタムランタイムはフットプリントを軽くできるので、ロードする時間も短くなることから多分起動時間も短くなるはずです。そして、ラムダのような呼び出されてから起動を開始するようなモデルに、カスタムランタイムはマッチしているわけで、これを使わないわけにはいきません。
ランタイムの作り方
AWS のドキュメントの以下のページを読むと、大まかな作り方がわかります。また、「AWS Lambda カスタムランタイム」で検索すると、様々な人がすでに挑戦していますので、参考にされるとよいと思います。
ポイントとしては、以下のとおりです。
bootstrap
という名前のスクリプト(あるいは実行バイナリー)が呼び出される- 以下の処理をループで実行する
- API エンドポイント
http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next
にGET
でアクセスして、リクエストID と リクエストペイロード(たいていはjson形式)を取得する - 関数を呼び出す
- API エンドポイント
http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/リクエストID/response
にPOST
でレスポンスを返す - API エンドポイント
http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/リクエストID/error
にPOST
でエラーを返す
- API エンドポイント
これをざっくり1ファイル/クラスで書くとこんな感じになります。ただし、今回はエラー処理を入れていません。
import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; public class LambdaApp { @SuppressWarnings("InfiniteLoopStatement") public static void main(String[] args) { System.out.println("Start lambda"); final String awsLambdaRuntimeApi = System.getenv("AWS_LAMBDA_RUNTIME_API"); if (awsLambdaRuntimeApi == null) { System.out.println("error AWS_LAMBDA_RUNTIME_API is not available."); System.exit(1); } System.out.println(awsLambdaRuntimeApi); final HttpClient client = HttpClient.newHttpClient(); System.out.println("client prepared."); while (true) { final URI uri = URI.create("http://" + awsLambdaRuntimeApi + "/2018-06-01/runtime/invocation/next"); System.out.println("uri : " + uri); final HttpRequest getEvent = HttpRequest.newBuilder(uri).GET().build(); try { final HttpResponse<String> response = client.send(getEvent, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); final String requestId = response.headers().firstValue("Lambda-Runtime-Aws-Request-Id").orElseThrow(); final String body = response.body(); System.out.println(body); final String payload = "{\"receive\":" + body + "}"; final URI resultUrl = URI.create( "http://" + awsLambdaRuntimeApi + "/2018-06-01/runtime/invocation/" + requestId + "/response"); final HttpRequest request = HttpRequest.newBuilder(resultUrl) .POST(HttpRequest.BodyPublishers.ofString(payload, StandardCharsets.UTF_8)) .build(); final HttpResponse<String> result = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); System.out.println(result.statusCode()); System.out.println(result.body()); } catch (InterruptedException | IOException e) { e.printStackTrace(); } } } }
このクラスを含むプロジェクトには次のような module-info.java
をつけました。
module lambda.example { requires java.base; requires java.net.http; }
さて、このプログラムで使っているのは上記のモジュールだけですので、 jlink
コマンドでそれらを指定して、カスタムランタイムを作ります。
jlink --compress=2 \ --module-path ${JAVA_HOME}/jmods \ --add-modules java.base,java.net.http \ --output lambda-custom-java-runtime
これでカスタムランタイムができました。
ちなみに lambda-custom-java-runtime
ディレクトリーの大きさを見てみると 28 MB くらいに収まっているようです。
比較として、比較対象としては適切ではありませんが、通常の JDK の jmods の大きさは 77 MB くらいあるようです。
このアプリケーションを起動するために次のような bootstrap
をシェルで組みます。
#!/usr/bin/env bash JAVA=./lambda-custom-java-runtime/bin/java ${JAVA} -p java-custom-runtime.jar -m lambda.example
たぶん、これで動くはずですが、普段は Mac を使っている僕はここから先が大変です。
ビルド
Mac でビルドしたクラスファイルは特に問題がありませんが、Mac 用の java
では Linux を使っている Lambda 上では動かせません。したがって、ビルドは Linux 上でやることになります。
今回は最初 Docker でやろうとしたのですが、(typo していてうまく動かせなかったため) Amazon Linux 上でビルドしようと考え、 ec2 に立てた Amazon Linux 上でビルドしました。
その際に、次のコマンドでカスタムランタイムを作ります。
MODULES=$(jdeps --list-deps build/libs/java-custom-runtime.jar | tr "\n" "," | tr -d [:space:]) jlink --compress=2 \ --module-path ${JAVA_HOME}/jmods \ --add-modules ${MODULES} \ --output build/mod/lambda-custom-java-runtime
リリース/デプロイ
次のように、成果物をあつめて、 zip で固めます。
bootstrap java-custom-runtime.jar lambda-custom-java-runtime
zip lambda.zip bootstrap java-custom-runtime.jar
zip -r lambda.zip lambda-custom-java-runtime
あとは、 aws
コマンドを叩くだけです。
aws lambda create-function \ --function-name java11-custom-runtime \ --runtime provided \ --role 適切なロールを見繕って指定 \ --zip-file fileb://lambda.zip \ --handler hogehoge #今回は汎用的な仕組みではないので、ハンドラーの名前は適当
今回は単なるオウム返しするだけの関数なのでテストデータもデフォルトの json を使います
次の通り実行されました