【完結編】QuarkusでネイティブコンパイルしてLambdaとDynamoDBを連携する

2019年3月のQuarkus公開から、AWS Lambda上でQuarkusでネイティブコンパイルしたJavaアプリを動作させるべく戦いを繰り広げていた。今回ようやくDynamoDBとの連携を実装することができた。

 

 

[これまでの戦いの歴史]

第1部の戦いはここから。Quarkusバージョン0.11.0の時代。

初戦はネイティブコンパイルしたアプリはAWS Lambda上では全く動かず、惨敗。 

kdnakt.hatenablog.com

  

第1部・完。Quarkusバージョン1.0.0の時代。

意外とあっさり動いてしまって拍子抜け。

kdnakt.hatenablog.com

 

第2部開始!

Lambda上でただネイティブコンパイルしたアプリを動かしても大したことはできない。より発展的なアプリを開発すべく、DynamoDBとの連携を試みた……が、惜敗。

kdnakt.hatenablog.com

  

[vert.xのエラー]

というわけで、2週間ぶりに前回のつづきである。

quarkus-amazon-lambdaエクステンションMaven Archetypeを利用して作成したプロジェクトに、DynamoDBに接続するためのコードを実装して、ビルドしたものをAWS Lambda上にデプロイした。

 

しかし、実行すると下記のエラーが発生した。

java.lang.IllegalStateException: Failed to create cache dir
	at io.vertx.core.file.impl.FileResolver.setupCacheDir(FileResolver.java:332)
	at io.vertx.core.file.impl.FileResolver.<init>(FileResolver.java:87)
	at io.vertx.core.impl.VertxImpl.<init>(VertxImpl.java:165)
	at io.vertx.core.impl.VertxImpl.vertx(VertxImpl.java:92)
	at io.vertx.core.impl.VertxFactoryImpl.vertx(VertxFactoryImpl.java:40)
	at io.vertx.core.impl.VertxFactoryImpl.vertx(VertxFactoryImpl.java:32)
	at io.vertx.core.Vertx.vertx(Vertx.java:85)
	at io.quarkus.vertx.core.runtime.VertxCoreRecorder.initializeWeb(VertxCoreRecorder.java:105)
	at io.quarkus.vertx.core.runtime.VertxCoreRecorder.initializeWeb(VertxCoreRecorder.java:87)
	at io.quarkus.deployment.steps.VertxCoreProcessor$buildWeb33.deploy_0(VertxCoreProcessor$buildWeb33.zig:71)
	at io.quarkus.deployment.steps.VertxCoreProcessor$buildWeb33.deploy(VertxCoreProcessor$buildWeb33.zig:96)
	at io.quarkus.runner.ApplicationImpl.doStart(ApplicationImpl.zig:145)
	at io.quarkus.runtime.Application.start(Application.java:94)
	at io.quarkus.runtime.Application.run(Application.java:218)
	at io.quarkus.runner.GeneratedMain.main(GeneratedMain.zig:41)
Exception in thread "main" java.lang.RuntimeException: Failed to start quarkus
	at io.quarkus.runner.ApplicationImpl.doStart(ApplicationImpl.zig:273)
	at io.quarkus.runtime.Application.start(Application.java:94)
	at io.quarkus.runtime.Application.run(Application.java:218)
	at io.quarkus.runner.GeneratedMain.main(GeneratedMain.zig:41)

 

前回のブログを投稿したところ、Twitterkuzuhaさんからこちらのページを教えていただいた。

 

参考に実装してみたところ、application.propertiesファイルにquarkus.vertx.classpath-resolving=falseのオプションを追加すると、Vert.xのエラーは表示されなくなった。

ただし、今度はエラーの内容が変化して、CloudWatch LogsのLambda実行ログに次のエラーが表示されるようになった。

