ローカルでのSource-To-Image (s2i)の検証

こんにちは、Red Hat Middleware Technical Account Managerのイアンです。

Technical Account Manager (TAM)として、お客様のシステムの安定稼働をさせるために私は、日々お客様からのサポートケースに対応しています。 その中で、お客様の環境、アプリ、現象などを速やかに再現して原因と対応方法を提供することを心がけています。

物理サーバーや仮想マシン上のミドルウェアをご利用している場合は、自分のローカル環境で割と簡単に再現環境を立ち上げられますが、最近はOpenShift上のアプリの問い合わせが増えてきました。 OpenShift Local (旧名: CodeReady Containers)を用いて検証用のOpenShift環境を立ち上げることができますが、起動するのに時間かかり、PCのリソースも結構必要です。 ただアプリをイメージ化し、コンテナを起動したいだけなのですが、もっといいやり方がないでしょうか?

OpenShift環境上でアプリケーションをデプロイする時は、アプリをソースからビルドするSource-To-Image (s2i)の仕組みがよく使用されるかと思います。 中々いい仕組みで、OpenShift環境がなくても、ローカルで利用することができます!

目次

推奨環境

Source-To-ImageをローカルPCで実行するためには、コンテナエンジンが必要です。 DockerPodmanがよく使われています。 当記事では、Fedora 35上の Podman を使用します。

なお、PodmanのコマンドラインツールはDockerと互換性がありますので、Dockerで実行する場合はpodmandockerに置き換えて頂ければ使えると思います。

Podmanについて

当記事では、Podmanの詳細には触れませんが、ご興味がある方には、弊社が提供しているトレーニングの受講をご検討頂ければ幸いです。 また、Red Hat Developerアカウントをお持ちの方は、developers.redhat.comから電子書籍を無償でダウンロードできます。 Podman入門としてPodman In Actionをおすすめします。

s2iでのアプリイメージ作成

まずは、最新のs2iバイナリをSource-To-Imageのリポジトリからダウンロードし、展開します。

また、サンプルアプリとして、JBoss Enterprise Application Platform (EAP)Quickstartsの中のhelloworld-rs(単純なRESTサービス)を利用するので、 jboss-eap-quickstarts リポジトリをローカルにcloneし、 jboss-eap-quickstarts ディレクトリに移動します。

$ git clone https://github.com/jboss-developer/jboss-eap-quickstarts.git
...
$ cd jboss-eap-quickstarts

s2iを使うためには、ビルダーイメージが必要です。 OpenShiftと同じRed Hatが提供しているJBoss EAP 7.4ビルダーイメージを使います。

$ podman pull registry.redhat.io/jboss-eap-7/eap74-openjdk11-openshift-rhel8:latest

NOTE: registry.redhat.ioからダウンロードするためには、podman login registry.redhat.ioを使ってRed Hatアカウントにログインする必要があります。

準備ができたら、以下のs2iコマンドを使って、 helloworld-rs のアプリをビルドし、イメージを作成します。 最終イメージ名は localhost/eap-app:latest になります。

s2i build . registry.redhat.io/jboss-eap-7/eap74-openjdk11-openshift-rhel8 eap-app \
    --copy \
    --pull-policy never \
    -e GALLEON_PROVISION_LAYERS=jaxrs-server \
    -e GALLEON_PROVISION_DEFAULT_FAT_SERVER=true \
    -e MAVEN_ARGS_APPEND="-f helloworld-rs/pom.xml" \
    -e MAVEN_S2I_ARTIFACT_DIRS=helloworld-rs/target

NOTE: OpenShiftと違って、いくつかの$MAVEN_...の環境変数を指定しています。 Quickstartsのソースのサブディレクトリの中の helloworld-rs をビルドするために必要です。 アプリのソースコードはソースディレクトリ直下にある場合は、別ディレクトリの pom.xml などを指定しなくても良いです。

ビルダーイメージからコンテナが起動され、Mavenのビルドが始まります。

