CUBE SUGAR CONTAINER

技術系のこと書きます。

Docker Compose を使って複数のコンテナを管理する

今回は Docker Compose を使って複数のコンテナをまとめて管理する方法について。 docker run コマンドを使ってチマチマとやるよりもぐっと楽にできる。 コンテナオーケストレータを使うほどでもないけど複数台コンテナを扱いたい…っていうシチュエーションかな?

今回使った環境は次の通り。 Docker のディストリビューションとしては Docker for Mac を使う。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.13.3
BuildVersion:   17D102

インストール

Docker Compose は Docker for Mac をインストールすれば一緒についてくる。

まずは Homebrew Cask を使って Docker をインストールする。

$ brew cask install docker

初期設定とサービスの起動をするために、インストールした Docker アプリケーションを実行する。

$ open /Applications/Docker.app

しばらくして Docker のサービスが立ち上がったら、次のように docker version コマンドを実行する。 クライアントとサーバがエラーなく表示されれば上手くいっている。

$ docker version
Client:
 Version:   18.03.0-ce
 API version:   1.37
 Go version:    go1.9.4
 Git commit:    0520e24
 Built: Wed Mar 21 23:06:22 2018
 OS/Arch:   darwin/amd64
 Experimental:  false
 Orchestrator:  swarm

Server:
 Engine:
  Version:  18.03.0-ce
  API version:  1.37 (minimum version 1.12)
  Go version:   go1.9.4
  Git commit:   0520e24
  Built:    Wed Mar 21 23:14:32 2018
  OS/Arch:  linux/amd64
  Experimental: true

Docker Compose を使わずに複数のコンテナを管理する場合

まずは、もし Docker Compose を使わないで複数のコンテナを管理しようとした場合について考えてみる。 ここでは二つのコンテナを用意して、ネットワーク的な疎通があるようにしたい状況を考えてみよう。

まずは一つ目のコンテナを docker run コマンドで起動する。

$ docker run --name container1 -it centos:7 /bin/bash

続いて二つ目のコンテナを起動する。 その際に --link オプションを使って、先ほど起動したコンテナの名前を解決できるようにする。 ここでは一つ目のコンテナを c1 という名前で名前解決できるようにしている。

$ docker run --name container2 --link container1:c1 -it centos:7 /bin/bash

二つ目のコンテナのシェルから、一つ目のコンテナに向けて ping を打ってみよう。

# ping -c 3 c1
PING c1 (172.17.0.2) 56(84) bytes of data.
64 bytes from c1 (172.17.0.2): icmp_seq=1 ttl=64 time=0.120 ms
64 bytes from c1 (172.17.0.2): icmp_seq=2 ttl=64 time=0.096 ms
64 bytes from c1 (172.17.0.2): icmp_seq=3 ttl=64 time=0.098 ms

--- c1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2090ms
rtt min/avg/max/mdev = 0.096/0.104/0.120/0.016 ms

ちゃんと疎通がとれた。

とはいえ、このやり方には色々と問題がある。 例を挙げると、それぞれのコンテナを起動するときのオプションを毎回正しく入力しなければ上手く動作しない。 正直そんなもの覚えていられないので、きっとそのうちシェルスクリプトを書き始めることになるだろう。

Docker Compose を使って複数のコンテナを管理する場合

先ほどは Docker Compose を使わずに複数のコンテナを協調させていた。 続いては Docker Compose を使った場合について書く。

Docker Compose では docker-compose.yml という設定ファイルを基本にしてコンテナを管理する。 これで、先ほどの問題点だったオプションを毎回覚えておくような必要はなくなる。

$ cat << 'EOF' > docker-compose.yml 
version: '3'

services:
  c1:
    image: centos:7
    command: /usr/sbin/init
  c2:
    image: centos:7
    command: /usr/sbin/init
EOF

起動するコマンドとして /usr/sbin/init を指定しているのは、コンテナをすぐに終了させないようにするため。

設定ファイルの書式については次の公式サイトに詳しく記載されている。

docs.docker.com

設定ファイルができたら docker-compose up コマンドを使ってコンテナを起動する。

$ docker-compose up
Creating network "compose_default" with the default driver
Creating compose_c2_1 ... done
Creating compose_c1_1 ... done
Attaching to compose_c1_1, compose_c2_1

別のターミナルから docker ps コマンドを使うと、それぞれのコンテナが起動していることが分かる。

$ docker ps                                          
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
a434cf41dc96        centos:7            "/usr/sbin/init"    20 seconds ago      Up 19 seconds                           compose_c2_1
a946853ecf00        centos:7            "/usr/sbin/init"    20 seconds ago      Up 20 seconds                           compose_c1_1

ちなみに docker-compose ps コマンドを使えば設定ファイルに含まれるコンテナだけの状態を確認できる。 これは docker-compose.yml ファイルがあるディレクトリで実行する。

