これは 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 を使います

次の通り実行されました

