連載 [第4回] :
  Red Hat OpenShiftの全貌

Projectとアプリケーションデプロイ

2019年1月18日(金)
長野 修
連載4回目となる今回は、OpenShiftにおけるProjectの解説、イメージのビルドやデプロイについて学びます。

イメージを自分で作成する:PaaSとしての利用

すでにあるイメージを使って運用していくだけなら、Kubernetesの機能だけでも十分かもしれません。OpenShiftではそれに加えて、S2Iビルドで安全なイメージを標準化された方法で作っていく機能、さらには系統的にCI/CDを回していく機能も提供しており、PaaS(Platform as a Service)としての側面も持っています。ここではすべてを紹介しきれませんが、特にJavaのS2Iビルドの例を詳しく見ていきます。

S2Iビルドとは

あるプログラミング言語で書かれたアプリケーションの最終的なイメージを作るにあたって必要なものとしては、以下のようなものがあげられます。

  • ベースとなるOS
  • そのプログラミング言語の実行環境(コンパイル型の言語であれば加えてビルド環境)
  • アプリケーションが利用するフレームワーク

そこで、開発者が書くソースコードを除いて必要なものすべてをビルダーイメージとしてあらかじめ用意し、あとはソースコードを入力として加えるだけで最終イメージを生成する仕組みを用意しました。それがS2I(Source to Image)ビルドです。

S2Iのビルダーイメージは、主要なプログラミング言語についてそれぞれ用意されており、各言語においてデファクトスタンダートとなっているビルドツールに対応しています。例えば、JavaであればMavenのpom.xmlファイルを、JavaScriptであればNPMのpackage.jsonファイルを自動で認識し、ビルドを行い、その結果できたアプリケーションを直接起動するイメージを生成します。ビルドや起動に必要な手順は、各ビルダーイメージ内にあるスクリプトにあらかじめ用意されており、アプリケーションのビルド環境およびランタイムの提供と、ソースコードの準備という二つの役割が、綺麗に分離されています。

一旦S2Iビルドをセットアップすると、ソースコードの更新をトリガーにして最終生成物であるアプリケーションイメージのデプロイまでが一気に自動化されます。

また、ビルダーイメージの方も必要に応じて更新されていきます。OpenShift側で用意されているビルダーイメージはRed Hatがメンテナンスしており、使用しているツールのバージョンアップやセキュリティフィクスのリリースに応じてビルダーイメージが更新され、同じようにアプリケーションイメージのビルドがトリガーされて結果を素早く確認できます。

それではJavaのSpring Bootアプリケーションを例にとって、S2Iビルドの具体例を見ていきましょう。

プログラムのソースコードの用意

ここではMavenのアーキタイプという機能を利用して、アプリケーションの原形を一気に生成してしまいます。

リスト36:アプリケーションの原形を生成

$ mvn archetype:generate -B \
    -DarchetypeGroupId=org.springframework.boot -DarchetypeArtifactId=spring-boot-sample-jetty-archetype \
    -DgroupId=com.example -DartifactId=sample-jetty

これだけで、シンプルではあるものの完動するJavaのWebアプリケーションができます。実際にビルドして動作確認してみます。

リスト37:すでにWebアプリケーションができている

$ cd sample-jetty
$ mvn package
$ java -jar target/sample-jetty-1.0-SNAPSHOT.jar &
$ curl http://localhost:8080/
Hello World

このアプリケーションはポート8080でリスンし、ルートパスにアクセスすると「Hello World」と返すだけのものです。それではビルドはこの後OpenShiftに任せますので、先に進む前にここでの作業物を掃除しておきましょう。

リスト38:ここまで作成したものを削除

$ kill %java
$ mvn clean

JavaのS2Iビルド

新しくOpenShiftのプロジェクト「my-javatest」を作成して、同じことをJavaのビルダーイメージを使ったS2Iビルドでやってみます。まだGitHub等には上げておらにず、ローカルにしかないソースファイルですので、入力としてローカルファイルが使えるようにバイナリービルドを行います。

