GitLab の CI/CD で Docker in Docker

GitLab の CI/CD の中で `docker`コマンドを実行するには、いくつか方法があるが、今回やるのは Docker in Docker(Dindとよく呼ばれている)の構成である。
Docker in Docker は、`.gitlab-ci.yml`に ↓ こう書くだけで実現できるが、Docker in Docker の仕組みを知らなければ何がどうなって、どう実行されているかチンプンカンプンである。

```yml
$ vim .gitlab-ci.yml
image: docker:18.09.7
services:
        - name: docker:18.09.7-dind
          alias: docker
default:
    script:
        - echo "hello"
        - docker run -itd --name tmp alpine:latest
        - docker run -itd --name tmp2 alpine:latest
        - docker container ls
    only:
        - master
```
ということで、これを頑張って解明していったため、そのプロセスを記事にしている。

## Docker in Docker を作った環境
GitLab も GitLab Runner も EC2 の上に Docker コンテナとして立てている。
![Untitled Diagram-Copy of Page-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606419/519d6578-e361-c359-f0f0-d9c829344077.png)

バージョンはこんな感じ。

- GitLab Docker Image : gitlab/gitlab-ce:12.7.8-ce.0
- GitLab Runner Docker Image : gitlab/gitlab-runner:ubuntu-v12.9.0

自明だが、EC2 上で`ls`したらこんな感じ。

```bash

$ sudo docker container ls
CONTAINER ID        IMAGE                                 COMMAND                  CREATED             STATUS                    PORTS                                                                                     NAMES
7cf38ee9ab29        gitlab/gitlab-runner:ubuntu-v12.9.0   "/usr/bin/dumb-init …"   9 minutes ago       Up 9 minutes                                                                                                        gitlab-runner
2bf98a756bf8        gitlab/gitlab-ce:12.7.8-ce.0          "/assets/wrapper"        3 days ago          Up 33 minutes (healthy)   0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp, 0.0.0.0:4567->4567/tcp, 0.0.0.0:10022->22/tcp   gitlab
```
CI/CD が走り出すと、GitLab Runner Container が CI/CD 用のコンテナを作成していく。また、GitLab Runner Container はホスト(EC2)の`/var/run/docker.sock`をマウントしているため、CI/CD 用のコンテナは、GitLab Runner Container や GitLab Repository と同じレイヤーに作成される。

GitLab Runner の立ち上げコマンドと設定はこんな感じ。特筆すべき点としては、`privileged = true`である(`privileged`を`true`にすることでGitLab Runner が立ち上げる Docker コンテナは、特権モードになる。これをしておかないと Docker コンテナが Docker プロセスを作ることができない。)。

```config
$ sudo docker run -d --name gitlab-runner --restart always \
-v /srv/gitlab-runner/config:/etc/gitlab-runner \
-v /var/run/docker.sock:/var/run/docker.sock \
gitlab/gitlab-runner:ubuntu-v12.9.0

$ sudo docker exec -it gitlab-runner vim /etc/gitlab-runner/config.toml
concurrent = 1
check_interval = 0

[session_server]
  session_timeout = 1800

[[runners]]
  name = "7cf38ee9ab29"
  url = "http://ec2-13-231-128-*.ap-northeast-1.compute.amazonaws.com/"
  token = "VZExYHrr1ssjyoHUBEsM"
  executor = "docker"
  [runners.custom_build_dir]
  [runners.cache]
    [runners.cache.s3]
    [runners.cache.gcs]
  [runners.docker]
    tls_verify = false
    image = "docker:18.09.7"
    privileged = true ← 注目!!
    disable_entrypoint_overwrite = false
    oom_kill_disable = false
    disable_cache = false
    volumes = ["/cache"]
    shm_size = 0
```

このGitlab Runner の設定で、かつ、冒頭に書いた`.gitlab-ci.yml`の CI/CD を走らせると CI/CD の中で`docker`コマンドが実行できる。

