これは、なにをしたくて書いたもの?
そういえば、Jibというものがあったけど、全然使ったことがないなと思い、1度試しておこうかなと。
Javaアプリケーションを自動的にコンテナイメージにビルドするツール「Jib」がバージョン1.0に到達 - Publickey
名前や情報自体は、時々見ていたんですけどね。
Jib?
Jibとは、Googleの開発したコンテナイメージをビルドするツールです。Javaアプリケーション向けに、最適化されているようです。
Build container images for your Java applications.
GitHub - GoogleContainerTools/jib: 🏗 Build container images for your Java applications.
Jibとはなにか、その説明を見てみます。
以下を特徴にしています。
- Dockerデーモンなしでコンテナイメージを作成できる
- Dockerイメージを作成するための、ベストプラクティスを熟知する必要がない
- Javaアプリケーション向けに最適化されたコンテナイメージを作成できる
- コンテナイメージのフォーマットは、DockerまたはOCIが選択可能
- Maven、Gradleのプラグイン、CLI、Javaライブラリの4形態で利用可能
Google Cloud Platform Blog: Introducing Jib — build Java Docker images better
Build containers faster with Jib, a Google image build tool for Java applications - Speaker Deck
また、高速であること、再現性、デーモンレスを目標として掲げています。
Dockerレジストリへのpushなどもできるようです。
FAQは、こちら。
jib/faq.md at master · GoogleContainerTools/jib · GitHub
Jib Core
今回は、Javaライブラリとして提供される、Jib Coreを使用してみます。MavenプラグインやGradleプラグインを使うと便利だとは
思うのですが、まずはこちらを1度使ってみた方が理解が進むかな、と。
https://github.com/GoogleContainerTools/jib/tree/v0.13.1-core/jib-core
利用するバージョンは、0.13.1です。
APIリファレンスは、こちら。
com.google.cloud.tools.jib.api (jib-core 0.13.1 API)
Jib Core自体は、汎用的なコンテナイメージのビルドツールのようです。Javaアプリケーション向けのAPIも備えてはいますが、
README.mdに沿ってAPIを使っていくと、割と汎用的なイメージを作っているような感覚を覚えるでしょう。
実際、そうみたいですし。
ちなみに、APIはまだベータ段階なので、今後大幅に変更される可能性があることに注意してください。
The API is currently in beta and may change substantially.
もちろん、MavenプラグインやGradleプラグインのベースにもなっています。
まあ、説明はこのあたりにして使っていってみましょう。
環境
今回の環境は、こちらです。
$ java --version openjdk 11.0.6 2020-01-14 OpenJDK Runtime Environment (build 11.0.6+10-post-Ubuntu-1ubuntu118.04.1) OpenJDK 64-Bit Server VM (build 11.0.6+10-post-Ubuntu-1ubuntu118.04.1, mixed mode, sharing) $ mvn --version Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 11.0.6, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "4.18.0-25-generic", arch: "amd64", family: "unix"
サンプルアプリケーション
まずは、JibでDockerイメージを作るためのお題となるアプリケーションを作りましょう。
簡単なJAX-RSアプリケーションを作ります。
pom.xmlの抜粋。
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-jdk-http</artifactId> <version>4.5.2.Final</version> </dependency> </dependencies>
ソースコード。
src/main/java/org/littlewings/jaxrs/Server.java
package org.littlewings.jaxrs; import java.io.IOException; import java.net.InetSocketAddress; import java.util.Optional; import java.util.concurrent.TimeUnit; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import com.sun.net.httpserver.HttpServer; import org.jboss.logging.Logger; import org.jboss.resteasy.plugins.server.sun.http.HttpContextBuilder; public class Server { public static void main(String... args) throws IOException { Logger logger = Logger.getLogger(Server.class); HttpServer server = HttpServer.create(new InetSocketAddress(8080), 10); try { HttpContextBuilder builder = new HttpContextBuilder(); builder.getDeployment().getActualResourceClasses().add(HelloResource.class); builder.bind(server); server.start(); logger.info("server start."); while (true) { try { TimeUnit.SECONDS.sleep(1L); } catch (InterruptedException e) { // ignore } } } finally { server.stop(0); logger.info("server stop."); } } @Path("hello") public static class HelloResource { @GET @Produces(MediaType.TEXT_PLAIN) public String message(@QueryParam("value") String value) { return "Hello " + Optional.ofNullable(value).orElse("World") + "!!"; } } }
QueryStringで受け取ったパラメーターを使って、メッセージを返すだけのJAX-RSリソースクラスが動作します。
簡単に動作確認してみましょう。ビルドしてサーバーを起動。
$ mvn compile exec:java -Dexec.mainClass=org.littlewings.jaxrs.Server
アクセス。
$ curl localhost:8080/hello Hello World!! $ curl localhost:8080/hello?value=JAX-RS Hello JAX-RS!!
OKですね。では、こちらをJibを使ったDockerイメージにすることを目標に、進めていきましょう。
Jib Coreを使う
それでは、Jib Coreを使ってDockerイメージを作成していきます。
先ほど作ったJAX-RSアプリケーションとは、別のMavenプロジェクトを用意します。依存関係は、こちら。
<dependency> <groupId>com.google.cloud.tools</groupId> <artifactId>jib-core</artifactId> <version>0.13.1</version> </dependency>
まずは、雛形を用意しましょう。
src/main/java/org/littlewings/jib/Builder.java
package org.littlewings.jib; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.time.Instant; import java.util.Arrays; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import com.google.cloud.tools.jib.api.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.Containerizer; import com.google.cloud.tools.jib.api.DockerDaemonImage; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.api.Jib; import com.google.cloud.tools.jib.api.Port; import com.google.cloud.tools.jib.api.RegistryException; public class Builder { public static void main(String... args) throws InvalidImageReferenceException, InterruptedException, ExecutionException, RegistryException, CacheDirectoryCreationException, IOException { // ここに、Jibを使ったコードを書く } }
こちらのmainメソッドの中身を、README.mdのExamplesを参考に埋めていきます。ちなみに、Dockerレジストリへのpushは行いません。
ベースイメージを決めて、Dockerイメージを作る
最初に行うのは、ベースイメージの決定と、作成するコンテナイメージの名前付けですね。Jibクラスをエントリポイントとして行います。
Jib .from("adoptopenjdk:11-jre-hotspot") .containerize( Containerizer .to( DockerDaemonImage.named("kazuhira/simple-jaxrs-server") ) );
これで、AdoptOpenJDK 11 HotSpot(JREのみ)をベースイメージとして、Dockerイメージ名「kazuhira/simple-jaxrs-server」で
コンテナイメージが作成されます。なお、バージョンタグは「latest」となります。
実行してみましょう(
$ mvn compile exec:java -Dexec.mainClass=org.littlewings.jib.Builder
できました。作成時間は、50年前になっていますけど。
$ docker image ls | grep simple-jaxrs-server kazuhira/simple-jaxrs-server latest f50fab05cd8b 50 years ago 225MB
ベースがAdoptOpenJDKのコンテナイメージなので、実行するとシェルが起動します。
$ docker container run -it --rm kazuhira/simple-jaxrs-server:latest root@1439d07d732b:/#
中身はさておき、Jib Coreを使ってDockerコンテナイメージが作れたことになります。
ここから、ちょっとずつ中身を変えていってみましょう。
今回作成したDockerイメージは、削除しておきます。
$ docker image rm kazuhira/simple-jaxrs-server:latest
タグを指定する
タグが「latest」になるのはイマイチですね。「0.0.1」にしてみます。
Jib .from("adoptopenjdk:11-jre-hotspot") .containerize( Containerizer .to( DockerDaemonImage.named("kazuhira/simple-jaxrs-server:0.0.1") ) );
コンテナを再作成すると、タグが変わったことが確認できますね。
$ docker image ls | grep simple-jaxrs-server kazuhira/simple-jaxrs-server 0.0.1 f50fab05cd8b 50 years ago 225MB
ここで指定する文字列のフォーマットは、内部的に呼び出されるImageReference#parseの説明を見ることで確認できます。
ちなみに、DockerDaemonImage#namedなどに、直接ImageReference#parseの結果を渡すことも可能です。
作成したイメージは、どの環境向けか?
ところで、先ほどからDockerデーモンレスと言いながらdockerコマンドで作成したイメージの情報を見ていますが、
これはContainerizer#toで指定しているものがポイントになっています。
Containerizer
.to(
DockerDaemonImage.named("kazuhira/simple-jaxrs-server:0.0.1")
)
Containerizer (jib-core 0.13.1 API)
今回はDockerDaemonImageを選択しましたが、他にRegistryImage、TarImageがあり、どのような環境向けにイメージを作成するかを
指定することができます。
DockerDaemonImage (jib-core 0.13.1 API)
RegistryImage (jib-core 0.13.1 API)
TarImage (jib-core 0.13.1 API)
たとえば、レジストリにpushしたかったら、RegistryImageを使います。Jib CoreのExamplesでは、RegistryImageを使うように
なっていますね。
ENTRYPOINTを指定する
次に、ENTRYPOINTを指定してみましょう。
たとえば、「java --version」を実行するようにEntrypointを設定してみましょう。
Jib .from("adoptopenjdk:11-jre-hotspot") .setEntrypoint(Arrays.asList("java", "--version")) .containerize( Containerizer .to( DockerDaemonImage.named("kazuhira/simple-jaxrs-server:0.0.1") ) );
確認。
$ docker container run -it --rm kazuhira/simple-jaxrs-server:0.0.1 openjdk 11.0.6 2020-01-14 OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.6+10) OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.6+10, mixed mode)
反映されましたね。
ちなみに、ProgramArgumentsを使うことで、ENTRYPOINTに対する引数を設定することも可能です。
Jib .from("adoptopenjdk:11-jre-hotspot") .setEntrypoint(Arrays.asList("java")) .setProgramArguments(Arrays.asList("--version")) .containerize( Containerizer .to( DockerDaemonImage.named("kazuhira/simple-jaxrs-server:0.0.1") ) );
イメージの作成時刻を変更する
最初にイメージを作った時に確認しましたが、作成時刻が50年前になっています。
これを変更する場合は、CreationTimeを指定します。実行時の時刻を使うようにしてみましょう。
Jib .from("adoptopenjdk:11-jre-hotspot") .setCreationTime(Instant.now()) .setEntrypoint(Arrays.asList("java", "--version")) .containerize( Containerizer .to( DockerDaemonImage.named("kazuhira/simple-jaxrs-server:0.0.1") ) );
ビルド後にイメージを確認すると、イメージの作成時刻が変更されました。
Jibで作成したイメージの時刻がやたら古い理由は、FAQに記載があります。
Why is my image created 48+ years ago?
同じ時刻で作成することで、再現性を担保することを目的にしています。作成時刻を変更するということは、再現性が犠牲になる
ということが注意事項になります、だそうで。
$ docker image ls | grep simple-jaxrs-server kazuhira/simple-jaxrs-server 0.0.1 ff8b454d2541 12 seconds ago 225MB
レイヤーを追加(ADD)して、ポートをEXPOSEする
そろそろ、最初に作ったアプリケーションを動かしたいところですね。アプリケーションのJARや、依存するライブラリなどを
イメージに追加していってみましょう。
とりあえず、Jib Coreを動かしているプロジェクトに、JARファイルを置く用のディレクトリを作成します。
$ mkdir -p container-target/libs
一方でアプリケーション側のプロジェクトでJARファイルを作成、依存ライブラリを取得して、Jib Coreを先ほど作成したディレクトリに
コピーします。
$ mvn package && mvn dependency:copy-dependencies -DincludeScope=compile $ cp target/simple-jaxrs-server-0.0.1-SNAPSHOT.jar ../jib-core-example/container-target $ cp target/dependency/* ../jib-core-example/container-target/libs
アプリケーション本体は作成したディレクトリの直下に、ライブラリは「libs」ディレクトリに置きました。
これらのJARファイルをイメージに追加するために、addLayerでJARファイルを追加していきます。
Jib .from("adoptopenjdk:11-jre-hotspot") .addLayer(Arrays.asList(Paths.get("container-target/simple-jaxrs-server-0.0.1-SNAPSHOT.jar")), AbsoluteUnixPath.fromPath(Paths.get("/"))) .addLayer( Files.find(Paths.get("container-target/libs"), 1, (path, attr) -> Files.isRegularFile(path) && path.getFileName().toString().endsWith(".jar")).collect(Collectors.toList()), "/libs" ) .setCreationTime(Instant.now()) .setEntrypoint(Arrays.asList("java", "--version")) .containerize( Containerizer .to( DockerDaemonImage.named("kazuhira/simple-jaxrs-server:0.0.1") ) );
ここですね。
.addLayer(Arrays.asList(Paths.get("container-target/simple-jaxrs-server-0.0.1-SNAPSHOT.jar")), AbsoluteUnixPath.fromPath(Paths.get("/"))) .addLayer( Files.find(Paths.get("container-target/libs"), 1, (path, attr) -> Files.isRegularFile(path) && path.getFileName().toString().endsWith(".jar")).collect(Collectors.toList()), "/libs" )
addLayerの第2引数には、ファイルをADDする先のディレクトリを指定します。
ENTRYPOINTも変えてしまいましょう。
Jib .from("adoptopenjdk:11-jre-hotspot") .addLayer(Arrays.asList(Paths.get("container-target/simple-jaxrs-server-0.0.1-SNAPSHOT.jar")), AbsoluteUnixPath.fromPath(Paths.get("/"))) .addLayer( Files.find(Paths.get("container-target/libs"), 1, (path, attr) -> Files.isRegularFile(path) && path.getFileName().toString().endsWith(".jar")).collect(Collectors.toList()), "/libs" ) .setCreationTime(Instant.now()) .setEntrypoint("java", "-cp", "/libs/*:/simple-jaxrs-server-0.0.1-SNAPSHOT.jar", "org.littlewings.jaxrs.Server") .containerize( Containerizer .to( DockerDaemonImage.named("kazuhira/simple-jaxrs-server:0.0.1") ) );
8080ポートでリッスンしますし、ポートもEXPOSEしておきましょう。
Jib .from("adoptopenjdk:11-jre-hotspot") .addLayer(Arrays.asList(Paths.get("container-target/simple-jaxrs-server-0.0.1-SNAPSHOT.jar")), AbsoluteUnixPath.fromPath(Paths.get("/"))) .addLayer( Files.find(Paths.get("container-target/libs"), 1, (path, attr) -> Files.isRegularFile(path) && path.getFileName().toString().endsWith(".jar")).collect(Collectors.toList()), "/libs" ) .setExposedPorts(Port.tcp(8080)) .setCreationTime(Instant.now()) .setEntrypoint("java", "-cp", "/libs/*:/simple-jaxrs-server-0.0.1-SNAPSHOT.jar", "org.littlewings.jaxrs.Server") .containerize( Containerizer .to( DockerDaemonImage.named("kazuhira/simple-jaxrs-server:0.0.1") ) );
ここですね。
.setExposedPorts(Port.tcp(8080))
では、イメージを作成して、起動。
$ docker container run -it --rm -p 8080:8080 kazuhira/simple-jaxrs-server:0.0.1 Mar 21, 2020 5:44:09 PM org.littlewings.jaxrs.Server main INFO: server start.
サーバーが起動しました。
確認。
$ curl localhost:8080/hello Hello World!! $ curl localhost:8080/hello?value=JAX-RS Hello JAX-RS!!
OKですね!
JibContainerBuilder
ここまで説明しませんでしたが、Jib#fromの戻り値は、JibContainerBuilderのインスタンスです。
JibContainerBuilder (jib-core 0.13.1 API)
JibContainerBuilderを使って、作成するコンテナの設定を行っていきます。JibContainerBuilderに定義されているメソッドを見ると、
どのような設定が可能なのかを確認することができます。
たとえば、USERやWORKDIRなどの指定はできそうですが、DockerfileにおけるRUNに相当することはできなさそうですね、など。
JibContainerBuilder#containerizeだけは戻り値がJibContainerBuilderにはならず、JibContainerとなりコンテナイメージの作成が
実行されます。
実際に利用する時は、JibContainerBuilderのAPIを眺めることになるでしょう。
ベースイメージのダウンロード先は?
ところで、今回はAdoptOpenJDKをベースイメージに選びましたが、「docker image ls」コマンドにAdoptOpenJDKは現れません。
$ docker image ls | grep adopt
どこにあるかというと、「$HOME/.cache/google-cloud-tools-java/jib」配下にあります。
$ find ~/.cache/google-cloud-tools-java/jib -type f $HOME/.cache/google-cloud-tools-java/jib/images/adoptopenjdk!11-jre-hotspot/config.json $HOME/.cache/google-cloud-tools-java/jib/images/adoptopenjdk!11-jre-hotspot/lock $HOME/.cache/google-cloud-tools-java/jib/images/adoptopenjdk!11-jre-hotspot/manifest.json $HOME/.cache/google-cloud-tools-java/jib/layers/f11b29a9c7306674a9479158c1b4259938af11b97359d9ac02030cc1095e9ed1/977183d4e9995d9cd5ffdfc0f29e911ec9de777bcb0f507895daa1068477f76f $HOME/.cache/google-cloud-tools-java/jib/layers/bf7abfeb06800729d4f24d971e8017c537c4fa86368ae0bca66a7b36ca2b4189/85fa6e80789c791dff92d9427ce7cc6ffdb5164aa7296c986cb0c1adcd6711f7 $HOME/.cache/google-cloud-tools-java/jib/layers/78bf9a5ad49e4ae42a83f4995ade4efc096f78fd38299cf05bc041e8cdda2a36/16542a8fc3be1bfaff6ed1daa7922e7c3b47b6c3a8d98b7fca58b9517bb99b75 $HOME/.cache/google-cloud-tools-java/jib/layers/930bda195c84cf132344bf38edcad255317382f910503fef234a9ce3bff0f4dd/6597da2e2e52f4d438ad49a14ca79324f130a9ea08745505aa174a8db51cb79d $HOME/.cache/google-cloud-tools-java/jib/layers/8a1e5c340bc4a84bc37bcb75bbaa4b90b9255c3cf2dfbf2891b350581bc687bd/3cd87aa74fc9d063ff863f789614cdfef8fbbf9b2908c1dc5f2e868b087f0a59 $HOME/.cache/google-cloud-tools-java/jib/layers/5bed26d33875e6da1d9ff9a1054c5fef3bbeb22ee979e14b72acf72528de007b/c8be1b8f4d60d99c281fc2db75e0f56df42a83ad2f0b091621ce19357e19d853
このあたりの情報が決められているのは、こちら。
もっとJava向けなAPIを使う
ここまで、JibContainerBuilderを使った汎用的なコンテナイメージの作成を行ってきました。
最後に、もう少しJavaアプリケーション向けのコンテナを作るためのAPI、JavaContainerBuilderを使ってみたいと思います。
JavaContainerBuilder (jib-core 0.13.1 API)
JavaContainerBuilderを使うと「/app/libs」に依存ライブラリが、「/app/classes」にクラスファイルが、
「/app/resources」にリソースファイルが配置されるように構成され、
Where is the application in the container filesystem?
ENTRYPOINTが「java -cp [クラスパス]」となります。
jib/JavaContainerBuilder.java at v0.13.1-core · GoogleContainerTools/jib · GitHub
この時、libsなどにはクラスパスを通した状態に設定してくれます。
サンプルコードの雛形は、こちら。先ほど作成したコンテナと、近い状態のものを実現していくことを目標にします。
src/main/java/org/littlewings/jib/JavaBuilder.java
package org.littlewings.jib; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.time.Instant; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; import com.google.cloud.tools.jib.api.Containerizer; import com.google.cloud.tools.jib.api.DockerDaemonImage; import com.google.cloud.tools.jib.api.InvalidImageReferenceException; import com.google.cloud.tools.jib.api.JavaContainerBuilder; import com.google.cloud.tools.jib.api.Port; import com.google.cloud.tools.jib.api.RegistryException; public class JavaBuilder { public static void main(String... args) throws IOException, InvalidImageReferenceException, InterruptedException, ExecutionException, RegistryException, CacheDirectoryCreationException { // ここに、JavaContainerBuilderを使ったコードを書く } }
JavaContainerBuilderも、Jibと同じようにJavaContainerBuilder#fromから始めるのですが、オーソドックスには
JavaContainerBuilder#fromDistrolessを選ぶのだと思います。
JavaContainerBuilder .fromDistroless()
これが指すベースイメージは、「gcr.io/distroless/java」なのでJava 8となります。
今回はJava 11で作っているので、先ほどと同じくAdoptOpenJDK 11 HotSpotをベースイメージにします。
で、できたのがこんな感じ。
JavaContainerBuilder .from("adoptopenjdk:11-jre-hotspot") .addProjectDependencies(Paths.get("container-target/simple-jaxrs-server-0.0.1-SNAPSHOT.jar")) .addDependencies(Files.find(Paths.get("container-target/libs"), 1, (path, attr) -> Files.isRegularFile(path) && path.getFileName().toString().endsWith(".jar")).collect(Collectors.toList())) .setMainClass("org.littlewings.jaxrs.Server") .toContainerBuilder() // to JibContainerBuilder .setExposedPorts(Port.tcp(8080)) .setCreationTime(Instant.now()) .containerize( Containerizer .to( DockerDaemonImage.named("kazuhira/simple-jaxrs-server:0.0.1") ) );
ある程度Java向けの設定を行ったら、JavaContainerBuilder#toContainerBuilderでJibContainerBuilderに切り替えます。
これはJavaContainerBuilderが内部的に持っているJibContainerBuilderで
JavaContainerBuilder#toContainerBuilderを呼び出した時に、ここまでに設定したJavaContainerBuilderの内容を元にJibContainerBuilderに
イメージの定義を反映したものを返してくれます。
今回は、プロジェクトの成果物(JARファイル)と依存ライブラリ、そしてmainクラスを指定しています。
.addProjectDependencies(Paths.get("container-target/simple-jaxrs-server-0.0.1-SNAPSHOT.jar")) .addDependencies(Files.find(Paths.get("container-target/libs"), 1, (path, attr) -> Files.isRegularFile(path) && path.getFileName().toString().endsWith(".jar")).collect(Collectors.toList())) .setMainClass("org.littlewings.jaxrs.Server")
その他、「/app/classes」や「/app/resources」にもファイルを追加できますし、これ以外の場所にもファイルを追加してクラスパスを
通すことができます。また、JavaVMへ渡すオプションも設定可能です。
では、コンテナイメージをビルドしてみます。
$ mvn compile exec:java -Dexec.mainClass=org.littlewings.jib.JavaBuilder
コンテナを起動。
$ docker container run -it --rm -p 8080:8080 kazuhira/simple-jaxrs-server:0.0.1 Mar 22, 2020 5:43:38 AM org.littlewings.jaxrs.Server main INFO: server start.
確認。
$ curl localhost:8080/hello Hello World!! $ curl localhost:8080/hello?value=JAX-RS Hello JAX-RS!!
OKですね。
まとめ
今回は、JibのMavenプラグイン、Gradleプラグインのベースとなっている、Jib Coreを使ってコンテナイメージを作成してみました。