リスト39:ローカルのソースをバイナリービルド

$ oc new-project my-javatest
$ oc new-build --name builder --strategy=source --binary java:8
$ oc start-build builder --from-dir=. --follow
(...)
Push successful

コマンドとオプションの解説を行います。oc new-buildコマンドでは、生成されるBuildConfigリソースの名前が「builder」になるように--nameオプションを付けています。--strategy=sourceがS2Iビルドの、--binaryがバイナリービルドの指定です。最後のオプション「java:8」が、使用するビルダーイメージのImageStreamTagになります。このビルダーイメージについての詳細は「oc describe istag java:8 -n openshift」で確認できます。oc new-buildコマンドは「openshift」プロジェクトにあるビルダーイメージ(tagsに「builder」があるもの)を探しに行くようになっていますので、このコマンドには名前空間を指定する-nオプションは不要です。最後にビルドを開始するoc start-buildコマンドを実行しています。バイナリービルドですので、ビルドの入力となる材料を--from-dirオプションで指定しています。このコマンド実行後に、OpenShiftにビルド用のPodが作られてそこでMavenビルドが行われます。

oc get allを実行して、OpenShiftプロジェクトにおけるビルドの成果物を確認してみます。

リスト40:ビルド結果か確認する

$ oc get all
NAME                  READY     STATUS      RESTARTS   AGE
pod/builder-1-build   0/1       Completed   0          12m

NAME                                     TYPE      FROM      LATEST
buildconfig.build.openshift.io/builder   Source    Binary    1

NAME                                 TYPE      FROM      STATUS     STARTED          DURATION
build.build.openshift.io/builder-1   Source    Binary    Complete   12 minutes ago   1m22s

NAME                                     DOCKER REPO                                            TAGS      UPDATED
imagestream.image.openshift.io/builder   docker-registry.default.svc:5000/my-testjava/builder   latest    10 minutes ago

oc runやoc new-appを実行した時には、DeploymentConfig、ReplicationController、Podの3階層のリソースができたことを覚えているでしょうか。それと同様にビルドの時には、BuildConfig、Build、Podの3階層のリソースが作成されます。DeploymentConfigの場合は稼働し続けるPodを管理するのが基本であるのに対し、BuildConfigではビルドを行う一回きりのPodを管理するという違いがあります。

ビルドの最終的な生成物はImageStreamであり、上記の出力の最下行に表示されています。ImageStream(より正確にはその一部としてのImageStreamTag)の背後には、それが参照する内部レジストリにあるDockerイメージがあります。

それではビルドしたイメージが正しく動作しているか確認してみましょう。同じプロジェクト内に「builder」の名前でImageStreamができていますので、oc new-appコマンドには引数にそれを与えるだけで済みます。

リスト41:ビルドしたイメージの動作確認

$ oc new-app builder
$ oc expose svc builder
$ oc get route builder
NAME      HOST/PORT                                       PATH      SERVICES   PORT       TERMINATION   WILDCARD
builder   builder-my-testjava.192.168.42.254.nip.io                 builder    8080-tcp                 None
$ curl http://builder-my-testjava.192.168.42.254.nip.io/
Hello World

期待どおりに「Hello World」が返ってきましたので、正常に動作していると言えるでしょう。

インクリメンタルビルドの有効化

一旦oc new-buildコマンドでBuildConfigを作ってしまえば、後は好きな時にoc start-buildコマンドでビルドを開始できます。

ただし今のままでは、ビルドのたびにすべてのMavenアーティファクトをダウンロードしにいくため、非常に時間がかかってしまいます。これはビルドの再現性の高さを確認するのにはいいですが、普段の開発には向いていません。そこでS2Iのビルダーイメージでは、ビルドの中間生成物(Javaの場合はMavenのローカルリポジトリである「.m2」ディレクトリ)を次のビルド用Podにコピーするためのスクリプトが用意されています。インクリメンタルビルドを有効化すると、そのスクリプトが機能するようになります。

インクリメンタルビルドを有効にするには、BuildConfigのマニフェストをoc editコマンドで直接編集します。

