UZABASE Tech Blog

株式会社ユーザベースの技術チームブログです。 主に週次の持ち回りLTやセミナー・イベント情報について書きます。

マルチホストでのDocker Container間通信 第1回: Dockerネットワークの基礎

こんにちは。SPEEDA開発チームの鈴木です。
調べてみるとなかなか興味深い技術であるマルチホストでのDocker Conainer間通信。
これをどのように実現しているのか説明したいと思います。 が、その前に今回の投稿では、まず基礎知識的な話としてDockerのネットワークについて順を追って説明をします。

Dockerのネットワーク

docker0

Dockerをインストールしたあと、ifconfigip addr showするとdocker0なるものが表示されるようになるので、気になっていた人もいるかと思います。
これは一体何者なのでしょうか。

[kenji@arch ~]$ ifconfig
docker0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        inet 172.17.0.1  netmask 255.255.0.0  broadcast 0.0.0.0
        ether 02:42:fb:28:ee:2d  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

 
このdocker0はDockerによって自動的に作成された仮想ブリッジで、Docker Container同士をつなぐためのものです。

f:id:kenji-suzuki:20170707184308p:plain

docker0に接続されているネットワーク・インタフェース

何かしらDocker Containerを起動した状態で、ブリッジを管理するbrctlコマンドを実行してみると次のようにdocker0に接続されているネットワーク・インタフェースが表示されます。 (brctlbridge-utilsパッケージに含まれています)

[kenji@arch ~]$ brctl show
bridge name     bridge id               STP enabled     interfaces
docker0         8000.0242fb28ee2d       no              veth269de4e
                                                        veth87e3222
                                                        vethbcff718

そしてこれがインタフェースをgrep vethしてみた結果です。たしかにこのネットワーク・インタフェースは存在するようですね。

[kenji@arch ~]$ ip addr show | grep veth
9: vethbcff718@if8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default 
11: veth269de4e@if10: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default 
15: veth87e3222@if14: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default 

 
ちなみにDocker Containerをすべて落としてみるとこうなります。docker0には何も接続されていないようですね。

[kenji@arch ~]$ brctl show
bridge name bridge id       STP enabled interfaces
docker0     8000.0242fb28ee2d   no  

さきほど存在したネットワーク・インタフェースもいなくなっています。

[kenji@arch ~]$ ifconfig | grep veth
[kenji@arch ~]$ 

 
なるほど。じゃあこのvethXXXがDocker Containerのネットワーク・インタフェースなのか、というとちょっと違うのですが、正確なところを説明する前にそもそもvethって何なのかを説明します。

veth

vethは virtualethernet device、つまり仮想的なネットワーク・インタフェースです。
vethは常にペアで作成され、このペア間で通信が行えます。
(vethペアの片割れをveth peerといいます)

f:id:kenji-suzuki:20170707192057p:plain

 
vethペアは次のようなコマンドで作成できます。

[kenji@arch ~]$ sudo ip link add name veth1 type veth peer name veth1-peer

ip link showで確認してみると、vethのペアが作成されていることが分かります。

[kenji@arch ~]$ ip link show | grep veth
18: veth1-peer@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
19: veth1@veth1-peer: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000

 
さて、ここで一つの疑問が生じます。

「Docker Containerを起動したときに作成されるvethって一つじゃね?」

Containerの起動後にネットワーク・インタフェースを表示してみると、たしかにそう見えます。

[kenji@arch ~]$ docker ps --format "table {{.ID}} {{.Names}}"
CONTAINER ID NAMES
b71e096208c2 kong
7add408c9775 kong-database

# Containerが2つなので4つvethが表示されるはず?
[kenji@arch ~]$ ip link show | grep veth
17: vethb96b887@if16: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default 
21: veth9c7e462@if20: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default 

 
この疑問に答えるキーワードがネットワーク名前空間です。

ネットワーク名前空間

Dockerのことを勉強すると必ずといっていいほど説明されていますが、Linuxはカーネルが扱う様々なリソースをある単位でまとめて分離する仕組みを持っています。 この仕組みを名前空間といいます。 分離されたリソースはその名前空間に属するプロセス以外からは直接見えなくなります。

例えばunshareコマンドでUTS名前空間を新たに作成した場合、その名前空間を作成した側と別のホスト名を持つことができます。

# 1. 名前空間のエントリを表示
kenji@test1:~$ sudo ls -l /proc/*/ns | grep uts | awk '{print $11}' | sort | uniq
uts:[4026531838]

# 2. UTS名前空間を新たに作成し、その名前空間でbashを実行
kenji@test1:~$ sudo unshare -u /bin/bash
root@test1:~# 

# 1,2 とは別のシェルで名前空間のエントリを表示すると一つ増えてることが分かる
kenji@test1:~$ sudo ls -l /proc/*/ns | grep uts | awk '{print $11}' | sort | uniq
uts:[4026531838]
uts:[4026532203]

