QuarkusでAWS LambdaとAmazon DynamoDBを連携する

この記事はAWS LambdaとServerless #1 Advent Calendar 2019の15日目の記事です。

 

qiita.com

 

 

[やること]

先月1.0がリリースされたQuarkusで、ネイティブコンパイルしたJavaアプリをAWS Lambdaにデプロイしました。

Quarkus自体の簡単な説明や解説サイトへのリンクはこちらの記事にあります。

kdnakt.hatenablog.com

 

前回作ったのはリクエストを受け取って文字列を結合するだけの非常にシンプルなアプリでした。

今回は、もう一歩踏み込んで、Amazon DynamoDBとの連携を目指します。

 

[QuarkusでDynamoDBを利用する]

Quarkusは様々な既存のJavaライブラリと連携するために、拡張機能が用意されています。

こちらのサイトを利用すると、必要な拡張機能にチェックをつけるだけで、Javaプロジェクトをzipでダウンロードすることができます。

f:id:kidani_a:20191215013917p:plain

 

が、今回は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クライアントを利用すると、このような実装となります。

github.com

ドキュメントにはDynamoDBの非同期クライアントを利用した実装方法についても説明がありましたが本記事では省略します。

 

いずれのDynamoDBクライアントを利用する場合であっても、HTTPクライアントの実装クラスを依存関係に含める必要があります。

ドキュメント中で説明されていたのはURL connection HTTPクライアントとApache HTTPクライアントでしたが、私は後者を利用したので、次のような実装となりました。

github.com

 

続いて、テストコードを修正します。

QuarkusFruitsテーブルにデータがない状態であれば空の配列が返ってくるので、そのように修正します。Quarkusは通常起動時のポートは8080ですが、テスト時は8081ポートを対象とするため、quarkus.http.test-port=8083という設定をapplication.propertiesファイルに追記しています。

github.com

 

これで、./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.xmladditionalBuildArgsタグを利用して--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の中身を差分を確認しながら追加します。

github.com

 

次にFruit.javaファイルなどを移植します。

github.com

 

最後に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>で検索するとすぐに答えが見つかった。

github.com

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で検索してみると、メモリが足りない場合などに発生する様子。

f:id:kidani_a:20191215033630p:plain

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のエラーを解消できず……。

github.com

このあたりの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に接続した
    • これをAWS Lambda上で動作させようとしたらエラーが出て詰まった【解消済み

 

また1つ、冬休みの宿題が出来てしまった……。

 

【2019/12/31追記】

無事2019年内に動作確認できました!

【2019/12/31追記ここまで】