GitLab のWeb画面から CI/CD の実行ログを見るとこんな感じ。長いので CI/CD の`script`の実行されているところをだけを抜粋している。CI/CD の中で`docker`コマンドが実行され、コンテナが2つ作成されていることがわかる。__大事なのは docker コマンドが実行できてるということ。__

```
 $ echo "hello"
 hello
 $ docker run -itd --name tmp alpine:latest
 Unable to find image 'alpine:latest' locally
 latest: Pulling from library/alpine
 aad63a933944: Pulling fs layer
 aad63a933944: Verifying Checksum
 aad63a933944: Download complete
 aad63a933944: Pull complete
 Digest: sha256:b276d875eeed9c7d3f1cfa7edb06b22ed22b14219a7d67c52c56612330348239
 Status: Downloaded newer image for alpine:latest
 c8b4df07f0954ded608c061ee8efcc36028440357271dda05e77c4e11c1f5a81
 $ docker run -itd --name tmp2 alpine:latest
 ae2559d3af2512490c494ba85ca4d64dcd7f873c36b0dc26b8ff8d76d9d5c44d
 $ docker container ls
 CONTAINER ID        IMAGE               COMMAND             CREATED                  STATUS                  PORTS               NAMES
 ae2559d3af25        alpine:latest       "/bin/sh"           Less than a second ago   Up Less than a second                       tmp2
 c8b4df07f095        alpine:latest       "/bin/sh"           1 second ago             Up Less than a second                       tmp
```

## CI/CD が走ったときのコンテナの構成図

![Untitled Diagram-Page-2 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606419/98fe35e0-3d5b-9995-39df-c4ef228e3364.png)

関係ないGitLab Repository は省いている(本当はGitLab Runner Container の横にある)。

- GitLab Runner Container : GitLab Runner 
- GitLab CI/CD Container(Docker Client) : 真ん中の Docker コンテナ。GitLab Runner によって立ち上げられる CI/CD のコンテナ 
- GitLab Dind Container(Docker Server) : 右の Docker コンテナ。同じくGitLab Runner によって立ち上げられるコンテナだが、CI/CD のコンテナではなく、`.gitlab-ci.yml`に書いてある通り`services`としてのコンテナ

バランスの問題で GitLab CI/CD Container(Docker Client) の上に Docker コンテナを3つ書いたが、さっきの`.gitlab-ci.yml`では、`docker run`を2回しているため、2つが正しい。

GitLab CI/CD Container(Docker Client) は、Docker Client として動作するため、Docker Image は、`docker:18.09.7`を使っている。`docker:18.09.7`のイメージは、Docker Daemon 以外の Docker の機能を持っている。GitLab Dind Container(Docker Server) は、Docker Server として動作するため、Docker Image は、`docker:18.09.7-dind`を使っている。`docker:18.09.7-dind`は、Docker Daemon を持っている。

冒頭の`.gitlab-ci.yml`に書いていた`script`だと、`docker run`コマンドを要求しているのは、GitLab CI/CD Container(Docker Client) であり、実際に`docker run`を実行しているのは Docker Daemon を持つ`GitLab Dind Container(Docker Server) `になる。


## 素手で Docker in Docker を作る

GitLab には、`.gitlab-ci.yml`に数行書けば、Docker in Docker の構成をとることができるが、意外と素手で作っていくと面倒である。理解を深めるためにもやってみる。GitLab Runner が CI/CD の中でやってくれていることを素手でやることになるため、作る部分としてはここ ↓。

<img width="251" alt="dind.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606419/47c09bd1-353e-851b-d150-809d06255dda.png">

Docker Client は、Docker Server に対して命令を出すが、Docker Server のエンドポイントは`tcp://docker:2376`にある。そのため、Docker Client は名前解決できないとエンドポイントに到達できないため、Docker Client と Docker Server は同一ネットワーク内にいる必要がある。ということでネットワークを作成。  

```
$ sudo docker network create some-network
$ sudo docker network inspect some-network
```

Docker Client と Docker Server 間の通信はSSLが推奨されており、SSLの場合は、`tcp://docker:2376`が使用される。SSLじゃない場合は、`tcp://docker:2375`となる。たしか、冒頭で設定したGitLab Runner はSSLじゃないが、せっかく素手でやるので、SSLで行こう。

Docker Server のコンテナを立ち上げると、立ち上げコマンドの中にSSLをするための証明書や公開鍵を自動で生成してくれる。`docker:18.09.7-dind`のソースコードを見ればわかる([github](https://github.com/docker-library/docker/blob/0bab8e3d0ebe6dc4b7a122bb1d0b2e017925c50d/19.03/dind/dockerd-entrypoint.sh))。
Docker Server が作成した公開鍵やらは、Docker Client 側に渡してあげる必要があるため、ホストにマウントしてあげる(ここで言うホストはEC2のこと)。後で Docker Client も同じところをマウントして、公開鍵を渡す。  

```bash
$ mkdir certs
/home/ec2-user/certs
$ mkdir ca
/home/ec2-user/certs/ca
$ mkdir client
/home/ec2-user/certs/client
```

Docker Server を立ち上げる。

```
$ sudo docker run --privileged --name some-docker -d \
    --network some-network --network-alias docker \
    -e DOCKER_TLS_CERTDIR=/certs \
    -v /home/ec2-user/certs/ca:/certs/ca \
    -v /home/ec2-user/certs/client:/certs/client \
    docker:18.09.7-dind
```

Docker Client にも立ち上がってもらい、`docker`の`version`を確認する。`docker version`コマンドの実行と同義。  

```
$ sudo docker run --rm --network some-network \
    -e DOCKER_TLS_CERTDIR=/certs \
    -v /home/ec2-user/certs/client:/certs/client:ro \
    docker:18.09.7 version

Client: Docker Engine - Community
 Version:           19.03.8
 API version:       1.40
 Go version:        go1.12.17
 Git commit:        afacb8b7f0
 Built:             Wed Mar 11 01:22:56 2020
 OS/Arch:           linux/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.8
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.12.17
  Git commit:       afacb8b7f0
  Built:            Wed Mar 11 01:30:32 2020
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          v1.2.13
  GitCommit:        7ad184331fa3e55e52b890ea95e65ba581ae3429
 runc:
  Version:          1.0.0-rc10
  GitCommit:        dc9208a3303feef5b3839f4323d9beb36df0a9dd
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683
```
実際に実行したときは Docker Image のバージョンを指定せずに、`lastet`で言ったため、表示されているバージョンと実行コマンドのバージョンに差異があることは許してほしい。

`Server`のバージョンが出てきているということは、問題なく Docker Server 側の Docker Daemon に命令を出すことができている、ということになる。もし、 Docker Daemon に到達できなければ、いつものこいつが出てくるはず。
  
```bash
Server:
ERROR: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
```

これで Client から`docker`コマンドの実行が可能になったため、Client の中で好きに Docker コンテナを作成し、Docker in Docker を楽しむことができる。

```bash
$ sudo docker run -it --rm --network some-network \
    -e DOCKER_TLS_CERTDIR=/certs \
    -v /home/ec2-user/certs/client:/certs/client:ro \
    docker:latest sh
$ sudo docker run -it --rm --name tmp-docker alpine:latest sh
$ sudo docker container ls
```

## おまけ

Docker Server として使っている`docker:18.09.7-dind` は、Docker Daemon を持っているため、わざわざ Docker Client と Docker Server にコンテナを分けずに、`docker:18.09.7-dind` のイメージを使うだけで CI/CD の中で`docker`コマンドを使うことができる,
と StackOverFlowで回答している人がいるし、実際にできた。

```yml
image: docker:dind

test:
    script:
      - dockerd &
      - sleep 5
      - docker run hello-world

```

https://stackoverflow.com/questions/47280922/role-of-docker-in-docker-dind-service-in-gitlab-ci
https://gitlab.com/saraedum/sandbox/blob/dind/.gitlab-ci.yml

## まとめ
Docker Server は、Docker Daemon とか Dockerd と書いたほうが適切かもしれない。ので、各自脳内変換して読み進めてほしい。
Docker は難しい。以上終わり!

## 参考
https://hub.docker.com/_/docker