見出し画像

GraalVMを使ってQuarkusアプリケーションからネイティブイメージを生成する


はじめに

Quarkusには、GraalVMを使ってアプリケーションからネイティブ実行可能なファイルを生成する機能があります。この記事では、Quarkus CLIを使って、このネイティブビルドを試すための簡単な手順を紹介します。

この記事では、SDKMANによって、Linux環境に以下のツールがすでにインストール済みであることを前提に手順を説明します。

$ sdk current

Using:

java: 21.0.5-tem
maven: 3.9.9
quarkus: 3.17.7


SDKMANや、SDKMANを使って上記のコマンドをインストール方法については、以下の記事を参照してください。

インストール環境の前提

この記事の内容は、Fedora 41とUbuntu 24.10で動作することを確認済みです。この記事は以下の仮想環境でログを取得しています。

  • Fedora 41

  • vCPU: 6

  • Memory: 16GiB

サンプルアプリケーション

説明に使うサンプルアプリケーションはQuarkus in Action Chapter 2に掲載されているものを使います。この記事で扱うビルド対象となるREST APIを実装するアプリケーションは以下のコマンドで生成することができます。

$ quarkus create app org.acme:quarkus-in-action --extension quarkus-rest

Quarkus in Actionについては以下の記事をご覧ください。

アプリケーションのビルド

Quarkusのアプリケーションを普通にビルドしてJARファイルを生成するには、quarkus buildコマンドを使います。targetディレクトリを削除してクリーンな状態からビルドする場合は、--clean オプションをつけます。

# アプリケーションディレクトリに移動
$ cd quarkus-in-action

# ビルドの実行
$ quarkus build --clean
[INFO] Scanning for projects...
<略>
[INFO] --- install:3.1.2:install (default-install) @ quarkus-in-action ---
[INFO] Installing /home/minamoto/quarkus-in-action/pom.xml to /home/minamoto/.m2/repository/org/acme/quarkus-in-action/1.0.0-SNAPSHOT/quarkus-in-action-1.0.0-SNAPSHOT.pom
[INFO] Installing /home/minamoto/quarkus-in-action/target/quarkus-in-action-1.0.0-SNAPSHOT.jar to /home/minamoto/.m2/repository/org/acme/quarkus-in-action/1.0.0-SNAPSHOT/quarkus-in-action-1.0.0-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  6.962 s
[INFO] Finished at: 2025-01-19T18:57:36+09:00
[INFO] ------------------------------------------------------------------------

注意
初回のビルドは、大量の依存関係のライブラリがダウンロードされるために時間がかかります。上の約7秒というビルド時間はすでにライブラリーがダウンロード済の状態で実施していますので、ダウンロード時間を含みません。後述するネイティブビルドについても同様です。

ビルド結果のJARファイルを実行します。JARファイルはtarget/quarkus-appディレクトリの下に配置されます。

$ java -jar ./target/quarkus-app/quarkus-run.jar
__  ____  __  _____   ___  __ ____  ______ 
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ 
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \   
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/ 
2025-01-19 19:00:55,982 INFO  [io.quarkus] (main) quarkus-in-action 1.0.0-SNAPSHOT on JVM (powered by Quarkus 3.17.7) started in 0.488s. Listening on: http://0.0.0.0:8080
2025-01-19 19:00:55,986 INFO  [io.quarkus] (main) Profile prod activated.
2025-01-19 19:00:55,986 INFO  [io.quarkus] (main) Installed features: [cdi, rest, smallrye-context-propagation, vertx]

ログを見ると、JARファイルの起動時間は 0.488s です。

ブラウザーから http://localhost:8080/hello でREST APIを呼び出します。

ネイティブイメージのビルド

ネイティブイメージとは、GraalVMによって生成されたネイティブ実行可能ファイルのことです。QuarkusはGraalVMを使うことでJavaアプリケーションからネイティブイメージを生成することができます。

