Java画像を最小化する方法

この記事では、**Spring Boot**ベースの**Java**アプリケーションを例に、**Java**イメージを最小化するための一般的なコツをいくつか紹介します。

*本ブログは英語版からの翻訳です。オリジナルは[こちら](https://www.alibabacloud.com/blog/common-tricks-for-minimizing-java-images_594956)からご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。*

#背景
コンテナ技術の普及に伴い、コンテナベースのアプリケーションが増えています。コンテナは頻繁に使用されていますが、ほとんどのコンテナユーザーは、コンテナイメージのサイズという単純だが重要な問題を無視しているかもしれません。この記事では、コンテナイメージを簡素化する必要性について簡単に説明し、Spring Boot ベースの Java アプリケーションを例に、Java イメージを最小化するための一般的なトリックをいくつか紹介します。

#コンテナイメージを簡素化する必要性
コンテナイメージの簡素化は非常に必要です。これについては、セキュリティとアジリティの両面から説明します。

#セキュリティについて
イメージから不要なコンポーネントを削除することで、攻撃面やセキュリティリスクを減らすことができます。Dockerでは、[Seccomp](https://docs.docker.com/engine/security/seccomp/?spm=a2c65.11461447.0.0.4c817eccQzbPjk)を使ってコンテナ内の操作を制限したり、[AppArmor](https://docs.docker.com/engine/security/apparmor/?spm=a2c65.11461447.0.0.4c817eccQzbPjk)を使ってコンテナのセキュリティポリシーを設定したりすることができます。ただし、これらを利用するにはセキュリティ分野での十分な習熟度が必要です。


#敏捷性
コンテナイメージを簡略化することで、コンテナのデプロイ速度を向上させることができます。アクセス トラフィックが突然バーストしたと仮定して、突然増加した圧力に対処するためにコンテナの数を増やす必要があるとします。一部のホストにターゲットイメージが含まれていない場合は、まずイメージを引っ張ってからコンテナを起動する必要があります。この場合、イメージを小さくすることで処理を高速化し、スケールアップの期間を短縮することができます。また、小さい画像の方がより早く構築でき、ストレージや伝送コストを節約することができます。

#よくあるコツ
Java アプリケーションをコンテナ化するには、以下の手順を実行します。

1、Java ソース・コードをコンパイルし、JAR パッケージを生成します。
2、JAR パッケージとサードパーティ製 JAR 依存関係を適切な位置に移動します。
このセクションで使用する例は、Spring Boot ベースの Java アプリケーションである [spring-boot-docker](https://github.com/brucewu-fly/spring-boot-docker?spm=a2c65.11461447.0.0.4c817eccQzbPjk) です。この例で使用した最適化されていない[dockerfile](https://github.com/brucewu-fly/spring-boot-docker/blob/master/dockerfiles/avoid/Dockerfile)は以下の通りです。

```
FROM maven:3.5-jdk-8
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
RUN mvn -f /usr/src/app/pom.xml clean package
ENTRYPOINT ["java","-jar","/usr/src/app/target/spring-boot-docker-1.0.0.jar"]
```

アプリケーションはMavenを使って作成したもので、dockerfileのベースイメージにはmaven:3.5.5-jdk-8を指定しています。このイメージのサイズは635MBです。この方法で作成した最終的なイメージのサイズは719MBとかなり大きいです。理由は、ベースイメージが大きいことと、最終イメージをビルドするためにMavenが多くのJARパッケージをダウンロードするためです。

#マルチステージビルド
Javaアプリケーションを実行するには、Javaランタイム環境（JRE）だけが必要です。MavenやJava Development Kit (JDK)のコンパイル、デバッグ、実行ツールは必要ありません。したがって、簡単な最適化方法は、Javaソースコードをコンパイルして作成するイメージと、Javaアプリケーションを実行するイメージを分離することです。そのためには、Docker 17.05のリリース前に2つのdockerfileファイルを維持する必要があり、イメージ構築の複雑さが増します。Docker 17.05からは、[マルチステージビルド](https://docs.docker.com/develop/develop-images/multistage-build/?spm=a2c65.11461447.0.0.4c817eccQzbPjk)機能により、1つのdockerfile内で複数のFROM文を使用できるようになりました。それぞれのFROM文で異なるベースイメージを指定し、全く新しいイメージ構築プロセスを開始することができます。前のイメージ構築ステージの製品を別のステージにコピーし、必要な内容だけを最終イメージに残すように選択することができます。最適化された[dockerfile](https://github.com/brucewu-fly/spring-boot-docker/blob/master/dockerfiles/multi-stage/Dockerfile?spm=a2c65.11461447.0.0.4c817eccQzbPjk)は以下のようになります。

```
FROM maven:3.5-jdk-8 AS build
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
RUN mvn -f /usr/src/app/pom.xml clean package

FROM openjdk:8-jre
ARG DEPENDENCY=/usr/src/app/target/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]
```

dockerfileでは、最初のステージのビルドイメージとして`maven:3.5-jdk-8`を使用し、Javaアプリケーションを実行するベースイメージとして`openjdk:8-jre`を使用しています。第一段階でコンパイルした`.class`ファイルのみを、サードパーティのJARの依存関係とともに最終イメージにコピーしています。最適化の結果、イメージのサイズは459MBに縮小されました。

#ベースイメージとしてディストロレスイメージを使用する
多段ビルドによって最終イメージのサイズは小さくなっていますが、それでも459MBは大きすぎます。総合的に分析した結果，ベースとなる`openjdk:8-jre`のサイズは443MBと大きすぎることがわかりました．そこで，次の最適化のステップとして，ベース画像のサイズを小さくすることにしました．

この問題を解決するために開発されたのが、Googleのオープンソースプロジェクトである[Distroless](https://github.com/GoogleContainerTools/distroless?spm=a2c65.11461447.0.0.4c817eccQzbPjk)です。Distrolessイメージは、アプリケーションとその実行時の依存関係だけを含んでいます。これらのイメージには、パッケージマネージャ、シェル、その他標準的なLinuxディストリビューションに含まれると思われるプログラムは含まれていません。現在、Distroless は [Java](https://github.com/GoogleContainerTools/distroless/blob/master/java/README.md?spm=a2c65.11461447.0.0.4c817eccQzbPjk&file=README.md)、[Python](https://github.com/GoogleContainerTools/distroless/blob/master/experimental/python2.7/README.md?spm=a2c65.11461447.0.0.4c817eccQzbPjk&file=README.md)、[Node.js](https://github.com/GoogleContainerTools/distroless/blob/master/experimental/nodejs/README.md?spm=a2c65.11461447.0.0.4c817eccQzbPjk&file=README.md)、[.NET](https://github.com/GoogleContainerTools/distroless/blob/master/experimental/dotnet/README.md?spm=a2c65.11461447.0.0.4c817eccQzbPjk&file=README.md) などの環境で動作するアプリケーション用のベースイメージを提供しています。

Distrolessイメージを使用した[dockerfile](https://github.com/brucewu-fly/spring-boot-docker/blob/master/dockerfiles/distroless/Dockerfile?spm=a2c65.11461447.0.0.4c817eccQzbPjk)ファイルは以下のようになります。

```
FROM maven:3.5-jdk-8 AS build
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
RUN mvn -f /usr/src/app/pom.xml clean package

FROM gcr.io/distroless/java
ARG DEPENDENCY=/usr/src/app/target/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]
```

この dockerfile と以前のものとの唯一の違いは、アプリケーションを実行するためのベースイメージが `openjdk:8-jre` (443 MB) から `gcr.io/distroless/java` (119 MB) に変更されたことです。その結果、最終的なイメージのサイズは135MBになります。

ディストロレスイメージを使うことの唯一の不便な点は、イメージにシェルが含まれていないことです。docker attachを使ってアプリケーションの標準入力、標準出力、標準エラー(またはこれら3つの組み合わせ)を実行中のコンテナにアタッチしてデバッグすることはできません。 distrolessの[debugイメージ](https://github.com/GoogleContainerTools/distroless?spm=a2c65.11461447.0.0.4c817eccQzbPjk#debug-images)はbusyboxシェルを提供します。しかし、このイメージをリパッケージしてコンテナをデプロイしなければならず、非デバッグイメージに基づいてデプロイされたコンテナには役に立ちません。セキュリティの観点から見ると、攻撃者はシェルを介して攻撃できないので、これは利点になるかもしれません。

#アルパインイメージをベースイメージとして使用する
docker attach を使用する必要があり、画像サイズを最小限に抑えたい場合は、ベース画像としてアルプスの画像を使用することができます。[アルパイン](https://github.com/gliderlabs/docker-alpine?spm=a2c65.11461447.0.0.4c817eccQzbPjk)画像は信じられないほど小さいのが特徴で、ベース画像のサイズは4MB程度しかありません。

アルペン画像を使用した[dockerfile](https://github.com/brucewu-fly/spring-boot-docker/blob/master/dockerfiles/alpine/Dockerfile?spm=a2c65.11461447.0.0.4c817eccQzbPjk)は以下のようになります。


```
FROM maven:3.5-jdk-8 AS build
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
RUN mvn -f /usr/src/app/pom.xml clean package

FROM openjdk:8-jre-alpine
ARG DEPENDENCY=/usr/src/app/target/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]
```

`openjdk:8-jre-alpine`はalpineをベースに構築されたもので、Javaランタイムを含んでいます。この dockerfile でビルドされたイメージのサイズは 99.2 MB で、 distroless イメージをベースにビルドされたイメージよりも小さくなっています。

実行中のコンテナにアタッチするには、`docker exec -ti <container_id> sh` コマンドを実行してください。

#Distroless と Alpine の比較
Distroless と Alpineはどちらも非常に小さなベース画像を提供することができます。本番環境ではどちらを使うべきでしょうか？セキュリティを第一に考えるのであれば、パッケージ化されたアプリケーションが実行できるのはバイナリファイルだけなので、distroless をお勧めします。イメージのサイズを重視するのであれば、alpine をお勧めします。

#その他のコツ
前述のコツに加えて、以下のような操作を行うことで、さらにイメージサイズをシンプルにすることができます。

1、dockerfile内の複数の命令を1つにまとめます。これにより、画像のレイヤー数が減り、画像サイズが小さくなります。
2、安定した大きなコンテンツをイメージの下層に配置し、頻繁に変化する小さなコンテンツを上層に配置します。この方法では、直接画像サイズを小さくすることはできません。しかし、イメージキャッシュの仕組みをフルに活用して、イメージの構築とコンテナのデプロイを高速化します。
Dockerfilesを最適化するためのヒントについては、[Dockerfilesを書くためのベストプラクティス](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/?spm=a2c65.11461447.0.0.4c817eccQzbPjk)を参照してください。

#概要
1、一連の最適化により、Javaアプリケーションの画像サイズは719MBから約100MBに縮小されました。アプリケーションが他の環境で動作する場合も、同様の原理で最適化することができます。
2、Javaイメージの場合、Googleが提供する別のツールである[jib](https://github.com/GoogleContainerTools/jib?spm=a2c65.11461447.0.0.4c817eccQzbPjk)は、複雑なイメージ構築プロセスを自動的に処理し、簡略化されたJavaイメージを提供することができます。これを使えば、dockerfileを書く必要はなく、Dockerをインストールする必要すらありません。
3、デバッグに不便なdistro-lessなどのコンテナについては、そのログを一元的に保存しておけば、問題のトレースやトラブルシューティングが容易になります。詳しくは、コンテナのログ処理のための技術的なベストプラクティスの記事を参照してください。


*アリババクラウドは日本に２つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
[アリババクラウドジャパン公式ページ](https://www.alibabacloud.com/ja)*