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のアーキタイプという機能を利用して、アプリケーションの原形を一気に生成してしまいます。
$ mvn archetype:generate -B \ -DarchetypeGroupId=org.springframework.boot -DarchetypeArtifactId=spring-boot-sample-jetty-archetype \ -DgroupId=com.example -DartifactId=sample-jetty
これだけで、シンプルではあるものの完動するJavaの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に任せますので、先に進む前にここでの作業物を掃除しておきましょう。
$ kill %java $ mvn clean
JavaのS2Iビルド
新しくOpenShiftのプロジェクト「my-javatest」を作成して、同じことをJavaのビルダーイメージを使ったS2Iビルドでやってみます。まだGitHub等には上げておらにず、ローカルにしかないソースファイルですので、入力としてローカルファイルが使えるようにバイナリービルドを行います。
$ 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プロジェクトにおけるビルドの成果物を確認してみます。
$ 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コマンドには引数にそれを与えるだけで済みます。
$ 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コマンドで直接編集します。
$ oc edit bc builder (...) strategy: sourceStrategy: from: kind: ImageStreamTag name: java:8 namespace: openshift incremental: true # この行を追加する (...)
エディタが起動しますので、上記の箇所の「from:」と同じ階層に「incremental: true」を追加して保存して下さい。ついでにJavaのソースファイルも編集して、メッセージを少し変えてみましょう。
$ vi src/main/java/com/example/jetty/service/HelloWorldService.java (...) @Value("${name:World!!!}") // "!!!"を追加する (...)
それではビルドを開始します。今度は大量のMavenアーティファクトのダウンロードがログに出ないことを確認できるはずです。
$ oc start-build --from-dir=. --follow $ curl http://builder-my-testjava.192.168.42.254.nip.io/ Hello World!!!
メッセージの結果も変っており、新しいソースコードが使われたことが分ります。
S2Iビルドのカスタマイズ
S2Iビルドの秘密は、ビルダーイメージに埋め込まれたいくつかのスクリプトにあります。まずはそのスクリプトがどこにあるかを、ImageStreamTagの出力から確認してみましょう。
$ 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からファイルをコピーし、ローカルのマシンでゆっくり確認するのがいいでしょう。
$ 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に委譲しています。
$ 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で作成されたリソースを削除しておきます(そのままでも特に問題はありません)。
$ oc delete all -l app=builder
「oc new-build --name builder」で作成された「builder」のBuildConfigやImageStreamはまだ残っているはずです。そのイメージから必要なファイルを抜き取る形で、新しい「runtime」イメージを作ります。
コマンドは少し複雑ですが、一度のoc new-buildで済みます。
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を使ってデプロイしてみましょう。
$ 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:」の配列を見れば分かります。
$ 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では使えません。詳しい設定に関しては、下記のドキュメントをご覧下さい。