WARNING: The sunec native library, required by the SunEC provider, could not be loaded. This library is usually shipped as part of the JDK and can be found under <JAVA_HOME>/jre/lib/<platform>/libsunec.so. It is loaded at run time via System.loadLibrary("sunec"), the first time services from SunEC are accessed. To use this provider's services the java.library.path system property needs to be set accordingly to point to a location that contains libsunec.so. Note that if java.library.path is not set it defaults to the current working directory.
2019-12-21 05:37:23,594 INFO  [io.quarkus] (main) quarkus-lambda-dynamodb 1.0-SNAPSHOT (running on Quarkus 1.1.0.Final) started in 0.238s. Listening on: http://0.0.0.0:8080
2019-12-21 05:37:23,599 INFO  [io.quarkus] (main) Profile prod activated. 
2019-12-21 05:37:23,599 INFO  [io.quarkus] (main) Installed features: [amazon-lambda, cdi, dynamodb, resteasy, resteasy-jsonb]
START RequestId: a9f3bd47-f081-4d0f-a9a1-2d74fa50e353 Version: $LATEST
2019-12-21 05:37:26,451 ERROR [io.qua.ama.lam.run.AmazonLambdaRecorder] (Lambda Thread) Failed to run lambda: software.amazon.awssdk.core.exception.SdkClientException: Unable to execute HTTP request: java.lang.RuntimeException: Unexpected error: java.security.InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty
	at software.amazon.awssdk.core.exception.SdkClientException$BuilderImpl.build(SdkClientException.java:97)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.RetryableStage$RetryExecutor.handleThrownException(RetryableStage.java:136)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.RetryableStage$RetryExecutor.execute(RetryableStage.java:94)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.RetryableStage.execute(RetryableStage.java:62)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.RetryableStage.execute(RetryableStage.java:42)
	at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206)
	at software.amazon.awssdk.core.internal.http.StreamManagingStage.execute(StreamManagingStage.java:57)
	at software.amazon.awssdk.core.internal.http.StreamManagingStage.execute(StreamManagingStage.java:37)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallTimeoutTrackingStage.executeWithTimer(ApiCallTimeoutTrackingStage.java:80)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallTimeoutTrackingStage.execute(ApiCallTimeoutTrackingStage.java:60)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallTimeoutTrackingStage.execute(ApiCallTimeoutTrackingStage.java:42)
	at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206)
	at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.ExecutionFailureExceptionReportingStage.execute(ExecutionFailureExceptionReportingStage.java:37)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.ExecutionFailureExceptionReportingStage.execute(ExecutionFailureExceptionReportingStage.java:26)
	at software.amazon.awssdk.core.internal.http.AmazonSyncHttpClient$RequestExecutionBuilderImpl.execute(AmazonSyncHttpClient.java:240)
	at software.amazon.awssdk.core.client.handler.BaseSyncClientHandler.invoke(BaseSyncClientHandler.java:96)
	at software.amazon.awssdk.core.client.handler.BaseSyncClientHandler.execute(BaseSyncClientHandler.java:120)
	at software.amazon.awssdk.core.client.handler.BaseSyncClientHandler.execute(BaseSyncClientHandler.java:73)
	at software.amazon.awssdk.core.client.handler.SdkSyncClientHandler.execute(SdkSyncClientHandler.java:44)
	at software.amazon.awssdk.awscore.client.handler.AwsSyncClientHandler.execute(AwsSyncClientHandler.java:55)
	at software.amazon.awssdk.services.dynamodb.DefaultDynamoDbClient.getItem(DefaultDynamoDbClient.java:1753)
	at io.quarkus.dynamodb.runtime.DynamodbClientProducer_ProducerMethod_client_15a968126f29bfeefe628e806398394e57fc3411_ClientProxy.getItem(DynamodbClientProducer_ProducerMethod_client_15a968126f29bfeefe628e806398394e57fc3411_ClientProxy.zig:26)
	at com.kdnakt.dynamodb.FruitSyncService.get(FruitSyncService.java:29)
	at com.kdnakt.dynamodb.FruitSyncService_ClientProxy.get(FruitSyncService_ClientProxy.zig:38)
	at com.kdnakt.DynamodbLambda.handleRequest(DynamodbLambda.java:19)
	at com.kdnakt.DynamodbLambda.handleRequest(DynamodbLambda.java:1)
	at io.quarkus.amazon.lambda.runtime.AmazonLambdaRecorder$2.run(AmazonLambdaRecorder.java:152)
	at java.lang.Thread.run(Thread.java:748)
	at com.oracle.svm.core.thread.JavaThreads.threadStartRoutine(JavaThreads.java:460)
	at com.oracle.svm.core.posix.thread.PosixJavaThreads.pthreadStartRoutine(PosixJavaThreads.java:193)
