11/13のOracle Groundbreakers APAC Tour in Tokyoで日本語講演されたJava in a World of Containersをもとに書いています。
はじめに
Javaでアプリケーションを作成するとき、このモジュールグラフに出ているものを全て使うことは稀でしょう。たとえば、java.xml,corba, javawsとかは必要無いときが多いです。
Dockerで何も考えずにLinuxとJDKの乗っているDockerイメージを作成すると結構な容量になります。そのため、コンテナとjavaは相性が悪いと言われることが多いようです。容量が多い、重い、起動が遅いなどなど。それでも、Javaが持つ特徴はコンテナに理想的でもあります。ランタイム、ハードやOSに依存しない、JVMによって保証されるセキュリティ互換性、環境が変化したときに安定した実行の保証、エコシステム、コンテナの最も適切な選択肢を取れるなどなどメリットとなる部分も数多くあります。デフォルトでは重すぎるJDKをJigsawでJDK自体をモジュール化して、依存関係を整理すれば改善できる。
Dockerfile
OSは、ubuntu:latestでイメージを作成します。最初は、デフォルトのフルサイズで、環境を作ってHelloWorldをやってみます。
FROM ubuntu:latest
ADD openjdk-11.0.1_linux-x64_bin.tar.gz /opt/jdk/
ADD HelloWorld.class HelloWorld.class
ENV PATH /opt/jdk/jdk-11.0.1/bin:$PATH
CMD [ "java", "-showversion", "HelloWorld" ]
イメージをビルドして実行します。
$ docker build -t helloworld-java .
$ docker run --rm helloworld-java
openjdk version "11.0.1" 2018-10-16
OpenJDK Runtime Environment 18.9 (build 11.0.1+13)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.1+13, mixed mode)
Hello World!
作成したイメージを見てみます。
$ docker images helloworld-java
REPOSITORY TAG IMAGE ID CREATED SIZE
helloworld-java latest 682ae9922b5b 2 minutes ago 396MB
おおよそ、400MBあります。
カスタムJRE
jdepsとjlinkを使って、カスタムJREを作成します。
jdepsを使うことで、モジュールの依存関係を見ることができます。HelloWorldのモジュールを見てみます。
$ jdeps --list-deps HelloWorld.class
java.base
内容がHelloWorldなので、依存が少ないです。HelloWorldレベルでは、java.baseモジュールだけを使用しているようです。
jlinkでカスタムしたJREを作成してみます。
jlink --compress=2 --module-path $JAVA_HOME/jmods --add-modules java.base --output jre-min
利用しているのが、java.baseだけなので、add-modulesこれだけを指定します。実行後、jre-minが作成されています。この環境で、HelloWorldを実行できる必要最低限になっています。容量も27MBでした。
.
├── Dockerfile
├── HelloWorld.class
├── HelloWorld.java
├── hello.jar
├── jre-min
│ ├── bin
│ │ ├── java
│ │ └── keytool
│ ├── conf
│ │ ├── net.properties
│ │ └── security
│ │ ├── java.policy
│ │ ├── java.security
│ │ └── policy
│ │ ├── README.txt
│ │ ├── limited
│ │ │ ├── default_US_export.policy
│ │ │ ├── default_local.policy
│ │ │ └── exempt_local.policy
│ │ └── unlimited
│ │ ├── default_US_export.policy
│ │ └── default_local.policy
│ ├── include
│ │ ├── classfile_constants.h
│ │ ├── darwin
│ │ │ └── jni_md.h
│ │ ├── jni.h
│ │ ├── jvmti.h
│ │ └── jvmticmlr.h
│ ├── legal
│ │ └── java.base
│ │ ├── COPYRIGHT
│ │ ├── LICENSE
│ │ ├── aes.md
│ │ ├── asm.md
│ │ ├── c-libutl.md
│ │ ├── cldr.md
│ │ ├── icu.md
│ │ ├── public_suffix.md
│ │ └── unicode.md
│ ├── lib
│ │ ├── classlist
│ │ ├── jli
│ │ │ └── libjli.dylib
│ │ ├── jrt-fs.jar
│ │ ├── jspawnhelper
│ │ ├── jvm.cfg
│ │ ├── libjava.dylib
│ │ ├── libjimage.dylib
│ │ ├── libjsig.dylib
│ │ ├── libnet.dylib
│ │ ├── libnio.dylib
│ │ ├── libosxsecurity.dylib
│ │ ├── libverify.dylib
│ │ ├── libzip.dylib
│ │ ├── modules
│ │ ├── security
│ │ │ ├── blacklisted.certs
│ │ │ ├── cacerts
│ │ │ ├── default.policy
│ │ │ └── public_suffix_list.dat
│ │ ├── server
│ │ │ ├── Xusage.txt
│ │ │ ├── libjsig.dylib
│ │ │ └── libjvm.dylib
│ │ └── tzdb.dat
│ └── release
└── openjdk-11.0.1_linux-x64_bin.tar.gz
この最小環境でHelloWorldを実行してみます。
$ jre-min/bin/java -showversion HelloWorld
java version "11.0.1" 2018-10-16 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.1+13-LTS)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.1+13-LTS, mixed mode)
Hello World!
最小構成のjavaでDockerビルド
jlinkでカスタムJREを作成してビルドしてみます。
FROM ubuntu:latest AS build
ADD openjdk-11.0.1_linux-x64_bin.tar.gz /opt/jdk/
ENV PATH /opt/jdk/jdk-11.0.1/bin:$PATH
RUN ["jlink", "--compress=2", "--module-path", "/opt/jdk/jdk-11/jmods", "--add-modules", "java.base", "--output", "/linked"]
FROM ubuntu:latest
COPY --from=build /linked /opt/jdk/
ENV PATH=$PATH:/opt/jdk/bin
ADD HelloWorld.class /
CMD [ "java", "-showversion", "HelloWorld" ]
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
helloworld-java latest e71329542c40 2 minutes ago 123MB
$ docker run -it helloworld-java
openjdk version "11.0.1" 2018-10-16
OpenJDK Runtime Environment 18.9 (build 11.0.1+13)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.1+13, mixed mode)
Hello World!
サイズが、先程の約4分の1弱になりました。
更に軽量な環境構築Dockerビルド
必要最低限のJavaは用意できましたが、それでもまだ123MBもあります。先程やり方だけだと、Javaしか軽量化できていないので、OSにも手を入れていきます。avaを動かすだけであれば、ubuntuは贅沢すぎます。そこで、alpine linux を使って必要最低限のlinux環境を作成します。alpineは、musl libcとBusyBoxをベースに構成されている軽量なLinuxです(ほんとに何も入っていない・・・)。最小構成だと5MBを下回る軽さのようです。AdoptOpenJDKで公開されているalpineベースのJava環境Dockerfileを参考に最小構成でイメージを作成します。
FROM adoptopenjdk/openjdk11:alpine-slim AS jlink
RUN ["jlink", "--compress=2", \
"--module-path", "/opt/java/openjdk/jmods", \
"--add-modules", "java.base", \
"--output", "/jlinked"]
FROM alpine
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=jlink /jlinked /opt/jdk/
ADD HelloWorld.class /
CMD ["/opt/jdk/bin/java", "-showversion", "HelloWorld"]
$ docker build -t helloworld-java .
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
helloworld-java latest 407614883e2b 38 seconds ago 52.1MB
先程の半分以下になりました。もとのものと比べると8分の1ほどになりました。
もちろん実行も可能
$ docker run -it helloworld-java
openjdk version "11.0.1" 2018-10-16
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.1+13)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.1+13, mixed mode)
Hello World!
まとめ
容量が多い、重い、起動が遅いなどなどコンテナとjavaは相性が悪いと言われることが多いようです。JDK9以降はjdepsとjlinkで専用の環境を作成することができます。必要最低限の環境を作ることができるので、軽いコンテナを作ることが可能です。OSにも手を入れていくことで更に軽量で自分好み環境も作成可能です。