Quarkusで利用可能なGraalVM

Quarkus in Actionによると、Quarkusで使えるGraalVMには以下の種類があります。

GraalVM EEは、GraalVM CEのエンタープライズ(有償版)です。Mandrelは、GraalVM CEのダウンストリームで、Quarkus用途に特化したものです。Mandrelは、Linux専用なので、MacOSやWindowsで使う場合には、Mandrelの代わりにGraalVM CEかGraalVM EEを使うことになります。

ネイティブイメージのビルド内容

ネイティブイメージを生成するには、quarkus build --nativeコマンドを実行します。コマンドをログを見ると以下のような順番で実行していきます。

  1. targetディレクトリのクリーン (--cleanを指定した場合)

  2. Javaコードのコンパイル

  3. Javaコードのテストを実行

  4. JARファイルの生成

  5. JARファイルからネイティブイメージの生成

  6. ネイティブイメージのテストを実行

上のステップ4では、GraalVMの native-image コマンドを使ってネイティブイメージを生成します。

ネイティブイメージのビルド方法 (コンテナービルドの場合)

Linux上でネイティブビルドをする場合、GraalVMのインストールは不要です(ただし、DockerまたはPodmanは事前にインストールが必要)。

ネイティブビルド時に環境を調べ、GraalVMがインストールされていない場合は、DockerやPodmanを使い、Mandrelコンテナーイメージを利用することでネイティブイメージを生成します。

コンテナービルドの場合のビルドログの抜粋を以下に示します。ネイティブイメージのビルドには1~2分の時間がかかりました。

