dind(Docker in Docker)で複数のdocker-composeを管理する

この記事はDocker Advent Calendar 2016の8日目の記事です。

Docker Advent Calendar 2016 - Qiita

動機

microservices運用してるし、検証用プロジェクトとか遊び用プロジェクトとかあるし、複数docker-composeをローカルで運用するのしんどい。

dind(ディーインディー)

dindとは何ぞやと思う方もいるかもしれないですが、dindはDocker in Dockerの略でDockerコンテナのでさらにDockerのデーモンを稼働させて、その内部のDocker上にさらにコンテナを配置するという手法です。

dind対応のdockerのDockerイメージはDockerHubから入手できます。

dindがついているのがdindのDockerイメージ。とりあえず今回は1.12.3-dindを使ってみることにします。あ、Docker環境は各自で用意してくださいw 当方はMacユーザーなのでDocker for Macを利用しています。

dindコンテナを立てる

次にdindコンテナを立てるわけですが、docker runをポチポチ実行していくよりちゃんと構成管理した方が良いと思うので、docker-compose を使ってdindコンテナを立てます。docker-composeはv2のスキーマを利用し、docker-compose.ymlを適当な場所に作成します。

version: "2"

services:
  dind:
    image: docker:1.12.3-dind
    privileged: true

基本的にDockerコンテナ内でさらにDockerコンテナを立てることはできないわけですが、privileged(特権)モードで起動することでDocker in Dockerが可能になり、中のDockerもデバイスにアクセスすることができるようになります。

このdocker-compose.ymlがあるディレクトリで、docker-composeを使ってdindコンテナを起動します。正しく起動すると、docker psコマンドでコンテナの存在を確認できるはずです。

$ docker-compose up -d
Creating dockercomposemultiple_dind_1

$ docker ps
CONTAINER ID        IMAGE                COMMAND                  CREATED             STATUS              PORTS               NAMES
e7090c2fc2e3        docker:1.12.3-dind   "dockerd-entrypoint.s"   9 hours ago         Up 25 seconds       2375/tcp            dockercomposemultiple_dind_1

複数docker-compose環境

いよいよ本題で、複数docker-compose環境をつくります。まずはdindを使ってDocker上に複数のDockerホストを立てます。これをdocker-composeで書くとこんな感じになります。

version: "2"

services:
  dind_a:
    image: docker:1.12.3-dind
    privileged: true
    tty: true
    ports:
      - 5301:2375 # dockerのポート
      - 9001:80 # dind内のwebサーバのポート

  dind_b:
    image: docker:1.12.3-dind
    privileged: true
    tty: true
    ports:
      - 5302:2375
      - 9002:80

  dind_c:
    image: docker:1.12.3-dind
    privileged: true
    tty: true
    ports:
      - 5303:2375
      - 9003:80

dind_a, dind_b, dind_cという3つのdindコンテナを立ててます。また、各dindのDockerポートのポート2375をローカルでポート衝突しないように5301, 5302, 5303と割り当て。さらに、dindの中でWebサーバ(Nginxだけど)を立てるので、9001-9003のポートでそれぞれバインド。

$ docker ps
CONTAINER ID        IMAGE                COMMAND                  CREATED             STATUS              PORTS                                          NAMES
2bc566a17f71        docker:1.12.3-dind   "dockerd-entrypoint.s"   3 minutes ago       Up 3 minutes        0.0.0.0:9003->80/tcp, 0.0.0.0:5303->2375/tcp   dockercomposemultiple_dind_c_1
d3a784a2cac5        docker:1.12.3-dind   "dockerd-entrypoint.s"   4 minutes ago       Up 3 minutes        0.0.0.0:9001->80/tcp, 0.0.0.0:5301->2375/tcp   dockercomposemultiple_dind_a_1
44ec044653da        docker:1.12.3-dind   "dockerd-entrypoint.s"   4 minutes ago       Up 3 minutes        0.0.0.0:9002->80/tcp, 0.0.0.0:5302->2375/tcp   dockercomposemultiple_dind_b_1

それぞれのdind Dockerにはこのポートを利用してローカルからTCPベースでアクセスが可能。例えば、DockerのRemote APIを大元のホストからはこんな感じで叩くことができます。

$ curl http://127.0.0.1:5301/containers/json
[]

