【2018年決定版】【Java11コンテナ化】SpringBootアプリケーションを限界までダウンサイジングした後にJibでビルドしてECRへPUSHする

世の中は空前のコンテナブームで、今年カンファレンスに足を運ぶと、k8sネタを聞かない日はありませんでした。

そんな中で、Javaはフットプリントが大きいし、LambdaなどのCloud Functionとも相性が悪いせいで、色々と敬遠されがち。弊社でもJavaはガッツリ利用しているにもかかわらず、弊社の[フューチャー Advent Calendar 2018](https://qiita.com/advent-calendar/2018/future)においては、誰もJavaネタ書いてませんね。。。

私はJavaもPythonもRubyもGoもES6も愛してるし、適材適所で使えばよいと思ってますが、今回はクリスマス・イブにJavaの背中を押す記事を。

## JDK戦国時代 〜オレ達はどのJDKを使えばいいのか〜

Sun MicrosystemsからOracleが元締めとなって無償で使い続けてこられたJavaがついに2018年9月のJava11から有償になるということで、世間はザワザワしました。

ですが、ちゃんと情報を整理すると、Oracleが有償サポートするOracleJDKのほかに、OracleがリードしてきたOpenJDKをベースに、コミュニティやベンダーがそれぞれLTS版のJDKをリリースするので、大騒ぎするような事態ではないと思います。

とはいえ、エンタープライズの世界では、脆弱性が発見されたときに、誰が責任をもってタイムリーにパッチ提供されるのか？ってところがポイントになるわけですが、セキュリティは多層防御が原則だし、Java自体よりもStrutsなどのライブラリ・フレームワークなどの脆弱性のほうが影響大きいので、ピュアに日本的なマインドが正常な判断を阻害しているとは思いますが、、、Oracle有償サポートもそういった会社にとっては有用だと思います。

| JDK         | Java8 | Java11 | 有償・無償 | LTS有 | コメント                       |
| ------------ | ------ | ------ | ---------- | ----- | ------------------------------ |
| OracleJDK | ○      | ○      | 有償       | ○     | 大企業、安心を買いたいならこれ |
| Oracle OpenJDK | ○      | ○      | 無償       | ☓     | LTS版なし、エンプラだと使わない |
| Redhat OpenJDK | ○      | ○      | 有償※       | ○     | Redhat使ってるなら選択肢 |
| AdoptOpenJDK | ○      | ○      | 無償       | ○     | 現時点で無償で使えてコミュニティもしっかりしているため、有力 |
| Amazon Corretto | ○      | ✗      | 無償       | ○     | Javaの父ゴスリンがAWSで開発しているため本命感あり。Java11のリリースが待たれる |
| GraalVM | ○      | ✗      | 無償       | ？     | Oracleが開発するPolyglotVM。ネイティブイメージ化すれば爆速になりそうだが、制約多し |

他にもAmazon/AzureでもOpenJDK提供、LTSサポートすると言ってますし、まさに戦国時代ですね。

他の言語ランタイムでは、こんな事態が起こりようがないため、皮肉な感じもしますが、いかにJavaの利用システムが多いかということがわかります^^;

さらにGraalVMの存在も見逃せません。まだネイティブイメージ化においてDynamic Loadingに対応できてないなど、様々な制約があるため、SpringBootなどでは使えませんが、時間の問題のようではあるので、楽しみに待つしかないですね。

### 参考

> JDKの長期商用サポート(LTS)の提供ベンダー比較（無償利用についても言及あり） - Qiita
> https://qiita.com/u-tanick/items/bb166929a58a4c20bb88
>
> GraalVMを試してみた - Qiita
> https://qiita.com/sonodar/items/dcfafdfba8af2db53b16
>
> Spring Boot ApplicationをMicronaut for Springでnative-imageにしてみる - Qiita
> https://qiita.com/h-r-k-matsumoto/items/5b82177294cd71df5024

## Java11 SpringBoot コンテナ化 〜ダウンサイジングはホストだけじゃないんだ〜

ここからは、今回はSpringBootによるWebアプリケーションを題材にしつつ、Java11で動作させることを考えてみます。

コンテナにしない場合は、単一起動Jar（fat-jar)にすれば、JDKインストール済の環境へデプロイするだけなので、非常に簡単です。jarのサイズも数十MBくらいで済むケースも多いと思います。

コンテナ化するとなった場合は、JDKが入ってるコンテナイメージに前述のjarをコピーして起動すればOKですが、ここで問題となるのはイメージサイズですね。レジストリへのPUSH/PULL、コンテナ自体の起動なんかも含めて、コンテナは小さいが正義。

