Java コンテナ化ツール「Jib」はどのくらい Docker のベストプラクティスを満たしているのか

# はじめに

Java アプリケーションをコンテナ化するツールに [jib](https://github.com/GoogleContainerTools/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](https://start.spring.io/) で作成した Maven プロジェクトを使います。

[jib-maven-plugin](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin) の README の通り、pom.xml に以下の記述を追加します。

```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>
```

ビルドしてみます。

```console
$ ./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 を一切書かず、イメージができました !

起動も通常通り成功しました。

```console
$ 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](https://cloud.google.com/blog/products/gcp/7-best-practices-for-building-containers) のようなベストプラクティスがあります。

今回は、Jib で作成したイメージが以下のプラクティスを満たしているのか調査します。

* PID 1 で起動
* ビルドキャッシュの最適化
* 不要なツールの削除
* イメージの最小化
* レイヤー数の削減
* 一般ユーザで実行

## PID 1 で起動

コンテナ停止時はコンテナ内の PID 1 のプロセスにシグナルが送られるため、PID 1 でアプリケーションを起動することが推奨されています。

Spring Boot 起動時のログを見ると ...

```console
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 を実行しようとすると、以下のようにエラーとなりました。

```console
$ 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](https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#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 コマンドは入っていませんでした。

```console
$ 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]

[^1]: マルチステージビルドでは .m2 のキャッシュが効かずビルドに時間がかかるため、マルチステージビルドは使っていません。マルチステージビルドで .m2 のキャッシュを利用する方法は「[Buildkitを使ってMulti-stage BuildでMavenのキャッシュを効かせる](https://qiita.com/os1ma/items/f4ce2bb947680be8fdd9)」の通りです。


```dockerfile
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 イメージをビルドします。

```console
$ ./mvnw clean package
$ docker build -t myimage-openjdk .
```

イメージサイズを比較すると ...

```console
$ 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](https://hub.docker.com/_/amazoncorretto) を利用した場合よりは小さくなっています。

[^2]: [Image using alpine #1](https://github.com/corretto/corretto-8-docker/issues/1)

```console
REPOSITORY          TAG                   IMAGE ID            CREATED             SIZE
myimage-amazon     latest                dd6500aa12d0        4 minutes ago       542MB
```

なお、Amazon Correto を使った場合の Dockerfile は以下です。

```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 から作成したイメージのレイヤ数を比較すると ...

```console
$ 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 ユーザで実行されていました。

```console
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 に追記が必要でした。

```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>
```

これで起動してみると ...

```console
$ ./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 ユーザでなくなりました。

ちなみに、コンテナのユーザは以下のように起動時に設定することも可能です。

```console
$ docker run --rm -u 1000:1000 myimage
```

# まとめ

Jib のセットアップは非常に簡単で、たしかに結構良いイメージを作ってくれました。

また、Jib はコンテナレジストリへのプッシュや Slaffold との連携も可能とのことなので、CI / CD の中で使うと面白そうです。

Java を Native バイナリとして動かす [Quarkus](https://quarkus.io/) にも対応してくれると嬉しいです。