S2I (Source to Image)解説 - 3. S2Iの機能、S2I 増分ビルド (S2I incremental build)を試してみる

S2I増分ビルド (incremental build)とはS2Iの機能の一つであり，前回のビルドとの差分を読み取ることで，ビルドプロセスを短縮する機能である．ここではgo言語を用いて，S2I 増分ビルドを実行する．

## S2I build workflow
まず，S2Iのビルドワークフローについて確認する．S2I buildを実行すると，次のようなワークフローに従って，コンテナイメージがビルドされる．：
![sti-flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2781216/f1d729ec-4938-1bb8-8ac0-baee41f3e83f.png)
この記事では，図の赤い四角で囲われたS2I増分ビルドプロセスについて，詳しく解説を行う．

## golangを例にしたS2I増分ビルドの実行
<!-- ## Mechanisms of S2I incremental build -->
まず，S2I 増分ビルドを行うまでの手順をコマンドを示して解説する．その後，`S2I build`を実行した後にどのようなメカニズムで増分ビルドのワークフローが実行されるかを図示しながら説明していく．また，S2I増分ビルドを実行するためのS2I scriptについては，次節で説明する．

### S2I増分ビルドを実行するまで
まず，s2i builder imageの作成を行う．前回pythonの環境用のコンテナイメージを構築したように，コンテナイメージのビルドを行う．

1. まず，コンテナイメージを作成するためのディレクトリ `s2i-golang-incremental`を作成し，以下のように用意する：
```bash
s2i-golang-incremental/
├── builder
│   ├── Dockerfile
│   └── s2i
│       └── bin
│           ├── assemble
│           ├── run
│           ├── save-artifacts
│           └── usage
└── test-app
    ├── app.go
    ├── go.mod
    └── go.sum
```
ここで，ディレクトリ `builder`と`test-app`はそれぞれS2I builder imageをビルドする用，ソースコード用のディレクトリである．`builder`には`Dockerfile`と`S2I script`が置いてある．これらのソースコードを以下に示す：

```Dockerfile
# ベースイメージ
FROM openshift/base-centos7

# ラベルを指定
LABEL io.openshift.s2i.scripts-url="image:///usr/libexec/s2i/bin"

# Goのバージョンとインストール先
ARG GO_VERSION=1.19 
ARG GO_INSTALL_DIR=/usr/local/

# 環境変数
ENV APP_ROOT=/opt/app-root
ENV BUILDER_VERSION 1.0
ENV GOPATH ${APP_ROOT}/src/go
ENV PATH=${PATH}:${GOPATH}/bin:${GO_INSTALL_DIR}/go/bin

# Goのインストール
RUN curl -sSL https://dl.google.com/go/go${GO_VERSION}.linux-amd64.tar.gz -o go${GO_VERSION}.linux-amd64.tar.gz && \
    tar -C ${GO_INSTALL_DIR} -xzf go${GO_VERSION}.linux-amd64.tar.gz && \
    rm -f go${GO_VERSION}.linux-amd64.tar.gz && \
    mkdir -p ${GOPATH}

# s2i scriptsをコンテナへコピー
COPY s2i /usr/libexec/s2i

# パーミッションを設定
RUN chown -R 1001:1001 ${APP_ROOT} && \
    chown -R 1001:1001 /usr/libexec/s2i

# default user
USER 1001

# default CMD for the image
CMD ["/usr/libexec/s2i/usage"]
```


