Galleonを使ってWildFlyのイメージサイズを小さくする

GalleonはWildFly16から対応した、WildFlyのバイナリを作るためのプロビジョニングツールです。詳細についてはWildFly News - WildFly 16 and Galleon, towards a cloud native EE application serverにまとめられていますが、Galleonにより以下が実現できます。

  • JAX-RS/CDI/JPAなど、よく使うAPIだけ含んだサイズの小さなWildFlyサーバを作る
  • WildFlyから使わない機能を削ってDockerイメージサイズを小さくする
  • Galleonで作った小さなWildFlyは従来のWildFlyと同じようにAPサーバとしての使い勝手で使える。ThorntailのようなUber-jarではない。

Galleonを使う

実際に使ってみる方がイメージが湧きやすいので、使い方から紹介します。

GalleonはWildFlyコミュニティで開発されているツールですが、WildFlyにはバンドルされておらず、GitHubGalloenのリポジトリからダウンロードします。このブログを書いた時点の最新バージョンは4.0.3.Finalです。

ダウンロードしたツールを任意のディレクトリに展開します

$ mkdir work
$ cp ~/Downloads/galleon-4.0.3.Final.zip .
$ unzip galleon-4.0.3.Final.zip
Archive:  galleon-4.0.3.Final.zip
   creating: galleon-4.0.3.Final/
   creating: galleon-4.0.3.Final/bin/
  inflating: galleon-4.0.3.Final/LICENSE
  inflating: galleon-4.0.3.Final/bin/galleon.sh
  inflating: galleon-4.0.3.Final/bin/galleon-cli-logging.properties
  inflating: galleon-4.0.3.Final/bin/galleon-cli.jar
  inflating: galleon-4.0.3.Final/bin/galleon.bat

まずは早速GalleonでWildFlyサーバを作成してみます。対象のWildFlyのバージョンはこの記事を書いている時点で最新の17.0.1.Final、--layersにはサーバに含める機能を指定するオプションでcloud-profileとはJAX-RS/CDI/JPA/JTAなどのよく使われるAPI実装に加えて、WildFly自体の動作に必要なサーバのコア機能のみ持つことを示します。--dirは作成したWildFlyサーバの出力先です。

Maven経由で必要なサブシステムをダウンロードしてWildFlyを組み立てるため、初回実行時は数分かかる場合もあります。

$ cd galleon-4.0.3.Final
$ bin/galleon.sh install wildfly:current#17.0.1.Final --layers=cloud-profile --dir=cloud-profile
Feature-packs resolved.
Feature-packs resolved.
Packages installed.
JBoss modules installed.
Configurations generated.
Feature pack installed.
======= ============ ==============
Product Build        Update Channel
======= ============ ==============
wildfly 17.0.1.Final current

Galleonによって生成された機能がそぎ落とされたWildFlyサーバの構成を確認すると一見して同じようなディレクトリ構造に見えます。GalleonはThorntailのように、Uber-jarに必要な機能をまとめるのではなく、通常のWildFlyと同じ構成を保ったまま、サブシステムを削ることで小サイズ化を実現しています。

$ cd cloud-profile
$ ls
LICENSE.txt		bin			jboss-modules.jar	standalone
README.txt		copyright.txt		modules

$WILDFLY_HOME/bin の中身を見ると、WildFlyを使ったことがある人なら違和感を感じると思います。jboss-cli.sh、add-user.shや、LinuxのOS起動時にWildFlyを起動させるinit.dスクリプトjboss-client.jarが含まれていません。

これらの$WILDFLY_HOME/binに含まれていたスクリプトを含んだWildFlyサーバを生成したい場合は、--layersにcore-toolsを追加します。

$ bin/galleon.sh install wildfly:current#17.0.1.Final --layers=cloud-profile,core-tools --dir=cloud-profile-with-tools

他にもどんなlayerがあるかは、WildFly Admin Guideの12.4. WildFly Galleon layersに解説があります。

ユースケース例として、WildFlySpring Frameworkを使ったアプリケーションをデプロイしたいので、GalleonによってEJBやremotingサブシステム(JMSサーバ接続やリモートEJBに使われるWildFly固有の機能)を削って、欲しい機能だけ持つWildFlyを作ることが考えられます。

Galleonが生まれた背景

コンテナのイメージサイズの削減がGalleonの目的です。

