複数台のホストを使ったテストをやりたいときに実際にホストを複数台用意するのは面倒なので、自身が使用しているのマシン上で仮想的に複数台のホストを用意したいと思います。
多くの場合は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)