OpenShift v3 と source-to-image (s2i)

### はじめに

S2Iは、アプリケーションのソースコードからDockerイメージを生成するためのフレームワークで、OpenShift v3のコンポーネントの一つとして、組み込まれていています。OpenShift v3では、面倒なJSONファイルを書かなくても、`oc new-app <GIT_REPO>`コマンドを実行するだけで、Dockerイメージを生成し、アプリケーション(Dockerコンテナ)をデプロイすることができるのですが、そのプロセスの中心は主にS2Iです。本エントリーでは、そのS2Iのビルドフローを追って、理解を深めようと思います。

### 基本用語

#### S2I(Source to Image)

S2I(Source to Image)は、アプリケーションソースコードをinputにし、アプリケーションのビルドと実行環境をアセンブルして、新しいDockerイメージを生成するためのフレームワークです。

#### builderイメージ

アプリケーションのビルドと、実行環境をアセンブルするDockerイメージです。このイメージがビルド環境だけでなく、実行環境にもなることがポイントです。アプリケーション実行時に使うアプリケーションサーバーのインストールやユーザーの指定もこのイメージのDockerfileに定義されています。

#### s2i スクリプト

builderイメージの実行コンテナ内で実行されるスクリプト群で、`run`,`assemble`,`save-artifacts`といったスクリプトがあります。主にビルドを行うスクリプト「`assemble`」と、アプリケーションやアプリをデプロイしたミドルウェアを起動するスクリプト「`run`」の2つは、builderイメージの実行時に必須のスクリプトです。


### クイックスタート

S2Iは、s2iコマンドを使った単体での実行か、OpenShift v3に組み込まれているS2Iを使うことが可能です。両方見ていきましょう。

- S2I 単体の場合 (注: s2iコマンドはOpenShiftには含まれていません。)

~~~
$ s2i build git://github.com/pmorie/simple-ruby openshift/ruby-20-centos7 test-ruby-app
$ docker run --rm -i -p :8080 -t test-ruby-app　# S2Iビルドしたコンテナの実行
~~~

- OpenShift v3 で S2Iビルドを実行する場合

~~~
$ oc new-app git://github.com/pmorie/simple-ruby
$ oc get pod  # ビルドしたコンテナ(Pod)の確認
NAME                  READY     STATUS      RESTARTS   AGE
simple-ruby-1-build   0/1       Completed   0          1h
simple-ruby-1-k0e7s   1/1       Running     0          1h
~~~

