この記事はAWS LambdaとServerless #1 Advent Calendar 2019の15日目の記事です。
- [やること]
- [QuarkusでDynamoDBを利用する]
- [DynamoDBクライアントとネイティブコンパイル]
- [Lambda: Java 8ランタイム]
- [Lambda: カスタムランタイム]
- [まとめ]
[やること]
先月1.0がリリースされたQuarkusで、ネイティブコンパイルしたJavaアプリをAWS Lambdaにデプロイしました。
Quarkus自体の簡単な説明や解説サイトへのリンクはこちらの記事にあります。
前回作ったのはリクエストを受け取って文字列を結合するだけの非常にシンプルなアプリでした。
今回は、もう一歩踏み込んで、Amazon DynamoDBとの連携を目指します。
[QuarkusでDynamoDBを利用する]
Quarkusは様々な既存のJavaライブラリと連携するために、拡張機能が用意されています。
こちらのサイトを利用すると、必要な拡張機能にチェックをつけるだけで、Javaプロジェクトをzipでダウンロードすることができます。
が、今回はGitHubにて公開されているドキュメントを参照しながらプロジェクトを作成していきます。
quarkus/dynamodb.adoc at master · quarkusio/quarkus · GitHub
前提として、DynamoDB Localをセットアップしておきます。共有モードで利用するため、公式サイトからjarファイルをダウンロードしてきて-sharedDb
オプションつきで起動するか、Localstackを利用します。
DynamoDB Localを用意できたらQuarkusFruitsテーブルを作成します。DynamoDB NoSQL Workbenchで利用可能なデータモデルは次のようになります。
{ "ModelName": "QuarkusFruits", "ModelMetadata": { "Author": "kdnakt", "DateCreated": "Dec 07, 2019, 1:11 PM", "DateLastModified": "Dec 07, 2019, 3:38 PM", "Description": "" }, "DataModel": [ { "TableName": "QuarkusFruits", "KeyAttributes": { "PartitionKey": { "AttributeName": "fruitName", "AttributeType": "S" } }, "TableData": [ { "fruitName": { "S": "orange" }, "fruitDescriptio": { "S": "This is an orange!" } } ], "DataAccess": { "MySql": {} } } ] }
こちらのjsonをNoSQL Workbench からインポートすると、DynamoDB Localだけでなく任意のAWSアカウントのDynamoDBにテーブルを作成することができます。
テーブルを用意できたら、Javaプロジェクトを作成していきます。ここではドキュメントに従ってMavenを利用します。2019年12月15日現在のQuarkusの最新の安定版は1.0.1.Final
なので、コマンドは次のようになります。
$ mvn io.quarkus:quarkus-maven-plugin:1.0.1.Final:create -DprojectGroupId=com.kdnakt -DprojectArtifactId=dynamodb-fruits-sample -DclassName="com.kdnakt.dynamodb.FruitResource" -Dpath="/fruits" -Dextensions="resteasy-jsonb,dynamodb"
実行すると次のようなJavaクラスを含んだプロジェクトが作成されます。
package com.kdnakt.dynamodb; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; @Path("/fruits") public class FruitResource { @GET @Produces(MediaType.TEXT_PLAIN) public String hello() { return "hello"; } }
実にシンプルで、/fruits
というパスにGETリクエストを送ると、helloという文字列を返すだけの実装で、AWS SDKの影も形もありません。ドキュメントにしたがって、Fruitオブジェクトを追加しDynamoDBクライアントを利用すると、このような実装となります。
ドキュメントにはDynamoDBの非同期クライアントを利用した実装方法についても説明がありましたが本記事では省略します。
いずれのDynamoDBクライアントを利用する場合であっても、HTTPクライアントの実装クラスを依存関係に含める必要があります。
ドキュメント中で説明されていたのはURL connection HTTPクライアントとApache HTTPクライアントでしたが、私は後者を利用したので、次のような実装となりました。
続いて、テストコードを修正します。
QuarkusFruitsテーブルにデータがない状態であれば空の配列が返ってくるので、そのように修正します。Quarkusは通常起動時のポートは8080ですが、テスト時は8081ポートを対象とするため、quarkus.http.test-port=8083
という設定をapplication.propertiesファイルに追記しています。
これで、./mvnw clean package
コマンドを実行するとテストが通ることを確認できるようになりました(実際にはもう少しテストケースを追加しています)。
[DynamoDBクライアントとネイティブコンパイル]
ただし、このままではネイティブコンパイル(./mvnw clean package -Pnative
)が次のエラーで終了してしまいます。
Caused by: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: com.oracle.graal.pointsto.constraints.UnresolvedElementException: Discovered unresolved type during parsing: software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient. To diagnose the issue you can use the --allow-incomplete-classpath option. The missing type is then reported at run time when it is accessed the first time. Detailed message: Trace: at parsing io.quarkus.dynamodb.runtime.DynamodbClientProducer.createUrlConnectionClientBuilder(DynamodbClientProducer.java:118) Call path from entry point to io.quarkus.dynamodb.runtime.DynamodbClientProducer.createUrlConnectionClientBuilder(SyncHttpClientConfig): at io.quarkus.dynamodb.runtime.DynamodbClientProducer.createUrlConnectionClientBuilder(DynamodbClientProducer.java:118) at io.quarkus.dynamodb.runtime.DynamodbClientProducer.initHttpClient(DynamodbClientProducer.java:109) at io.quarkus.dynamodb.runtime.DynamodbClientProducer.client(DynamodbClientProducer.java:49) at io.quarkus.dynamodb.runtime.DynamodbRecorder.createClient(DynamodbRecorder.java:23) at io.quarkus.deployment.steps.DynamodbProcessor$buildClients21.deploy_0(DynamodbProcessor$buildClients21.zig:79) at io.quarkus.deployment.steps.DynamodbProcessor$buildClients21.deploy(DynamodbProcessor$buildClients21.zig:128) at io.quarkus.runner.ApplicationImpl.doStart(ApplicationImpl.zig:196) 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) at com.oracle.svm.core.JavaMainWrapper.runCore(JavaMainWrapper.java:151) at com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:186) at com.oracle.svm.core.code.IsolateEnterStub.JavaMainWrapper_run_5087f5482cc9a6abc971913ece43acb471d2631b(generated:0) at com.oracle.graal.pointsto.constraints.UnsupportedFeatures.report(UnsupportedFeatures.java:130) at com.oracle.graal.pointsto.BigBang.finish(BigBang.java:565) at com.oracle.svm.hosted.NativeImageGenerator.runPointsToAnalysis(NativeImageGenerator.java:688) ... 7 more
詳しくは確認していないのですが、おそらくデフォルトのURL connection HTTPクライアントへの依存が解決できず、エラーになっているものと思われます。
エラーメッセージにしたがって、pom.xmlにadditionalBuildArgs
タグを利用して--allow-incomplete-classpath
オプションを追加するとnative-imageを作成することができます。
<execution> <goals> <goal>build</goal> </goals> <configuration> <additionalbuildargs> --allow-incomplete-classpath </additionalbuildargs> </configuration> </execution>
[Lambda: Java 8ランタイム]
前回作成したプロジェクトの構成はこのようになっていました。
GitHub - kdnakt/quarkus-lambda-sample
ここに、先ほど作成したプロジェクトのファイルをコピーしていきます。
まずはpom.xmlの中身を差分を確認しながら追加します。
次にFruit.javaファイルなどを移植します。
最後にFruitSyncServiceを呼び出すハンドラを用意するなどして、実装は完了です。
quarkus-sandbox/DynamodbLambda.java at master · kdnakt/quarkus-sandbox · GitHub
まずはJava 8ランタイムでデプロイして動作確認を行います。
$ bash ./create.sh $ bash ./invoke.sh {"errorMessage":"2019-12-14T12:49:14.368Z 8e606e2f-ea15-4766-9515-608c367cdc84 Task timed out after 3.00 seconds"}
なんとタイムアウトです。仕方がないので3秒から30秒に設定を変更して再度実行してみます。
$ bash ./invoke.sh START RequestId: 2041305a-2000-46d5-9f37-ac87355d6f92 Version: $LATEST 2019-12-14 12:46:22,106 WARN [io.qua.net.run.NettyRecorder] (Thread-1) Localhost lookup took more than one second, you need to add a /etc/hosts entry to improve Quarkus startup time. See https://thoeni.io/post/macos-sierra-java/ for details. java.lang.OutOfMemoryError: Metaspace at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:763) at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) at java.net.URLClassLoader.defineClass(URLClassLoader.java:468) at java.net.URLClassLoader.access$100(URLClassLoader.java:74) at java.net.URLClassLoader$1.run(URLClassLoader.java:369) at java.net.URLClassLoader$1.run(URLClassLoader.java:363) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:362) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) at software.amazon.awssdk.regions.GeneratedServiceMetadataProvider.(GeneratedServiceMetadataProvider.java:184) at software.amazon.awssdk.regions.MetadataLoader. (MetadataLoader.java:29) at software.amazon.awssdk.regions.ServiceMetadata.of(ServiceMetadata.java:68) at software.amazon.awssdk.awscore.endpoint.DefaultServiceEndpointBuilder.getServiceEndpoint(DefaultServiceEndpointBuilder.java:53) at software.amazon.awssdk.awscore.internal.EndpointUtils.buildEndpoint(EndpointUtils.java:44) at software.amazon.awssdk.awscore.client.builder.AwsDefaultClientBuilder.lambda$resolveEndpoint$1(AwsDefaultClientBuilder.java:156) at software.amazon.awssdk.awscore.client.builder.AwsDefaultClientBuilder$$Lambda$147/998351292.get(Unknown Source) at java.util.Optional.orElseGet(Optional.java:267) at software.amazon.awssdk.awscore.client.builder.AwsDefaultClientBuilder.resolveEndpoint(AwsDefaultClientBuilder.java:156) at software.amazon.awssdk.awscore.client.builder.AwsDefaultClientBuilder.finalizeChildConfiguration(AwsDefaultClientBuilder.java:130) at software.amazon.awssdk.core.client.builder.SdkDefaultClientBuilder.syncClientConfiguration(SdkDefaultClientBuilder.java:147) at software.amazon.awssdk.services.dynamodb.DefaultDynamoDbClientBuilder.buildClient(DefaultDynamoDbClientBuilder.java:34) at software.amazon.awssdk.services.dynamodb.DefaultDynamoDbClientBuilder.buildClient(DefaultDynamoDbClientBuilder.java:22) at software.amazon.awssdk.core.client.builder.SdkDefaultClientBuilder.build(SdkDefaultClientBuilder.java:119) at io.quarkus.dynamodb.runtime.DynamodbClientProducer.client(DynamodbClientProducer.java:50) at io.quarkus.dynamodb.runtime.DynamodbClientProducer_ClientProxy.client(DynamodbClientProducer_ClientProxy.zig:250) at io.quarkus.dynamodb.runtime.DynamodbRecorder.createClient(DynamodbRecorder.java:23) at io.quarkus.deployment.steps.DynamodbProcessor$buildClients19.deploy_0(DynamodbProcessor$buildClients19.zig:79) at io.quarkus.deployment.steps.DynamodbProcessor$buildClients19.deploy(DynamodbProcessor$buildClients19.zig:104) at io.quarkus.runner.ApplicationImpl.doStart(ApplicationImpl.zig:196) at io.quarkus.runtime.Application.start(Application.java:94) END RequestId: 2041305a-2000-46d5-9f37-ac87355d6f92 REPORT RequestId: 2041305a-2000-46d5-9f37-ac87355d6f92 Duration: 30030.16 ms Billed Duration: 30000 msMemory Size: 128 MB Max Memory Used: 49 MB 2019-12-14T12:46:39.225Z 2041305a-2000-46d5-9f37-ac87355d6f92 Task timed out after 30.03 seconds
今度はOutOfMemoryが発生したので、メモリの設定値を128Mから256Mに変更してみます。
$ bash ./invoke.sh $ cat ./out {"name":"orange","description":"this is an orange"}
AWS上に用意しておいたDynamoDBのQuarkusFruitsテーブルから、無事にデータを取得することができました。
[Lambda: カスタムランタイム]
次にカスタムランタイムを利用したネイティブコンパイルしたバージョンの動作確認をします。
$ mvn clean install -Dnative $ bash ./create-native.sh $ bash ./invoke-native.sh $ cat ./out {"errorType":"Runtime.ExitError","errorMessage":"RequestId: 25d09cf9-ff84-42c6-ab97-53494e88bd7a Error: &{0xc00005e2a0 map[invoke_id:25d09cf9-ff84-42c6-ab97-53494e88bd7a sandbox_id:0] 2019-12-14 12:50:11.057987153 +0000 UTC m=+0.045830567 panic <nil> Runtime failed to start: fork/exec /var/task/bootstrap: exec format error <nil> }"}
……動かない。そしてなんか見覚えがあるエラー……。fork/exec /var/task/bootstrap: exec format error <nil>
で検索するとすぐに答えが見つかった。
1ヶ月前と同じミスをする私orz。
という訳で、mvn clean install -Pnative -Dquarkus.native.container-build=true
コマンドを実行してみると……またnative-image作成中に新しいエラー。
# # A fatal error has been detected by the Java Runtime Environment: # # SIGBUS (0x7) at pc=0x00007f8806ac3662, pid=19, tid=0x00007f87b15ff700 # # JRE version: OpenJDK Runtime Environment (8.0_232-b07) (build 1.8.0_232-20191008104205.buildslave.jdk8u-src-tar--b07) # Java VM: OpenJDK 64-Bit GraalVM CE 19.2.1 (25.232-b07-jvmci-19.2-b03 mixed mode linux-amd64 compressed oops) # Problematic frame: # C [libzip.so+0x12662] newEntry+0x62 # # Failed to write core dump. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again # # An error report file with more information is saved as: # /tmp/hs_err_pid19.log
SIGBUSで検索してみると、メモリが足りない場合などに発生する様子。
Dockerの設定をみてみると、メモリ2GのSwap1G。とりあえず両方4Gにしてみたところ、ビルドにトータルで200秒くらいかかるものの、なんとかnative-imageを作成することができた。
そしてこれをデプロイして実行すると、また新たなエラーが。
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)
半日ほどとりくんだのですが、vert.xのエラーを解消できず……。
このあたりのissueを参考に、-Dvertx.disableFileCaching=true
や-Dvertx.disableFileCPResolving=true
などの設定値を試してみたのですが、効果なく同じエラーメッセージに現在も悩まされています。
という訳で、今の段階ではまだネイティブコンパイルした状態でAWS LambdaとDynamoDBを接続することはできていません。
【2019/12/31追記】
無事2019年内に動作確認できました!
【2019/12/31追記ここまで】
[まとめ]
- Quarkusを利用してJavaアプリでAmazon DynamoDBに接続した
- さらに、AWS Lambdaでも動作確認した
- Quarkusを利用してネイティブアプリでAmazon DynamoDBに接続した
また1つ、冬休みの宿題が出来てしまった……。
【2019/12/31追記】
無事2019年内に動作確認できました!
【2019/12/31追記ここまで】