$ docker-compose ps
    Name          Command       State   Ports
---------------------------------------------
compose_c1_1   /usr/sbin/init   Up           
compose_c2_1   /usr/sbin/init   Up

同じく、コンテナでコマンドを実行したいときは docker-compose exec コマンドを使う。 この場合 docker-compose.yml ファイルにある名称でコンテナを指定できる。

$ docker-compose exec c1 /bin/bash

もちろん docker exec コマンドを使っても構わない。

$ docker exec -it compose_c1_1 /bin/bash

シェルで ping コマンドを使ってコンテナ間で名前が解決できることを確認しよう。

[root@a946853ecf00 /]# ping -c 3 c2
PING c2 (172.18.0.3) 56(84) bytes of data.
64 bytes from compose_c2_1.compose_default (172.18.0.3): icmp_seq=1 ttl=64 time=0.184 ms
64 bytes from compose_c2_1.compose_default (172.18.0.3): icmp_seq=2 ttl=64 time=0.101 ms
64 bytes from compose_c2_1.compose_default (172.18.0.3): icmp_seq=3 ttl=64 time=0.110 ms

--- c2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2107ms
rtt min/avg/max/mdev = 0.101/0.131/0.184/0.039 ms

ばっちり。

コンテナの作成順序を指定する

先ほどの例ではコンテナを作成する順序関係は関係なかった。 とはいえ、データベースなどを扱う場合には特定のコンテナを先に作って欲しいといったことがある。 次はそのような場合について扱う。

今度はデータベースを扱うシステムを想定してコンテナの一つを mysql イメージにしてみた。 コンテナの作成に関係する記述は clientdepends_on になる。 これを記述しておくと、そのコンテナは依存するコンテナよりも後に作られることになる。 また、ports では外部に公開するポートを指定している。

$ cat << 'EOF' > docker-compose.yml 
version: '3'

services:
  db:
    image: mysql
    environment:
      - MYSQL_ROOT_PASSWORD
    ports:
      - 3306:3306
  client:
    image: centos:7
    depends_on:
      - db
    command: /usr/sbin/init
EOF

上記の設定ファイルでは db コンテナに environment も指定していた。 これはコンテナに渡す環境変数を指定する書式になっている。 一般的には <key>=<value> という形で書くものの、上記では <key> のみになっている。 これは Docker ホストで定義されている環境変数をそのまま渡すことを意味している。 パスワードなど設定ファイルに書けない秘密情報は、このようにしておくと良い。

$ export MYSQL_ROOT_PASSWORD=rootpasswd

docker-compose up コマンドを使ってコンテナを起動すると、作成順序が必ず client -> db の順になっているはず。

$ docker-compose up

ただし、この機能には注意すべき点が一つある。 それは、作成順序自体は指定できるもののコンテナの起動までは待ってくれないというところ。 実際のところはコンテナが起動するまで待ってほしいというニーズの方が大きいと思う。 なので、おそらくはシェルスクリプトなどを使ってサービスが起動するまで待つ (リトライする) ようなコードが必要になるだろう。

コンテナイメージも一緒に管理する

続いてはカスタマイズした Docker イメージを Docker Compose で管理する方法について。 Docker Compose ではコンテナ自体だけでなくコンテナイメージまで設定ファイルで管理できる。

先ほどの例ではデータベースのクライアントに相当するコンテナは作ったものの、実際に操作することはできなかった。 これは MySQL クライアントがインストールされていなかったため。 そこで、試しに MySQL クライアントをインストールした Docker イメージに変更してみよう。

まずは Docker イメージをビルドするのに必要な Dockerfile を用意する。 場所は docker-compose.yml の下に client というディレクトリを作って、そこに配置した。

$ mkdir client
$ cat << 'EOF' > client/Dockerfile
FROM centos:7

RUN yum -y update \
 && yum -y install mysql
EOF

ひとまず単体でイメージがビルドできることを確認しておこう。 単体で成功しないと docker-compose.yml に組み込む以前の問題になってしまうため。

$ docker build -t example/client client 
...
Successfully tagged example/client:latest

イメージからコンテナを起動して mysql コマンドが実行できることも確認しておく。

$ docker run -it example/client mysql --help
mysql  Ver 15.1 Distrib 5.5.56-MariaDB, for Linux (x86_64) using readline 5.1
Copyright (c) 2000, 2017, Oracle, MariaDB Corporation Ab and others.

Usage: mysql [OPTIONS] [database]
...

よさそうだ。

確認できたので、次は上記のコンテナイメージの情報を docker-compose.yml に組み込む。 具体的には、次のように image をビルド後のビルドイメージの名前にする。 その上で build を指定して Dockerfile のある client ディレクトリを指定すれば良い。

