MandrelでQuarkusのアプリをネイティブイメージ化する

はじめに

Quarkusのネイティブイメージ化したアプリを作ったことなかったのと、Mandrelという名前自体は聞いていたのですがQuarkusに関係するGraalVMぐらいの理解でしか無かったので、ちょっとまとめて動かしてみようかと思います。
基本的にはQuarkusのガイド(BUILDING A NATIVE EXECUTABLE)に従いつつやりますが、自分が気なったところを少しだけ深ぼってまとめるようにしようと思います。

Mandrelってなんぞ?

Oracle GraalVM Community Editionのダウンストリームに当たるGraalVMのディストリビューションの1つです。そのメインの目的としては、Quarkusのためにデザインされたネイティブイメージ化の方法を提供することにあります。基本的にアップストリームからの大きな変更はないようですが、Quakusのアプリに不要なものが取り除かれているようです。MandrelはOracle GraalVM CEと同じネイティブイメージ化の能力を提供しますが、polyglotなどのサポートが取り除かれているようです。Mandrelは現在、Linuxコンテナの環境におけるネイティブイメージのビルドだけの利用をが推奨されており、WindowsMacOSに対するネイティブイメージを作成する場合は、 Oracle GraalVMを利用することが推奨されるようです。
理解が足りてない部分があるかもですが、おそらくCI上でのビルドやDockerのマルチステージビルドなどでMandrelを使えば諸々のコストの削減になるのでは無いかと思われます。

ネイティブイメージ化してみる

環境

今回の動作環境は以下のとおりです。

$ java --version
openjdk 11.0.10 2021-01-19
OpenJDK Runtime Environment 18.9 (build 11.0.10+9)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.10+9, mixed mode)

$ uname -srvmpio
Linux 5.4.0-66-generic #74-Ubuntu SMP Wed Jan 27 22:54:38 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
LSB Version:    core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.2 LTS
Release:    20.04
Codename:   focal

Mandrelに関してはこのブログ内でインストールします。

Mandrelをインストールする

ダウンロードはGitHubここのページからおこなうことが可能です。
具体的には以下の手順でセットアップをおこなします。

$ cd ${YOUR_GRAALVM_INSTALL_DIR}
$ wget https://github.com/graalvm/mandrel/releases/download/mandrel-21.0.0.0-Final/mandrel-java11-linux-amd64-21.0.0.0-Final.tar.gz
$ tar -xf mandrel-java11-linux-amd64-21.0.0.0-Final.tar.gz
$ export JAVA_HOME="$( pwd )/mandrel-java11-21.0.0.0-Final"
$ export GRAALVM_HOME="${JAVA_HOME}"
$ export PATH="${JAVA_HOME}/bin:${PATH}"

QuarkusアプリのネイティブイメージのビルドはQuarkusのMavenラッパーを使って行いますが、その際に自身で指定するGraalVMでのビルドを行いたい場合はPathがとおっているGraalVMに対してnative-imageコマンドがインストールされている必要があります。
通常、native-imageコマンドはgu等を使ってインストールしないと行けなかった気がしますが、Mandrelの場合最初から内包されているようです。

Nativeイメージ化するプロジェクトの作成

Quarkus - Start coding with code.quarkus.ioを使って、 こんな感じの設定でプロジェクトを作成します。
この際にExample CodeはYes, Pleaseを選択肢します(自分でコード書いても良いのですが、ちょいめんどくいさいので)。

f:id:yuya_hirooka:20210310194526p:plain

Exampleのコードを生成を有効にしたので、GreetingResource.javaというハンドラーのコードが生成されているはずです。起動して、/hello-resteasyのパスにアクセスするとHello RESTEasyという文字列が返ってきます。

$ ./mvnw compile quarkus:dev

# 別ターミナルで
$ curl localhost:8080/hello-resteasy
Hello RESTEasy

作成されたプロジェクトのPomをちょっと見てみる

作成されたプロジェクトのPomを見てみると以下のようなプロファイルの設定が記述されているのが確認できます。

    <profile>
      <id>native</id>
      <activation>
        <property>
          <name>native</name>
        </property>
      </activation>
      <build>
        <plugins>
          <plugin>
            <artifactId>maven-failsafe-plugin</artifactId>
            <version>${surefire-plugin.version}</version>
            <executions>
              <execution>
                <goals>
                  <goal>integration-test</goal>
                  <goal>verify</goal>
                </goals>
                <configuration>
                  <systemPropertyVariables>
                    <native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
                    <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
                    <maven.home>${maven.home}</maven.home>
                  </systemPropertyVariables>
                </configuration>
              </execution>
            </executions>
          </plugin>
        </plugins>
      </build>
      <properties>
        <quarkus.package.type>native</quarkus.package.type>
      </properties>
    </profile>

Maven Failsafe Pluginを用いたインテグレーションテストの設定が記述されています(詳しくは後述)。

ネイティブイメージをビルドする

Quarkusアプリをネイティブイメージでビルドする場合は以下のコマンドを用いて行います。

$ cd ${YOUR_PROJECT_DIR}
$ ./mvnw package -Pnative

この際にPathがとおっているGraalVMにnative-imageコマンドが無かった場合は、以下のようなログを出力し、Dockerイメージをプルしてきてビルドを行ってくれます。

[WARNING] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Cannot find the `native-image` in the GRAALVM_HOME, JAVA_HOME and System PATH. Install it using `gu install native-image` Attempting to fall back to container build.

ビルドが完了するとデフォルトではtarget配下に実行可能なバイナリが${project.artifactId}-${project.version}-runnerの名前でできています。 今回作成されたバイナリは以下のように実行することができます。

$ ./target/native-image-1.0.0-SNAPSHOT-runner 
__  ____  __  _____   ___  __ ____  ______ 
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ 
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \   
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/   
2021-03-10 21:12:31,788 INFO  [io.quarkus] (main) native-image 1.0.0-SNAPSHOT native (powered by Quarkus 1.12.1.Final) started in 0.030s. Listening on: http://0.0.0.0:8080
2021-03-10 21:12:31,789 INFO  [io.quarkus] (main) Profile prod activated. 
2021-03-10 21:12:31,789 INFO  [io.quarkus] (main) Installed features: [cdi, resteasy]

ネイティブイメージを使ったテストを実行する

ネイティブイメージ化すると、jarでビルドする際と比較して予想しない問題が起こることがありえます。そのため、ネイティブイメージで動くアプリのインテグレーションテストを行なって置くことが推奨されます。

今回は、Exampleコードの生成を有効化しているためデフォルトで以下のようなNativeGreetingResourceITというクラスが生成されていると思います。

import io.quarkus.test.junit.NativeImageTest;

@NativeImageTest
public class NativeGreetingResourceIT extends  {

    // Execute the same tests but in native mode.
}

@NativeImageTestを付与されたテストクラスにインテグレーションテストを記述しておくと、前述した、PomのMaven Failsafe Pluginの設定で指定されるネイティブイメージを利用したテストを実施することが可能です。

ExampleではGreetingResourceTestを拡張しており、このクラスはGreetingResourceをテストがRest Assuredで行われています。 このテストを実行されるには以下のコマンドを実行します。

$  ./mvnw verify -Pnative

このコマンドを実行すると、ネイティブイメージのビルドが行われ、その後、そのイメージを使ったテストが実施されます。