QuarkusでAWS Lambdaを作る

Quarkus(https://quarkus.io/) とは`SUPERSONIC SUBATOMIC JAVA`で、

> A Kubernetes Native Java stack tailored for OpenJDK HotSpot and GraalVM, crafted from the best of breed Java libraries and standards.

だそうです。
何言ってるかわかりませんがすごそうです。

GraalVMの機能でJavaのプログラムをネイティブに変換することが可能なので、AWS Lambdaのカスタムランタイムと組み合わせることで AWS Lambda上で動作するJavaアプリケーションの特性である **コールドスタートが遅い問題** の解決に期待ができます。

公式サイトの手順に従い試してみましたので、手順を残します。

QUARKUS - BUILDING A NATIVE EXECUTABLE
https://quarkus.io/guides/building-native-image

QUARKUS - AMAZON LAMBDA
https://quarkus.io/guides/amazon-lambda

# 環境

* Docker Desktop (Mac) 2.3.0.4
* VSCode + Visual Studio Code Remote - Containers extension
* Amazon Linux 2 (on Docker)

# 雛形アプリのデプロイ手順

## 手順1. Amazon Linux 2の環境設定

DockerイメージのAmazon Linux 2は`tar`や`unzip`ができないので色々入れておきます。
全部必要かわからないですが、これぐらい入れておきました。

```shell
yum install -y sudo shadow-utils procps tar.x86_64 gzip xz unzip witch git python3 tree
```

## 手順2. GraalVMのインストール

公式サイトからダウンロードして展開します。

```shell
curl -s -L -o /tmp/graalvm-ce-java11-linux-amd64-20.1.0.tar.gz https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-20.1.0/graalvm-ce-java11-linux-amd64-20.1.0.tar.gz
tar zxf /tmp/graalvm-ce-java11-linux-amd64-20.1.0.tar.gz -C /opt/
ln -s /opt/graalvm-ce-java11-20.1.0 /opt/graalvm
```

`JAVA_HOME`をGraalVMにして、MavenでのビルドにGraalVMを使用します。

```shell
export GRAALVM_HOME=/opt/graalvm
export JAVA_HOME=$GRAALVM_HOME
export PATH=$GRAALVM_HOME/bin:$PATH
```

最後にNativeビルド時に必要な`native-image`をインストールします。コマンドは`gu`(GraalVM Updater)です。

```shell
gu install native-image
```

## 手順3. Mavenのインストール

Quarkusのビルドで使用するMavenは3.6.2以上のバージョンが必要です。`yum`でインストールできるバージョンが古かったので、公式サイトからダウンロードしてインストールしました。

```shell
curl -s -L -o /tmp/apache-maven-3.6.3-bin.tar.gz https://downloads.apache.org/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.tar.gz
tar zxf /tmp/apache-maven-3.6.3-bin.tar.gz -C /opt/
ln -s /opt/apache-maven-3.6.3 /opt/apache-maven
```

mvnのバージョン確認

```console
bash-4.2# mvn --version
Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
Maven home: /opt/apache-maven
Java version: 11.0.7, vendor: GraalVM Community, runtime: /opt/graalvm-ce-java11-20.1.0
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "4.19.76-linuxkit", arch: "amd64", family: "unix"
bash-4.2# 
```

GraalVMのJVMで動作していることがわかります。

## 手順4. Mavenでプロジェクト作成

`mvn`コマンドでプロジェクトを生成します。

```shell
mvn archetype:generate \
    -DarchetypeGroupId=io.quarkus \
    -DarchetypeArtifactId=quarkus-amazon-lambda-archetype \
    -DarchetypeVersion=1.6.0.Final
```

しばらくするとプロンプトで質問されますので答えます。
[]で囲んだ部分がユーザー入力です。

```console
Define value for property 'groupId': [myGroup]
Define value for property 'artifactId': [myArtifact]
Define value for property 'version' 1.0-SNAPSHOT: : []
Define value for property 'package' myGroup: : [example]
Confirm properties configuration:
groupId: myGroup
artifactId: myArtifact
version: 1.0-SNAPSHOT
package: example
 Y: : [Y]
```

`artifactId`(myArtifact)でディレクトリが作成され、プロジェクトが生成されます。

作成直後のプロジェクト構成はこんな感じ。

```console
bash-4.2# tree myArtifact/
myArtifact/
├── build.gradle
├── gradle.properties
├── payload.json
├── pom.xml
├── settings.gradle
└── src
    ├── main
    │   ├── java
    │   │   └── example
    │   │       ├── InputObject.java
    │   │       ├── OutputObject.java
    │   │       ├── ProcessingService.java
    │   │       ├── StreamLambda.java
    │   │       ├── TestLambda.java
    │   │       └── UnusedLambda.java
    │   └── resources
    │       └── application.properties
    └── test
        ├── java
        │   └── example
        │       └── LambdaHandlerTest.java
        └── resources
            └── application.properties

9 directories, 14 files
bash-4.2# 
```

## 手順5. Lambdaで起動するHandlerの設定

Lambdaで呼び出されるHandlerは`resources/application.properties`の`quarkus.lambda.handler`にて設定します。

```properties:resources/application.properties
quarkus.lambda.handler=test
```

上記設定の場合は、以下の`test`と名前をつけたクラスが呼び出されます。

```java:main/java/example.TestLambda.java
@Named("test")
public class TestLambda implements RequestHandler<InputObject, OutputObject> {
}
```

あとは通常のLambdaと同様にコーディングします。
雛形には`test`の他に`stream`なども用意されています。

## 手順6. デプロイパッケージの作成

一旦雛形のままデプロイパッケージを作成してみます。

```shell
mvn clean package -Pnative
```

めちゃくちゃ時間がかかります。10分以上かかると思います。

## 手順7. デプロイパッケージの内容の確認

無事デプロイパッケージができると、`target/function.zip`が生成されいます。試しに展開してみると、中身は`bootstrap`のみでした。

```console
bash-4.2# unzip function.zip 
Archive:  function.zip
  inflating: bootstrap               
bash-4.2# 
```

## 手順8. Lambdaのデプロイ

`target`ディレクトリ内には、`manage.sh`というファイルも生成されており、ここからAWS上にデプロイしたりできるようです。
私はカスタムランタイムが初めてだったこともあり、マネジメントコンソールから試してみました。
デプロイパッケージ作成後に、`target/sam.native.yaml`というファイルも生成されるので、ハンドラー名や環境変数はこちらを参考にしました。

関数を新規作成します。
ランタイムを`ユーザー独自のブートストラップを提供する`とします。

![ap-northeast-1.console.aws.amazon.com_lambda_home_region=ap-northeast-1(Laptop with MDPI screen).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/fba763b2-3412-a743-29f4-cf5b3692dcad.png)

関数を作成したら、プログラムをアップロードします。
関数コードのところの`アクション`から`.zipファイルをアップロード`を選び、`function.zip`を選択します。

![ap-northeast-1.console.aws.amazon.com_lambda_home_region=ap-northeast-1(Laptop with MDPI screen) (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/81f58cc4-6c92-a1c5-eb6e-6f7a8ab37596.png)

ハンドラーは`not.used.in.provided.runtime`となります。

![ap-northeast-1.console.aws.amazon.com_lambda_home_region=ap-northeast-1(Laptop with MDPI screen) (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/3bbc02dd-b976-12ce-85c3-2b35555c0785.png)

環境変数に`DISABLE_SIGNAL_HANDLERS`を追加し値を`true`とします。

![ap-northeast-1.console.aws.amazon.com_lambda_home_region=ap-northeast-1(Laptop with MDPI screen) (4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/8d330b69-f656-7f72-d06b-97daa30ad10c.png)

ここまでで設定は完了です。以下のJSONをインプットにテスト実行してみましょう。

![ap-northeast-1.console.aws.amazon.com_lambda_home_region=ap-northeast-1(Laptop with MDPI screen) (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/6e4b881d-5c70-c60a-a77f-a807df6a4ec6.png)

```JSON
{
  "name": "Bill",
  "greeting": "hello"
}
```

動作結果がこちら。無事動きました。

![ap-northeast-1.console.aws.amazon.com_lambda_home_region=ap-northeast-1(Laptop with MDPI screen) (5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/f3544eef-e463-c0e8-0c01-85106c06c480.png)


# AWS SDK を追加

AWS SDKを使用するには単純にpom.xmlに追加するだけでなく、いくつか設定が必要です。

## 手順1. SSL通信の有効化

`resources/application.properties`に以下の内容を追加します。（公式ドキュメント的にはデフォルトで有効って書いてある気もしますが）

```properties:resources/application.properties
quarkus.ssl.native=true
```

## 手順2. 依存関係の追加

まずは、`quarkus-jaxb`が必要とのことで、これを追加します。

```xml:pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-jaxb</artifactId>
</dependency>
```

次にAWS SDKのライブラリーを追加するのですが、

> For native image, however the URL Connection client must be preferred over the Apache HTTP Client when using synchronous mode, due to issues in the GraalVM compilation (at present).

翻訳すると
`ただし、ネイティブイメージの場合、GraalVMコンパイルの問題（現在）のため、同期モードを使用するときは、Apache HTTPクライアントよりもURL接続クライアントを優先する必要があります。`
だそうです。そのため、以下のような記述となります。

```xml:pom.xml
<properties>
      <aws.sdk2.version>2.10.69</aws.sdk2.version>
  </properties>

  <dependencyManagement>
      <dependencies>

          <dependency>
              <groupId>software.amazon.awssdk</groupId>
              <artifactId>bom</artifactId>
              <version>${aws.sdk2.version}</version>
              <type>pom</type>
              <scope>import</scope>
          </dependency>

      </dependencies>
  </dependencyManagement>
  <dependencies>

      <dependency>
          <groupId>software.amazon.awssdk</groupId>
          <artifactId>url-connection-client</artifactId>
      </dependency>

      <dependency>
          <groupId>software.amazon.awssdk</groupId>
          <artifactId>apache-client</artifactId>
          <exclusions>
              <exclusion>
                  <groupId>commons-logging</groupId>
                  <artifactId>commons-logging</artifactId>
              </exclusion>
          </exclusions>
      </dependency>

      <dependency>
          <groupId>software.amazon.awssdk</groupId>
          <artifactId>s3</artifactId>
          <exclusions>
              <!-- exclude the apache-client and netty client -->
              <exclusion>
                  <groupId>software.amazon.awssdk</groupId>
                  <artifactId>apache-client</artifactId>
              </exclusion>
              <exclusion>
                  <groupId>software.amazon.awssdk</groupId>
                  <artifactId>netty-nio-client</artifactId>
              </exclusion>
              <exclusion>
                  <groupId>commons-logging</groupId>
                  <artifactId>commons-logging</artifactId>
              </exclusion>
          </exclusions>
      </dependency>

      <dependency>
          <groupId>org.jboss.logging</groupId>
          <artifactId>commons-logging-jboss-logging</artifactId>
          <version>1.0.0.Final</version>
      </dependency>
  </dependencies>
```

## 手順3. Javaコードの作成

S3にアクセスする例ですが、S3Clientを生成する際に、`httpClient`にて明示的に`UrlConnectionHttpClient`を指定します。

```java
S3Client s3 = S3Client.builder()
  .region(Region.AP_NORTHEAST_1)
 
 .httpClient(software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient.builder().build())
  .build();
```

## 手順4. SSL通信に必要な設定

SSL通信を行うためにデプロイパッケージに以下を含める必要があります。

* カスタムBootstrap
* libsunec.so
* cacerts

まず、`src/main/zip.native/`ディレクトリを作成し、`bootstrap`を作成します。

```shell:zip.native/bootstrap
#!/usr/bin/env bash

./runner -Djava.library.path=./ -Djavax.net.ssl.trustStore=./cacerts
```

次に`libsunec.so`と`cacerts`をコピーします。この２つのファイルはGraalVMに含まれています。

```shell
cp $GRAALVM_HOME/lib/libsunec.so $PROJECT_DIR/src/main/zip.native/
cp $GRAALVM_HOME/lib/security/cacerts $PROJECT_DIR/src/main/zip.native/
```

## 手順5. デプロイパッケージの作成

デプロイパッケージの作成手順は変わりません。

```shell
mvn clean package -Pnative
```

## 手順6. デプロイパッケージの内容の確認

SSL有効化した状態でデプロイパッケージを作成すると、`target/function.zip`の中身が変わります。

```console
bash-4.2# unzip function.zip 
Archive:  function.zip
  inflating: bootstrap               
  inflating: cacerts                 
  inflating: libsunec.so             
  inflating: runner                  
bash-4.2# 
```

事前準備した`bootstrap``cacerts``libsunec.so`が含まれているのがわかります。

# 終わりに

通常のJavaランタイムよりも、コールドスタートが早くなる検証結果はこちらで確認ください。

QuarkusがJava Lambdaを救う！？
https://qiita.com/moritalous/items/4de31a66edac728ba088