$ cat << 'EOF' > docker-compose.yml 
version: '3'

services:
  db:
    image: mysql
    environment:
      - MYSQL_ROOT_PASSWORD
    ports:
      - 3306:3306
  client:
    image: example/client
    build:
      context: client
    depends_on:
      - db
    command: /usr/sbin/init
EOF

こうしておけば docker-compose build コマンドで設定ファイルに含まれるコンテナイメージが一気にビルドできる。

$ docker-compose build                      
db uses an image, skipping
Building client
Step 1/2 : FROM centos:7
 ---> e934aafc2206
Step 2/2 : RUN yum -y update  && yum -y install mysql
 ---> Using cache
 ---> f82342f62e31
Successfully built f82342f62e31
Successfully tagged example/client:latest

あとは先ほどと同じように docker-compose up でコンテナたちを起動する。

$ docker-compose up

クライアントに対応するコンテナのシェルをつかもう。

$ docker-compose exec client /bin/bash 

データベースのコンテナに MySQL クライアントで接続してみる。

# mysql -u root -prootpasswd -h db
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.21 MySQL Community Server (GPL)

Copyright (c) 2000, 2017, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [(none)]> 

ちゃんと接続できた!

ディスクを永続化する

先ほどの設定では MySQL がデータベースの情報を保存する先がコンテナのファイルシステム上になっていた。 これではコンテナが起動するごとにデータが消えてしまう。 そこで、次は MySQL のデータディレクトリとして Docker ホストのディレクトリをマウントさせることにしよう。

やることは単純で volumes を指定するだけ。 あとはマウントする対応関係を <host-dir>:<container-dir> という形で記述する。

$ cat << 'EOF' > docker-compose.yml 
version: '3'

services:
  db:
    image: mysql
    environment:
      - MYSQL_ROOT_PASSWORD
    ports:
      - 3306:3306
    volumes:
      - ./db-data:/var/lib/mysql
  client:
    image: example/client
    build:
      context: client
    depends_on:
      - db
    command: /usr/sbin/init
EOF

ディレクトリのマウント周りは前のコンテナの情報が残っているといまいち上手くいかない感じみたい。 なので、一旦先ほどのコンテナを終了した上で全て削除しておこう。

$ docker system prune -f

そして設定ファイルを元にコンテナを起動する。

$ docker-compose up

Docker ホスト側でディレクトリを確認すると MySQL のデータディレクトリが指定した名前で作成されていることが分かる。

$ ls db-data 
auto.cnf        ib_logfile0     private_key.pem
ca-key.pem      ib_logfile1     public_key.pem
ca.pem          ibdata1         server-cert.pem
client-cert.pem     ibtmp1          server-key.pem
client-key.pem      mysql           sys
ib_buffer_pool      performance_schema

ちゃんとデータが永続化されているか確認しておこう。 まずはクライアントのシェルをつかむ。

$ docker-compose exec client /bin/bash

適当にデータを投入しておく。

# mysql -u root -prootpasswd -h db
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.21 MySQL Community Server (GPL)

Copyright (c) 2000, 2017, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [(none)]> CREATE DATABASE mydb;
Query OK, 1 row affected (0.01 sec)

MySQL [(none)]> USE mydb;
Database changed

MySQL [mydb]> CREATE TABLE users (
    ->   name TEXT,
    ->   age INTEGER
    -> );
Query OK, 0 rows affected (0.03 sec)

MySQL [mydb]> INSERT INTO users VALUES
    ->   ('Alice', 20),
    ->   ('Bob', 25),
    ->   ('Carol', 30);
Query OK, 3 rows affected (0.01 sec)
Records: 3  Duplicates: 0  Warnings: 0

一旦コンテナを全て終了してから立ち上げ直す。

$ docker-compose down
$ docker-compose up

もう一度クライアントのシェルをつかむ。

$ docker-compose exec client /bin/bash

確認すると、前回の内容が残っていることが分かる。 これで、コンテナを終了してもデータは Docker ホスト側に残り続けるので消えなくなった。

# mysql -u root -prootpasswd -h db

# mysql -u root -prootpasswd -h db
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.21 MySQL Community Server (GPL)

Copyright (c) 2000, 2017, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [(none)]> SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mydb               |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.03 sec)

MySQL [(none)]> USE mydb;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed

MySQL [mydb]> SHOW TABLES;
+----------------+
| Tables_in_mydb |
+----------------+
| users          |
+----------------+
1 row in set (0.01 sec)

MySQL [mydb]> SELECT * FROM users;
+-------+------+
| name  | age  |
+-------+------+
| Alice |   20 |
| Bob   |   25 |
| Carol |   30 |
+-------+------+
3 rows in set (0.02 sec)

とりあえず、これくらい覚えておけば大丈夫そうかな。