Checking if image "registry.redhat.io/jboss-eap-7/eap74-openjdk11-openshift-rhel8" is available locally ...
Checking if image "registry.redhat.io/jboss-eap-7/eap74-openjdk11-openshift-rhel8" is available locally ...
Provisioning WildFly server...
INFO Performing Maven build in /opt/jboss/container/wildfly/s2i/galleon/provisioning/generic_layers
INFO Using MAVEN_OPTS -XX:+UseParallelOldGC -XX:MinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=20 -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -XX:+ExitOnOutOfMemoryError
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.713 s
[INFO] Finished at: 2023-01-22T12:27:45Z
[INFO] ------------------------------------------------------------------------
INFO Copying deployments from helloworld-rs/target to /deployments...
'/tmp/src/helloworld-rs/target/ROOT.war' -> '/deployments/ROOT.war'
INFO Cleaning up source directory (/tmp/src)
INFO Copying server to /s2i-output
INFO Linking /opt/eap to /s2i-output
Build completed successfully

s2iのビルドが完了したら、新しい eap-app イメージがローカルのリポジトリに作成されます。

$ podman images
REPOSITORY                                                              TAG         IMAGE ID      CREATED         SIZE
localhost/eap-app                                                       latest      6c2779ffb971  43 seconds ago  1.15 GB
registry.redhat.io/jboss-eap-7/eap74-openjdk11-openshift-rhel8          latest      a34c07e25cbf  6 weeks ago     1.01 GB

このイメージはそのまま実行できます。

$ podman run --rm \
    --name eap \
    -p 18080:8080 \
    -e ENABLE_ACCESS_LOG=true \
    localhost/eap-app:latest \
    /opt/eap/bin/openshift-launch.sh

アプリの動作を確認します。

$ curl -w "\n" http://localhost:18080/rest.json
{"result":"Hello World!"}

コンテナ起動時に$ENABLE_ACCESS_LOGの環境変数を指定したので、コンテナの標準出力に以下のようなアクセスログも出ます。

$ podman logs --tail=10 eap
...
12:29:14,076 INFO  [io.undertow.accesslog] (default task-1) 10.0.2.100 - - [22/Jan/2023:12:29:14 +0000] - "GET /rest/json HTTP/1.1" 200 25

これで、OpenShiftと同様のs2i仕組みを使って、アプリのイメージを作成し、起動できました。

でも、これだけではありません。

ランタイムイメージの作成

先に作ったアプリのイメージのサイズをご覧になりましたでしょうか? 1GBほどとなってしまい、ビルダーイメージとほぼ同じサイズです。

ビルダーイメージの上にアプリを載せただけですので、アプリに必要ではないビルド関連のファイル(Maven, s2i)が含まれています。 ローカルでの検証だけなので、サイズがそんなに問題ではないかもしれませんが、OpenShiftと同様のランタイムイメージを使ってイメージを作成できます。

通常のOpenShift上のビルドは、ビルドから作成したアーティファクトを自動的にランタイムイメージにコピーしてくれますが、ローカルの場合は自動的にできません。

jboss-eap-quickstarts ディレクトリに .s2i/bin ディレクトリを作って、中の assemble-runtime ファイルを作って、中身を以下のように設定します。

