はじめに
Java アプリケーションをコンテナ化するツールに jib があります。
GitHub の README には、Docker のベストプラクティスを知らなくても最適な Docker イメージを作ってくれると書かれています。
Jib builds optimized Docker and OCI images for your Java applications without a Docker daemon - and without deep mastery of Docker best-practices.
そんな Jib がどのくらい Docker イメージのベストプラクティスを満たしているのか調査してみました。
準備
検証には、Spring Initializr で作成した Maven プロジェクトを使います。
jib-maven-plugin の README の通り、pom.xml に以下の記述を追加します。
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>1.0.2</version>
<configuration>
<to>
<image>myimage</image>
</to>
</configuration>
</plugin>
ビルドしてみます。
$ ./mvnw compile jib:dockerBuild
:
:
:
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 18.444 s
[INFO] Finished at: 2019-03-23T13:59:31+09:00
[INFO] ------------------------------------------------------------------------
Docker イメージを確認すると ...
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
myimage latest 8ef0bcee6c5d 49 years ago 141MB
Dockerfile を一切書かず、イメージができました !
起動も通常通り成功しました。
$ docker run --rm myimage
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.3.RELEASE)
2019-03-23 05:01:06.902 INFO 1 --- [ main] s.SpringDockerExampleApplication : Starting SpringDockerExampleApplication on 072b9539cd19 with PID 1 (/app/classes started by root in /)
:
:
:
ここまでハマる点は一切なく、想像以上に簡単でした。
ベストプラクティスへの対応の調査
コンテナイメージをビルドするには、7 best practices for building containers のようなベストプラクティスがあります。
今回は、Jib で作成したイメージが以下のプラクティスを満たしているのか調査します。
- PID 1 で起動
- ビルドキャッシュの最適化
- 不要なツールの削除
- イメージの最小化
- レイヤー数の削減
- 一般ユーザで実行
PID 1 で起動
コンテナ停止時はコンテナ内の PID 1 のプロセスにシグナルが送られるため、PID 1 でアプリケーションを起動することが推奨されています。
Spring Boot 起動時のログを見ると ...
2019-03-23 05:01:06.902 INFO 1 --- [ main] s.SpringDockerExampleApplication : Starting SpringDockerExampleApplication on 072b9539cd19 with PID 1 (/app/classes started by root in /)
Starting SpringDockerExampleApplication on 072b9539cd19 with PID 1
と出力されており、PID 1 で起動していることが分かります。
ビルドキャッシュの最適化
Jib の README にビルドキャッシュの最適化を目指していることが書かれています。
Fast - Deploy your changes fast. Jib separates your application into multiple layers, splitting dependencies from classes. Now you don’t have to wait for Docker to rebuild your entire Java application - just deploy the layers that changed.
詳細は調べていませんが、これを自作の Dockerfile で再現するには、ある程度 Docker の知識が必要になってきそうです。
不要なツールの削除
コンテナには何が入っているのか調べるため docker exec で sh を実行しようとすると、以下のようにエラーとなりました。
$ docker exec -it $(docker container ls | grep myimage | awk '{print $1}') sh
OCI runtime exec failed: exec failed: container_linux.go:344: starting container process caused "exec: \"sh\": executable file not found in $PATH": unknown
FAQ の where is bash によると ...
By default, Jib uses distroless/java as the base image. Distroless images contain only runtime dependencies. They do not contain package managers, shells or any other programs you would expect to find in a standard Linux distribution. Check out the distroless project for more information about distroless images.
If you would like to include a shell for debugging, set the base image to gcr.io/distroless/java:debug instead. The shell will be located at /busybox/sh. Note that :debug images are not recommended for production use.
実行に必要な依存関係しか入っておらず、デバッグ用にシェルがほしいならベースイメージを変更するようにとのことです。
ここまで不要なツールを削除した Java のイメージを自作するのは結構大変だと思います。
ちなみに、Java についても、java コマンドは入っていましたが、javac コマンドは入っていませんでした。
$ docker run --rm --entrypoint java myimage -version
openjdk version "1.8.0_181"
OpenJDK Runtime Environment (build 1.8.0_181-8u181-b13-2~deb9u1-b13)
OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode)
$ docker run --rm --entrypoint javac myimage -version
docker: Error response from daemon: OCI runtime create failed: container_linux.go:344: starting container process caused "exec: \"javac\": executable file not found in $PATH": unknown.
~/Documents/src/os1ma/spring-docker-example (master) $
イメージの最小化
イメージサイズを自作の Dockerfile からビルドした場合と比較します。
Dockerfile は以下の通りです。1
FROM openjdk:8u181-jre-alpine3.8
RUN addgroup -S -g 1000 app \
&& adduser -D -H -S -G app -u 1000 app
USER app
WORKDIR /app
COPY target/*.jar .
# *.jar を展開するために sh -c を実行し、
# さらに PID 1 で java プロセスを起動するため exec を使用
CMD ["sh", "-c", "exec java -jar *.jar"]
JAR と Docker イメージをビルドします。
$ ./mvnw clean package
$ docker build -t myimage-openjdk .
イメージサイズを比較すると ...
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
myimage-openjdk latest 262fd5db197f 9 seconds ago 99.8MB
myimage latest 8ef0bcee6c5d 49 years ago 141MB
openjdk:8u181-jre-alpine3.8 をベースに自作した方が小さくなりました。
とはいえ、Alpine ベースで提供されていない2 Amazon Correto を利用した場合よりは小さくなっています。
REPOSITORY TAG IMAGE ID CREATED SIZE
myimage-amazon latest dd6500aa12d0 4 minutes ago 542MB
なお、Amazon Correto を使った場合の Dockerfile は以下です。
FROM amazoncorretto:8u202
RUN groupadd -r -g 1000 app \
&& useradd -M -r -g app -u 1000 app
USER app
WORKDIR /app
COPY target/*.jar .
# *.jar を展開するために sh -c を実行し、
# さらに PID 1 で java プロセスを起動するため exec を使用
CMD ["sh", "-c", "exec java -jar *.jar"]
レイヤー数の削減
Jib で作成したイメージと Dockerfile から作成したイメージのレイヤ数を比較すると ...
$ docker image inspect myimage | jq '.[0].RootFS.Layers'
[
"sha256:44873b569cf340a616026da244ebee1bbd7d3f2bb7a0dbf7a6526c4416dea61a",
"sha256:87c747af6dc3478b57493f1f3a2e0821696f8d56b86b1fef1128d1d74881cf7c",
"sha256:6189abe095d53c1c9f2bfc8f50128ee876b9a5d10f9eda1564e5f5357d6ffe61",
"sha256:cdfa1ce6eb7e772884aeab7a2560bee6988f7bba47addeedb636842d72a23702",
"sha256:5e1ddec1ac755324b4489ba4030512f5f461ace13be7f9617982b0a12dcaec16",
"sha256:edb7e86863542fc9cfaee3eb48af4678560b396dd5dcc18ee89e089aee23abf4",
"sha256:769f5c896c76f8bf79881949a0d55cbd768405580f1ae87b4519b16da83d2387"
]
$ docker image inspect myimage-openjdk | jq '.[0].RootFS.Layers'
[
"sha256:7bff100f35cb359a368537bb07829b055fe8e0b1cb01085a3a628ae9c187c7b8",
"sha256:dbc783c89851d29114fb01fd509a84363e2040134e45181354051058494d2453",
"sha256:178e89c683ce4b8f572eb7d89c48a70e39f740b2c81274a58092e02a764732d6",
"sha256:9f4e44fd5bdc31d8dbe3074d8db6b5ccc13504f65cc3e3bfac580f2f1710840b",
"sha256:f5892329ace5f98e26db46cf436780fa28747a95303a2e283a9ae41be0246551",
"sha256:426afce096026f233072c74b991ceec088b7278166096d364bf4db9f214cb238"
]
自作した方が 1 つレイヤーが少なくなっています。
Jib ではビルド時にレイヤキャッシュを利用していることが関係しているのかもしれません。
一般ユーザで実行
コンテナ内で root ユーザを使わないよう、Dockerfile で一般ユーザを作成するというプラクティスがあります。
しかし、Jib で作成したイメージを起動した際のログには (/app/classes started by root in /)
と書かれており、root ユーザで実行されていました。
2019-03-23 05:01:06.902 INFO 1 --- [ main] s.SpringDockerExampleApplication : Starting SpringDockerExampleApplication on 072b9539cd19 with PID 1 (/app/classes started by root in /)
一般ユーザで起動するイメージを作成するには、pom.xml に追記が必要でした。
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>1.0.2</version>
<configuration>
<to>
<image>myimage</image>
</to>
<container>
<user>1000:1000</user>
</container>
</configuration>
</plugin>
これで起動してみると ...
$ ./mvnw compile jib:dockerBuild
$ docker run --rm myimage
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.3.RELEASE)
2019-03-23 06:24:42.445 INFO 1 --- [ main] s.SpringDockerExampleApplication : Starting SpringDockerExampleApplication on c864fdbdb6a7 with PID 1 (/app/classes started by ? in /)
(/app/classes started by ? in /)
と表示されており、たしかに root ユーザでなくなりました。
ちなみに、コンテナのユーザは以下のように起動時に設定することも可能です。
$ docker run --rm -u 1000:1000 myimage
まとめ
Jib のセットアップは非常に簡単で、たしかに結構良いイメージを作ってくれました。
また、Jib はコンテナレジストリへのプッシュや Slaffold との連携も可能とのことなので、CI / CD の中で使うと面白そうです。
Java を Native バイナリとして動かす Quarkus にも対応してくれると嬉しいです。
-
マルチステージビルドでは .m2 のキャッシュが効かずビルドに時間がかかるため、マルチステージビルドは使っていません。マルチステージビルドで .m2 のキャッシュを利用する方法は「Buildkitを使ってMulti-stage BuildでMavenのキャッシュを効かせる」の通りです。 ↩