Caused by: javax.net.ssl.SSLException: java.lang.RuntimeException: Unexpected error: java.security.InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty
	at sun.security.ssl.Alerts.getSSLException(Alerts.java:208)
	at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1946)
	at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1903)
	at sun.security.ssl.SSLSocketImpl.handleException(SSLSocketImpl.java:1886)
	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1402)
	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1379)
	at org.apache.http.conn.ssl.SSLConnectionSocketFactory.createLayeredSocket(SSLConnectionSocketFactory.java:436)
	at org.apache.http.conn.ssl.SSLConnectionSocketFactory.connectSocket(SSLConnectionSocketFactory.java:384)
	at software.amazon.awssdk.http.apache.internal.conn.SdkTlsSocketFactory.connectSocket(SdkTlsSocketFactory.java:113)
	at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:142)
	at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:374)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at software.amazon.awssdk.http.apache.internal.conn.ClientConnectionManagerFactory$Handler.invoke(ClientConnectionManagerFactory.java:80)
	at com.sun.proxy.$Proxy179.connect(Unknown Source)
	at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:393)
	at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:236)
	at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:186)
	at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185)
	at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83)
	at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:56)
	at software.amazon.awssdk.http.apache.internal.impl.ApacheSdkHttpClient.execute(ApacheSdkHttpClient.java:72)
	at software.amazon.awssdk.http.apache.ApacheHttpClient.execute(ApacheHttpClient.java:240)
	at software.amazon.awssdk.http.apache.ApacheHttpClient.access$500(ApacheHttpClient.java:106)
	at software.amazon.awssdk.http.apache.ApacheHttpClient$1.call(ApacheHttpClient.java:221)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.MakeHttpRequestStage.executeHttpRequest(MakeHttpRequestStage.java:66)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.MakeHttpRequestStage.execute(MakeHttpRequestStage.java:51)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.MakeHttpRequestStage.execute(MakeHttpRequestStage.java:35)
	at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206)
	at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206)
	at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206)
	at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallAttemptTimeoutTrackingStage.execute(ApiCallAttemptTimeoutTrackingStage.java:73)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallAttemptTimeoutTrackingStage.execute(ApiCallAttemptTimeoutTrackingStage.java:42)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.TimeoutExceptionHandlingStage.execute(TimeoutExceptionHandlingStage.java:77)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.TimeoutExceptionHandlingStage.execute(TimeoutExceptionHandlingStage.java:39)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.RetryableStage$RetryExecutor.doExecute(RetryableStage.java:113)
	at software.amazon.awssdk.core.internal.http.pipeline.stages.RetryableStage$RetryExecutor.execute(RetryableStage.java:86)
	... 28 more
Caused by: java.lang.RuntimeException: Unexpected error: java.security.InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty
	at sun.security.validator.PKIXValidator.(PKIXValidator.java:91)
	at sun.security.validator.Validator.getInstance(Validator.java:181)
	at sun.security.ssl.X509TrustManagerImpl.getValidator(X509TrustManagerImpl.java:318)
	at sun.security.ssl.X509TrustManagerImpl.checkTrustedInit(X509TrustManagerImpl.java:179)
	at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:193)
	at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:132)
	at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1621)
	at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:223)
	at sun.security.ssl.Handshaker.processLoop(Handshaker.java:1037)
	at sun.security.ssl.Handshaker.process_record(Handshaker.java:965)
	at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1064)
	at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1367)
	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1395)
	... 60 more
Caused by: java.security.InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty
	at java.security.cert.PKIXParameters.setTrustAnchors(PKIXParameters.java:200)
	at java.security.cert.PKIXParameters.(PKIXParameters.java:120)
	at java.security.cert.PKIXBuilderParameters.(PKIXBuilderParameters.java:104)
	at sun.security.validator.PKIXValidator.(PKIXValidator.java:89)
	... 72 more
END RequestId: a9f3bd47-f081-4d0f-a9a1-2d74fa50e353

 

[HTTPS通信の罠]

一番最後にログに出力されている根本原因と思われるInvalidAlgorithmParameterExceptionで検索するとQuarkusの公式サイトにたどり着くことができた。

 

ネイティブバイナリでSSL通信する方法……これこそ求めていた内容!

 

To make it work, you need to manually set java.library.path and javax.net.ssl.trustStore to point to the new GraalVM home:


./target/rest-client-1.0-SNAPSHOT-runner -Djava.library.path=<new-graalvm-home>/jre/lib/amd64 -Djavax.net.ssl.trustStore=<new-graalvm-home>/jre/lib/security/cacerts

どうやら、ネイティブコンパイルしたバイナリを呼び出す際に、java.library.pathjavax.net.ssl.trustStoreの2つのJVM引数を設定すれば良さそうだ。

  

Quarkusのamazon-lambdaエクステンションで作ったプロジェクトは、ネイティブコンパイルしたバイナリをMaven Assemblyプラグインを利用して、AWS Lambda用のzipファイルを作成している。

この機構を利用して、cacertsファイルと、必要なネイティブライブラリをzipに詰め込むことにする。

 

これによると、Quarkus(が利用しているGraalVM)でネイティブコンパイルしたときは、SSL通信時にlibsunec.soというライブラリが必要になるらしい。 

確かに、先ほどのログの先頭を見ると、次のようにsunecネイティブライブラリをロードできなかったjava.library.pathシステムプロパティを利用してlibsunec.soファイルの位置を伝えよ、と警告が出ている。

WARNING: The sunec native library, required by the SunEC provider, could not be loaded. This library is usually shipped as part of the JDK and can be found under <JAVA_HOME>/jre/lib/<platform>/libsunec.so. It is loaded at run time via System.loadLibrary("sunec"), the first time services from SunEC are accessed. To use this provider's services the java.library.path system property needs to be set accordingly to point to a location that contains libsunec.so. Note that if java.library.path is not set it defaults to the current working directory.

 

