vagrantの代わりにdockerで仮想的な複数ホストを構築する際のTIPS

複数台のホストを使ったテストをやりたいときに実際にホストを複数台用意するのは面倒なので、自身が使用しているのマシン上で仮想的に複数台のホストを用意したいと思います。
多くの場合はvagrantで複数のVMを立ち上げてテストすることが多いと思います。
しかし使っているマシンが非力だとVMを3個くらい立ち上げるだけで悲鳴をあげることがあります。
ちなみに自分のラップトップは4core4GmemなのでVMを立ち上げるのは厳しいです。

そこでdockerを使って複数ホスト(コンテナ)を立ち上げて代替させる方法とその際に役に立つ小技をメモ代わりに残しておきます。
なお、基本的にはDockerの新しい何かを解説するというわけではなく、既にいろんなところで解説されていることをまとめているだけです。

## dockerのネットワーク基礎の基礎

dockerのネットワークについて詳しいことは他の記事でたくさん解説されているので省略します。

dockerはコンテナを作成すると(特に何も設定していない場合)`docker0`という仮想ブリッジにぶら下がる形でコンテナに対してインターフェイスとIPアドレスが割り当てられます。
コンテナ内からホスト外に出る場合は`docker0`を通ってNATされて外と通信されますが、
コンテナ同士の通信は`docker0`の中のネットワークで閉じているのでNATされず直接コンテナのIPアドレスで通信することができます。

```
$ ifconfig docker0
docker0   Link encap:Ethernet  HWaddr 02:42:a2:84:a7:93  
          inet addr:172.17.0.1  Bcast:0.0.0.0  Mask:255.255.0.0
          inet6 addr: fe80::42:a2ff:fe84:a793/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:2982 errors:0 dropped:0 overruns:0 frame:0
          TX packets:6274 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0 
          RX bytes:171299 (171.2 KB)  TX bytes:8451392 (8.4 MB)
```

デフォルトでは`172.17.0.1/16`のネットワークが構築され、コンテナを起動すると(ある程度連番ではあるけど)この空間からランダムなIPアドレスがコンテナに割り当てられます。
自身のマシン(以下ホスト)から各コンテナへも相互に通信することができます。


なのでdockerを使って仮想ホストを構築する際は、特に何も考えずにコンテナを起動すれば終わりです。

## 3台のコンテナを起動してみる

最初にディレクトリを作成してから端末を3つ開いてdocker runで3つのコンテナを起動します。

```
[host] $ mkdir /tmp/shared
[host] $ docker run -it --name node1 -h node1 -v /tmp/shared:/shared ubuntu:trusty bash
[host] $ docker run -it --name node2 -h node2 -v /tmp/shared:/shared ubuntu:trusty bash
[host] $ docker run -it --name node3 -h node3 -v /tmp/shared:/shared ubuntu:trusty bash
```

`--name`でコンテナ名を指定しておくことでコンテナの操作を簡単にしておきます。
`-h`でホスト名を指定してどのコンテナなのかわかりやすくしておきます。
`-v`でホスト側のディレクトリを各コンテナにマウントしておくことで、vagrantのSynced Folderのようなものになります。

`--rm`をつけてないのは間違えて落としてしまっても元に戻せるようにしてます。
もしコンテナを終了した場合は以下のコマンドを実行すれば再度コンテナに入ることができます。

```
## node1の場合
[host] $ docker start node1
[host] $ docker attach node1
```

### コンテナのIPアドレスを確認

各コンテナのIPアドレスを確認したい場合はホスト側から`docker inspect`で取得するのが一番良いです。

```
[host] $ docker inspect node1 node2 node3 | jq '.[] | {name: .Name, addr : .NetworkSettings.IPAddress}'
{
  "addr": "172.17.0.2",
  "name": "/node1"
}
{
  "addr": "172.17.0.3",
  "name": "/node2"
}
{
  "addr": "172.17.0.4",
  "name": "/node3"
}
```

単体でIPアドレスだけ取りたければ以下のようにします。

```
[host] $ docker inspect node1 | jq '.[].NetworkSettings.IPAddress'
"172.17.0.2"
```

## コンテナ間とコンテナホスト間で通信する

node1で簡易HTTPサーバを起動してnode2とnode3から通信してみます。

まずはnode1でncを使ってHTTPサーバを起動します。

```
[node1] $ while :; do (echo -e "HTTP/1.0 200 OK\n\nHelloWorld" | nc -l 8080) || break; done
```