$ quarkus build --clean --native
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< org.acme:quarkus-in-action >---------------------
[INFO] Building quarkus-in-action 1.0.0-SNAPSHOT
[INFO]   from pom.xml
<略>
INFO] --- quarkus:3.17.7:build (default) @ quarkus-in-action ---
[WARNING] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Cannot find the `native-image` in the GRAALVM_HOME, JAVA_HOME and System PATH. Attempting to fall back to container build.
[INFO] [io.quarkus.deployment.pkg.steps.JarResultBuildStep] Building native image source jar: /home/minamoto/quarkus-in-action/target/quarkus-in-action-1.0.0-SNAPSHOT-native-image-source-jar/quarkus-in-action-1.0.0-SNAPSHOT-runner.jar
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Building native image from /home/minamoto/quarkus-in-action/target/quarkus-in-action-1.0.0-SNAPSHOT-native-image-source-jar/quarkus-in-action-1.0.0-SNAPSHOT-runner.jar
<略>
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Running Quarkus native-image plugin on MANDREL 23.1.5.0 JDK 21.0.5+11-LTS
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildContainerRunner] Using podman to run the native image builder
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildRunner] podman run --env LANG=C --rm --user 1000:1000 --userns=keep-id -v /home/minamoto/quarkus-in-action/target/quarkus-in-action-1.0.0-SNAPSHOT-native-image-source-jar:/project:z --name build-native-RmyAB quay.io/quarkus/ubi-quarkus-mandrel-builder-image:jdk-21 -J-Dlogging.initial-configurator.min-level=500 -J-Duser.language=en -J-Duser.country=US -J-Dsun.nio.ch.maxUpdateArraySize=100 -J-Djava.util.logging.manager=org.jboss.logmanager.LogManager -J-Dio.netty.leakDetection.level=DISABLED -J-Dio.netty.allocator.maxOrder=3 -J-Dvertx.logger-delegate-factory-class-name=io.quarkus.vertx.core.runtime.VertxLogDelegateFactory -J-Dvertx.disableDnsResolver=true -H:+UnlockExperimentalVMOptions -H:IncludeLocales=en-US -H:-UnlockExperimentalVMOptions -J-Dfile.encoding=UTF-8 --features=io.quarkus.runner.Feature,io.quarkus.runtime.graal.DisableLoggingFeature -J--add-exports=java.security.jgss/sun.security.krb5=ALL-UNNAMED -J--add-exports=java.security.jgss/sun.security.jgss=ALL-UNNAMED -J--add-opens=java.base/java.text=ALL-UNNAMED -J--add-opens=java.base/java.io=ALL-UNNAMED -J--add-opens=java.base/java.lang.invoke=ALL-UNNAMED -J--add-opens=java.base/java.util=ALL-UNNAMED -H:+UnlockExperimentalVMOptions -H:BuildOutputJSONFile=quarkus-in-action-1.0.0-SNAPSHOT-runner-build-output-stats.json -H:-UnlockExperimentalVMOptions -H:+UnlockExperimentalVMOptions -H:+GenerateBuildArtifactsFile -H:-UnlockExperimentalVMOptions --strict-image-heap -H:+UnlockExperimentalVMOptions -H:+AllowFoldMethods -H:-UnlockExperimentalVMOptions -J-Djava.awt.headless=true --no-fallback --link-at-build-time -H:+UnlockExperimentalVMOptions -H:+ReportExceptionStackTraces -H:-UnlockExperimentalVMOptions -H:-AddAllCharsets --enable-url-protocols=http --enable-monitoring=heapdump -H:+UnlockExperimentalVMOptions -H:-UseServiceLoaderFeature -H:-UnlockExperimentalVMOptions -J--add-exports=org.graalvm.nativeimage/org.graalvm.nativeimage.impl=ALL-UNNAMED --exclude-config io\.netty\.netty-codec /META-INF/native-image/io\.netty/netty-codec/generated/handlers/reflect-config\.json --exclude-config io\.netty\.netty-handler /META-INF/native-image/io\.netty/netty-handler/generated/handlers/reflect-config\.json quarkus-in-action-1.0.0-SNAPSHOT-runner -jar quarkus-in-action-1.0.0-SNAPSHOT-runner.jar
<略>
[INFO] --- install:3.1.2:install (default-install) @ quarkus-in-action ---
[INFO] Installing /home/minamoto/quarkus-in-action/pom.xml to /home/minamoto/.m2/repository/org/acme/quarkus-in-action/1.0.0-SNAPSHOT/quarkus-in-action-1.0.0-SNAPSHOT.pom
[INFO] Installing /home/minamoto/quarkus-in-action/target/quarkus-in-action-1.0.0-SNAPSHOT.jar to /home/minamoto/.m2/repository/org/acme/quarkus-in-action/1.0.0-SNAPSHOT/quarkus-in-action-1.0.0-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  01:42 min

注意
ログの途中に、"Cannot find the `native-image` in the GRAALVM_HOME, JAVA_HOME and System PATH. Attempting to fall back to container build."が出力されています。これは、実行パスにnative-imageコマンドが見つからないので、コンテナービルドに切り替えたという意味です 。さらに、"Running Quarkus native-image plugin on MANDREL 23.1.5.0 JDK 21.0.5+11-LTS"という行もあります。これはネイティブイメージのビルドにMandrel 23.1.5.0 が使用されていることを示しています。このネイティブビルドの環境はFedora 41なので、コンテナーライタイムとしてはPodmanがインストールされていて、PodmanからMandrelのイメージを実行しています。

ビルドによって生成されたネイティブイメージは、target/quarkus-in-action-1.0.0-SNAPSHOT-runnerに存在します。

$ ./target/quarkus-in-action-1.0.0-SNAPSHOT-runner 
__  ____  __  _____   ___  __ ____  ______ 
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ 
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \   
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/  
2025-01-20 00:07:40,606 INFO  [io.quarkus] (main) quarkus-in-action 1.0.0-SNAPSHOT native (powered by Quarkus 3.17.7) started in 0.020s. Listening on: http://0.0.0.0:8080
2025-01-20 00:07:40,606 INFO  [io.quarkus] (main) Profile prod activated.
2025-01-20 00:07:40,606 INFO  [io.quarkus] (main) Installed features: [cdi, rest, smallrye-context-propagation, vertx]

