Spring Boot で作成したアプリケーションをコンテナ化する際、jlink コマンドを利用してカスタム JRE を作成し、軽量なベース・イメージを利用する事で、349MB から 105MB と大幅にコンテナのイメージ・サイズを削減する事ができます。今回はその方法を詳細に紹介します。
作成方法の手順
サンプルプロジェクトの作成
Spring Starter を利用してサンプルのプロジェクトを作成します。
サンプル RESTful エンドポイントの作成
HelloController.java
ファイルを作成し下記を実装します。
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "Hello World";
}
}
ソースコードのビルド
プロジェクトを作成したのち、ソースコードをビルドします。
> mvn clean package
プロジェクトを構築すると下記のファイルが target ディレクトリ配下に作成されます。
> ls target
classes hello-sample-0.0.1-SNAPSHOT.jar maven-status
generated-sources hello-sample-0.0.1-SNAPSHOT.jar.original surefire-reports
generated-test-sources maven-archiver test-classes
ここで、hello-sample-0.0.1-SNAPSHOT.jar
ファイルが Spring Boot の実行可能ファイルになります。
下記のコマンドを実行し動作確認を行う事ができます。
java -jar hello-sample-0.0.1-SNAPSHOT.jar
ブラウザもしくは curl コマンドでエンドポイントに接続します。
> curl localhost:8080/hello
Hello World
カスタム JRE とコンテナ・イメージの作成
次に、作成したプロジェクトをコンテナ上で実行できるように、コンテナ・イメージを作成します。下記の Dockerfile
を作成してください。
##################################################################
# Stage 1: Create Custom Java VM for Spring Boot
##################################################################
FROM mcr.microsoft.com/openjdk/jdk:17-ubuntu AS CREATE-CUSTOM-JDK
WORKDIR /workspace/app
RUN jlink --no-header-files --no-man-pages --compress=2 --strip-debug --add-modules java.base,java.logging,java.naming,java.desktop,java.management,java.security.jgss,java.instrument,java.sql --output custom-jre
##################################################################
# Stage 2: Need Zlib for JVM on Distroless Image
##################################################################
FROM mcr.microsoft.com/cbl-mariner/base/core:2.0 AS ZLIB-INSTALLER
RUN mkdir /staging \
&& tdnf install -y --releasever=2.0 --installroot /staging zlib
##################################################################
FROM mcr.microsoft.com/cbl-mariner/distroless/minimal:2.0 AS CBLMIN
VOLUME /tmp
WORKDIR /app
ARG DEPENDENCY=/workspace/app/
COPY --from=CREATE-CUSTOM-JDK ${DEPENDENCY}/custom-jre /app/jre
COPY --from=ZLIB-INSTALLER /staging/ /
ENV LANG='ja_JP.UTF-8' LANGUAGE='ja_JP:ja' LC_ALL='ja_JP.UTF-8'
ENV TZ='Asia/Tokyo'
ENV JAVA_HOME=/app
# COPY the Spring Boot application artifact from Local
COPY ./target/hello-sample-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["/app/jre/bin/java","-jar","/app/app.jar"]
EXPOSE 8080
Dockerfile
を作成した後、下記のコマンドを実行しコンテナ・イメージを作成します。
> docker build -t tyoshio2002/hello-spring:1.0 .
作成したイメージのファイル・サイズを確認してください。105MB
のイメージが作成できた事を確認できます。
> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
tyoshio2002/hello-spring 1.0 5ef97f8669d8 26 seconds ago 105MB
作成したコンテナ・イメージを実行してください。
> docker run -it -p 8080:8080 tyoshio2002/hello-spring:1.0
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.7.3)
2022-09-21 14:03:37.631 INFO 1 --- [ main] com.yoshio3.HelloSampleApplication : Starting HelloSampleApplication v0.0.1-SNAPSHOT using Java 17.0.4.1 on ae593530aadc with PID 1 (/app/app.jar started by root in /app)
2022-09-21 14:03:37.634 INFO 1 --- [ main] com.yoshio3.HelloSampleApplication : No active profile set, falling back to 1 default profile: "default"
2022-09-21 14:03:38.420 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2022-09-21 14:03:38.429 INFO 1 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2022-09-21 14:03:38.429 INFO 1 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.65]
2022-09-21 14:03:38.477 INFO 1 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2022-09-21 14:03:38.478 INFO 1 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 797 ms
2022-09-21 14:03:38.748 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2022-09-21 14:03:38.756 INFO 1 --- [ main] com.yoshio3.HelloSampleApplication : Started HelloSampleApplication in 1.481 seconds (JVM running for 1.869)
ブラウザもしくは curl コマンドでエンドポイントに接続します。
> curl localhost:8080/hello
Hello World
この Dockerfile
を使用した方法により、105MB
という非常に小さなコンテナ・イメージを作成できました。
Azure 上で、小さなコンテナ・イメージを利用したい場合で、かつ Java 11 以降の環境では上記のように、jlink コマンドを使用したカスタム JRE を作成し、Microsoft が正式サポートする軽量な Linux コンテナ・イメージを作成しご利用いただく事ができます。
技術の詳細:この方法をおすすめする理由
以降では、なぜ上記の方法をお勧めするか、より詳しく説明します。
OpenJDK をインストールしたコンテナ・イメージの確認
まず、OpenJDK をインストールしたコンテナ・イメージのファイル・サイズを確認します。
Microsoft が提供する MS Build OpenJDK 関連のコンテナのベース・イメージは下記より入手可能です。
これらのイメージを実際に pull して確認すると、下記を確認できます。(2022/09/21 時点)
mcr.microsoft.com/openjdk/jdk:17-ubuntu 412MB
mcr.microsoft.com/openjdk/jdk:17-mariner 364MB
mcr.microsoft.com/openjdk/jdk:17-distroless 331MB
こちらをご覧いただくと、一番下の行に記載されている、Mariner の distroless のイメージ・サイズが最も小さい事がわかります。カスタム JRE を作成しない場合は、一番下のイメージをベース・イメージとしてご利用いただくのがお勧めです。
しかし、他のプログラミング言語で利用するコンテナのイメージサイズと比較して、330MB を超えるイメージは如何でしょうか? 実際には、自分が作成したアプリケーションをデプロイするため、このサイズに対しプラス・α のイメージ・サイズになります。
なぜ、このようにイメージ・サイズが大きくなっているのかを次で確認してみたいと思います。
OpenJDK 自身のファイルサイズの確認
それでは次に、OpenJDK のファイル・サイズを確認します。
MS Build OpenJDK では下記の URL より、各プラットフォーム毎の OpenJDK を入手する事ができます。
こちらから、Linux 版の JDK 17.0.4.1 をダウンロードして確認しました。その結果、アーカイブされた状況で 191MB
でした。
-rw-r--r--@ 1 teradayoshio staff 191382575 9 21 17:03 microsoft-jdk-17.0.4.1-linux-x64.tar.gz
アーカイブを展開すると、約 324MB
になりました。この事から、distroless のイメージの大半が OpenJDK のファイル群である事がお分かりいただけるかと思います。
> du -sk .
324656 .
完全な OpenJDK のイメージを含んだバージョンのイメージを利用する場合、それだけで大きなサイズのコンテナ・イメージが作成される事が上記からお分かり頂けるかと思います。
もちろん、プロバイダーは src.zip を削除したり、いらないファイルを削除する事でスリム化を試みていますが、OpenJDK を完全にインストールしたイメージを利用した場合、サイズを小さくするのに限界があります。
そこで以降では、より小さなイメージを作成するための方法をご紹介します。
jlink を利用したカスタム JRE の作成
サイズをより小さくするために、Java 9
で導入された jlink コマンドを利用します。
jlink コマンドでカスタムな Java Runtime を作成する事で、自分のアプリケーションを動かすために必要最小限の、小さな Java 実行環境を手に入れる事ができます。
Dockerfile
中の Stage 1
で、ローカルでビルドを行った Java の成果物(./target/yoshio-sample-0.0.1-SNAPSHOT.jar
)をコピーし、jlink
コマンドで、SpringBoot アプリケーションの実行に必要なモジュールを読み込み、カスタムな JRE(custom-jre
) を作成しています。
カスタム JRE は custom-jre
ディレクトリ配下に生成されます。
##################################################################
# Stage 1: Create Custom Java VM for Spring Boot
##################################################################
FROM mcr.microsoft.com/openjdk/jdk:17-ubuntu AS CREATE-CUSTOM-JDK
WORKDIR /workspace/app
RUN jlink --no-header-files --no-man-pages --compress=2 --strip-debug --add-modules java.base,java.logging,java.naming,java.desktop,java.management,java.security.jgss,java.instrument,java.sql --output custom-jre
ご注意:
jlink
コマンドを含む OpenJDK が必要なため、Stage 1
ではmcr.microsoft.com/openjdk/jdk:17-ubuntu
のイメージを利用しています。実行環境はStage 1
で作成したカスタムな JRE をコピーして利用します。また、カスタム JRE は一度作成したら、極力再利用をしてください。Container はレイヤー構造になっており、変更のないレイヤーは再利用されます。変更のあった部分だけを更新する事で、コンテナー・イメージの Upload や Pull の容量を減らす事ができ、起動時間の短縮に繋がります。
ご注意:
仮にアプリの中で SSL 通信が必要な場合は、jdk.crypto.ec
をモジュールに追加してください。
最終成果物のコンテナ・イメージの作成
Microsoft のコンテナ環境で推奨サポートされるコンテナ・イメージ
カスタム JRE を作成したので最後に、サービスとして提供するコンテナ・イメージを作成します。
最終的な実行環境のベース・コンテナー・イメージとして利用するのは、マイクロソフトが正式にサポートする、CBL-Mariner というイメージを利用します。
CBL-Mariner Linux は、クラウド環境に必要なパッケージだけを含んだ軽量でセキュアな OS で、アプリケーションの要件に合わせてカスタマイズも可能です。
CBL-Mariner は、Microsoft クラウド サービスおよび関連製品で使用するために推奨されている Linux ディストリビューションで、Microsoft Build OpenJDK を含むコンテナ・イメージでもベース・イメージとして採用されています。
現在 CBL-Mariner は 2.0 がリリースされており "Announcing CBL-Mariner 2.0"こちらのバージョンを利用する事が推奨されています。
ご注意
古い Mariner 1.0 は、Mariner 2.0 の正式リリース後、約6か月間は維持されますが、Mariner 2.0 の使用を推奨します。
コンテナ・イメージは下記から Pull して利用できます。
-
Base container
- mcr.microsoft.com/cbl-mariner/base/core:2.0
-
Distroless containers
- mcr.microsoft.com/cbl-mariner/distroless/base:2.0
- mcr.microsoft.com/cbl-mariner/distroless/debug:2.0
- mcr.microsoft.com/cbl-mariner/distroless/minimal:2.0
実際にそれぞれの・イメージを入手してイメージ・サイズを比較した結果が下記です。
mcr.microsoft.com/cbl-mariner/base/core:2.0 66.1MB
mcr.microsoft.com/cbl-mariner/distroless/base:2.0 31.1MB
mcr.microsoft.com/cbl-mariner/distroless/debug:2.0 32.6MB
mcr.microsoft.com/cbl-mariner/distroless/minimal 2.0 203kB
これらのイメージの中から、本番環境で利用するコンテナ・イメージを選択します。
コンテナ・イメージの作成
最後に、最終成果物のコンテナ・イメージを作成します。Stage 3
が最終的なサービス提供用のコンテナ・イメージです。しかし Stage 3 の前に、Stage 2
を行わないと zlib
関連のファイルが存在しないためエラーが発生します。そこでカスタム JRE の実行ができません。そこで、Stage 2
で zlib
を入手し、Stage 3
のイメージにコピーしています。 これにより不要なファイルをインストールせずに、必要なライブラリだけを Stage 3
のイメージにコピーして作成する事ができます。
##################################################################
# Stage 2: Need Zlib for JVM on Distroless Image
##################################################################
FROM mcr.microsoft.com/cbl-mariner/base/core:2.0 AS ZLIB-INSTALLER
RUN mkdir /staging \
&& tdnf install -y --releasever=2.0 --installroot /staging zlib
##################################################################
FROM mcr.microsoft.com/cbl-mariner/distroless/minimal:2.0 AS CBLMIN
VOLUME /tmp
WORKDIR /app
ARG DEPENDENCY=/workspace/app/
COPY --from=CREATE-CUSTOM-JDK ${DEPENDENCY}/custom-jre /app/jre
COPY --from=ZLIB-INSTALLER /staging/ /
ENV LANG='ja_JP.UTF-8' LANGUAGE='ja_JP:ja' LC_ALL='ja_JP.UTF-8'
ENV TZ='Asia/Tokyo'
ENV JAVA_HOME=/app
# COPY the Spring Boot application artifact from Local
COPY ./target/hello-sample-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["/app/jre/bin/java","-jar","/app/app.jar"]
EXPOSE 8080
最終的に、Stage 1
で作成した、カスタム JRE、Stage 2
の zlib をコピーし、ロケールやタイムゾーンを設定し最後にビルドしたアプリケーションの jar ファイル(app.jar
)をコピーして起動しています。
こちらの方法は、MS Build OpenJDK の提供イメージを参考にして作成した方法です。
カスタム JRE を利用したより軽量なサイズのコンテナ・イメージを作成する方法として、開発チームにも確認をしサポートされる内容ですので、どうぞ参考にしてご活用ください。