`git://github.com/pmorie/simple-ruby`がアプリケーションのソースコードで、`openshift/ruby-20-centos7`がbuilderイメージです。後者のOpenShift v3でS2Iビルドを実行する場合には、builderイメージを指定していないことが分かります。これは、OpenShift v3の機能で、アプリケーションの言語を自動検知して、builderイメージを選択してくれているのです。アプリケーションの言語検知機能については、[こちらの](https://docs.openshift.com/enterprise/3.1/dev_guide/new_app.html#language-detection)公式ドキュメントを確認するとして、このbuilderイメージが、OpenShift v3の重要な機能(リソース)の一つ、*ImageStream*でポイントされたイメージから選択されていることが、とてもとても重要な点になります。この*ImageStream*は、S2IでなくOpenShift v3の別機能になるので、ImageStreamについては別の機会に説明するとして、今回はOpenShift上でのS2Iのビルドフローを見ていきましょう。

### [補足] oc new-app 実行時のフローについて

`oc new-app` の実行について正確に説明すると、S2Iビルドプロセスが走っているだけではありません。全体としては、以下の図のリソースとビルドプロセスが生成されています。今回のS2Iビルドは、ちょうど赤丸で囲っている部分になります。

![helloworld-process-updated.png](https://qiita-image-store.s3.amazonaws.com/0/90143/cb8d66a3-b6ee-3902-3321-099a23da9617.png)



### S2I のビルドフロー

S2Iの基本フローは、公式マニュアルにフロー図があります。これと、先ほど見た、`ruby-20-centos7`builderイメージの[Dockerfile](https://github.com/openshift/sti-ruby/blob/f36084f4d10847c1befb4eec089da7a06c7f949f/2.0/Dockerfile)や`Wildfly` builderイメージ[Dockerfile](https://github.com/openshift/sti-wildfly/tree/6394c2cd3fae23b4f0822075e2d3b4825b2592bb/9.0)の例を見ながら流れを追っていきましょう。

![sti-flow.png](https://qiita-image-store.s3.amazonaws.com/0/90143/833b8aeb-f182-a398-607b-ef5085c4c636.png)


#### 1. s2iスクリプトのダウンロード

s2iスクリプトは、1. `--scripts-url`で指定したディレクトリ、2. `.sti/bin`ディレクトリ、3. label `io.openshift.s2i.scripts-url`で指定されたディレクトリ、の順番で存在しているディレクトリからダウンロードされます。[sti-ruby](https://github.com/openshift/sti-ruby/tree/f36084f4d10847c1befb4eec089da7a06c7f949f/2.0)には、`.sti/bin`も `io.openshift.s2i.scripts-url`の設定もないのですが、これは、ベースとなっているDockerイメージsti-baseの[Dockerfile](https://github.com/openshift/sti-base/blob/67415e30c6e2d05372b00767b43afdd37f813375/Dockerfile#L8)にラベルが指定されているためです。ラベルで指定したディレクトリには、s2iスクリプトがbuilderイメージ内で[コピー](https://github.com/openshift/sti-ruby/blob/f36084f4d10847c1befb4eec089da7a06c7f949f/2.0/Dockerfile#L22)されています。

(sti-baseのDockerfile)

~~~
LABEL io.openshift.s2i.scripts-url=image:///usr/libexec/s2i
...
ENV STI_SCRIPTS_PATH=/usr/libexec/s2i
~~~

(sti-rubyのDockerfile)

~~~
COPY ./s2i/bin/ $STI_SCRIPTS_PATH
~~~


ちなみに、`--scripts-url`は、OpenShiftで指定できないです！といった心配も要りません。指定しようと思えば、[BuildConfigに指定](https://docs.openshift.com/enterprise/3.1/dev_guide/builds.html#override-builder-image-scripts)することも可能です。なので、s2iスクリプトを差し替えて、ビルドや実行コマンドをカスタマイズすることも可能です。

#### 2. incremental build による前回生成物の復元

`incremental build`は前回ビルドした時の中間生成物を次のビルドでも利用するための機能です。例えば、Javaアプリケーションをmavenビルドする場合、`.m2`以下に毎回jarファイルをダウンロードしていると時間が掛かるので、前回ビルド時に使ったjarファイルを再利用しようというわけです。`incremental build`の利用は任意ですが、利用する場合には、以下の4点が必要になります。

1. s2iスクリプトにsave-artifactsスクリプトが配置されている
2. s2iスクリプトのassembleスクリプトに保存していた中間生成物(上述mavenビルドの例で言うとjarファイル)を復元するロジックが実装されている
3. ユーザーがs2iビルド実行時に、BuildConfigで`incremental`をtrueに[指定する](https://docs.openshift.com/enterprise/3.1/dev_guide/builds.html#incremental-builds)
4. 過去にs2iビルドされたDockerイメージがOpenShiftのIntegerated Dockerレジストリに存在している

1と2に関しては、実際のスクリプトを見た方が早いでしょう。sti-wildflyのbuilderイメージの[assemble](https://github.com/openshift/sti-wildfly/blob/6394c2cd3fae23b4f0822075e2d3b4825b2592bb/9.0/s2i/bin/assemble#L10-L15)と[save-artifacts](https://github.com/openshift/sti-wildfly/blob/6394c2cd3fae23b4f0822075e2d3b4825b2592bb/9.0/s2i/bin/save-artifacts)で`.m2`を復元している実装例を参考にしてください。

ちなみに上のフロー図で、「Save artifacts」が来る順番が早いのでは？と思われる方もいらっしゃるかもしれません。ですが、この「Save artifacts」というのは、`save-artifacts` スクリプトが実行されるタイミングで間違いありません。`save-artifacts`の仕組みを説明しておくと、例えば、上のWildflyのsave-artifactsスクリプトには `tar cf - ./.m2` と書かれていて、何やらtarアーカイブ生成しているようです。これは、S2Iのプログラムが、前回ビルドしたイメージからコンテナを立ち上げて、`tar cf - ./.m2`で`.m2`以下のディレクトリをtarアーカイブにして標準出力に流し込んでいるのです。これを新builderコンテナが標準入力で[受け取って](https://github.com/openshift/source-to-image/blob/2b6410bd573413b427b18c6ba56bc8dc72d91c52/pkg/docker/docker.go#L408-L409)、展開するのです。ちなみに、上の4.過去のDockerイメージが存在しない場合には、incremental buildは自動的にスキップされる実装になっています。
余談になりますが、このS2Iの **tar流し** (と私が勝手に呼んでいる)実装は、個人的にOpenShift v3のコンポーネントで1,2を争う面白いアイディアだと思っています。

#### 3. アプリケーションソースコードのダウンロードとbuilderイメージへの展開

ダウンロードしたアプリケーションのソースコードは、一度tarに固められた後に、builderイメージに "tar流し" で渡されます。渡される先は、biulderイメージの`io.openshift.s2i.destination`に指定された場所+`/src`か、指定がなければデフォルトで`/tmp/`+`src`になります。ちなみに、上のsave-artifactsのtar流しされた中間生成物は、`io.openshift.s2i.destinationの指定`+`artifacts`か、こちらも指定がなければデフォルトで`/tmp/`+`artifacts`になります。

#### 4. assemble の実行

いよいよbuildになりますが、この先は`assemble`スクリプトで各自実装されたビルドスクリプトでビルドし、builderイメージのDockerfileで指定していた実行基盤のアプリケーションのデプロイディレクトリにコピーしたりすれば良いだけです。各言語やデプロイ先のアプリケーションサーバー等で実装が異なるので、説明は省きます。

#### 5. image の push

ビルドが終われば、OpenShiftのIntegrated Docker reigistryにイメージがpushされます。S2Iでビルドされたコンテナは、s2iスクリプトの`run`スクリプトで起動されます。

~~~
# docker inspect c221da86319d
...
        "Cmd": [
            "/usr/libexec/s2i/run"
        ],
~~~


### おわり

`oc new-app <GIT_REPO>`の裏で、S2Iが頑張ってくれていることや、s2iスクリプトの差し替えや`incremental build`といったオプション機能が活用できることが分かって頂けたかと思います。ただ、S2Iなくても、自分でソースコードをビルドするスクリプト書いてDockerfile書けば十分じゃん！と思われた方もいるかもしれません。
はい、OpenShiftでも、`oc new-app http://<URL_TO_DOCKERFILE>`で、Dockerfileからのビルドもできますし、S2Iビルドに似たツールもある程度は実現可能でしょう。ですが、OpenShiftでは、クイックスタートで触れた**ImageStream**や多様なビルドオプションを使って、builderイメージとソースコードのアセンブルに大きな利益をもたらしています。ImageStreamの話については、また別の機会に記事を投稿したいと思います。