GoのカスタムS2Iイメージを作ってS2Iビルドしてみる

OpenShiftのSource to Imageの理解のため、GoのカスタムS2Iイメージを作成し、Minishiftで動かしてみたメモ。

Minishiftを動かすまでは以下を参考。

- [macOSでMinishiftを起動してサンプルアプリケーションをデプロイ](https://qiita.com/sotoiwa/items/0158436a0e2090ab73f3)

|コンポーネント|バージョン|
|---|---|
|Minishift|v1.34.1+c2ff9cb|
|OpenShift|v3.11.0|
|s2i|v1.1.14-dirty|

## 準備

Minishiftを起動しておく。

```shell
minishift start
```

s2iコマンドをインストールしておく。

```shell
brew install source-to-image
```

## S2Iビルダーイメージの作成

`s2i`コマンドで雛形を作成する。

```shell
s2i create custom-go-s2i custom-go-s2i
```

作成されたファイルを確認。

```shell-session
$ tree -a custom-go-s2i
custom-go-s2i
├── Dockerfile
├── Makefile
├── README.md
├── s2i
│   └── bin
│       ├── assemble
│       ├── run
│       ├── save-artifacts
│       └── usage
└── test
    ├── run
    └── test-app
        └── index.html

4 directories, 9 files
$
```

`assemble`、`run`を編集する。`usage`はそのまま使う。`save-artifacts`は今回は使わないので削除する。

|ファイル|説明|
|---|---|
|assemble|ソースからアプリをビルドして適切な場所に配置する。|
|run|DockerfileのCMDとして実行される。|
|save-artifacts|キャッシュを使ってビルドを早くしたいときに使う。|
|usage|イメージの使い方の説明を書く。|

```shell:assemble
#!/bin/bash -e

# If the 'custom-go-s2i' assemble script is executed with the '-h' flag, print the usage.
if [[ "$1" == "-h" ]]; then
	exec /usr/libexec/s2i/usage
fi

echo "---> Installing application source..."
cp -Rf /tmp/src/. /opt/app-root/

echo "---> Building application from source..."
XDG_CACHE_HOME=/tmp/.cache go build app.go
```

```shell:run
#!/bin/bash -e

exec /opt/app-root/app
```

```shell:usage
#!/bin/bash -e
cat <<EOF
This is the custom-go-s2i S2I image:
To use it, install S2I: https://github.com/openshift/source-to-image

Sample invocation:

s2i build <source code path/URL> custom-go-s2i <application image>

You can then run the resulting image via:
docker run <application image>
EOF
```

Dockerfileを編集する。ベースイメージは[Red Hat Container Catalog](https://access.redhat.com/containers/)から取得するのが望ましいと思われるが、今回はDockerHubのものを使う。

```Dockerfile:Dockerfile
FROM golang:1.12

LABEL maintainer="Your Name <your@email.com>"

LABEL io.k8s.description="A Go S2I builder image" \
      io.k8s.display-name="Go S2I builder image" \
      io.openshift.expose-services="8080:http" \
      io.openshift.s2i.scripts-ulr="image:///usr/libexec/s2i" \
      io.openshift.tags="builder, go, golang"

ENV APP_ROOT /opt/app-root

COPY ./s2i/bin/ /usr/libexec/s2i

RUN mkdir -p ${APP_ROOT} && \
    chown -R 1001:1001 ${APP_ROOT} && \
    chgrp -R 0 ${APP_ROOT} && \
    chmod -R g=u ${APP_ROOT}

USER 1001

EXPOSE 8080

WORKDIR ${APP_ROOT}}

CMD ["/usr/libexec/s2i/usage"]
```

ビルドする。

```shell
cd custom-go-s2i
docker build -t custom-go-s2i:1.12 .
```

```shell-session
$ docker build -t custom-go-s2i:1.12 .
Sending build context to Docker daemon  22.53kB
Step 1/10 : FROM golang:1.12
 ---> 80bb9c6de3f2
Step 2/10 : LABEL maintainer="Your Name <your@email.com>"
 ---> Using cache
 ---> 613e6931e678
Step 3/10 : LABEL io.k8s.description="A Go S2I builder image"       io.k8s.display-name="Go S2I builder image"       io.openshift.expose-services="8080:http"       io.openshift.s2i.scripts-url="image:///usr/libexec/s2i"       io.openshift.tags="builder, go, golang"
 ---> Using cache
 ---> 5abe1287a367
Step 4/10 : ENV APP_ROOT /opt/app-root
 ---> Using cache
 ---> 03d0dac2e9d6
Step 5/10 : COPY ./s2i/bin/ /usr/libexec/s2i
 ---> Using cache
 ---> 4d0267aa8319
Step 6/10 : RUN mkdir -p ${APP_ROOT} &&     chown -R 1001:1001 ${APP_ROOT} &&     chgrp -R 0 ${APP_ROOT} &&     chmod -R g=u ${APP_ROOT}
 ---> Using cache
 ---> 21c9c19cb0ad
Step 7/10 : USER 1001
 ---> Using cache
 ---> 3364c5ab36b3
Step 8/10 : EXPOSE 8080
 ---> Using cache
 ---> c07ab7cf6395
Step 9/10 : WORKDIR ${APP_ROOT}
 ---> Using cache
 ---> 8fe5f15618e5
Step 10/10 : CMD ["/usr/libexec/s2i/usage"]
 ---> Using cache
 ---> e41359221ed9
Successfully built e41359221ed9
Successfully tagged custom-go-s2i:1.12
$
```

```shell-session
$ docker images custom-go-s2i
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
custom-go-s2i       1.12                e41359221ed9        4 minutes ago       814MB
$
```

## ビルダーイメージの動作確認

GoのWebサーバーのコードを書く。

```go:app.go
package main

import (
	"fmt"
	"net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "Hello World")
}

func main() {
	http.HandleFunc("/", handler)
	http.ListenAndServe(":8080", nil)
}
```

ローカルでビルダーイメージとコードを統合してイメージを作成してみる。

```shell
s2i build test/test-app/ custom-go-s2i:1.12 custom-go-app
```

```shell-session
$ s2i build test/test-app/ custom-go-s2i:1.12 custom-go-app
---> Installing application source...
---> Building application from source...
Build completed successfully
$
```

作成されたイメージを確認。

```shell-session
$ docker images custom-go-app
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
custom-go-app       latest              ef6d1ee144fd        9 seconds ago       821MB
$
```

ローカルで実行してアクセス確認。OpenShiftでは適当なUIDが割り振られて実行されるため、適当なUIDで実行する。

```shell-session
$ docker run --rm -it -d --name go-test -u 1234 -p 8080:8080 custom-go-app
4370e075cdd1b448f0b23afb6b2436e5ca396db521aa59d9fcbb9904d6481cce
$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
4370e075cdd1        custom-go-app       "/usr/libexec/s2i/run"   8 seconds ago       Up 7 seconds        0.0.0.0:8080->8080/tcp   go-test
$ curl http://localhost:8080
Hello World
$
```

## Minishiftへの登録

Minishiftの`oc`コマンドへパスを通す。

```shell
eval $(minishift oc-env)
```

`developer`でログインしていることを確認。ログインしていなければログイン。

```shell
oc whoami
oc login -u developer
```

プロジェクトを作成。

```shell
oc new-project custom-go
```

Minishiftでは内部レジストリーが公開されていないが、その場合にイメージを登録する方法は以下の記事が分かりやすい。Minishift内部のDockerエンジンからなら内部レジストリーに接続できる。

- https://thinkit.co.jp/article/15696

一旦イメージをファイルに保存する。

```shell
docker save custom-go-s2i:1.12 -o custom-go-s2i.tar
```

`docker`コマンドの向き先をMinishift内のDockerエンジンに向ける。

```shell
eval $(minishift docker-env)
```

ファイルに保存しておいたイメージをロード。

```shell
docker load -i custom-go-s2i.tar
```

タグ付けしてMinishiftの内部レジストリーにプッシュ。

```shell
docker tag custom-go-s2i:1.12 $(minishift openshift registry)/custom-go/custom-go-s2i:1.12
docker login -u $(oc whoami) -p $(oc whoami -t) $(minishift openshift registry)
docker push $(minishift openshift registry)/custom-go/custom-go-s2i:1.12
```

ImageStreamが登録されたことを確認する。

```shell-session
$ oc get is
NAME            DOCKER REPO                               TAGS      UPDATED
custom-go-s2i   172.30.1.1:5000/custom-go/custom-go-s2i   1.12      18 seconds ago
$
```

`docker`コマンドの向き先を戻しておく。

```shell
unset DOCKER_TLS_VERIFY
unset DOCKER_HOST
unset DOCKER_CERT_PATH
```

## S2Iでのアプリのデプロイ

GoのWebアプリのソースコードのGitHubリポジトリーを作成する。

- https://github.com/sotoiwa/go-sample

アプリをデプロイ。

```shell:コマンド
oc new-app --name=hello custom-go-s2i:1.12~https://github.com/sotoiwa/go-sample
```

```shell-session:実行ログ
$ oc new-app --name=hello custom-go-s2i:1.12~https://github.com/sotoiwa/go-sample
--> Found image e413592 (About an hour old) in image stream "custom-go/custom-go-s2i" under tag "1.12" for "custom-go-s2i:1.12"

    Go S2I builder image
    --------------------
    A Go S2I builder image

    Tags: builder,  go,  golang

    * A source build using source code from https://github.com/sotoiwa/go-sample will be created
      * The resulting image will be pushed to image stream tag "hello:latest"
      * Use 'start-build' to trigger a new build
    * This image will be deployed in deployment config "hello"
    * Port 8080/tcp will be load balanced by service "hello"
      * Other containers can access this service through the hostname "hello"

--> Creating resources ...
    imagestream.image.openshift.io "hello" created
    buildconfig.build.openshift.io "hello" created
    deploymentconfig.apps.openshift.io "hello" created
    service "hello" created
--> Success
    Build scheduled, use 'oc logs -f bc/hello' to track its progress.
    Application is not exposed. You can expose services to the outside world by executing one or more of the commands below:
     'oc expose svc/hello'
    Run 'oc status' to view your app.
$
```

アプリが動き出したことを確認。

```shell_session
$ oc get po
NAME            READY     STATUS      RESTARTS   AGE
hello-1-build   0/1       Completed   0          1m
hello-1-xfkzb   1/1       Running     0          56s
$
```

アプリを外部公開。

```shell
oc expose svc hello --hostname hello.example.com
```

アクセス確認。

```shell
curl -H "Host: hello.example.com" http://$(minishift ip)
```

```shell-session
$ curl -H "Host: hello.example.com" http://$(minishift ip)
Hello World!!
$
```