# 1,2 のシェルでホスト名を変更
root@test1:~# hostname updated-host
root@test1:~# hostname
updated-host

# 1,2 とは別のシェルでホスト名を表示すると、こちらには上記の変更が反映されないことが分かる
kenji@test1:~$ hostname
test1

 

この名前空間には色々種類があるのですが、ネットワークに対する名前空間がネットワーク名前空間です。
そして前述のvethペアの片割れは、作成したネットワーク名前空間に移動させることができるのです。
つまりvethペアによってネットワーク名前空間同士を仮想的なケーブルでつなぐことができます。

f:id:kenji-suzuki:20170709234136p:plain

移動させたvethは移動元からは見えなくなります。これがDocker Containerのvethの片割れが見えなかった理由です。
ここまでを踏まえるとDockerのネットワークが次のようになっていることが理解できるでしょう。

f:id:kenji-suzuki:20170710000413p:plain

更に付け加えると、veth(peer)はDocker Container内ではeth0として見えるようになっています。 またDockerによりveth(peer)にIPアドレスが割り当てられます。

f:id:kenji-suzuki:20170710132020p:plain

Docker Containerのネットワーク名前空間を参照する

ネットワーク名前空間の一覧はip netnsコマンドで参照でき、ip netns exec ネットワーク名前空間名コマンドを使うと指定したネットワーク名前空間で任意のコマンドが実行できるのですが、Docker Containerのネットワーク名前空間はDockerにより隠蔽されています。
しかし以下2つの情報を知っていれば、強引に参照することができます。

  1. Docker Containerのネットワーク名前空間は/proc/Docker ContainerのPID/ns/netに割り当てられる。
  2. ip netnsコマンドで表示されるネットワーク名前空間は、/var/run/netnsディレクトリ配下のものである。

どういうことかというと、Docker ContainerのPIDを調べてネットワーク名前空間を見つけて、/var/run/netnsへのリンクを貼るのです。
試してみたいけど面倒くさいという方のためにシェルを用意してあります(github)。
プログラムの性質上、実行にroot権限が必要になるので、気になるようでしたら内容をご確認ください。

git clone https://github.com/pujoheadsoft/docker-netns.git
cd docker-netns
chmod +x docker-netns.sh 

# 実行例
~/docker-netns$ sudo ./docker-netns.sh visible
container's network namespace to visible, you can show container's nemespace by [ip netns] command.
kenji@master:~/docker-netns$ ip netns
bfe34b8a7972
2ec174520fda

# vethとvethに対応するContainerを表示
kenji@test1:~/docker-netns$ sudo ./docker-netns.sh showveth
VETH                 CONTAINER ID         NAMES                         
veth2753b2f          51f82f5e9712         /mywordpress_wordpress_1      
vetha334043          4939c68f1740         /mywordpress_db_1  

# ContainerのIPを表示(リソースはcontainer側のものですが、コマンドはホスト側のものを使えるので、IPを表示するコマンドがないContainerでもIPを表示できます)
kenji@test1:~/docker-netns$ sudo ./docker-netns.sh showip
CONTAINER ID    IP                                       NAMES
51f82f5e9712    inet 172.18.0.3/16 scope global eth0     /mywordpress_wordpress_1
4939c68f1740    inet 172.18.0.2/16 scope global eth0     /mywordpress_db_1

ルーティング

Dockerにより、Docker Containerから外部のネットワークに接続できるようIPマスカレードが行われるようなルーティングになっています。
これはIPテーブルを参照してみると分かります。

[kenji@arch ~]$ ip addr show | grep docker
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    inet 172.17.0.1/16 scope global docker0

# iptablesの情報を参照
[kenji@arch ~]$ sudo iptables-save 
# Generated by iptables-save v1.6.1 on Mon Jul 10 01:15:10 2017
*nat
# --中略--
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER

# これがIPマスカレードの部分。Dockerのネットワーク(172.17.0.0/16)かつ、
# docker0以外のインタフェースからパケットが出力された場合IPマスカレードする
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A DOCKER -i docker0 -j RETURN

COMMIT
# --中略--

これまでの図に加えると、このような感じになりますね。

f:id:kenji-suzuki:20170710131809p:plain

以上がDockerのネットワークの説明になります。
Docker Composeを使った場合、bridgeやルーティングはまたこれとは少し違ったものになるのですが、そこは割愛します。
次回は、いよいよマルチホストでのDocker Container間通信の話をします。


株式会社ユーザベースでは、DockerやDocker関連技術に興味のあるエンジニアを大募集中です!

シリーズ

第1回: マルチホストでのDocker Container間通信 第1回: Dockerネットワークの基礎 (当記事)
第2回: マルチホストでのDocker Container間通信 第2回: Port Forwarding と Overlay Network
第3回: マルチホストでのDocker Container間通信 第3回: Kubernetesのネットワーク(CNI, kube-proxy, kube-dns)