docker-composeを複数管理する

各dindコンテナに対応するdocker-composeをそれぞれ作成します。先程のdocker-composeが親dockerのものなら、これは子dockerのためのものです。ディレクトリ構成はこんな感じ。

(path-to-path)----docker-compose.yml
                |--service_a
                       |--docker-compose.yml
                |--service_b
                       |--docker-compose.yml
                |--service_b
                       |--docker-compose.yml

service_a, b, cはもちろんdind_a, dind_b, dind_cに対応。しかし、docker-compose入れ子構成を実際にどのように扱うのかという疑問が残るはずです。

direnv

direnvという便利ツールがあります(Homebrewでインストール可能)。direnvを利用すれば、移動した先のディレクトリによって環境変数を変えるということができます。これを利用します。 Dockerは環境変数によって、利用対象のDockerホストを変えることができます。この特性を利用すれば、各dindに対応したディレクトリに移動した際に、あたかもdindコンテナにSSHログインしたかのように各dind内のdocker操作ができる、というわけです。

direnvでserviceディレクトリ別の環境変数設定

各サービスディレクトリで環境変数を設定します。

(service_a) $ direnv edit .

とするとvimが開くので、環境変数DOCKER_HOSTを設定します。設定する値はdindのTCPアドレスです。service_aであれば5301をバインドしているので、以下のようになります。

export DOCKER_HOST="tcp://127.0.0.1:5301"

dindコンテナ上のDockerで、Nginxを動かす

実際にNginxをdindのDocker上で動かして見ましょう。service_aのdocker-compose.ymlを以下のように用意します。 

version: "2"

services:
  nginx:
    image: nginx:latest
    ports:
      - 80:80

何の変哲もないデフォルトのNginxです。素のNginxは80でHTTPをListenしており、dindコンテナの80ポートにバインドします(ローカルの80ではない)。大元のdocker-composeでは、dindの80ポートを9001-9003でそれぞれバインドしています。つまり、ローカルからは http://localhost:9001 - http://localhost:9003 でアクセスできるという算段です。

service_aディレクトリでdocker-composeを操作して、dind上のNginxを起動します。

(service_a) $ docker-compose up -d
Starting servicea_nginx_1

(service_a) $ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                         NAMES
4b035809daf9        nginx:latest        "nginx -g 'daemon off"   17 minutes ago      Up 30 seconds       0.0.0.0:80->80/tcp, 443/tcp   servicea_nginx_1

dindコンテナ上でNginxコンテナが起動しました。では、この状態で一つ上のディレクトリに戻ってみると・・・

(service_a) $ cd ..
(root) $ docker ps
$ docker ps
CONTAINER ID        IMAGE                COMMAND                  CREATED             STATUS              PORTS                                          NAMES
2bc566a17f71        docker:1.12.3-dind   "dockerd-entrypoint.s"   16 minutes ago      Up 16 minutes       0.0.0.0:9003->80/tcp, 0.0.0.0:5303->2375/tcp   dockercomposemultiple_dind_c_1
d3a784a2cac5        docker:1.12.3-dind   "dockerd-entrypoint.s"   17 minutes ago      Up 16 minutes       0.0.0.0:9001->80/tcp, 0.0.0.0:5301->2375/tcp   dockercomposemultiple_dind_a_1
44ec044653da        docker:1.12.3-dind   "dockerd-entrypoint.s"   17 minutes ago      Up 16 minutes       0.0.0.0:9002->80/tcp, 0.0.0.0:5302->2375/tcp   dockercomposemultiple_dind_b_1

このように、dindの3つのコンテナが表示されます。環境変数によって、ターゲットとなるDockerホストが変わったからですね。

では最後に、dind上のNginxにアクセスしてみましょう。これはもちろんどこのディレクトリだろうが、ブラウザからだろうが関係なく見れます。

$ curl http://localhost:9001
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

これはdind上のNginxが返したレスポンス。このような構成にすると、完全に独立したdocker-compose環境を複数同時に稼働させることが可能ですね。

まとめ

このdindネタ、まだまだ色々応用が効きます。本当はこのdocker-composeをそのままCIに持っていってE2Eテストをする、っていうネタまで書きたかったけどあまりにもボリュームが大きくなりすぎだし、今日はここまでにしておきます。