### OpenJDK（オフィシャル）

> OpenJDK11のdokcerイメージ(1GB)が大きいのでalpine linux+ jlinkで小さいイメージ(85MB)を作成する - Qiita
> https://qiita.com/h-r-k-matsumoto/items/294eeb838cfd062d75b6

こちらで紹介されているように、OpenJDKの公式イメージだと1GBです。それにLTSもないし・・・ってことで、選択肢からは外れます。

### Distroless by Google

> distrolessイメージを使って、ランタイムDockerイメージを作ってみる - Qiita
> https://qiita.com/some-nyan/items/90b624b0f148231748f0)

Googleがランタイム用イメージとして公開されているDistolessも良さそうだということで調べてみましたが、現時点ではJava8しか対応しておらず、Java11対応を待つしかなさそうです。

### AdoptOpenJDK

AdoptOpenJDKの公開している[adoptopenjdk/openjdk11:alpine](https://hub.docker.com/r/adoptopenjdk/openjdk11/tags)を見てみると、200MBくらいで、だいぶスリムなので、これを使っていくのが、2018年時点のベターな選択肢と言えそうです。

しかし、アプリを乗せたらもっと重くなるので、↑の記事でも紹介されているように、Java11では、モジュールシステムの恩恵もあり、jlinkを使ってカスタムJREを作り、さらにダウンサイジングさせていきます。

そこで、まずはSpringBootを動作するのに必要最低限にスリム化したJDKをAdoptOpenJDKのAlpineイメージで作成し、次にAlpineの素のイメージにコピーするというフローになります。

ここで注意が必要なのは、素のAlpineにカスタムJREをコピーしただけでは、動作しないということです。そのため、下記のようにglibcを追加で入れます。

```Dockerfile:Dockerfile
###########################################################
# spring-boot-jre-min-11
# Custom JRE from AdobtOpenJDK11 for spring-boot
# 
###########################################################
FROM adoptopenjdk/openjdk11:alpine AS builder

# create custom jre
RUN jlink \
    --module-path="${JAVA_HOME}/jmods" \
    --compress=2 \
    --add-modules=java.base,java.logging,java.xml,jdk.unsupported,java.sql,java.naming,java.desktop,java.management,java.security.jgss,java.instrument,jdk.charsets \
    --no-header-files \
    --no-man-pages \
    --verbose \
    --output=/opt/jre-min

# pull plane alpine
FROM alpine:3.8

ENV JAVA_HOME="/opt/jre-min"
ENV PATH="$PATH:/opt/jre-min/bin"
ENV JAVA_VERSION="jdk-11.0.1+13"
ENV JAVA_TOOL_OPTIONS="-XX:+UseContainerSupport"

# add glibc-compat
RUN apk --update add --no-cache ca-certificates curl openssl binutils xz \
    && GLIBC_VER="2.28-r0" \
    && ALPINE_GLIBC_REPO="https://github.com/sgerrand/alpine-pkg-glibc/releases/download" \
    && GCC_LIBS_URL="https://archive.archlinux.org/packages/g/gcc-libs/gcc-libs-8.2.1%2B20180831-1-x86_64.pkg.tar.xz" \
    && GCC_LIBS_SHA256=e4b39fb1f5957c5aab5c2ce0c46e03d30426f3b94b9992b009d417ff2d56af4d \
    && ZLIB_URL="https://archive.archlinux.org/packages/z/zlib/zlib-1%3A1.2.9-1-x86_64.pkg.tar.xz" \
    && ZLIB_SHA256=bb0959c08c1735de27abf01440a6f8a17c5c51e61c3b4c707e988c906d3b7f67 \
    && curl -Ls https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub -o /etc/apk/keys/sgerrand.rsa.pub \
    && curl -Ls ${ALPINE_GLIBC_REPO}/${GLIBC_VER}/glibc-${GLIBC_VER}.apk > /tmp/${GLIBC_VER}.apk \
    && apk add /tmp/${GLIBC_VER}.apk \
    && curl -Ls ${GCC_LIBS_URL} -o /tmp/gcc-libs.tar.xz \
    && echo "${GCC_LIBS_SHA256}  /tmp/gcc-libs.tar.xz" | sha256sum -c - \
    && mkdir /tmp/gcc \
    && tar -xf /tmp/gcc-libs.tar.xz -C /tmp/gcc \
    && mv /tmp/gcc/usr/lib/libgcc* /tmp/gcc/usr/lib/libstdc++* /usr/glibc-compat/lib \
    && strip /usr/glibc-compat/lib/libgcc_s.so.* /usr/glibc-compat/lib/libstdc++.so* \
    && curl -Ls ${ZLIB_URL} -o /tmp/libz.tar.xz \
    && echo "${ZLIB_SHA256}  /tmp/libz.tar.xz" | sha256sum -c - \
    && mkdir /tmp/libz \
    && tar -xf /tmp/libz.tar.xz -C /tmp/libz \
    && mv /tmp/libz/usr/lib/libz.so* /usr/glibc-compat/lib \
    && apk del binutils \
    && rm -rf /tmp/${GLIBC_VER}.apk /tmp/gcc /tmp/gcc-libs.tar.xz /tmp/libz /tmp/libz.tar.xz /var/cache/apk/*

COPY --from=builder /opt/jre-min /opt/jre-min
```

これをDockerHubにプッシュしたのがこちらです。

> shoutstar/spring-boot-jre-min-11 - Docker Hub
> https://hub.docker.com/r/shoutstar/spring-boot-jre-min-11

なんとか48MBまでダウンサイジングできました :smile: 

## ビルド時にコンテナ化 〜キミはJibを知ってるか？〜

> GoogleContainerTools/jib: Build container images for your Java applications.
> https://github.com/GoogleContainerTools/jib

Jibは、Googleが今年7月にOSS公開したJavaアプリケーションをMaven/Gradleビルド時にコンテナイメージにして、レジストリへの登録までやってくれるツールです。下記が詳しいですね。

> Javaアプリケーションを自動的にDockerイメージにビルドしてくれる「Jib」、Googleがオープンソースで公開 － Publickey
> https://www.publickey1.jp/blog/18/javadockerjibgoogle.html

Jibは、DockerHubだけでなく、GCPのGCR、AWSのECRにも自動的にPUSHする仕組みを提供しています。Dockerfileを書くこともなく、dockerコマンドを打たずしてここまで自動化できてしまうのが激アツ :fire:  ですね。

あれ、さっきDockerfile書いてたような・・・実はこれには理由があります。
Jibでは設定でfromイメージを指定するのですが、このfromイメージに対して、上述のjlinkを実行してカスタマイズするようなことはできないんですね。
なので、jlinkでカスタムJREを使って実行させたいという場合は、事前にベースイメージを作成してレジストリに公開しておく必要があるわけです。

> https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#i-need-to-run-commands-like-apt-get

なお、クラスパスに入ってるリソースは当然自動的にコピーされるわけですが、クラスパスに入ってない任意のファイルコピーぐらいは設定で可能になっています。

> https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#i-need-to-run-commands-like-apt-get

Jibでは、レジストリへのPUSHのほかに、ローカルのDockerデーモンへの登録、tarの作成などがサポートされています。

```bash
# レジストリへPUSH
$ mvn compile jib:build

# ローカルDockerデーモンへの登録（要Docker起動）
$ mvn compile jib:dockerBuild

# tarの作成
$ mvn compile jib:buildTar
```

それでは、今回はJibのMavenプラグインでECRへプッシュする設定をしていきます。

```xml:pom.xml
<plugin>
    <groupId>com.google.cloud.tools</groupId>
    <artifactId>jib-maven-plugin</artifactId>
    <version>0.10.0</version>
    <configuration>
        <!-- プロキシ利用時に入れとくと便利なケースもある -->
        <allowInsecureRegistries>true</allowInsecureRegistries>
        <from>
            <!-- DockerHubからカスタムJREだけにした軽量イメージをPULLする -->
            <image>shoutstar/spring-boot-jre-min-11</image>
        </from>
        <to>
            <!-- ビルド時に-Djib.to.imageで置き換える -->
            <image>future/sprinb-boot-app</image>
            <!-- ビルド時に-Djib.to.credHelprで置き換える
            <credHelper>ecr-login</credHelper>
            -->
        </to>
        <container>
            <!-- springのプロファイルとかはここで渡す -->
            <jvmFlags>
                <jvmFlag>-Dadd-opens=java.base/java.lang=ALL-UNNAMED</jvmFlag>
                <jvmFlag>-Dadd-opens=java.base/java.lang.invoke=ALL-UNNAMED</jvmFlag>
                <jvmFlag>-Dspring.profiles.active=${spring.profiles.active}</jvmFlag>
            </jvmFlags>
            <!-- イメージの作成時間をビルド時にする -->
            <useCurrentTimestamp>true</useCurrentTimestamp>
        </container>
    </configuration>
</plugin>
```

### pom.xmlの補足

#### jib.from.image

前述のカスタムJREを指定しています。  
DockerHubの`shoutstar/spring-boot-jre-min-11`です。

#### jib.to.image, jib.to.credHelpr

開発時にローカルのDockerに配置したいケースもあるので、基本的には、`-Djib.to.image`、`-Djib.to.credHelper`にてビルド時に置き換えるのがベターかなと思っています。

### ECRへのPUSH

本来は`$(aws ecr get-login --no-include-email --region ap-northeast-1)`コマンドでdocker loginコマンドを発行してログインしてからPUSHするのですが、jibでは下記コマンドラインツールと連携してPUSHすることができます。

> awslabs/amazon-ecr-credential-helper: Automatically gets credentials for Amazon ECR on docker push/docker pull
> https://github.com/awslabs/amazon-ecr-credential-helper

上述のREADMEに従い、インストールします。golang1.6以上のインストール必須です。

```bash
$ go get -u github.com/awslabs/amazon-ecr-credential-helper/ecr-login/cli/docker-credential-ecr-login
```

`$GOPATH/bin/docker-credential-ecr-login`にシンボリックリンクを貼るなりして、PATHを通しておきます。

そして、すべてのECRレジストリへのログインをOKとするために下記のconfig.jsonを作成します。

```json:~/.docker/config.json
{
    "credsStore": "ecr-login"
}
```

ビルド環境がEC2ならIAM Roleを割り当てておけば、他に設定は不要です。それができない環境の場合は、`~/.aws/credentials`にアクセスキー・シークレットを設定するか、環境変数で`AWS_ACCESS_KEY_ID`と`AWS_SECRET_ACCESS_KEY`をそれぞれ指定しておきます。

ECRにプッシュする際は下記のようなコマンドになります。

```bash
$ mvn clean compile jib:build \
-Djib.to.image=xxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/future/spring-boot-app \
-Djib.to.credHelper=ecr-login

...中略

[INFO] --- jib-maven-plugin:0.10.0:build (default-cli) @ spring-boot-app ---
[WARNING] Setting image creation time to current time; your image may not be reproducible.
[INFO] 
[INFO] Containerizing application to xxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/future/spring-boot-app ...
[WARNING] Base image 'shoutstar/spring-boot-jre-min-11' does not use a specific image digest - build may not be reproducible
[INFO] Retrieving registry credentials for xxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com...
[INFO] Getting base image shoutstar/spring-boot-jre-min-11...
[INFO] Building snapshot dependencies layer...
[INFO] Building resources layer...
[INFO] Building dependencies layer...
[INFO] Building classes layer...
[INFO] The base image requires auth. Trying again for shoutstar/spring-boot-jre-min-11...
[INFO] Retrieving registry credentials for registry.hub.docker.com...
[INFO] 
[INFO] Container entrypoint set to [java, -Dadd-opens=java.base/java.lang=ALL-UNNAMED, -Dadd-opens=java.base/java.lang.invoke=ALL-UNNAMED, -Dspring.profiles.active=container, -cp, /app/resources:/app/classes:/app/libs/*, jp.co.future.sample.SpringBootApplication]
[INFO] Finalizing...
[INFO] 
[INFO] Built and pushed image as xxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/future/spring-boot-app
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
```

無事ECRへプッシュができました。

ちなみに、筆者が社内フレームワークの動作確認用途で作成しているspring-petclinicクローンのようなアプリで、イメージサイズは95MBくらいになっています。2018年末段階においては、ここら辺が限度かなと:sweat_smile:

## 終わりに

JibによってJavaエンジニアにとってもコンテナは身近で手軽な存在になっているので、まだ触ったことのない方は是非試してほしいなぁと思います。結局、私自身は試行錯誤の中でdockerコマンドを叩きまくっていたので、Dockerを知らないJavaユーザーでも使えるとか言う気はないですが。。。

今後は、GraalVMによって起動速度が早くなってくれば、Cloud Functionでも利用できそうですし、SpringBootにインスパイアされつつもGraalVMと親和性の高いMicronautも注目ですね。

ちょうど数日前にAmazon EKSが東京リージョンにも来たので、そのうちECRにプッシュしたイメージをEKSにデプロイするところなんかも記事にしたいと思います。

> Amazon EKS が 東京リージョンに対応しました。 | Amazon Web Services ブログ
> https://aws.amazon.com/jp/blogs/news/amazon-eks-tokyo-region/