リスト42:インクリメンタルビルドを有効にする

$ oc edit bc builder
(...)
  strategy:
    sourceStrategy:
      from:
        kind: ImageStreamTag
        name: java:8
        namespace: openshift
      incremental: true # この行を追加する
(...)

エディタが起動しますので、上記の箇所の「from:」と同じ階層に「incremental: true」を追加して保存して下さい。ついでにJavaのソースファイルも編集して、メッセージを少し変えてみましょう。

リスト43:ソースファイルも変更しておく

$ vi src/main/java/com/example/jetty/service/HelloWorldService.java
(...)
    @Value("${name:World!!!}") // "!!!"を追加する
(...)

それではビルドを開始します。今度は大量のMavenアーティファクトのダウンロードがログに出ないことを確認できるはずです。

リスト44:出力にソースの変更が反映されていることを確認

$ oc start-build --from-dir=. --follow
$ curl http://builder-my-testjava.192.168.42.254.nip.io/
Hello World!!!

メッセージの結果も変っており、新しいソースコードが使われたことが分ります。

S2Iビルドのカスタマイズ

S2Iビルドの秘密は、ビルダーイメージに埋め込まれたいくつかのスクリプトにあります。まずはそのスクリプトがどこにあるかを、ImageStreamTagの出力から確認してみましょう。

リスト45:スクリプトの保存サキを確認

$ oc describe istag java:8 -n openshift | grep io.openshift.s2i.scripts-url
                io.openshift.s2i.scripts-url=image:///usr/local/s2i

S2Iのビルダーイメージは、そのDockerfileにおけるLABELインストラクションでいくつかのラベルを指定することで自分を区別しています。特に「io.openshift.s2i.scripts-url」のラベルには、S2Iスクリプトのパスが格納されています。

いま稼働しているPodにoc rshコマンドで入ってそれらのスクリプトを確認することもできますが、対象のイメージにはlessのような基本的なコマンドも入っていませんので、何かと不便です。そこでoc cpコマンドを使ってPodからファイルをコピーし、ローカルのマシンでゆっくり確認するのがいいでしょう。

リスト46:スクリプトをローカル環境で確認

$ oc cp builder-2-mss2q:/usr/local/s2i/ /tmp/s2i
$ ls -l /tmp/s2i
total 48
-rw-r--r--. 1 onagano onagano 6154 Dec 16 21:19 assemble
-rw-r--r--. 1 onagano onagano 5746 Dec 16 21:19 common.sh
-rw-r--r--. 1 onagano onagano 8493 Dec 16 21:19 jboss-settings.xml
-rw-r--r--. 1 onagano onagano  682 Dec 16 21:19 run
-rw-r--r--. 1 onagano onagano  234 Dec 16 21:19 s2i-setup
-rw-r--r--. 1 onagano onagano  194 Dec 16 21:19 save-artifacts
-rw-r--r--. 1 onagano onagano   33 Dec 16 21:19 scl-enable-maven
-rw-r--r--. 1 onagano onagano   38 Dec 16 21:19 usage

これらのスクリプトファイルのうち、S2Iビルドにおいて必須とされているのがassembleとrunです。assembleとrunがそれぞれどういう役割を担っているかは、通常のDockerfileを書くときを思い出せば明確になります。

Dockerfileでは、RUNやCOPYといったインストラクションを実行するたびにレイヤーができて、イメージのサイズを大きくしてしまいます。そこでRUNインストラクションでは実行したいコマンドを無理に「&&」でつなげて一つのレイヤーにしてしまうことが慣習になっていますが、これでは可読性を損ねてしまいます。結果的に、RUNインストラクションで実行したい内容はすべてスクリプトファイルに外出しするという折衷案ができました。その外出しされたスクリプトがassembleに相当します。Javaのビルダーイメージのassembleスクリプトの場合は、pom.xmlを認識してmvnコマンドを実行するといった複雑な処理を一手に担っているのです。