WildFlyには、元々@Statelessが付与されたコードがデプロイされてからEJBサブシステムをクラスロードするなど、アプリケーションで使われている機能をデプロイ処理で検知して必要な機能だけロードして余分なMetaspaceを消費しないようにする仕組みが備わっています。このため、Galleonによって使われないサブシステムを削除しても、メモリ削減にはあまり効果は期待できません。

前述のcloud-profileの場合、通常のWildFlyと比較しても起動直後のJavaヒープメモリのフットプリント削減の効果は手元で計測する限りでは10MB程度です。

APサーバとJDK、S2Iツール群など含んだJBossのコンテナイメージはどうしてもサイズが大きくなります。JBoss EAP7の場合、最新のイメージでは940MBと大きなイメージです。

$ docker pull registry.redhat.io/jboss-eap-7/eap72-openjdk11-openshift-rhel8:1.0-4
$ docker images
REPOSITORY                                                       TAG                  IMAGE ID            CREATED             SIZE
registry.redhat.io/jboss-eap-7/eap72-openjdk11-openshift-rhel8   1.0-4                cbc9919e54f5        3 weeks ago         940MB

Dockerイメージのアプリのレイヤだけ別で、全て最新の同一バージョンのJBoss EAPが同一のワーカーノードで起動している場合はDockerによるイメージレイヤの共有が期待できます。しかし、別バージョンのJBoss EAPが複数起動している場合はディスクサイズをそれなりに消費します。

Web管理コンソールやリモートJMXサーバなど、EAP自体にもコンテナ環境ではあまり使わない機能が含まれています。このモノリスなAPサーバから使わない機能を削ってイメージサイズをなるべく小さくすることがGalleonの目的です。

必要な機能だけアーカイブに含める考え方はThorntailでも実現できていましたが、Quarkusの登場に伴い、Thorntailは今後メジャーバージョンアップせずにメンテナンスモードとなる*1ため、WildFlyとしてもイメージサイズ削減に手を打っています。

OpenShiftにGalleonで作ったWildFlyをデプロイする

Galloenを使ったWildFlyコンテナをデプロイするためのテンプレートやImageStreamはwildfly-s2iリポジトリで配布されています。テンプレートの使い方などの詳しいドキュメントは READMEに書かれています。

Galleonには前述のCLIの他にもMavenプラグインのgalleon-maven-pluginがあり、WildFlyのS2Iビルダイメージでは、BuildConfigによるイメージのビルド時にMavenプラグインのGalleonを動かして小さなWildFlyバイナリを生成します。

単純にS2Iビルド時にGalleonでWildFlyをビルドしても、元々のコンテナイメージのレイヤのサイズは小さくならず、ただレイヤを重ねてビルダイメージよりも大きくなるだけです。このためS2Iのツール群やJDKWildFly本体を含んだビルダイメージquay.io/jfdenise/wildfly-centos7:latestと、WildFly本体やS2Iツール群を削ってJDKと便利スクリプトのみ含められたランタイムイメージquay.io/jfdenise/wildfly-runtime-centos7:latestの2種類のコンテナイメージが配布されています。

テンプレートには2つのBuildConfigが含まれています。1つはWildFlyにビルダイメージとソースコードをインプットに従来と同等のアプリケーションイメージを作成するBuildConfigと、生成されたアプリケーションイメージからGalleonで生成したディレクトリを抜き取って、ランタイムイメージにマージしたサイズの小さなアプリケーションイメージを生成するBuildConfigです。絵にすると以下のようなイメージです。

f:id:n_agetsuma:20190725210525p:plain

"抜き取る"とは、具体的には以下のようにBuildConfigを定義して、アプリケーションイメージの /s2i-output/server/ の内容をランタイムイメージにコピーしています。

- apiVersion: build.openshift.io/v1
  kind: BuildConfig
  ...
  spec:
    output:
      ...
    source:
      dockerfile: |-
        FROM wildfly-runtime-centos7:latest
        COPY /server $JBOSS_HOME
        USER root
        RUN chown -R jboss:root $JBOSS_HOME && chmod -R ug+rwX $JBOSS_HOME
        RUN ln -s $JBOSS_HOME /wildfly 
        USER jboss
        CMD $JBOSS_HOME/bin/openshift-launch.sh
      images:
        - from: 
            kind: ImageStreamTag
            name: ${APPLICATION_NAME}-build-artifacts:latest
          paths: 
          - sourcePath: /s2i-output/server/
            destinationDir: "."