ログを見ると、このネイティブイメージの起動時間は 0.020s です。

ブラウザーから http://localhost:8080/hello でREST APIを呼び出してみます。気のせいだと思いますが、表示のレスポンスが早い気がします (笑)。

ネイティブイメージのビルド方法 (GraalVMをインストールする場合)

Linux環境の場合は、GraalVMをインストールせずに、コンテナーを利用することでネイティブイメージを生成できたのですが、MacOSやWindowsの場合は、別途GraalVMをインストールする必要があります。以下のGraalVM CEやMandrelのドキュメントを参考にしてインストールしてください。


注意
GraalVMをインストールして使う場合には、OS側にgccやzlibなどのライブラリを事前にインストールしておかないと、Quarkusでネイティブイメージをビルドするときにエラーになります。ドキュメントを参照してライブラリーをイントールしてからビルドしてください。


QuarkusからネイティブイメージビルドにGraalVMを使う場合、2つの方法があります。

(1) アプリのコンパイルや実行に使うJDKとネイティブイメージ生成のGraalVMを別々にインストールする方法です。GRAALVM_HOME環境変数にインストールパスを登録することで、Quarkusはこの環境変数を参照して、ネイティブイメージを生成するnative-imageコマンドを呼び出します。

(2) GraalVMをアプリ開発のJVMとネイティブイメージの生成の両方に使う方法です。この場合は、native-imageコマンドはすでにパスに通っているはずなので、GRAALVM_HOME環境変数の設定は不要です。

後者の方法であれば、SDKMAN使ったGraalVMやMandrelのインストールが可能ですので、その方法を次に紹介します。

SDKMANを使ったGraalVMのインストール

SDKMANを使えば、デフォルトのJavaを好みのGraalVMに切り替えるということが可能です。GraalVMの手動によるインストールやGRAALVM_HOME環境変数の設定が不要なのですぐに動作を確認できます。以下は、GraalVM CEに切り替えた例になります。Mandrelでも同様です。

# GraalVM CEのインストール
$ sdk install java 21.0.2-graalce 

# GraalVM CEをデフォルトJavaとして指定
$ sdk use java 21.0.2-graalce 

# SDKMANでインストールされたデフォルトを確認
$ sdk current

Using:

java: 21.0.2-graalce 
maven: 3.9.9 
quarkus: 3.17.7

ネイティブイメージを生成するには、同様に、quarkus build --nativeコマンドを実行します。