#!/usr/bin/env bash
mkdir /opt/eap
cp -r /home/jboss/s2i/s2i-output/server/* /opt/eap
chown -R jboss:root /opt/eap && chmod -R ug+rwX /opt/eap

NOTE: assemble-runtimeのコマンドはOpenShiftのeap74-basic-s2i.jsonテンプレートを参照して作成しました。

chmod u+x .s2i/bin/assemble-runtimeで実行可能にします。

また、ランタイムイメージをpullしましょう。

$ podman pull registry.redhat.io/jboss-eap-7/eap74-openjdk11-runtime-openshift-rhel8

これができたら、前回のs2iコマンドにruntimeイメージ関連のオプションを足して実行します。

$ s2i build /path/to/jboss-eap-quickstarts registry.redhat.io/jboss-eap-7/eap74-openjdk11-openshift-rhel8 eap-app \
    --runtime-artifact /s2i-output:s2i \
    --runtime-image registry.redhat.io/jboss-eap-7/eap74-openjdk11-runtime-openshift-rhel8 \
    --runtime-pull-policy never
    --copy \
    --pull-policy never \
    -e GALLEON_PROVISION_LAYERS=jaxrs-server \
    -e GALLEON_PROVISION_DEFAULT_FAT_SERVER=true \
    -e MAVEN_ARGS_APPEND="-f helloworld-rs/pom.xml" \
    -e MAVEN_S2I_ARTIFACT_DIRS=helloworld-rs/target

実行すると、s2iは前回と同様にアプリをビルドしますが、ビルドのアーティファクトが含まれている /s2i-output ディレクトリをランタイムイメージの /home/jboss/s2i ディレクトリにマウントします。 その後、用意した assemble-runtime スクリプトが動き、ビルド結果を /opt/eap にコピーします。 最後にランタイムイメージを eap-app としてローカルリポジトリにプッシュします。

出来上がった eap-app イメージにはMaven, s2iなどのビルド関連のファイルがないので、ビルダーイメージの場合よりサイズを小さくできます。

$ podman images
REPOSITORY                                                              TAG         IMAGE ID      CREATED         SIZE
localhost/eap-app                                                       latest      352389a94bf3  15 seconds ago  814 MB
registry.redhat.io/jboss-eap-7/eap74-openjdk11-openshift-rhel8          latest      a34c07e25cbf  6 weeks ago     1.01 GB
registry.redhat.io/jboss-eap-7/eap74-openjdk11-runtime-openshift-rhel8  latest      164d988e5c07  6 weeks ago     532 MB

Stupid s2i/container Tricks

ビルドの加速化

ここまで来て、数回s2iでアプリをビルドして気づいたかと思いますが、毎回依存モジュールのダウンロードが必要となり、時間がかかっています。 サポートエンジニアとして、現象を再現する時は、コードを修正し、実行することを何回も繰り返すので、s2iのビルドを加速させたらビルドに要している時間を省いて早く再現することができます。

ビルダーイメージは /tmp/artifacts/m2 をローカルMavenリポジトリとして使用しています。 そのため、コンテナ用のボリュームを /tmp/artifacts にマウントすれば、ダウンロードした依存モジュールを今後のビルドでも再利用できます。 s2i-vオプションでボリュームマウントを指定できます。

$ podman volume create eap-build-repo
$ s2i build . registry.redhat.io/jboss-eap-7/eap74-openjdk11-openshift-rhel8 eap-app \
    --copy \
    --pull-policy never \
    -e GALLEON_PROVISION_LAYERS=jaxrs-server \
    -e GALLEON_PROVISION_DEFAULT_FAT_SERVER=true \
    -e MAVEN_ARGS_APPEND="-f helloworld-rs/pom.xml" \
    -e MAVEN_S2I_ARTIFACT_DIRS=helloworld-rs/target \
    -v "eap-build-repo:/tmp/artifacts"

もう一度s2i buildコマンドを実行すると、アプリのビルドがすぐに終わります。

latest以外のイメージの利用方法

当記事を書いた時のeap74-openjdk11-openshift-rhel8イメージはJBoss EAP 7.4.8を使用していますが、 例えば、お客様の環境に合わせて、7.4.2を使いたい場合はどうすればいいでしょうか?

残念ながら、以下のようなコマンドを使っても、ビルダーイメージのlatestが使用されてしまいます。

$ s2i build . registry.redhat.io/jboss-eap-7/eap74-openjdk11-openshift-rhel8:7.4.2 eap-app \
    ...

ここでイメージのタグを使えます! 以下のコマンドでjboss-eap-7/eap74-openjdk11-openshift-rhel8:7.4.2イメージをlocalhost/eap74-openjdk11-openshift-rhel8:latestとしてタグ付けします。

$ podman pull registry.redhat.io/jboss-eap-7/eap74-openjdk11-openshift-rhel8:7.4.2
$ podman tag registry.redhat.io/jboss-eap-7/eap74-openjdk11-openshift-rhel8:7.4.2 localhost/eap74-openjdk11-openshift-rhel8:latest

タグ付けしたら、s2i buildコマンドで新規にタグ付けしたイメージを使います。

$ s2i build . localhost/eap74-openjdk11-openshift-rhel8 eap-app \
    ...

Source-To-Imageはlocalhost/eap74-openjdk11-openshift-rhel8:latestを使いますが、そのタグは実はjboss-eap-7/eap74-openjdk11-openshift-rhel8:7.4.2を指していますので、JBoss EAP 7.4.2のイメージが出来上がります。

このやり方はランタイムイメージにも適用できます。

データベースドライバ導入

OpenShiftと同じs2i仕組みですので、環境変数を使って、データベースドライバの導入と設定を行えます。

今回のサンプルアプリはデータベースを使っていませんが、もしお持ちのアプリがデータベースを使っている場合の設定方法を紹介します。

NOTE: 以下の手順はGetting Started with JBoss EAP for OpenShift Container Platform Red Hat JBoss Enterprise Application Platform 7.4Datasources章を参照して作成しました。

まずは、 jboss-eap-quickstarts ディレクトリに extensions ディレクトリを作成します。 その中に、 modules/org/postgresql/main ディレクトリを作成します。

extensions/modules/org/postgresql/main の中にPostgreSQL 14のJDBCドライバー(記事作成当時は postgresql-42.5.1.jar)を置きます。 同じディレクトリに module.xml を作成し、中身を以下のように設定します。

NOTE: resource-rootpath属性をダウンロードしたJDBCドライバーのファイル名に設定します。

<?xml version="1.0" ?>
<module xmlns="urn:jboss:module:1.1" name="org.postgresql">
  <resources>
    <resource-root path="postgresql-42.5.1.jar"/>
  </resources>
  <dependencies>
    <module name="wildflyee.api"/>
    <module name="sun.jdk"/>
    <module name="ibm.jdk"/>
    <module name="javax.api"/>
    <module name="javax.transaction.api"/>
  </dependencies>
</module>

extensions/drivers.env ファイルを作って、中身を以下にします。 このファイルはEAP起動時のデータベースドライバーの登録に使用されます。

DRIVER
DRIVERS=POSTGRES
POSTGRES_DRIVER_NAME=postgresql
POSTGRES_DRIVER_MODULE=org.postgresql
POSTGRES_DRIVER_CLASS=org.postgresql.Driver
POSTGRES_XA_DATASOURCE_CLASS=org.postgresql.xa.PGXADataSource

最後に extensions/install.sh を作ります。 このファイルはSource-To-Buildの中で実行され、 modules サブディレクトリを /opt/eap/modules にコピーし、 drivers.env/opt/eap/bin/openshift にコピーします。

NOTE: install.shの詳細はModules, Drivers, and Generic Deploymentsをご参照ください。

#!/bin/bash
injected_dir=$1
source /usr/local/s2i/install-common.sh
install_modules ${injected_dir}/modules
configure_drivers ${injected_dir}/drivers.env

最終的に extensions ディレクトリの構成は以下のようになります。

extensions
├── drivers.env
├── install.sh
└── modules
    └── org
        └── postgresql
            └── main
                ├── module.xml
                └── postgresql-42.5.1.jar

ここでs2i buildを実行して、アプリのイメージを作成します。

NOTE: 今回は$CUSTOM_INSTALL_DIRECTORIESの環境変数を指定しています。

$ s2i build /path/to/jboss-eap-quickstarts registry.redhat.io/jboss-eap-7/eap74-openjdk11-openshift-rhel8 eap-app \
    --copy \
    --pull-policy never \
    -e GALLEON_PROVISION_LAYERS=jaxrs-server \
    -e GALLEON_PROVISION_DEFAULT_FAT_SERVER=true \
    -e MAVEN_ARGS_APPEND="-f helloworld-rs/pom.xml" \
    -e MAVEN_S2I_ARTIFACT_DIRS=helloworld-rs/target \
    -e CUSTOM_INSTALL_DIRECTORIES=extensions
...
INFO Copying deployments from helloworld-rs/target to /deployments...
'/tmp/src/helloworld-rs/target/ROOT.war' -> '/deployments/ROOT.war'
INFO Processing ImageSource mounts: extensions
INFO Processing ImageSource from /tmp/src/extensions
INFO Cleaning up source directory (/tmp/src)
INFO Copying server to /s2i-output
INFO Linking /opt/eap to /s2i-output
Build completed successfully

s2iは無事に extensions ディレクトリの install.sh を実行できました。

起動する時は、環境変数を用いてデータソースを設定します。

$ podman run \
    --rm \
    --name eap \
    -p 18080:8080 \
    -e ENABLE_ACCESS_LOG=true \
    -e DB_SERVICE_PREFIX_MAPPING=test-postgresql=TEST \
    -e TEST_NONXA=true \
    -e TEST_CONNECTION_CHECKER=org.jboss.jca.adapters.jdbc.extensions.postgres.PostgreSQLValidConnectionChecker \
    -e TEST_EXCEPTION_SORTER=org.jboss.jca.adapters.jdbc.extensions.postgres.PostgreSQLExceptionSorter \
    -e TEST_USERNAME=postgres \
    -e TEST_PASSWORD=changeme \
    -e TEST_POSTGRESQL_SERVICE_HOST=localhost \
    -e TEST_POSTGRESQL_SERVICE_PORT=5432 \
    -e TEST_DATABASE=test \
    localhost/eap-app:latest \
    /opt/eap/bin/openshift-launch.sh

EAPのCLIを使って、データソースが作成されたことを確認できますが、実際のデータベースがないため、接続テストを行うとエラーになります。

$ podman exec eap /opt/eap/bin/jboss-cli.sh -c "/subsystem=datasources/data-source=test_postgresql-TEST:test-connection-in-pool"
{
    "outcome" => "failed",
    "failure-description" => "WFLYJCA0040: failed to invoke operation: WFLYJCA0047: Connection is not valid",
    "rolled-back" => true
}

ローカル環境でもEAPとデータベースを試せたらいいですね。

EAPとデータベースのPod作成

Podmanの機能の一つとして、複数コンテナを一つPodとして管理することが可能です(Kubernetesと同じような概念です)。 この機能を用いて、PostgreSQLとEAPの2つのコンテナを起動し、お互いに接続できるようにします。

3つのコマンドを使って、Podを作成して、PostgreSQLとEAPのコンテナをPodの中に作成します。

$ podman pod create -n eap \
    -p 18080:8080
$ podman create \
    --pod eap \
    --name postgres \
    -e POSTGRES_USER=postgres \
    -e POSTGRES_PASSWORD=changeme \
    -e POSTGRES_DB=test \
    postgres:14
$ podman create \
    --pod eap \
    --name eap-app \
    -e ENABLE_ACCESS_LOG=true \
    -e DB_SERVICE_PREFIX_MAPPING=test-postgresql=TEST \
    -e TEST_NONXA=true \
    -e TEST_CONNECTION_CHECKER=org.jboss.jca.adapters.jdbc.extensions.postgres.PostgreSQLValidConnectionChecker \
    -e TEST_EXCEPTION_SORTER=org.jboss.jca.adapters.jdbc.extensions.postgres.PostgreSQLExceptionSorter \
    -e TEST_USERNAME=postgres \
    -e TEST_PASSWORD=changeme \
    -e TEST_POSTGRESQL_SERVICE_HOST=localhost \
    -e TEST_POSTGRESQL_SERVICE_PORT=5432 \
    -e TEST_DATABASE=test \
    localhost/eap-app:latest \
    /opt/eap/bin/openshift-launch.sh

作成したPodをpodman pod start eapで起動します。

2つコンテナが起動したことを確認できます。

NOTE: pauseのコンテナは cgroups の名前空間を管理するためのもので、Podmanが自動的に起動/停止します。

$ podman ps
CONTAINER ID  IMAGE                          COMMAND               CREATED         STATUS             PORTS                    NAMES
d27be4db5138  k8s.gcr.io/pause:3.5                                 39 seconds ago  Up 27 seconds ago  0.0.0.0:18080->8080/tcp  d15a4ef315f2-infra
2e7170d72f23  docker.io/library/postgres:14  postgres              39 seconds ago  Up 26 seconds ago  0.0.0.0:18080->8080/tcp  postgres
205e1e9c1691  localhost/eap-app:latest       /opt/eap/bin/open...  39 seconds ago  Up 26 seconds ago  0.0.0.0:18080->8080/tcp  eap-app

今回は、データベースへの接続テストを行うと、成功します。

$ podman exec eap-app \
    /opt/eap/bin/jboss-cli.sh -c "/subsystem=datasources/data-source=test_postgresql-TEST:test-connection-in-pool"
{
    "outcome" => "success",
    "result" => [true]
}

Podを停止したい場合はpodman pod stop eapを実行します。

まとめ

OpenShift環境を使わくても、s2iの仕組みやコンテナ技術を使って、ローカルで色々検証することが可能です。 お客様の環境や現象をすぐに再現し、原因究明を行うことはTAMとして誇りに思います。

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。