ローカルのMacBookにインストールしたJavaディレクトリを掘っていくと、近いもの(libsunec.dylib)は見つかったが、探しているファイルは見つけられなかった。

OSによって異なる部分のようなので、なんとかしてLinux用のJavaからlibsunec.soを抜き出してくる必要がある。

 

LinuxマシンをEC2で立てるのは流石に無駄なので、dockerイメージを利用する。Java 11のイメージでも良さそうだが、安心感のあるJava 8を利用することに。

 

$ docker run -it openjdk/8-jre /bin/bash

こちらのコマンドでJavaのインストールされたLinuxコンテナを起動する。

 

今回はホストマシンであるMacBook側にコピーする

プロジェクトのディレクトリで次のコマンドを実行し、目的のファイルをコピーする。

$ sudo docker cp <コンテナID>:/usr/local/openjdk-8/lib/amd64/libsunec.so .

 

のちのち他のネイティブライブラリが必要になったときのことも考えて、プロジェクト直下にamd64というディレクトリを作成し、コピーしてきたlibsunec.soファイルをそこに移動させた。

 

次に、AWS Lambdaカスタムランタイム用のbootstrapファイルを用意する。

#!/bin/sh
./runner -Djavax.net.ssl.trustStore=./cacerts

 

先ほどのWARNINGログによると、java.library.pathプロパティを設定しない場合は、カレントディレクトリがデフォルトとして利用される、とある。したがって、zipファイル内に次のようにファイルが含まれるように、zip.xmlを修正する。

├── bootstrap
├── cacerts
├── libsunec.so
└── runner

 

add JVM args to bootstrap for HTTPS call to DynamoDB · kdnakt/quarkus-sandbox@eac39e1 · GitHub

修正の詳細はリンク先のリポジトリに譲るとして、修正の概要は次の通り。

  • bootstrapファイルの内容に合わせて、ネイティブバイナリの名前をbootstrapからrunnerに変更
  • cacertsファイルに読み取り権限を付与
  • libsunec.soファイルを配置したamd64ディレクトリはfileSetsタグを用いてディレクトリの中身をまるごとzipに含める

 

こうして作成したzipをAWS Lambdaにデプロイすると、無事DynamoDBからデータを取得することができた。

これができれば、簡単なWeb APIを実装することができそうだ。

 

[Java 8ランタイムと速度比較]

ついでにJava 8ランタイムで同じ実装をデプロイしたときの速度比較を簡単にやってみた結果を載せておく。

結論から言うと、以前シンプルに入力値を受け取って返すだけのLambdaで検証したときJava 8ランタイムの方がネイティブコンパイルしたカスタムランタイムよりも早かったが、今回は逆の結果となった。

 

Java 8ランタイムを利用してコールドスタート込みで、連続で5回関数を呼び出した結果は次の通り。

  • 1回目:8811.53 ms(+Init Duration 3361.59 ms)
  • 2回目:    36.27 ms
  • 3回目:    16.40 ms
  • 4回目:    35.63 ms
  • 5回目:    72.80 ms

 

ネイティブコンパイルしたカスタムランタイムを利用してコールドスタート込みで、連続で5回関数を呼び出した結果は次の通り。

  • 1回目:227.39 ms(+Init Duration 344.25 ms)
  • 2回目:  78.63 ms
  • 3回目:  13.51 ms
  • 4回目:    5.41 ms
  • 5回目:    4.86 ms

 

平均しても明らかにカスタムランタイムの方が速いし、コールドスタート時に至っては桁違いに速度が違う。最高速度もカスタムランタイムの方が圧倒的に速い。

ウォームアップされたJava 8ランタイムも十分に速いが、今後アプリケーションがより複雑になった場合、1度のLambda関数の実行で複数回のHTTPS通信が発生するようなケースを考えると、ネイティブコンパイルの方に分があるのかもしれない。

 

[まとめ]

というわけで、QuarkusでネイティブコンパイルしたJavaアプリをDynamoDBと連携しつつAWS Lambda上で動作させることができた。祝、第2部・完🎉

  • vert.xのIllegalStateException: Failed to create cache dirはquarkus.vertx.classpath-resolving=falseオプションで回避できる
  • InvalidAlgorithmParameterException: the trustAnchors parameter must be non-emptyはcacertsとlibsunec.soを組み合わせることで回避できる
  • HTTPS通信を行うアプリの場合、AWS LambdaのJava 8ランタイムよりネイティブコンパイルしたカスタムランタイムの方が速そう

最終的なコードはGitHubにまとめてある。

 

2019年これでもう思い残すことはない😇

 

2020年はより発展的なアプリを実装するぞ!