$ quarkus build --clean --native
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< org.acme:quarkus-in-action >---------------------
[INFO] Building quarkus-in-action 1.0.0-SNAPSHOT
[INFO]   from pom.xml
<略> 
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Running Quarkus native-image plugin on GRAALVM 23.1 JDK 21.0.2+13-jvmci-23.1-b30
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildRunner] /home/minamoto/.sdkman/candidates/java/21.0.2-graalce/bin/native-image -J-Dsun.nio.ch.maxUpdateArraySize=100 -J-Dlogging.initial-configurator.min-level=500 -J-Duser.language=en -J-Duser.country=US -J-Djava.util.logging.manager=org.jboss.logmanager.LogManager -J-Dvertx.logger-delegate-factory-class-name=io.quarkus.vertx.core.runtime.VertxLogDelegateFactory -J-Dvertx.disableDnsResolver=true -J-Dio.netty.leakDetection.level=DISABLED -J-Dio.netty.allocator.maxOrder=3 -H:+UnlockExperimentalVMOptions -H:IncludeLocales=en-US -H:-UnlockExperimentalVMOptions -J-Dfile.encoding=UTF-8 --features=io.quarkus.runner.Feature,io.quarkus.runtime.graal.DisableLoggingFeature -J--add-exports=java.security.jgss/sun.security.krb5=ALL-UNNAMED -J--add-exports=java.security.jgss/sun.security.jgss=ALL-UNNAMED -J--add-opens=java.base/java.text=ALL-UNNAMED -J--add-opens=java.base/java.io=ALL-UNNAMED -J--add-opens=java.base/java.lang.invoke=ALL-UNNAMED -J--add-opens=java.base/java.util=ALL-UNNAMED -H:+UnlockExperimentalVMOptions -H:BuildOutputJSONFile=quarkus-in-action-1.0.0-SNAPSHOT-runner-build-output-stats.json -H:-UnlockExperimentalVMOptions -H:+UnlockExperimentalVMOptions -H:+GenerateBuildArtifactsFile -H:-UnlockExperimentalVMOptions --strict-image-heap -H:+UnlockExperimentalVMOptions -H:+AllowFoldMethods -H:-UnlockExperimentalVMOptions -J-Djava.awt.headless=true --no-fallback --link-at-build-time -H:+UnlockExperimentalVMOptions -H:+ReportExceptionStackTraces -H:-UnlockExperimentalVMOptions -H:-AddAllCharsets --enable-url-protocols=http -H:NativeLinkerOption=-no-pie --enable-monitoring=heapdump -H:+UnlockExperimentalVMOptions -H:-UseServiceLoaderFeature -H:-UnlockExperimentalVMOptions -J--add-exports=org.graalvm.nativeimage/org.graalvm.nativeimage.impl=ALL-UNNAMED --exclude-config io\.netty\.netty-codec /META-INF/native-image/io\.netty/netty-codec/generated/handlers/reflect-config\.json --exclude-config io\.netty\.netty-handler /META-INF/native-image/io\.netty/netty-handler/generated/handlers/reflect-config\.json quarkus-in-action-1.0.0-SNAPSHOT-runner -jar quarkus-in-action-1.0.0-SNAPSHOT-runner.jar
<略>
[INFO] --- install:3.1.2:install (default-install) @ quarkus-in-action ---
[INFO] Installing /home/minamoto/quarkus-in-action/pom.xml to /home/minamoto/.m2/repository/org/acme/quarkus-in-action/1.0.0-SNAPSHOT/quarkus-in-action-1.0.0-SNAPSHOT.pom
[INFO] Installing /home/minamoto/quarkus-in-action/target/quarkus-in-action-1.0.0-SNAPSHOT.jar to /home/minamoto/.m2/repository/org/acme/quarkus-in-action/1.0.0-SNAPSHOT/quarkus-in-action-1.0.0-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  01:53 min
[INFO] Finished at: 2025-01-20T00:26:58+09:00
[INFO] ------------------------------------------------------------------------

注意
ログを見ると、今度はPodmanを使わずに直接GraalVMのnative-imageコマンドが呼び出されていることが確認できます。

まとめ

Quarkusのネイティブイメージを生成する機能とインストール手順について紹介しました。

Linux環境の場合はGraalVMをインストールしなくとも、コンテナー内でビルドを実行するため、ネイティブビルドができました。MacOSやWindowsでネイティブビルドを実行する場合は、別途、GraalVMをインストールして、GRAALVM_HOME環境変数にパスを設定する必要があります。

SDKMANを使える環境であれば、デフォルトのJavaをGraalVM CEやMadrelに切り替えることで、GRAALVM_HOME環境変数にパスを設定しなくともすぐにネイティブビルドを試すことができました。

ネイティブイメージの速度を比較すると、JVMでのアプリケーションの実行は約0.5秒、ネイティブコードの実行は約0.02秒でした。その一方で、Javaのビルドは7秒程度でしたが、ネイティブビルドには1~2分という長い時間がかかりました(もちろん、これらの数値はアプリケーションや実行環境によって大きく変わりますので、参考程度にお願いします)。

次回はQuarkusアプリケーションをコンテナー化する手順について紹介する予定です。


いいなと思ったら応援しよう!