runスクリプトも同様です。Dockerfileで最後に書くCMDやENTRYPOINTインストラクションで何か複雑なことをしたい場合は、これもスクリプトに外出しすることになります。それに相当するのがrunスクリプトなのです。

他に、save-artifactsスクリプトは必須ではありませんが、前述のインクリメンタルビルドを有効に機能させるにはこれも必要になってきます。

これらのスクリプトは、ソースファイルに「.s2i/bin」というディレクトリを作り、そこに同名のスクリプトを置くことで上書きできます。ここでは、assembleを上書きしてそれが実際にビルド時に呼ばれていることを確認する例を載せておきます。まともなassembleスクリプトをすべて書くのは大変ですので、実際の処理はすべてオリジナルの/usr/local/s2i/assembleに委譲しています。

リスト47:スクリプトの機能を上書き

$ mkdir -p .s2i/bin
$ vi .s2i/bin/assemble
#!/bin/bash
echo "===MY SCRIPT IS CALLED==="
exec /usr/local/s2i/assemble
$ chmod +x .s2i/bin/assemble
$ oc start-build builder --from-dir=. --follow
(...)
===MY SCRIPT IS CALLED===
(...)

チェーンビルド

いま実行しているbuilderイメージには、アプリケーションの実行には不必要なファイルも多く含まれています。例えば、mvnコマンドは明らかに実行には必要ありませんし、ビルドの中間生成物も不要です。最終的な実行のために配布するイメージのサイズを小さくするために、もう一段階別のBuildConfigを設けて、小さなruntimeイメージを作ってみましょう。このような多段階のビルドは、チェーンビルドやマルチステージビルドと呼ばれています。

初めに、いま稼働しているoc new-app builderで作成されたリソースを削除しておきます(そのままでも特に問題はありません)。

リスト48:以前に作成したリソースの削除

$ oc delete all -l app=builder

「oc new-build --name builder」で作成された「builder」のBuildConfigやImageStreamはまだ残っているはずです。そのイメージから必要なファイルを抜き取る形で、新しい「runtime」イメージを作ります。

コマンドは少し複雑ですが、一度のoc new-buildで済みます。

リスト49:チェーンビルドの準備

cat <<EOS | oc new-build --name=runtime --source-image=builder \
  --source-image-path=/deployments/sample-jetty-1.0-SNAPSHOT.jar:. \
  --dockerfile=-
FROM fedora
RUN dnf -y update \
 && dnf -y install java-1.8.0-openjdk-headless \
 && dnf clean all
COPY sample-jetty-1.0-SNAPSHOT.jar /deployments/runtime.jar
EXPOSE 8080
CMD java -jar /deployments/runtime.jar
EOS

オプションの説明を行います。--name=runtimeと指定していますので、できあがるBuildConfigの名前は「runtime」になります。よって、ビルドの様子まで見たい場合は「oc logs -f bc/runtime」も実行して下さい。

--source-imageと--source-image-pathで、指定したイメージの指定したパスにあるファイル(/deployments/sample-jetty-1.0-SNAPSHOT.jar)を、ビルドコンテキストのカレントディレクトリ(コロンの後の「.」)にコピーしています。

そして--dockerfile=-で標準出力からDockerfileを読み込むようにし、Dockerビルドを行うように指示しています。DockerfileではDocker Hubにある最新のFedoraで、さらにパッケージのアップデートを行い、Javaプログラムの実行に必要なパッケージ(java-1.8.0-openjdk-headless)だけをインストールしています。その後、ビルドコンテキストにコピーされてくるsample-jetty-1.0-SNAPSHOT.jarを新たな場所に置き、最後に`java -jar`コマンドでそれを実行しています。

それではできたImageStreamを使ってデプロイしてみましょう。

リスト50:runtimeをデプロイ

$ oc new-app runtime
$ oc expose svc runtime
$ oc get route runtime
NAME      HOST/PORT                                       PATH      SERVICES   PORT       TERMINATION   WILDCARD
runtime   runtime-my-testjava.192.168.42.254.nip.io                 runtime    8080-tcp                 None
$ curl http://runtime-my-testjava.192.168.42.254.nip.io/
Hello World!!!