node2とnode3からnode1に対してアクセスします。

```
[node2] $ curl -D - 172.17.0.2:8080
HTTP/1.0 200 OK

HelloWorld

[node3] $ curl -D - 172.17.0.2:8080
HTTP/1.0 200 OK

HelloWorld
```

次にホスト側からコンテナ(node1)にアクセスします。

```
[host] $ curl -D - 172.17.0.2:8080
HTTP/1.0 200 OK

HelloWorld
```

次にホスト側でHTTPサーバを立ててnode1からアクセスしてみます。
`docker0`経由で来ていることを明確にするために`172.17.0.1`でlistenしています。

```
[host] $ while :; do (echo -e "HTTP/1.0 200 OK\n\nHelloWorld" | nc -l 172.17.0.1 8080) || break; done

[node1] $ curl -D - 172.17.0.1:8080
HTTP/1.0 200 OK

HelloWorld
```

## ホストとコンテナ間でファイルを共有する

コンテナ起動時に`-v`オプションで`/tmp/shared`をマウントしているのでホスト側から`/tmp/shared`以下にファイルをおけば、各コンテナから参照することができます。コンテナ側は`/shared`にマウントされています。

```
[host] $ echo "hello" > /tmp/shared/foo

[node1] $ cat /shared/foo 
hello
[node2] $ cat /shared/foo 
hello
[node2] $ cat /shared/foo 
hello
```

ディレクトリをマウントしていない場合やファイルを一度移動させたりするのが面倒な場合は`docker cp`コマンドを使えばホストとコンテナ間で直接ファイルを移動できます。

node1でファイルを生成してホスト側にコピーします。

```
[node1] $ echo "hello world" > /root/output.txt

[host] $ docker cp node1:/root/output.txt /tmp/ 
[host] $ cat /tmp/output.txt
hello world
```

今度にホスト側のファイルをnode2にコピーします。

```
[host] $ docker cp /tmp/output.txt node2:/root/

[node2] $ cat /root/output.txt
hello world
```

ちょっとしたスクリプトを書けば実行ファイルを配ったりするのが楽です。

```
[host] $ for N in node{1..3}; \
     do docker cp /usr/local/bin/script.sh $N:/usr/local/bin/script.sh; \
   done
```

## 各コンテナに対してコマンドを実行する

いちいち各ノードの端末を開いてコマンドを実行するのは面倒なのでホスト側から操作したいこともあります。
`docker exec`コマンドで各コンテナにアタッチしてコマンドを実行することができます。

```
[host] $ docker exec -it node1 hostname
node1
```

これもスクリプトを書いておけば各コンテナの状態を簡単に確認できます。



```
[host] $ for N in node{1..3}; do \
    echo "==$N=="; \
    docker exec -it $N hostname;\
  done
==node1==
node1
==node2==
node2
==node3==
node3
```

単体のコマンドを実行するというよりも`vagrant ssh`のようにログインして調査したい場合は`docker exec`でbashなどを実行すればだいたいそのような挙動になります。

```
[host] $ docker exec -it node1 bash
```

## 通信をキャプチャする

コンテナの通信は必ず`docker0`を通るのでホスト側からtcpdumpでdocker0をキャプチャすると全ての通信を見ることができます。

```
[host] $ sudo tcpdump -i docker0
```

## ホストを増やす

起動したときのイメージからパッケージのインストールなどの操作をたくさん行った場合、ホストを追加して再度同様のコマンドを実行しなおすのは面倒です。
`docker commit`でコンテナからイメージを作り直し、そのイメージからコンテナを起動することですぐにホストを追加することができます。

node1からイメージを作りnode4を起動する。

```
[host] $ docker commit node1 nodeimage
[host] $ docker run -it --name node4 -h node4 -v /tmp/shared:/shared nodeimage bash 
```

node4のIPアドレスを確認と追加でインストールしていたcurlが入っているか確認する。

```
[host] $ docker inspect node4 | jq '.[].NetworkSettings.IPAddress'
"172.17.0.5"
[host] $ docker exec -it node4 sh -c 'dpkg -l | grep curl'
ii  curl                            7.35.0-1ubuntu2                  amd64        command line tool for transferring data with URL syntax
ii  libcurl3:amd64                  7.35.0-1ubuntu2                  amd64        easy-to-use client-side URL transfer library (OpenSSL flavour)
```