令和元年に送る！Docker for WindowsでのJenkins（Docker in Docker）環境構築ハンズオン

# はじめに
元号が変わり、2020年を迎えるにあたって「CI環境も存在しない開発体制をこの先も続けていくつもりなのか？」と悶々とする日々から脱却すべく「JenkinsのパイプラインでDockerfileを指定してビルド実行」を行うための環境構築に試行錯誤をした成果を紹介します。


## 目的
* ゼロからDocker入りJenkinsコンテナの立ち上げまでを、最小手順で実現する
* 手順の意図を理解する

以上の2点を目指しています。

私自身、DockerもJenkinsも最低限の知識しか持ち合わせておらず、結論に到達するまでに数多くのWebページを駆けずり回ることとなりました。
この記事は、まさしく**「一ヶ月前の私がほしかったモノ」**です。


# 動作確認環境
* Windows10 Pro 1903 - 64bit
* Docker Desktop for Windows 2.1.0.5 (stable)


## 1. Docker入りJenkinsコンテナの構築〜立ち上げ


### 1.1. `Dockerfile` 作成

```dockerfile:Dockerfile.sample
# 1.1.1.
FROM jenkins:2.60.3

# 1.1.2.
USER root

# 1.1.3.
RUN apt-get update && apt-get install -y \
	apt-transport-https \
	ca-certificates \
	curl \
	gnupg2 \
	software-properties-common \
	&& curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add - \
	&& apt-key fingerprint 0EBFCD88 \
	&& 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 docker-ce-cli containerd.io \
	&& apt-get clean && rm -rf /var/lib/apt/lists/*

# 1.1.4.
RUN usermod -aG docker jenkins

# 1.1.5.
RUN cd /tmp/ \
	&& wget https://updates.jenkins-ci.org/latest/jenkins.war \
	&& mv ./jenkins.war /usr/share/jenkins/

# 1.1.6.
USER jenkins
```


#### 1.1.1. ベースイメージ指定
ここでは奇をてらわずに、Jenkinsの公式イメージを指定しています。
タグについては、今後の更新で手順が変更になる可能性を考慮して、投稿時点で最新の `2.60.3` を明示的に指定しています。


#### 1.1.2. ユーザー変更（`root` <- `jenkins`）
jenkinsイメージ内で、ユーザーが `jenkins` に変更されています。
`jenkins` ユーザーはroot権限を持たないため、以降のコマンドを実行できるよう一時的に `root` ユーザーに変更します。


#### 1.1.3. Dockerインストール
[Get Docker Engine - Community for Debian | Docker Documentation](https://docs.docker.com/install/linux/docker-ce/debian/)を参考に、Dockerエンジンを導入します。
インストール後には、Dockerfileのお作法としてキャッシュを削除しておきます。

> 余談ですが、 `curl` で指定するURLは、[Docker v17.09以前のドキュメント](https://docs.docker.com/v17.09/engine/installation/linux/docker-ce/debian/#set-up-the-repository)では `$(. /etc/os-release; echo "$ID")` の結果を参照していました。
> ホスト環境がRaspberry Piの場合等、URLにしたい文字列と `$(. /etc/os-release; echo "$ID")` の結果とが一致しないケースがあることから、最新のドキュメントでは見直されているのではないかと想像します。 ※要出典


#### 1.1.4. `docker` を `jenkins` ユーザーのセカンダリグループとして追加
[Post-installation steps for Linux | Docker Documentation](https://docs.docker.com/install/linux/linux-postinstall/)を参考に、`jenkins` ユーザーが `sudo` を指定せずに `docker` コマンドを実行するための準備としてグループ設定を行います。


#### 1.1.5. Jenkinsアップデート
`jenkins:2.60.3` の最終更新日は、投稿時点で一年以上前となっていることから、最新のJenkinsを利用するために、アップデートします。


#### 1.1.6. ユーザー変更（`jenkins` <- `root`）
一時的に変更したユーザーをもとに戻します。


### 1.2. `docker-compose.yml` 作成

```yml:docker-compose.yml
version: "3"

services:
    jenkins:
        build: .
        ports:
            - 8080:8080
        volumes:
# 1.2.1.
            - ./jenkins_home:/var/jenkins_home
# 1.2.2.
            - /var/run/docker.sock:/var/run/docker.sock:rw
```


#### 1.2.1. ホスト環境との共有ディレクトリの指定
JenkinsコンテナにホストPC上で作成した `Dockerfile` を送り込めるように、Jenkinsのホームディレクトリを共有ディレクトリ化します。


#### 1.2.2. Dockerデーモンの共有
Dockerコンテナ内でDockerコンテナを動かすために、ホストPCのDockerソケットを共有します。
※この手順については [Dockerコンテナ内からDockerを使うことについて - Qiita](https://qiita.com/esaka/items/37e5d60d66c6061c19d0) の記事が非常に参考になりました。



### 1.3. `.env` 作成（しません）

Docker for WindowsでDockerデーモンを共有するために `.env` を作成して `COMPOSE_CONVERT_WINDOWS_PATHS=1` を指定する必要がある、という記事を目にしましたが、私の環境では作成せずに進めることができました。

```:.env
COMPOSE_CONVERT_WINDOWS_PATHS=1
```


### 1.4. イメージ構築～コンテナ作成～バックグラウンド実行

カレントディレクトリに先に示した `Dockerfile` と `docker-compose.yml` とを配置して、コマンドプロンプトから以下のコマンドを実行します。

```
$ docker-compose up --build -d && docker-compose exec -u root jenkins chown root:docker /var/run/docker.sock
```

Docker for Windowsでは（？） `docker` グループに `/var/run/docker.sock` へのアクセス権限が割り当てられていないようです。
そのため、コンテナのバックグラウンド実行後にグループ所有権を設定します。

※コマンドプロンプト以外のCUIツールを使用した場合、パスの解決がうまくいかずに `chown root:docker /var/run/docker.sock` を実行できないケースがあるようです。私はこれにハマって半日つぶしました。


## 2. Jenkinsコンテナ上でDocker Pipeline Pluginの登録〜実行


### 2.1. Jenkinsセットアップ
任意のWebブラウザで `localhost:8080` へアクセスし、正規の手順でセットアップします。
※以下、テキストベースで説明していますが、他に詳しく正確で分かりやすい記事が山のようにありますので、そちらも参考にしてください。


#### 2.1.1. Unlock Jenikins
いきなり「Administrator password」を求められます。
ここには `docker-compose logs -f jenkins` を実行して、以下のとおり出力されるパスワードを入力します。

```
...
jenkins_1  | 2019-12-15 08:10:47.628+0000 [id=30]       INFO    jenkins.install.SetupWizard#init:
jenkins_1  |
jenkins_1  | *************************************************************
jenkins_1  | *************************************************************
jenkins_1  | *************************************************************
jenkins_1  |
jenkins_1  | Jenkins initial setup is required. An admin user has been created and a password generated.
jenkins_1  | Please use the following password to proceed to installation:
jenkins_1  |
jenkins_1  | <Administrator password>
jenkins_1  |
jenkins_1  | This may also be found at: /var/jenkins_home/secrets/initialAdminPassword
jenkins_1  |
jenkins_1  | *************************************************************
jenkins_1  | *************************************************************
jenkins_1  | *************************************************************
jenkins_1  |
jenkins_1  | --> setting agent port for jnlp
jenkins_1  | --> setting agent port for jnlp... done
...
```


#### 2.1.2. Customize Jenkins
特にこだわりがなければ「Install suggested plugins」を選択して問題ないでしょう。


#### 2.1.3. Gettings Started
オールグリーンになるまで数分の休憩です。 :coffee: 


#### 2.1.4. Create First Addmin User
初期ユーザの情報を求められるので、適宜入力欄を埋めます。


#### 2.1.5. Instance Configuration
JenkinsのURLを指定することができます。
特にこだわりがなければデフォルトの `http://localhost:8080/` で問題ないでしょう。

#### 2.1.6. Jenkins is ready!
これでJenkinsのセットアップです！


### 2.2. パイプライン登録

以下、「sample」という名前のジョブを作成し、Dockerfileを指定してコンソールに「hello world.」と表示する例における手順です。

#### 2.2.1. 下準備
1. 以下内容の `Dockerfile` を作成

```dockerfile:Dockerfile
FROM alpine:latest
```

2. `~/jenkins_home/workspace/sample/` ディレクトリを作成し、配下に `1.` のファイルを配置


#### 2.2.2. WEB画面操作
1. ホーム画面「新規ジョブ作成」をクリック
2. 「Enter an item name」に「sample」と入力して「パイプライン」を選択し、「OK」をクリック
3. Generalタブ「パイプライン」「Pipeline script」を選択して「Script」に以下の内容を入力し、「保存」をクリック

```:Script
node {
    docker.build("sample-${BUILD_ID}", "-f Dockerfile .").inside() {
        sh 'echo "Hello World!"'  
    }
}
```


### 2.3. Docker Pipeline Plugin実行〜結果確認

`2.2.` で作成したジョブ画面の「ビルド実行」をクリックします。
今回のサンプルでは、コンソールに `Hello World!` と出力するだけなので、ビルド履歴画面「Console Output」から、以下のような出力がなされていれば成功です！ :tada:

```:コンソール出力
Started by user admin
Running in Durability level: MAX_SURVIVABILITY
[Pipeline] Start of Pipeline
[Pipeline] node
Running on Jenkins in /var/jenkins_home/workspace/sample
[Pipeline] {
[Pipeline] isUnix
[Pipeline] sh
+ docker build -t sample-9 -f Dockerfile .
Sending build context to Docker daemon  2.048kB

Step 1/1 : FROM alpine:latest
 ---> 965ea09ff2eb
Successfully built 965ea09ff2eb
Successfully tagged sample-9:latest
[Pipeline] isUnix
[Pipeline] sh
+ docker inspect -f . sample-9
.
[Pipeline] withDockerContainer
Jenkins seems to be running inside container 0fb06afff12dcba3803da391819176848ca54934737d486d99a72210b37c9955
$ docker run -t -d -u 1000:1000 -w /var/jenkins_home/workspace/sample --volumes-from 0fb06afff12dcba3803da391819176848ca54934737d486d99a72210b37c9955 -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** sample-9 cat
$ docker top 3702a7909fdacf3b77b04edcd381dba1309c8c9d0991def0ab1e392e07eb56b2 -eo pid,comm
[Pipeline] {
[Pipeline] sh
+ echo 'Hello World!'
Hello World!
[Pipeline] }
$ docker stop --time=1 3702a7909fdacf3b77b04edcd381dba1309c8c9d0991def0ab1e392e07eb56b2
$ docker rm -f 3702a7909fdacf3b77b04edcd381dba1309c8c9d0991def0ab1e392e07eb56b2
[Pipeline] // withDockerContainer
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS
```


# おわりに
0 -> 1のステップを超えるためには多大な労力を要します（要しました）。
この記事が、第二、第三のワタシの労力を減らすお役にたつことががあれば幸いです。


# 参考
## Docker関連
* [docker/Dockerfile at 2.60.3 · jenkinsci/docker](https://github.com/jenkinsci/docker/blob/587b2856cd225bb152c4abeeaaa24934c75aa460/Dockerfile)
* [Dockerfileにおけるキャッシュの削除 - ばいばいバイオ](https://www.kimoton.com/entry/20181123/1542961698)
* [Get Docker Engine - Community for Debian | Docker Documentation](https://docs.docker.com/install/linux/docker-ce/debian/)
* [Post-installation steps for Linux | Docker Documentation](https://docs.docker.com/install/linux/linux-postinstall/)
* [Docker in Docker のベタープラクティス - Qiita](https://qiita.com/sugiyasu-qr/items/85a1bedb6458d4573407)
* [Dockerコンテナ内からDockerを使うことについて - Qiita](https://qiita.com/esaka/items/37e5d60d66c6061c19d0)
* [Docker for windows で /var/run/docker.sock をマウントできるようにする - Qiita](https://qiita.com/furu8ma/items/6c4f68a593beaae27102)
* [How to fix docker: Got permission denied while trying to connect to the Docker daemon socket | DigitalOcean](https://www.digitalocean.com/community/questions/how-to-fix-docker-got-permission-denied-while-trying-to-connect-to-the-docker-daemon-socket)
* [Docker build does not work on Windows · Issue #1341 · containous/traefik](https://github.com/containous/traefik/issues/1341)


## Jenkins（Docker Pipeline Plugin）関連
* [Jenkins 導入手順 2018年版 - Qiita](https://qiita.com/kmdsbng/items/223c93d09bbd7816b8d5)
* [Jenkinsジョブ（ビルド～テスト）をDockerコンテナ上で実行する ～Docker Pipeline Pluginを使ってみる～ - SIerだけど技術やりたいブログ](http://kimulla.hatenablog.com/entry/2017/06/13/003123)
* [爆速でJenkinsをマスターしよう(Docker編) ~ JenkinsコンテナへのDockerインストール方法からJob DSL、Jenkins Pipelineまで ~ | Casual Developers Note](https://casualdevelopers.com/tech-tips/how-to-install-and-use-jenkins-on-docker-for-dockerized-application/)


## CIツール選定
* [CIマニアから見た各種CIツールの使い所 - くりにっき](https://sue445.hatenablog.com/entry/2018/12/07/114638)