新しいPodができるまで暫く時間がかかるかもしれませんが、最終的には上記のように「Hello World!!!」のメッセージが返るのを確認できるはずです。

ビルドのトリガーについて

なお、上記のビルドではoc new-buildコマンドの後にoc start-buildを実行しなくとも、自動でビルドが始まっていました。実は手動でoc start-buildが必要なのは--binaryを指定したバイナリービルドだけで、様々なイベントをトリガーにして自動でビルドが始まるのが通常です。上記の例では、ビルドに必要なソースはすべてコマンドラインとビルド済みのイメージから取得できますので、ローカルファイルを供給するバイナリービルドは使っていません。

ビルドが何によってトリガーされるかは、BuildConfigの内容をYAML形式で出力させ、「triggers:」の配列を見れば分かります。

リスト51:ビルドのトリガーを確認

$ oc get bc runtime -o yaml
(...)
  triggers:
  - github:
      secret: yrgrW-S3V5oIXV2TUlef
    type: GitHub
  - generic:
      secret: zWi2n3Wexlixg5USIkIy
    type: Generic
  - imageChange:
      from:
        kind: ImageStreamTag
        name: builder:latest
        namespace: my-testjava
      lastTriggeredImageID: docker-registry.default.svc:5000/my-testjava/builder@sha256:4b3110(...)
    type: ImageChange
  - type: ConfigChange
  - imageChange:
      lastTriggeredImageID: fedora@sha256:f6d888(...)
    type: ImageChange
(...)

配列の要素に、「type : ConfigChange」があります。これが、このBuildConfigに変更があった時にビルドをトリガーする設定です。BuildConfigの初回作成も対象の変更に含まれますので、oc new-buildコマンドだけでもビルドがトリガーされたのです。

他に、「type: ImageChange」のところでbuilderイメージが指定されています。これによりbuilderイメージの変更を検知して、自動でruntimeイメージもビルドされます。builderイメージはバイナリービルドで作成しましたので、何か変更を行い「oc start-build builder --from-dir=. --follow」を実行して動作を確認してみて下さい。

その他、「type: GitHub」から推測できるように、ソースコードをGitHubに上げて、ビルドのソースとしてインターネット上のそのリポジトリを指定して利用することもできます。そしてGitHubにはWebhookを設定できますので、GitHubへのコミットをトリガーにしてビルドを開始することもできるのです。ただし、GitHubのWebhookを利用するには、OpenShiftクラスタが外部からもアクセスできるよう公開されていることが必要になります。またMinishiftでは使えません。詳しい設定に関しては、下記のドキュメントをご覧下さい。

OCP 3.11 開発者ガイド 8.7. ビルドのトリガー

レッドハット株式会社

サービス事業統括本部 第1コンサルティング部 コンサルタント
金融とITの共通分野でキャリアを築くことを決意し、転職を繰り返して米系大手投資銀行でポータル開発の職にたどりつくも、リーマンショックであえなく撃沈。今はIT一本に絞って、JBossミドルウェアやOpenShiftの案件をデリバリしている。何らかのOSSプロジェクトにもっと具体的な貢献をしたいと思いつつ、めまぐるしく変わっていく流行りの技術に流され気味。いずれブロックチェーンに手を出す気がしている。

連載バックナンバー

クラウド技術解説
第5回

OpenShift:アプリケーションの構成と運用

2019/3/28
連載5回目となる今回は、OpenShiftでデプロイしたアプリケーションの運用に関するトピックを紹介する。
クラウド技術解説
第4回

Projectとアプリケーションデプロイ

2019/1/18
連載4回目となる今回は、OpenShiftにおけるProjectの解説、イメージのビルドやデプロイについて学びます。
クラウド技術解説
第3回

開発環境の準備とOpenShiftへのアクセス

2018/12/20
連載3回目となる今回は、OpenShiftの学習と開発のための環境を準備する手順を紹介いたします。

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています