GitLab の CI/CD で Docker in Docker


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

$ 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 コンテナとして立てている。

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

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

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


$ 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である(privilegedtrueにすることでGitLab Runner が立ち上げる Docker コンテナは、特権モードになる。これをしておかないと Docker コンテナが Docker プロセスを作ることができない。)。

$ 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 が走ったときのコンテナの構成図

関係ない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 の中でやってくれていることを素手でやることになるため、作る部分としてはここ ↓。

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)。
Docker Server が作成した公開鍵やらは、Docker Client 側に渡してあげる必要があるため、ホストにマウントしてあげる(ここで言うホストはEC2のこと)。後で Docker Client も同じところをマウントして、公開鍵を渡す。

$ 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 にも立ち上がってもらい、dockerversionを確認する。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 に到達できなければ、いつものこいつが出てくるはず。

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 を楽しむことができる。

$ 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で回答している人がいるし、実際にできた。

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 は難しい。以上終わり!

参考