ノート:
- このチュートリアルは、Oracle提供の無料ラボ環境で入手できます。
- Oracle Cloud Infrastructureの資格証明、テナンシおよびコンパートメントに例の値を使用します。演習を完了するときは、これらの値をクラウド環境に固有の値に置き換えます。
GraalVM Native Image、Spring、Containerisation
イントロダクション
このラボは、GraalVM Native Imageアプリケーションをコンテナ化する方法について詳しく理解したい開発者向けです。
GraalVM Native Image技術では、Javaコードをネイティブの実行可能ファイルに事前にコンパイルします。実行可能ファイルに含まれているのは、実行時にアプリケーションが必要とするコードのみです。
ネイティブ・イメージによって生成される実行可能ファイルには、次のような重要な利点があります。
- JVMに必要なリソースの一部を使用するため、実行速度が低い
- ミリ秒単位で開始
- ウォームアップなく、ピーク・パフォーマンスをすぐに実現
- 軽量コンテナ・イメージにパッケージ化することで、より迅速かつ効率的なデプロイメントを実現できます。
- 攻撃対象領域を削減します(今後のラボではさらに活用)。
業界をリードするマイクロサービス・フレームワークの多くは、Micronaut、Spring、Helidon、Quarkusなど、GraalVM Native Imageのコンパイルを事前にサポートしています。
また、ネイティブ・イメージ用のMavenおよびGradleプラグインがあるため、Javaアプリケーションを実行可能ファイルとして簡単に構築、テストおよび実行することができます。
注意: Oracle Cloud Infrastructure (OCI)は、追加コストなしでGraalVM Enterpriseを提供します。
推定ラボ時間: 90分
ラボの目的
この演習では、次のことを行います。
- Dockerイメージに基本的なSpring Bootアプリケーションを追加して実行します
- GraalVM Native Imageを使用して、このアプリケーションからネイティブ実行可能ファイルを構築
- Dockerイメージへのネイティブ実行可能ファイルの追加
- GraalVM Native ImageとDistrolessコンテナでアプリケーションdockerイメージ・サイズを縮小
- GraalVM Native Buildツールの使用方法、Mavenプラグインを参照してください
注:演習にラップトップ・アイコンが表示されている場合は、コマンドの入力などを実行する必要があります。その事に注意しなさい。
# This is where you will need to do something
ステップ1:仮想ホストへの接続および開発環境の確認
開発環境は、リモート・ホスト(Oracle Linux 8、4コアおよび32GBのメモリーを備えたOCIコンピュート・インスタンス)によって提供されます。Luna Labsデスクトップ環境は、リモート・ホストの準備が完了する前に表示され、最大2分かかります。
リモート・ホストに接続するには、Lunaデスクトップ環境でセットアップ・スクリプトを実行します。このスクリプトは、「リソース」タブで使用できます。
-
デスクトップの「Luna Lab」アイコンをダブルクリックしてブラウザを開きます。
-
「リソース」タブが表示されます。コンピュート・インスタンスがクラウドにプロビジョニングされている間は、「リソース」タイトルの横に表示された歯車が回転することに注意してください。
-
インスタンスがプロビジョニングされると(最大で2分かかる場合があります)、「リソース」タブに次が表示されます。
-
「リソース」タブからVSコード環境を設定する構成スクリプトをコピーします。「詳細の表示」リンクをクリックして、構成を表示します。次のスクリーンショットに示すように、これをコピーします。
-
次のスクリーンショットに示すように、ターミナルを開きます。
-
構成コードを端末に貼り付け、VSコードを開きます。
-
VSコード・ウィンドウが開き、プロビジョニングされているVMインスタンスに自動的に接続します。「続行」をクリックして、マシンのフィンガープリントを受け入れます。
完了しました。これで、Oracle Cloudのリモート・ホストに正常に接続されました。
上のスクリプトは、演習のソース・コードを開いてリモート・コンピュート・インスタンスに接続されたVSコードを開きます。
次に、VSコード内で端末を開く必要があります。この端末を使用して、リモートホストを操作できます。端末は、メニュー「Terminal」>「New Terminal」を使用してVS Codeで開くことができます。
この端末は演習の残りの部分で使用します。
開発環境でのノート
このラボでは、Java環境としてGraalVM Enterprise 22を使用します。GraalVMは、信頼性が高く安全なOracle Java SE上に構築された、Oracleからの高パフォーマンスのJDKディストリビューションです。
この演習に必要なGraalVMとネイティブ・イメージ・ツールがあらかじめ構成されています。
端末で次のコマンドを実行すると、簡単に確認できます。
java -version
native-image --version
ステップ2:サンプルJavaアプリケーションの紹介
この演習では、非常に最小限のRESTベースのAPIでシンプルなアプリケーションを構築します。次に、Dockerを使用してこのアプリケーションをコンテナ化します。まず、簡単なアプリケーションをすばやく見ていきましょう。
このアプリケーションのソース・コードとビルド・スクリプトが提供されており、ソース・コードを含むフォルダがVSコードで開きます。
アプリケーションはSpring Bootフレームワーク上に構築され、Spring Native Project(GraalVM Native Imageを使用してネイティブの実行可能ファイルを生成するSpringインキュベータ)を利用します。
アプリケーションには、src/main/java
にある2つのクラスがあります。
com.example.demo.DemoApplication
: HTTPエンドポイント/jibber
も定義するメインのSpring Bootクラスcom.example.demo.Jabberwocky
:アプリケーションのロジックを実装するユーティリティ・クラス
そのため、アプリケーションは何を行いますか。アプリケーション内で定義されているエンドポイントREST /jibber
をコールすると、Lewis Carrollによって、Jabberwocky Poemのスタイルで生成されたセンスでないバージョンが返されます。このプログラムは、Markov Chainを使用して元の詩(基本的には統計モデル)をモデル化することで、これを実現しています。このモデルは新しいテキストを生成します。
サンプル・アプリケーションでは、アプリケーションに詩のテキストを指定してテキスト・モデルを生成した後、元のテキストに類似した新しいテキストを生成するために使用されます。RiTaライブラリを使用して負荷の高い作業を行っているため、Markov Chainの構築と使用をサポートします。
次に、モデルを作成するユーティリティ・クラスcom.example.demo.Jabberwocky
の2つのスニペットを示します。text
変数には、元の詩のテキストが含まれます。このスニペットは、モデルを作成してtext
に移入する方法を示しています。これはクラス・コンストラクタからコールされ、クラスをシングルトンとして定義します(そのため、クラスのインスタンスは1つのみ作成されます)。
this.r = new RiMarkov(3);
this.r.addText(text);
ここでは、元のテキストに基づいて、モデルから新しいバージョンの生成方法を確認できます。
public String generate() {
String[] lines = this.r.generate(10);
StringBuffer b = new StringBuffer();
for (int i=0; i< lines.length; i++) {
b.append(lines[i]);
b.append("<br/>\n");
}
return b.toString();
}
コードを表示して理解するには少し時間がかかります。
アプリケーションを構築するには、Mavenを使用します。pom.xml
ファイルはSpring Initializrを使用して生成され、Spring Nativeツールの使用のサポートが含まれています。これは、GraalVM Native Imageをターゲットとする場合、Spring Bootプロジェクトに追加した依存関係です。Mavenを使用している場合は、Spring Nativeのサポートを追加すると、デフォルトのビルド構成に次のプラグインが挿入されます。
<plugin>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-aot-maven-plugin</artifactId>
<version>${spring-native.version}</version>
<executions>
<execution>
<id>test-generate</id>
<goals>
<goal>test-generate</goal>
</goals>
</execution>
<execution>
<id>generate</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
次に、アプリケーションを構築します。リポジトリのルート・ディレクトリから、シェルで次のコマンドを実行します。
mvn clean package
これにより、実行可能なJARファイルが生成され、そのJARファイルには、アプリケーションのすべての依存関係と正しく構成されたMANIFEST
ファイルが含まれます。このJARファイルを実行してから、アプリケーションのエンドポイントを"ping"して、返される内容を確認できます。プロンプトが戻されるように、&
を使用してコマンドをバックグラウンドに配置します。
java -jar ./target/jibber-0.0.1-SNAPSHOT-exec.jar &
コマンドラインからcurl
コマンドを使用して、エンド・ポイントをコールします。
コマンドを端末に投稿すると、次に示すように、VSコードによってブラウザでURLを開くように求められることがあります。
次のことを実行して、HTTPエンドポイントをテストします。
curl http://localhost:8080/jibber
センスでないバージョンを取り戻しましたか。作業アプリケーションを構築したので、終了してコンテナ化に進みます。アプリケーションを終了できるように、アプリケーションをフォアグラウンドに移動します。
fg
アプリケーションを終了するには、<ctrl-c>
と入力します。
<ctrl-c>
ステップ3: DockerによるJavaアプリケーションのコンテナ化
JavaアプリケーションをDockerコンテナとしてコンテナ化することは、問題なく比較的単純です。JDKディストリビューションを含むイメージに基づいて、新しいDockerイメージを作成できます。そのため、このラボでは、JDK container-registry.oracle.com/java/openjdk:17-oraclelinux8
がすでに含まれているコンテナを使用します。これは、OpenJDKを使用したOracle Linux 8イメージです。
次に、Dockerイメージの構築方法を説明するDockerfileの内訳を示します。コンテンツの説明については、コメントを参照してください。
FROM container-registry.oracle.com/java/openjdk:17-oraclelinux8 # Base Image
ARG JAR_FILE # Pass in the JAR file as an argument to the image build
EXPOSE 8080 # This image will need to expose TCP port 8080, as this is the port on which your app will listen
COPY ${JAR_FILE} app.jar # Copy the JAR file from the `target` directory into the root of the image
ENTRYPOINT ["java"] # Run Java when starting the container
CMD ["-jar","app.jar"] # Pass in the parameters to the Java command that make it load and run your executable JAR file
Javaアプリケーションをコンテナ化するDockerfileは、ディレクトリ00-containerise
にあります。
アプリケーションを含むDockerイメージを作成するには、端末から次のコマンドを実行します。
docker build -f ./00-containerise/Dockerfile \
--build-arg JAR_FILE=./target/jibber-0.0.1-SNAPSHOT-exec.jar \
-t localhost/jibber:java.01 .
Dockerを問い合せて、新しく構築したイメージを確認します。
docker images | head -n2
新しいイメージがリストされます。このイメージを次のように実行します。
docker run --rm -d --name "jibber-java" -p 8080:8080 localhost/jibber:java.01
次に、curl
コマンドを使用する前に、エンドポイントをコールします。
curl http://localhost:8080/jibber
非センスな文章を見ましたか。アプリケーションの起動にかかった時間を確認します。Spring Bootアプリケーションは起動までの時間をログに書き込むため、ログから抽出できます。
docker logs jibber-java
たとえば、アプリケーションは3.896sで開始されます。ログからの抽出を次に示します。
2022-03-09 19:48:09.511 INFO 1 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 3.896 seconds (JVM running for 4.583)
OK、コンテナを終了して、次に移動します:
docker kill jibber-java
Dockerを問い合せてイメージのサイズを取得することもできます。これを行うスクリプトが用意されています。端末で次を実行します。
echo $((`docker inspect -f "" localhost/jibber:java.01`/1024/1024))
これにより、イメージのサイズがMB (606MB)で出力されます。
ステップ4:ネイティブ実行ファイルの構築
これまでの内容を要約します。
- HTTPエンドポイント
/jibber
を使用してSpring Bootアプリケーションを構築しました - 正常にコンテナ化されました
次に、GraalVM Native Imageを使用して、アプリケーションからネイティブ実行可能ファイルを作成する方法を説明します。このネイティブ実行可能ファイルには、次のような多くの興味深い特性があります。
- きわめて高速に始めよう
- 使用するリソースが対応するJavaアプリケーションよりも少なくなります。
GraalVMとともにインストールされたネイティブ・イメージ・ツールを使用して、コマンドラインからアプリケーションのネイティブ実行可能ファイルを構築できます。ただし、すでにMavenを使用しているため、GraalVM Native Build Tools for Mavenを適用します。このツールを使用すると、Mavenを使用して構築を続行できます。
ネイティブ実行可能ファイルを構築するためのサポートを追加する1つの方法は、Mavenのプロファイルを使用することです。これにより、JARファイルのみを構築するか、ネイティブ実行可能ファイルを作成するかを決定できます。
提供されているMaven pom.xml
ファイルで、ネイティブ実行可能ファイルをビルドするプロファイルを追加しました。詳細は、次を参照してください。
まず、プロファイルを宣言して名前を付ける必要があります。
<profiles>
<profile>
<id>native</id>
<!-- Rest of profile hidden, to highlight relevant parts -->
</profile>
</profiles>
次に、プロファイル内にGraalVM Native Imageビルド・ツール・プラグインを含め、Mavenのpackage
フェーズにアタッチします。これは、package
フェーズの一部として実行されることを意味します。<buildArgs>
セクションを使用して、基礎となるネイティブ・イメージ構築ツールに構成引数を渡すことができることに注意してください。個々のbuildArg
タグでは、native-image
ツールと同様にパラメータを渡すことができます。そのため、native-image
ツールで機能するすべてのパラメータを使用できます。
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>${native-buildtools.version}</version>
<extensions>true</extensions>
<executions>
<execution>
<id>build-native</id>
<phase>package</phase>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
<configuration>
<imageName>jibber</imageName>
<buildArgs>
<buildArg>-H:+ReportExceptionStackTraces</buildArg>
</buildArgs>
</configuration>
</plugin>
<!-- Rest of profile hidden, to high-light relevant parts -->
</plugins>
</build>
次のように、プロファイルを使用してMavenビルドを実行します(プロファイル名は-P
フラグで指定されます)。
mvn package -Pnative
これにより、プラットフォーム用のネイティブ実行可能ファイルがjibber
というtarget
ディレクトリに生成されます。ファイルのサイズを確認します。
ls -lh target/jibber
このネイティブ実行可能ファイルを実行し、テストします。端末で次のコマンドを実行して、ネイティブ実行可能ファイルを実行し、&
を使用してバックグラウンドに配置します。
./target/jibber &
curl
コマンドを使用してエンドポイントをコールします。
curl http://localhost:8080/jibber
これで、非常に高速に起動されるアプリケーションのネイティブ実行可能ファイルができました。
移動する前にアプリケーションを終了します。アプリケーションをフォアグラウンドに移動します。
fg
<ctrl-c>
で終了します。
<ctrl-c>
ステップ5:ネイティブ実行ファイルのコンテナ化
現在は、ネイティブの実行可能バージョンのアプリケーションがあり、それが動作していることを確認しているため、それをコンテナ化します。
このネイティブ実行可能ファイルをパッケージ化するための単純なDockerfileが提供されています。これはディレクトリnative-image/containerisation/lab/01-native-image/Dockerfile
にあります。次に、各行を説明するコメントとともに内容を示します。
FROM container-registry.oracle.com/os/oraclelinux:8-slim
ARG APP_FILE # Pass in the native executable
EXPOSE 8080 # This image will need to expose TCP port 8080, as this is port your app will listen on
COPY ${APP_FILE} app # Copy the native executable into the root directory and call it "app"
ENTRYPOINT ["/app"] # Just run the native executable :)
構築するには、端末から次を実行します。
docker build -f ./01-native-image/Dockerfile \
--build-arg APP_FILE=./target/jibber \
-t localhost/jibber:native.01 .
新しく構築したイメージを確認します。
docker images | head -n2
これを実行して、端末から次のようにテストできます。
docker run --rm -d --name "jibber-native" -p 8080:8080 localhost/jibber:native.01
curl
を使用して、端末からエンドポイントをコールします。
curl http://localhost:8080/jibber
また、詩のジャブバーウォッキーのスタイルで、よりナンセンスでないものを見るべきでした。アプリケーションの起動にかかった時間を調べるには、以前に作成したログを確認します。端末から次を実行して、起動時間を探します。
docker logs jibber-native
次は、アプリケーションが0.074sで開始したことを示しています。これは、3.896sのオリジナルと比較して大きな改善です。
2022-03-09 19:44:12.642 INFO 1 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 0.074 seconds (JVM running for 0.081)
コンテナを終了し、次のステップに進みます。
docker kill jibber-native
ただし、次のステップに進む前に、生成されたコンテナのサイズを確認します。
echo $((`docker inspect -f "" localhost/jibber:native.01`/1024/1024))
表示したコンテナ・イメージ・サイズは199
MBでした。最初のJavaコンテナとはかなり小さくなりました。
ステップ6:非常に静的実行可能ファイルを構築して、ストロレス・イメージにパッケージ化します
ここでも、これまでに行ったことを思い出してください。
- HTTPエンドポイント
/jibber
を使用してSpring Bootアプリケーションを構築しました - 正常にコンテナ化されました
- ネイティブ・イメージ・ビルド・ツールfor Mavenを使用して、アプリケーションのネイティブ実行可能ファイルを構築しました
- ネイティブ実行可能ファイルをコンテナ化しました
コンテナはダウンロードと起動が速いため、コンテナのサイズをさらに縮小できるとよいでしょう。GraalVM Native Imageを使用すると、システム・ライブラリを生成したネイティブ実行可能ファイルに静的にリンクできます。静的にリンクされたネイティブ実行可能ファイルを構築する場合、ネイティブ実行可能ファイルを空のDockerイメージ(scratch
コンテナとも呼ばれる)に直接パッケージ化できます。
もう1つのオプションは、統計的にリンクされたネイティブ実行可能ファイルと呼ばれるものを生成することです。これにより、標準のCライブラリglibc
を除くすべてのシステム・ライブラリで静的にリンクできます。このようなネイティブ実行可能ファイルでは、glibc
ライブラリ、標準ファイル、SSLセキュリティ証明書を含むGoogleのDistrolessなどの小さいコンテナを使用できます。標準のDistrolessコンテナのサイズは約20MBです。
ほぼ静的にリンクされた実行可能ファイルをビルドし、それをDistrolessコンテナにパッケージ化します。
もう1つのMavenプロファイルを追加して、このほとんどが静的にリンクされたネイティブ実行可能ファイルを構築しました。このプロファイルの名前はdistroless
です。このプロファイルと以前に使用したプロファイルnative
の唯一の違いは、パラメータ-H:+StaticExecutableWithDynamicLibC
を渡すことです。想定どおり、これは、ほぼ静的にリンクされたネイティブ実行可能ファイルを構築するようnative-image
に指示します。
次に示すとおり、静的にリンクされたネイティブ実行可能ファイルを構築できます。
mvn package -Pdistroless
とても簡単です。生成されたネイティブ実行可能ファイルは、ターゲット・ディレクトリjibber-distroless
にあります。
次に、Distrolessコンテナにパッケージ化します。これを実行するDockerfileは、ディレクトリnative-image/containerisation/lab/02-smaller-containers/Dockerfile
にあります。Dockerfileの内容を確認します。この内容には、各行について説明するためのコメントがあります。
FROM gcr.io/distroless/base # Our base image, which is Distroless
ARG APP_FILE # Everything else is the same :)
EXPOSE 8080
COPY ${APP_FILE} app
ENTRYPOINT ["/app"]
構築するには、端末から次を実行します。
docker build -f ./02-smaller-containers/Dockerfile \
--build-arg APP_FILE=./target/jibber-distroless \
-t localhost/jibber:distroless.01 .
新しく作成したDistrolessイメージを確認します。
docker images | head -n2
次に、次のように実行してテストできます。
docker run --rm -d --name "jibber-distroless" -p 8080:8080 localhost/jibber:distroless.01
curl http://localhost:8080/jibber
素晴らしいです!うまくいきました。しかし、コンテナはどれくらい小さいですか、大きいですか。スクリプトを使用してイメージ・サイズを確認します。
echo $((`docker inspect -f "" localhost/jibber:distroless.01`/1024/1024))
サイズは約107MBです!コンテナを92MB縮小しました。Javaコンテナの開始サイズから約600MBまでの長い道のり。
まとめ
この実習を楽しんでおり、途中でいくつかのことを学びましょう。Javaアプリケーションをコンテナ化する方法を見てきました。次に、そのJavaアプリケーションをネイティブ実行可能ファイルに変換する方法を学習し、Javaアプリケーションより大幅に高速に開始します。その後、ネイティブ実行可能ファイルをコンテナ化し、ネイティブ実行可能ファイルを含むDockerイメージのサイズがJava Dockerイメージよりもはるかに小さいことを確認しました。
最後に、ネイティブ・イメージを使用して、主に静的にリンクされたネイティブ実行可能ファイルを構築する方法を見てきました。これらはDistrolessなどの小規模コンテナにパッケージ化でき、これらによってDockerイメージのサイズがさらに縮小されます。
さらに学ぶ
- ネイティブ・イメージ・アーキテクトであるChristian Wimmer GraalVM Native Image: Javaの大規模な静的分析によるプレゼンテーションをご覧ください
- GraalVM Native Imageのリファレンス・ドキュメント
その他の学習リソース
docs.oracle.com/learnの他のラボを調べるか、Oracle Learning YouTubeチャネルでさらに無料の学習コンテンツにアクセスします。さらに、education.oracle.com/learning-explorerにアクセスしてOracle Learning Explorerにします。
製品ドキュメントは、Oracleヘルプ・センターを参照してください。
GraalVM Native Image, Spring and Containerisation
F54865-01
March 2022
Copyright © 2022, Oracle and/or its affiliates.