世の中は空前のコンテナブームで、今年カンファレンスに足を運ぶと、k8sネタを聞かない日はありませんでした。
そんな中で、Javaはフットプリントが大きいし、LambdaなどのCloud Functionとも相性が悪いせいで、色々と敬遠されがち。弊社でもJavaはガッツリ利用しているにもかかわらず、弊社のフューチャー Advent Calendar 2018においては、誰も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/bb166929a58a4c20bb88GraalVMを試してみた - Qiita
https://qiita.com/sonodar/items/dcfafdfba8af2db53b16Spring 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を見てみると、200MBくらいで、だいぶスリムなので、これを使っていくのが、2018年時点のベターな選択肢と言えそうです。
しかし、アプリを乗せたらもっと重くなるので、↑の記事でも紹介されているように、Java11では、モジュールシステムの恩恵もあり、jlinkを使ってカスタムJREを作り、さらにダウンサイジングさせていきます。
そこで、まずはSpringBootを動作するのに必要最低限にスリム化したJDKをAdoptOpenJDKのAlpineイメージで作成し、次にAlpineの素のイメージにコピーするというフローになります。
ここで注意が必要なのは、素のAlpineにカスタムJREをコピーしただけでは、動作しないということです。そのため、下記のようにglibcを追加で入れます。
###########################################################
# 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までダウンサイジングできました
ビルド時にコンテナ化 〜キミは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コマンドを打たずしてここまで自動化できてしまうのが激アツ ですね。
あれ、さっきDockerfile書いてたような・・・実はこれには理由があります。
Jibでは設定でfromイメージを指定するのですが、このfromイメージに対して、上述のjlinkを実行してカスタマイズするようなことはできないんですね。
なので、jlinkでカスタムJREを使って実行させたいという場合は、事前にベースイメージを作成してレジストリに公開しておく必要があるわけです。
なお、クラスパスに入ってるリソースは当然自動的にコピーされるわけですが、クラスパスに入ってない任意のファイルコピーぐらいは設定で可能になっています。
Jibでは、レジストリへのPUSHのほかに、ローカルのDockerデーモンへの登録、tarの作成などがサポートされています。
# レジストリへPUSH
$ mvn compile jib:build
# ローカルDockerデーモンへの登録(要Docker起動)
$ mvn compile jib:dockerBuild
# tarの作成
$ mvn compile jib:buildTar
それでは、今回はJibのMavenプラグインでECRへプッシュする設定をしていきます。
<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以上のインストール必須です。
$ 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を作成します。
{
"credsStore": "ecr-login"
}
ビルド環境がEC2ならIAM Roleを割り当てておけば、他に設定は不要です。それができない環境の場合は、~/.aws/credentials
にアクセスキー・シークレットを設定するか、環境変数でAWS_ACCESS_KEY_ID
とAWS_SECRET_ACCESS_KEY
をそれぞれ指定しておきます。
ECRにプッシュする際は下記のようなコマンドになります。
$ 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年末段階においては、ここら辺が限度かなと
終わりに
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/