jenkinsコンテナでmaster-slaveクラスタを作るPOC

###1. 概要

- dockerコンテナでmaster-slaveを作れるのか試したい
- サーバ間でjenkinsのmaster-slaveが作れるからコンテナ間でもできるはず！
- 結論から言うと、出来る。でも後述の通り別のjenkinsプラグインを使った方がスマートに感じる

###2. コンテナで作る単純なmaster-slave

- jenkinsの[docs](https://jenkins.io/doc/book/installing/)を読むとmasterコンテナは次のように簡単に起動できる

```
docker run \
  -u root \
  --rm \
  -d \
  -p 8080:8080 \
  -p 50000:50000 \
  -v jenkins-data:/var/jenkins_home \
  -v /var/run/docker.sock:/var/run/docker.sock \
  jenkinsci/blueocean
```

- `--name master`をこのコマンドオプションに追加して起動。
- このイメージは初めからdockerがインストールされている。ただしdockerのバージョンは最新ではない。
- このmasterにslaveを追加したいが、真面目にやろうとするとsshdの設定がめんどくさくなる(時間の浪費)。
- でも大丈夫。 [jenkinsci/ssh-slave](https://github.com/jenkinsci/docker-ssh-slave)という便利なイメージがdocker hub上にある。それ以外にも`jenkins/ssh-slave`もある。
- この`jenkinsci/ssh-slave`のreadmeの通りにslaveを起動させれば、masterから従来の方法で接続できる。

###3. Docker Pluginを使ったslaveのスポット起動

- 上述の方法は、常にslaveコンテナを起動させておかなければならない。
- swarmクラスタでmasterとslaveを同時に起動させておいて、、、とか考えるとリソースの無駄なのではないか。
- スポット起動という言い方が妥当かどうかは置いておいて、さっきのssh-slaveのreadmeをちゃんと読むと、Docker pluginでの使い方も書いてある。こっちの方が良さそう。なぜならジョブを実行するときだけslaveを起動させる、というスタイルなので。

####3-1. sshキーペア
- まずは、sshキーペアの作成です。masterコンテナがjob起動のたびに立ち上がるslaveに通信するためのsshキーペアを作成します。
- やり方としては、masterコンテナを起動させた後、そのコンテナにログインしてからsshキーペアを作成するか、ホストサーバで作成してからmasterコンテナにコピーするかどちらかです。私は後者を選択。

```bash
ssh-keygen -f jenkins
ls
# jenkins
# jenkins.pub
docker cp jenkins.pub master:/tmp/
docker cp jenkins master:/tmp/
```
- これでホスト上にあるキーをmasterコンテナのtmpディレクトリ直下に置いた。結構適当です。

####3-2. docker plugin インストールとイメージの登録
- 次にweb GUI上からmasterでdocker pluginをインストール
- [manage jenkins]>[configure system]>[cloud]という項目が出現するのでそこでdockerを選ぶ
- Nameは適当(test-docker)、Docker Host URIは`unix://<ホストサーバのipアドレス>`とした
- enabledにチェック
- `docker agent templates`をクリックして、使いたいイメージを登録する (ここらへんからだんだんめんどくさくなってくる、、、)
- labels ... slave01(適当)
- Enabledにチェック
- Docker Image ここでdocker hubやdocker trusted registry(DTR)から取得できるイメージ名を書く(`jenkinsci/ssh-slave`など)。
- ここで、自分でローカルにビルドしたdocker imageを登録しようとしたらできなかった。自分のやり方が悪いのかプラグインがまだそれに対応してないのか、、、わかりませんでした。
- 次に`container settings ...`というボタンを押す
- Environmentというインプットフィールドに、pubキーの中身をコピーしてから下記のように記入(これは`jenkinsci/ssh-slave`の要件に従ってるだけ)

```
JENKINS_SLAVE_SSH_PUBKEY=ssh-rsa....
```
- その後の手順
- Instance Capacity ...1(適当)
- Remote Filing System Root ... `/home/jenkins` (これはdockerfileでjenkins homeがどのように指定されているかに寄る)
- usage ... use this node as much as possible
- Connect method ... attach docker container
- Pull strategy .... pull once and update latest


###4. (応用編)slaveコンテナの中でdockerコマンド使いたい

- [jenkinsci/ssh-slave](https://github.com/jenkinsci/docker-ssh-slave)のDockerfileをハックする。
- といってもただ単にこのDockerfileにdockerインストール手順を紛れ込ませるだけです。
- 手順はdockerの公式手順からコピペする
- イメージのビルド
- docker hubにpush
- 具体的にはmasterコンテナが動いているホストサーバ上で下記のような手順を実施

```bash
git clone https://github.com/jenkinsci/docker-ssh-slave
cd docker-ssh-slave
vim Dockerfile
# ここでdockerのインストール手順も入れてしまう。
docker build -t <dockerhub-userid>/<name-whatever-youwant>:1.0 .
docker login  # if needed
docker push <dockerhub-userid>/<name-whatever-youwant>:1.0
```

- Dockerfileは結局こんな感じに。もう、なんというかコピペの嵐です。もっと整理できるとは思います。

```Go
FROM openjdk:8-jdk

ARG user=jenkins
ARG group=jenkins
ARG uid=1000
ARG gid=1000
ARG JENKINS_AGENT_HOME=/home/${user}

ENV JENKINS_AGENT_HOME ${JENKINS_AGENT_HOME}
ENV DEBIAN_FRONTEND noninteractive

RUN groupadd -g ${gid} ${group} \
    && useradd -d "${JENKINS_AGENT_HOME}" -u "${uid}" -g "${gid}" -m -s /bin/bash "${user}" \
    && apt-get update \
    && apt-get install -y git curl \
      lsb-release \
      apt-transport-https \
      ca-certificates \
      curl \
      gnupg2 \
      software-properties-common \
    && apt-get install --no-install-recommends -y openssh-server \
    && curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add - \
    && add-apt-repository \
       "deb [arch=amd64] https://download.docker.com/linux/debian \
       $(lsb_release -cs) \
       stable" \
    && apt-get update \
    && apt-get install -y docker-ce \
    && usermod -aG docker jenkins \
    && rm -rf /var/lib/apt/lists/*
RUN sed -i /etc/ssh/sshd_config \
        -e 's/#PermitRootLogin.*/PermitRootLogin no/' \
        -e 's/#RSAAuthentication.*/RSAAuthentication yes/'  \
        -e 's/#PasswordAuthentication.*/PasswordAuthentication no/' \
        -e 's/#SyslogFacility.*/SyslogFacility AUTH/' \
        -e 's/#LogLevel.*/LogLevel INFO/' && \
    mkdir /var/run/sshd

VOLUME "${JENKINS_AGENT_HOME}" "/tmp" "/run" "/var/run"
WORKDIR "${JENKINS_AGENT_HOME}"

COPY setup-sshd /usr/local/bin/setup-sshd

EXPOSE 22

ENTRYPOINT ["setup-sshd"]

```

- このイメージを先ほどと同様の手順で`docker agent templates`で登録する。今度はlabelを`slave02`などとしておく
- この時に注意すべきなのは、このslaveコンテナからdockerコマンドが実行されると言うことです。
- dockerをある程度知ってる人なら常識だと思うが、dockerコンテナの中でさらにdockerプロセスが起動する、と言うようなことは無理なのでエラーになる。
- このため、コンテナが持つdockerソケットにホストサーバのdockerソケットを接続させる必要がある。つまりコンテナでdockerコマンドが実行されたら、ホストサーバでそのコマンドを実行させる。この要件を実現するためにslaveコンテナを起動させるときに下記のようなオプションをつけなければならない。

```
-v /var/run/docker.sock:/var/run/docker.sock
```
- これを`docker agent templates`のどこで指定すれば良いのか、、、面倒だが細かく見るとなんとなくわかる。
- `container settings ...`というボタンを押したら`Volumes`というインプットフィールドが出てくるのでここに先ほどのオプション`/var/run/docker.sock:/var/run/docker.sock`を指定する
- それ以外は先ほどと同じ。

###5. (応用編)jenkins pipelineでslaveコンテナ経由で別のコンテナをpullしてそのコンテナの中で何らかのコマンドを実行する

- 言葉にすると非常にややこしいが、多分dockerを利用したジョブの設計ではこれが必要になることが多いと想定される。
- 例えば以下のようなjenkins pipelineを考えて見る。

```groovy
pipeline {
    agent none
    stages {
        stage('Back-end') {
            agent {
                docker { 
                    label 'slave02'
                    image 'maven:3-alpine' 
                }
            }
            steps {
                sh 'mvn --version'
            }
        }
        stage('Front-end') {
            agent {
                docker { 
                    label 'slave02'
                    image 'node:7-alpine' 
                }
            }
            steps {
                sh 'node --version'
            }
        }
        stage('Last') {
            agent {
                label 'slave01'
            }
            steps {
                echo 'successfully done !!!'
            }
        }
    }
}
```

- ここでは３つのステージがある。
- これまでの作業でslave01(ただのslaveコンテナ)とslave02（dockerをインストールしてあるslaveコンテナ）のイメージを登録してあるのでこれを使い分けて見る。
まずslave02でmavenやnodeのイメージをpullしてきて起動させ、そのコンテナの中でバージョン確認をして見る。
その後slave01でechoを実行する。
- このジョブを実行して見るとこうなる

```bash
Started by user anonymous
Running in Durability level: MAX_SURVIVABILITY
[Pipeline] stage
[Pipeline] { (Back-end)
[Pipeline] node
Running on docker-1e73a283a19 on test-docker in /home/jenkins/workspace/testpipeline2
[Pipeline] {
[Pipeline] sh
[testpipeline2] Running shell script
+ docker inspect -f . maven:3-alpine
.
[Pipeline] withDockerContainer
docker-1e73a283a19 on test-docker seems to be running inside container ec831590af64aa21b10cc8a61b4baea61e7f35a05de5b66a15305a11bc8380f8
$ docker run -t -d -u 0:0 -w /home/jenkins/workspace/testpipeline2 --volumes-from ec831590af64aa21b10cc8a61b4baea61e7f35a05de5b66a15305a11bc8380f8 -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** maven:3-alpine cat
$ docker top 17481eaa159c73797d4ddab2241a5ee249076971296e046f54581c4c3c9d1f2d -eo pid,comm
[Pipeline] {
[Pipeline] sh
[testpipeline2] Running shell script
+ mvn --version
Apache Maven 3.5.0 (ff8f5e7444045639af65f6095c62210b5713f426; 2017-04-03T19:39:06Z)
Maven home: /usr/share/maven
Java version: 1.8.0_131, vendor: Oracle Corporation
Java home: /usr/lib/jvm/java-1.8-openjdk/jre
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "3.10.0-862.2.3.el7.x86_64", arch: "amd64", family: "unix"
[Pipeline] }
$ docker stop --time=1 17481eaa159c73797d4ddab2241a5ee249076971296e046f54581c4c3c9d1f2d
$ docker rm -f 17481eaa159c73797d4ddab2241a5ee249076971296e046f54581c4c3c9d1f2d
[Pipeline] // withDockerContainer
[Pipeline] }
[Pipeline] // node
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Front-end)
[Pipeline] node
Still waiting to schedule task
All nodes of label ‘slave02’ are offline
Running on docker-1f2d8027424 on test-docker in /home/jenkins/workspace/testpipeline2
[Pipeline] {
[Pipeline] sh
[testpipeline2] Running shell script
+ docker inspect -f . node:7-alpine
.
[Pipeline] withDockerContainer
docker-1f2d8027424 on test-docker seems to be running inside container a7a3d0b13cf643b1212530c74a02e9823f81d0472a9b885d396dc319463241eb
$ docker run -t -d -u 0:0 -w /home/jenkins/workspace/testpipeline2 --volumes-from xxxxxxxxx -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** node:7-alpine cat
$ docker top f85668fc79d882da1678300a498efc4b13173d79b07c3ce250d57cc194319858 -eo pid,comm
[Pipeline] {
[Pipeline] sh
[testpipeline2] Running shell script
+ node --version
v7.10.1
[Pipeline] }
$ docker stop --time=1 f85668fc79d882da1678300a498efc4b13173d79b07c3ce250d57cc194319858
$ docker rm -f f85668fc79d882da1678300a498efc4b13173d79b07c3ce250d57cc194319858
[Pipeline] // withDockerContainer
[Pipeline] }
[Pipeline] // node
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Last)
[Pipeline] node
Running on docker-16c3cd9865d on test-docker in /home/jenkins/workspace/testpipeline2
[Pipeline] {
[Pipeline] echo
successfully done !!!
[Pipeline] }
[Pipeline] // node
[Pipeline] }
[Pipeline] // stage
[Pipeline] End of Pipeline
Finished: SUCCESS

```

- ちゃんとラベルでslaveのコンテナが使い分けられていることがわかる。
- また、slaveのジョブがホストサーバにmavenやnodeのイメージをpullしてきてrunして、その中で`mvn --version`とか`node --version`とかが実行されたことがわかる。
- これはとても便利！
- ちなみにホストサーバで確認して見るとnodeとmavenが新たにpullされている
- あとでよく観察してみたところ、stageごとにslaveコンテナが生まれては消えていきました。
- おなじラベルを指定していたとしても、stageごとにいったん消えるようです。

```bash
docker images
REPOSITORY                 TAG                 IMAGE ID            CREATED             SIZE
xxxxxx/customized-slave    1.0                 xxxxxxxxxxx        4 days ago          1.1GB
openjdk                    8-jdk               xxxxxxxxxxx        5 days ago          624MB
jenkinsci/blueocean        latest              xxxxxxxxxxx        9 days ago          440MB
hello-world                latest              xxxxxxxxxxx        5 weeks ago         1.85kB
jenkinsci/ssh-slave        latest              xxxxxxxxxxx        6 weeks ago         731MB
node                       7-alpine            xxxxxxxxxxx        10 months ago       58.3MB
maven                      3-alpine            xxxxxxxxxxx        10 months ago       116MB
```

###6. 結論として

- ジョブが比較的単純なものであってslaveの設定が面倒であればmasterだけでいいんじゃないかと思う。
- slaveを使うかどうかに関わりなく、dockerはやはりjenkinsなどのCICDツールを使う上で欠かせなくなってくる(というかすでにそうなってる)ので押さえておいて損はないから勉強は怠らないようにしよう。。。。