```bash:assemble.sh
#!/bin/bash -e
# usageスクリプトを実行
if [[ "$1" == "-h" ]]; then
	exec /usr/libexec/s2i/usage
fi

# S2I増分ビルドを行うためのスクリプト
echo
echo "---> Checking for cache..."
if [ "$(ls /tmp/artifacts 2>/dev/null)" ]; then
  pushd /tmp/artifacts >/dev/null
  echo "-----> Pulling cache..."
  shopt -s dotglob
  if [ -d src ]; then
    echo "Restoring cache ${GOPATH}/src/..."
    mv src ${GOPATH}/src
  fi
  if [ -d pkg ]; then
    echo "Restoring cache ${GOPATH}/pkg/..."
    mv pkg ${GOPATH}/pkg 
  fi
  shopt -u dotglob
  popd >/dev/null
fi

# Goソースファイルのビルド
echo
echo "---> Building application from source..."
pushd /tmp/src/ >/dev/null
go build -o ${APP_ROOT}/bin/app
popd >/dev/null
```


```bash:run.sh
${APP_ROOT}/bin/app
```

```bash:save-artifacts.sh
pushd ${GOPATH} >/dev/null
if [ -d src ]; then
    chmod -R +w src
    tar cf - src
fi
if [ -d pkg ]; then
    chmod -R +w pkg
    tar cf - pkg
fi
popd >/dev/null
```
上に示したコードの解説は，[s2i-scriptsの解説](#s2i-scriptsの解説)で行う．また，`test-app`には，今回使用するgolangのサンプルファイルが置いてある．ソースコードを以下に示す：
```go:main.go
package main

import (
	"fmt"

	_ "github.com/golang/mock/gomock"
	_ "github.com/gorilla/mux"
)

func main() {
	greeting := fmt.Sprintf("Hello, %s", "World")
	fmt.Println(greeting)
}
```

2.Dockerfileとs2iスクリプトのあるディレクトリで以下を実行
現在，`s2i-golang`というディレクトリにいるとする．そこで，以下のコマンドでs2i builder imageをビルドする：
```bash
$ docker build -t s2i-golang  s2i-golang/builder
```
このコマンド実行は，`s2i-golang/builder`のディレクトリ内の`s2i script`と`Dockerfile`を用いて，`s2i-golang`というs2i builder imageをビルドする．


3.1回目のs2iビルドを実行する.
まず，増分を行う前のコンテナイメージをビルドする．
```bash
$ s2i build --copy test-app/ s2i-golang my-go-app
```
- `--copy` : githubなどのリポジトリの代わりにローカルのディレクトリからアプリケーションソースコードをコンテナへコピーするためのオプションである．

4.goのライブラリを追加し，S2i増分ビルドを行う．
まず，以下のように`main.go`のライブラリ部分を書き加え，
```golang:main.go
package main

import (
	"fmt"

	_ "github.com/golang/mock/gomock"
	_ "github.com/gorilla/mux"
	_ "github.com/joho/godotenv"
	_ "github.com/labstack/echo/v4"
	_ "github.com/sirupsen/logrus"
	_ "gorm.io/driver/mysql"
)

func main() {
	greeting := fmt.Sprintf("Hello, %s", "James")
	fmt.Println(greeting)
}
```

```bash
$ go mod tidy
```
を実行することで，`go.mod`, `go.sum`が更新され，ライブラリ情報が追加される．その後，S2I incremental buildを実行する：
```bash
$ s2i build --rm --copy test-app/ s2i-golang my-go-app --incremental=true
```
このコマンドは，`my-go-app`というコンテナイメージを増分ビルドしている．コマンドの引数は以下：
- `--rm` : 増分前のコンテナイメージを削除する
- `--incremental=true` : 増分ビルドを行うためのオプションである



### S2I増分ビルドのメカニズム
S2I incremental buildを実行すると，以下のプロセスが実行される：
1. S2Iがローカルに存在する前回作成されたコンテナイメージ (`docker images`で見ることができるコンテナイメージ)を探索

2. S2Iが前回ビルドしたイメージからコンテナを立ち上げる


3. 立ち上げたコンテナ内のライブラリ (それぞれの言語で作られたバイナリのこと) を抽出し, tar archiveにまとめ、標準出力に流す


4. 標準入力として新しいコンテナイメージがtar archiveを受け取り、S2I がtarを展開する

5. そして，assembleが実行され，新しいコンテナイメージがビルドされる

![S2I-incremental.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2781216/a92de979-5b2d-560f-ceaf-186c0c2055db.png)


<!-- ## S2I Scripts For S2I Incremental Build -->
## s2i-scriptsの解説
ここでは，S2I scriptに記述された内容について，詳細を説明する．
### assemble script
```bash:assemble.sh
#!/bin/bash -e
# usageスクリプトを実行
if [[ "$1" == "-h" ]]; then
	exec /usr/libexec/s2i/usage
fi

# S2I増分ビルドを行うためのスクリプト
echo
echo "---> Checking for cache..."
if [ "$(ls /tmp/artifacts 2>/dev/null)" ]; then
  pushd /tmp/artifacts >/dev/null
  echo "-----> Pulling cache..."
  shopt -s dotglob
  if [ -d src ]; then
    echo "Restoring cache ${GOPATH}/src/..."
    mv src ${GOPATH}/src
  fi
  if [ -d pkg ]; then
    echo "Restoring cache ${GOPATH}/pkg/..."
    mv pkg ${GOPATH}/pkg 
  fi
  shopt -u dotglob
  popd >/dev/null
fi

# Goソースファイルのビルド
echo
echo "---> Building application from source..."
pushd /tmp/src/ >/dev/null
go build -o ${APP_ROOT}/bin/app
popd >/dev/null
```

スクリプトの中身を順にみていく．
```bash
echo "---> Checking for cache..."
if [ "$(ls /tmp/artifacts 2>/dev/null)" ]; then
  pushd /tmp/artifacts >/dev/null
  echo "-----> Pulling cache..."
  shopt -s dotglob
```
`/tmp/artifacts/`ディレクトリが存在するかを確認。もし存在する場合は、`pushd /tmp/artifacts >/dev/null`で`/tmp/artifacts/`ディレクトリに移動する。`pushd`コマンドを使うと、移動先の履歴をスタックに保存したままディレクトリに移動できる。`>/dev/null`は出力の破棄を行うリダイレクトである。`shopt -s dotglob`で`dotglob`オプションを有効にする。
`shopt` コマンドはシェルのオプションを変更することができるコマンドで、-s でどのオプションを変更するかを決める．`dotglob`オプションを有効にすると，ファイル名展開時に`.`から始まるファイルも含めることができる．

```bash
  if [ -d src ]; then
    echo "Restoring cache ${GOPATH}/src/..."
    mv src ${GOPATH}/src
  fi
```
`src`ファイルが存在するならば、そのとき、`src`ディレクトリを`${GOPATH}/src`へ移動する。


```bash
  if [ -d pkg ]; then
    echo "Restoring cache ${GOPATH}/pkg/..."
    mv pkg ${GOPATH}/pkg 
  fi
  shopt -u dotglob
  popd >/dev/null
fi
```
`pkg`ディレクトリが存在するならば`${GOPATH}/pkg` へ移動する。`shopt -u dotglob`で`dotglob`オプションを無効にする。`pushd`でスタックに保存したディレクトリの履歴`/tmp/artifacts/`へ移動する。


```bash
echo
echo "---> Building application from source..."
pushd /tmp/src/ >/dev/null
go build -o ${APP_ROOT}/bin/app
popd >/dev/null
```

`/tmp/src`ディレクトリに履歴をスタックにして残しながら、移動。`${APP_ROOT}/bin/app`にディレクトリに対して，`go build`を実行する。`popd`でスタックに保存していたディレクトリ先に戻る。


### save-artifactスクリプト
`save-artifacts` スクリプトはS2I増分ビルドを行うためには，必須のスクリプトになる．このスクリプトは増分前の情報をキャッシュしておく役目をする．スクリプトの中身を詳しく見ていく．
```bash::save-artifacts.sh
pushd ${GOPATH} >/dev/null
if [ -d src ]; then
    chmod -R +w src
    tar cf - src
fi
if [ -d pkg ]; then
    chmod -R +w pkg
    tar cf - pkg
fi
popd >/dev/null
```

コマンドを解説する：
```bash
chmod -R +w src
tar cf - src
```
`chmod`コマンドと`tar`コマンドによる操作は
- chmod : ファイルやディレクトリのパーミッション (アクセス権限) を変更するコマンドである．
ここでは，`src`ディレクトリ内のすべてのファイルとサブディレクトリへの書き込み権限を追加している。オプション`-R`はディレクトリ内のすべてのファイルとサブディレクトリに対して再帰的 (Recursive)[^1]に操作を行う．また，オプション`+w`は`src`へ書き込み権限を追加する．
[^1]: ここで，再帰的とは，`dir1/dir2/dir3/・・・`などのように階層型になっているディレクトリに対して，`dir1`に対して，`dir1/dir2`に対して，・・・とまとめて操作を行うことを意味する．

- tar : ファイルやディレクトリをアーカイブ（まとめてひとつのファイルにすること）するために使用される．
ここでは`src`ディレクトリを`tar`アーカイブにパッキングし、そのアーカイブを標準出力に書き出す操作をしている。ここで，`cf`は、アーカイブを作成するためのオプションであり、`-`はアーカイブを標準出力に書き出すことを意味する。



## S2i増分ビルド中のログの確認

### ログレベルオプション
`s2i build`コマンドでは，`--lolevel`というオプションを使うことでビルドプロセスのログを詳しくみることができる．コマンドは以下のように実行すればよい．
```bash
$ s2i < 他のオプション >  --loglevel=5 &> /tmp/s2i.log
```
ログレベルはレベル0から5まである：
- Level `0` - `assemble` と `assemble-runtime`スクリプトを実行しているコンテナからの出力と遭遇したエラーすべてを表示する．
- Level `1` - 実行されたプロセスに関して標準的な情報を表示する
- Level `2` - 実行されたプロセスに関して非常に詳細な情報を表示する．
- Level `3` - 実行されたプロセスに関して非常に詳細な情報を、また，tar の内容も表示する。
- Level `4` - 現在はlevel `3`と同じ情報が表示される．
- Level `5` - 実行されたプロセスに関して非常に詳細な情報，tar の内容Dockerレジストリの認証情報、コピーされたソースファイルを一覧表示する。


### ログ情報の観測
ビルドプロセスのログ情報を観測することで，S2Iがどのようにビルドを実行しているかや各オプションによって何が実行されるかがわかる：

まず，増分ビルドのオプション`--incremental`を実行した場合，なにが行われているかを見てみる：
まず、`my-go-app `というs2iコンテナイメージ（アプリケーションイメージ）がない場合に以下のコマンドでs2iビルドを実行する：
```bash
$ s2i build --rm --copy test-app/ s2i-golang my-go-app --incremental=true
```
`--incremental`は次のような
すると、以下のようなログが得られる：
```bash
docker.go:487] Image "my-go-app:latest" not available locally, pulling ...
docker.go:566] pulling image error : Error response from daemon: pull access denied for my-go-app, repository does not exist or may require 'docker login': denied: requested access to the resource is denied
sti.go:497] Unable to pull previously built image "my-go-app": unable to get my-go-app:latest
sti.go:215] Clean build will be performed
```
1. まず、イメージ`my-go-app`がローカルに存在しないので、それをpullしようとする。
2. このとき、イメージ`my-go-app`が存在しないため、**pulling image error**が発生した。
3. そのため、s2iがイメージ`my-go-app`をpullできないと判断する。
4. cleanな新しいビルドが実行される。

つまり、参照するためのイメージが存在しない場合、普通のs2iビルドが実行されることがわかる。

次に、コンテナイメージ`my-go-app`が存在する場合にs2i incremental buildを実行する：
```bash
$ s2i build --rm --copy test-app/ s2i-golang my-go-app --incremental=true
```
このとき、以下のようなログを得る：
```bash
docker.go:491] Using locally available image "my-go-app:latest"
sti.go:213] Existing image for tag my-go-app detected for incremental build
sti.go:218] Performing source build from test-app/
```

1. ローカルに存在するimage `my-go-app:latest`の使用を宣言
2. incremental buildを行うための使用可能なイメージ`my-go-app:latest`がローカルに存在することを特定。
3. `test-app/`からソースのビルドを実行する


```bash
sti.go:524] Saving build artifacts from image my-go-app to path /tmp/s2i2961405759/upload/artifacts
```
- イメージ`my-go-app`内のpath`/tmp/s2i2961405759/upload/artifacts`からbuild artifactsを保存する。

次に、artifactsを抽出するために、user1001でassembleを使用。s2iが前回ビルドしたイメージからコンテナを立ち上げ、artifactsからパッケージを抽出し、tarアーカイブにする。tarアーカイブを標準出力に流し込み、新しいビルドコンテナが標準入力として受け取り、展開する。
```bash
sti.go:550] Using assemble user "1001" to extract artifacts
docker.go:731] Image sha256:c3a02d781... contains io.openshift.s2i.scripts-url set to "image:///usr/libexec/s2i/bin"
docker.go:805] Base directory for S2I scripts is '/usr/libexec/s2i/bin'. Untarring destination is '/tmp'.
docker.go:971] Creating container with options {Name:"s2i_my_go_app_f4cb4822" Config:{Hostname: Domainname: User:1001  ...
docker.go:1003] Attaching to container ...
docker.go:1014] Starting container ...
```

この後はひたすら、creatingとextractingを繰り返し，ライブラリに関する情報を`tar`に集めていく：
```bash
tar.go:391] Creating directory /tmp/s2i3409979752/upload/artifacts/pkg
tar.go:391] Creating directory /tmp/s2i3409979752/upload/artifacts/pkg/mod
tar.go:391] Creating directory /tmp/s2i3409979752/upload/artifacts/pkg/mod/github.com
tar.go:391] Creating directory /tmp/s2i3409979752/upload/artifacts/pkg/mod/github.com/golang
tar.go:391] Creating directory /tmp/s2i3409979752/upload/artifacts/pkg/mod/github.com/golang/mock@v1.6.0
tar.go:391] Creating directory /tmp/s2i3409979752/upload/artifacts/pkg/mod/github.com/golang/mock@v1.6.0/ci
tar.go:400] Creating directory /tmp/s2i3409979752/upload/artifacts/pkg/mod/github.com/golang/mock@v1.6.0/ci
......
......
......
tar.go:424] Done extracting tar stream
```
そして，S2Iが増分前のビルドでダウンロードしたライブラリについては，キャッシュを使う．追加で加えたライブラリについては，新たにダウンロードしていることがわかる：
- 増分前のログ
```bash
sti.go:713] ---> Checking for cache...
sti.go:713] 
sti.go:713] ---> Building application from source...
sti.go:717] go: downloading github.com/golang/mock v1.6.0
sti.go:717] go: downloading github.com/gorilla/mux v1.8.0
docker.go:1070] Invoking PostExecute function
```

- 増分後のログ
```bash
sti.go:713] ---> Checking for cache...
sti.go:713] -----> Pulling cache...
sti.go:713] Restoring cache /opt/app-root/src/go/pkg/...
sti.go:713] 
sti.go:713] ---> Building application from source...
sti.go:717] go: downloading github.com/joho/godotenv v1.5.1
.....
.....
.....
sti.go:717] go: downloading golang.org/x/text v0.11.0
docker.go:1070] Invoking PostExecute function
```

今回で，S2Iに関する解説記事は終わりです。