minishiftで実際に試してみます。

# プロジェクトの作成
$ oc new-project wildfly-galleon

# ImageStreamとTemplateのロード
$ oc create -f https://raw.githubusercontent.com/wildfly/wildfly-s2i/wf-17.0/templates/wildfly-builder-imagestream.yml
$ oc create -f https://raw.githubusercontent.com/wildfly/wildfly-s2i/wf-17.0/templates/wildfly-runtime-imagestream.yml
$ oc create -f https://raw.githubusercontent.com/wildfly/wildfly-s2i/wf-17.0/templates/wildfly-s2i-chained-build-template.yml

# テンプレートwildfly-s2i-chained-build-templateによるイメージのビルド
$ oc new-app --template=wildfly-s2i-chained-build-template -p APPLICATION_NAME=cloudprofile-app -p GIT_REPO=https://github.com/nagetsum/eap-debug.git -p GIT_CONTEXT_DIR=cloudprofile-app -p IMAGE_STREAM_NAMESPACE=wildfly-galleon -p GALLEON_PROVISION_SERVER=cloud-profile-h2

テンプレートのパラメータGALLEON_PROVISION_SERVERがGalleonに渡すパラメータで、この例ではJAX-RS/CDI/JPA + H2インメモリDBを示すcloud-profile-h2を指定します。その他のパラメータは前述のREADMEに言及があります。

イメージのサイズは従来のアプリケーションイメージが1.63GBに対し、小さいなアプリケーションイメージは568MBと約3分の1となりました。

$ minishift ssh
[docker@minishift ~]$ docker images
REPOSITORY                                                         TAG                 IMAGE ID            CREATED             SIZE
172.30.1.1:5000/wildfly-galleon/cloudprofile-app                   latest              43b73a025880        28 minutes ago      568 MB
172.30.1.1:5000/wildfly-galleon/cloudprofile-app-build-artifacts   latest              a18f05787add        29 minutes ago      1.63 GB
quay.io/jfdenise/wildfly-centos7                                   <none>              6983f22f7f32        6 weeks ago         1.22 GB
quay.io/jfdenise/wildfly-runtime-centos7                           <none>              c449eca1cc04        6 weeks ago         436 MB

テンプレートを実行しただけではイメージがビルドされるだけなので、作ったイメージをデプロイします。

$ oc new-app cloudprofile-app:latest
--> Found image 43b73a0 (37 minutes old) in image stream "wildfly-galleon/cloudprofile-app" under tag "latest" for "cloudprofile-app:latest"

    Tags: wildfly, wildfly17

    * This image will be deployed in deployment config "cloudprofile-app"
    * Ports 8080/tcp, 8778/tcp will be load balanced by service "cloudprofile-app"
      * Other containers can access this service through the hostname "cloudprofile-app"

--> Creating resources ...
    deploymentconfig.apps.openshift.io "cloudprofile-app" created
    service "cloudprofile-app" created
--> Success
    Application is not exposed. You can expose services to the outside world by executing one or more of the commands below:
     'oc expose svc/cloudprofile-app'
    Run 'oc status' to view your app.

$ oc expose svc/cloudprofile-app
route.route.openshift.io/cloudprofile-app exposed

このアプリケーションではJAX-RSのエンドポイントにアクセスすると、JPAを使ってインメモリのH2データベースから本情報を取り出しています。

$ oc get route
NAME               HOST/PORT                                              PATH      SERVICES           PORT       TERMINATION   WILDCARD
cloudprofile-app   cloudprofile-app-wildfly-galleon.192.168.64.7.nip.io             cloudprofile-app   8080-tcp                 None

$ curl cloudprofile-app-wildfly-galleon.192.168.64.7.nip.io/cloudprofile-app/api/books/1; echo
{"author":"Joshua Bloch","id":1,"title":"Effective Java"}

JBoss EAPではまだGalleonに対応していませんが、今後のバージョンで対応するように検討が行われています。

まとめ

  • Galleonは欲しい機能だけを含んだWildFlyを生成するプロビジョニングツールです
  • uber-jarではなく、従来のWildFlyディレクトリ構成や使い勝手を維持しながらスリム化します
  • Galleonの目的はコンテナイメージサイズの削減です