ノート:

GraalVM Native Image、Spring、Containerisation

イントロダクション

このラボは、GraalVM Native Imageアプリケーションをコンテナ化する方法について詳しく理解したい開発者向けです。

GraalVM Native Image技術では、Javaコードをネイティブの実行可能ファイルに事前にコンパイルします。実行可能ファイルに含まれているのは、実行時にアプリケーションが必要とするコードのみです。

ネイティブ・イメージによって生成される実行可能ファイルには、次のような重要な利点があります。

業界をリードするマイクロサービス・フレームワークの多くは、Micronaut、Spring、Helidon、Quarkusなど、GraalVM Native Imageのコンパイルを事前にサポートしています。

また、ネイティブ・イメージ用のMavenおよびGradleプラグインがあるため、Javaアプリケーションを実行可能ファイルとして簡単に構築、テストおよび実行することができます。

注意: Oracle Cloud Infrastructure (OCI)は、追加コストなしでGraalVM Enterpriseを提供します。

推定ラボ時間: 90分

ラボの目的

この演習では、次のことを行います。

注:演習にラップトップ・アイコンが表示されている場合は、コマンドの入力などを実行する必要があります。その事に注意しなさい。

# This is where you will need to do something

ステップ1:仮想ホストへの接続および開発環境の確認

開発環境は、リモート・ホスト(Oracle Linux 8、4コアおよび32GBのメモリーを備えたOCIコンピュート・インスタンス)によって提供されます。Luna Labsデスクトップ環境は、リモート・ホストの準備が完了する前に表示され、最大2分かかります。

リモート・ホストに接続するには、Lunaデスクトップ環境でセットアップ・スクリプトを実行します。このスクリプトは、「リソース」タブで使用できます。

  1. デスクトップの「Luna Lab」アイコンをダブルクリックしてブラウザを開きます。

    Lunaデスクトップアイコン

  2. 「リソース」タブが表示されます。コンピュート・インスタンスがクラウドにプロビジョニングされている間は、「リソース」タイトルの横に表示された歯車が回転することに注意してください。

    Lunaリソース・タブ

  3. インスタンスがプロビジョニングされると(最大で2分かかる場合があります)、「リソース」タブに次が表示されます。

    Lunaリソース・タブ

  4. 「リソース」タブからVSコード環境を設定する構成スクリプトをコピーします。「詳細の表示」リンクをクリックして、構成を表示します。次のスクリーンショットに示すように、これをコピーします。

    構成スクリプトのコピー

  5. 次のスクリーンショットに示すように、ターミナルを開きます。

    ターミナルを開く

  6. 構成コードを端末に貼り付け、VSコードを開きます。

    端末1を貼り付け

    端末2を貼り付け

  7. VSコード・ウィンドウが開き、プロビジョニングされているVMインスタンスに自動的に接続します。「続行」をクリックして、マシンのフィンガープリントを受け入れます。

    VSコード受入

完了しました。これで、Oracle Cloudのリモート・ホストに正常に接続されました。

上のスクリプトは、演習のソース・コードを開いてリモート・コンピュート・インスタンスに接続されたVSコードを開きます。

次に、VSコード内で端末を開く必要があります。この端末を使用して、リモートホストを操作できます。端末は、メニュー「Terminal」>「New Terminal」を使用してVS Codeで開くことができます。

VSコード・ターミナル

この端末は演習の残りの部分で使用します。

開発環境でのノート

このラボでは、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つのクラスがあります。

そのため、アプリケーションは何を行いますか。アプリケーション内で定義されているエンドポイント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を開くように求められることがあります。

VSコード

次のことを実行して、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:ネイティブ実行ファイルの構築

これまでの内容を要約します。

  1. HTTPエンドポイント/jibberを使用してSpring Bootアプリケーションを構築しました
  2. 正常にコンテナ化されました

次に、GraalVM Native Imageを使用して、アプリケーションからネイティブ実行可能ファイルを作成する方法を説明します。このネイティブ実行可能ファイルには、次のような多くの興味深い特性があります。

  1. きわめて高速に始めよう
  2. 使用するリソースが対応する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))

表示したコンテナ・イメージ・サイズは199MBでした。最初のJavaコンテナとはかなり小さくなりました。

ステップ6:非常に静的実行可能ファイルを構築して、ストロレス・イメージにパッケージ化します

ここでも、これまでに行ったことを思い出してください。

  1. HTTPエンドポイント/jibberを使用してSpring Bootアプリケーションを構築しました
  2. 正常にコンテナ化されました
  3. ネイティブ・イメージ・ビルド・ツールfor Mavenを使用して、アプリケーションのネイティブ実行可能ファイルを構築しました
  4. ネイティブ実行可能ファイルをコンテナ化しました

コンテナはダウンロードと起動が速いため、コンテナのサイズをさらに縮小できるとよいでしょう。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イメージのサイズがさらに縮小されます。

さらに学ぶ

その他の学習リソース

docs.oracle.com/learnの他のラボを調べるか、Oracle Learning YouTubeチャネルでさらに無料の学習コンテンツにアクセスします。さらに、education.oracle.com/learning-explorerにアクセスしてOracle Learning Explorerにします。

製品ドキュメントは、Oracleヘルプ・センターを参照してください。