こんばんは、GxP高田です。
これはグロースエクスパートナーズ Advent Calendar 2022のカレンダーの22日目の記事です。2が多めで縁起がいいですね。
サンタクロースも走り回る師走を忙しくお過ごしの皆様に、さっと読めて来年からと言わず明日から使える知識をお届けしたいと思います。
Javaアプリのコンテナベースイメージどれにする?
アプリの実行環境として、Dockerに代表されるようなコンテナで実行されることがほとんどになってきました。
Javaを稼働させるためには、Java実行環境(Java Runtime Environment)が必要になるため、どうしてもコンテナイメージサイズが大きくなりがちです。どのイメージがいいのか、イメージサイズ、セキュリティ、メンテナンスが続くのか、などで大いにお悩みなのではないでしょうか。
JDK11から利用可能になったjlinkというツールを使うと、必要な機能に絞った可搬性の高いカスタムJREを作成できます。カスタムJREによって、ベースイメージにJDKを含む必要がなくなるため、ベースイメージ選択肢を広げることができます。
早速やってみましょう。
環境
今回はwindows11のwsl上でeclipse-temulin17.0.5で実行しています
$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=22.04
DISTRIB_CODENAME=jammy
DISTRIB_DESCRIPTION="Ubuntu 22.04.1 LTS"
$ java -version
openjdk version "17.0.5" 2022-10-18
OpenJDK Runtime Environment Temurin-17.0.5+8 (build 17.0.5+8)
OpenJDK 64-Bit Server VM Temurin-17.0.5+8 (build 17.0.5+8, mixed mode, sharing)
アプリは2020年のアドベントカレンダーの記事「Testcontainersでspring Data JPAをいい感じにテストする」で作ったものを再利用します。springboot3にupdateしたり、RESTful APIを追加していますが本記事とは無関係なので割愛します。
リポジトリはこちら。
なお、以降に出てくるコマンドはチェックアウトしたディレクトリで実行しているものとします。
コンパイルなど
まずはコンパイル。gradleアプリなのでbuildコマンドでjarまで作成しましょう。
$ ./gradlew build
$ ls -l ./buid/libs/
合計 39800
drwxrwxrwx 1 root root 4096 12月 20 13:39 ./
drwxrwxrwx 1 root root 4096 12月 20 13:39 ../
-rwxrwxrwx 1 root root 9826 12月 20 13:39 testcontainer-spring-demo-2022.12-plain.jar*
-rwxrwxrwx 1 root root 40741269 12月 20 13:39 testcontainer-spring-demo-2022.12.jar*
カスタムJREを作ろう
jdepsで依存するモジュールを調べる
jlinkでは、モジュールという単位で必要な機能を列挙してカスタムJREを作成します。JDK9以降には、必要なモジュールを知るために、jdepsというコマンドラインツールが用意されています。
jdepsにはいろいろなオプションがありますが、jlinkのモジュールを判断するために必要なのは--class-path
, --print-module-deps
, --ignore-missing-deps
です。
使い方はヘルプを見てみましょう。出力が多いので絞って結果を表示します。
$ jdeps --help
...
-cp <path>
-classpath <path>
--class-path <path> クラス・ファイルを検索する場所を指定します
...
--print-module-deps モジュール依存性のカンマ区切りリスト
を出力する--list-reduced-depsと同じです。
この出力は、これらのモジュールとその推移的な
依存性を含むカスタム・イメージを作成するために
jlink --add-modulesで使用できます。
--ignore-missing-deps 欠落している依存性を無視します。
...
依存関係の指定方法
springbootアプリはjar内部に/BOOT-INF/lib/
というディレクトリに依存ライブラリを保持し、起動時に読み込む仕組みを持っています。これらのjarをjdepsのクラスパスに含める必要があります。
jdeps: 依存ライブラリ指定なしで(正しくない!)
$ jdeps --print-module-deps --ignore-missing-deps ./build/lib/testcontainer-spring-demo-2022.12.jar
java.base,java.logging
jdeps: 依存ライブラリ指定あり
# jarを展開
$ unzip testcontainer-spring-demo-2022.12.jar -d flatten
$ jdeps -R -cp "flatten/BOOT-INF/lib/*" --print-module-deps --ignore-missing-deps --multi-release 17 build/libs/testcontainer-spring-demo-2022.12.jar
java.base,java.compiler,java.desktop,java.instrument,java.management,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql.rowset,jdk.jfr,jdk.unsupported
出力結果のモジュール数が2と15で全然違いますね。
jlinkでカスタムJREを作ってみる
jdepsコマンドの結果により依存するモジュールが分かったところで、いよいよカスタムJREを作成します。jlinkコマンドを実行します。
$ jlink \
--add-modules java.base,java.compiler,java.desktop,java.instrument,java.management,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql.rowset,jdk.jfr,jdk.unsupported \
--strip-debug \
--no-man-pages \
--no-header-files \
--compress=2 \
--output ./javaruntime
jlinkコマンドのオプション
add-modules
以外の指定したオプションについては
Oracleのjlinkコマンドやman jlink
を確認してください
javaruntimeディレクトリが作成されます。サイズを測ってみましょう。54MBという結果でした。
$ du -sh javaruntime/
54M javaruntime/
コンテナ化した際の容量も比較してみましょう。ベースイメージが異なるので比較にあまり意味はないですが、小さくなった実感は得られるのではないでしょうか。
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
local/demo-2022.12/eclipse-temurin17 latest ff176a736649 About a minute ago 545MB
local/demo-2022.12/custom-jre latest 36130c388e5b 2 minutes ago 166MB
eclipse-temurin17としているイメージはeclipse-temurin:17.0.5_8-jdk
をベースイメージにアプリのjarファイルを追加したもの、custom-jreはjlinkで作成したカスタムJREを格納したイメージです。
イメージの作成方法
詳細はリポジトリ中のDockerfile, Dockerfile.eclipse-temurin17を参照してください
まとめ
jdeps、jlinkというコマンドラインツールを用いてカスタムJREを作る方法をご紹介しました。Microsoft AzureでJavaを稼働させる際の公式ドキュメント、eclipse-temurinのdockerhubの説明にも手順が紹介されているなど、徐々に浸透してきている印象ですが、springbootでのモジュール一覧の取得方法などのノウハウと合わせて紹介しました。
JDK8時代の開発のノウハウをアップデートする一助になると幸いです。
おまけ
カスタムJREはjfrでプロファイルできるの?
できますよ。
JavaにはJava Fright Recorderという、パフォーマンス分析のための情報収集ツールが存在します。カスタムJREでもjdk.jfr
モジュールをjlinkコマンドのadd-modulesオプションに加えることで可能になります。
サンプルのDockerfileでは起動設定でjfrを有効にしています。
ENTRYPOINT ["java","-XX:StartFlightRecording=name=app-profile,filename=/app-profile.jfr,dumponexit=true,maxsize=10m,settings=profile,disk=true","-jar","/app.jar"]
アプリを起動するとオプションのfilename
で指定した場所にファイルが作成されます。
docker exec -ti testcontainers-spring-demo-app-1 /bin/bash
root@71b169124d59:/# ls -l
total 39852
-rw-r--r-- 1 root root 0 Dec 20 15:39 app-profile.jfr