はじめに
こんにちは。澤井です!
Dockerを触る機会が多くなりました。
本記事はDockerのブリッジネットワークにおけるコンテナ間通信について記載しています。
具体的にイメージできるようにネットワークインターフェースを流れるパケットも観察したいと思います。
ネットワークインターフェースは、ネットワークインターフェースカード(NIC)やネットワークアダプタと呼ぶこともあります。
以降、本記事ではネットワークインターフェースを単にインターフェースと記載します。
目次
検証環境
本記事は以下の環境で確認しました。
- Amazon EC2 Ubuntu 20.04.4 LTS (Focal Fossa)
準備
本記事を読むにあたり、知っておくと良い内容を簡単にまとめます。
- 本記事ではネットワークドライバが
bridge
のDockerネットワークをブリッジネットワークと記載します - Dockerでドライバを指定せずにネットワークを作成(
docker network create
)するとブリッジネットワークが作成されます - ブリッジネットワークは仮想ブリッジを使用します(ブリッジはOSI参照モデルのデータリンク層における通信を制御します)
- Dockerは仮想イーサネット(
veth
)を使用します(仮想イーサネットは両端にインターフェースを持ちます) - ユーザーが作成したブリッジネットワークをユーザー定義ブリッジネットワークと呼びます
本記事はユーザー定義ブリッジネットワークに配置したコンテナ間の通信について記載します。
ユーザー定義ブリッジネットワークを作成
ユーザー定義ブリッジネットワークを作成する前の状態
まずはユーザー定義ブリッジネットワークを作成する前のホスト
のインターフェースを確認します。
以降、コマンド発行結果で本記事に直接関係のない内容は省略しています。
// ホストのインターフェースを確認
host$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
inet 127.0.0.1/8 scope host lo
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc fq_codel state UP group default qlen 1000
inet 10.0.0.2/24 brd 10.0.0.255 scope global dynamic eth0
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
Dockerをインストールした直後のホスト
のインターフェースは3つです。
1: lo
: ループバックのインターフェース(172.0.0.1
)2: eth0
:ホスト
のデフォルトのインターフェース(10.0.0.2
)- デフォルトゲートウェイ(
10.0.0.1
)と接続されたインターフェース
- デフォルトゲートウェイ(
4: docker0
:Dockerインストール時に自動で作成されるデフォルトのブリッジネットワークのインターフェース(172.17.0.1
)
ではユーザー定義ブリッジネットワークを作成します。
最終的なネットワーク図
本記事で作成するユーザー定義ブリッジネットワークは以下のようになります。
ブリッジネットワークを作成
sample
という名前でブリッジネットワークを作成します。
host$ sudo docker network create sample
29faca36a6b4...
sample
ネットワークが作成されました(ネットワークアドレスは172.18.0.0/16
)。
host$ sudo docker network inspect sample
[
{
"Name": "sample",
"Id": "29faca36a6b4...",
"Driver": "bridge",
"IPAM": {
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
}
]
},
}
]
sample
ネットワークの作成に伴って仮想ブリッジが作成されます。
ホスト
に仮想ブリッジ(br-29faca36a6b4
)が作成されますホスト
に仮想ブリッジのインターフェース(br-29faca36a6b4
)が作成されます
※ 例ではブリッジ名とブリッジのインターフェース名が同じです。
上記を図にすると以下のようになります。
ブリッジを確認します。
// brctlはブリッジを表示する
host$ sudo brctl show
bridge name bridge id STP enabled interfaces
br-29faca36a6b4 8000.0242846801c8 no
br-29faca36a6b4
というブリッジが作成されています。
※ この時点ではinterfaces
がno
になっています(理由は後述します)。
ホスト
のインターフェースを確認します。
host$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
inet 127.0.0.1/8 scope host lo
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc fq_codel state UP group default qlen 1000
inet 10.0.0.2/24 brd 10.0.0.255 scope global dynamic eth0
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
5: br-29faca36a6b4: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
inet 172.18.0.1/16 brd 172.18.255.255 scope global br-29faca36a6b4
br-29faca36a6b4
インターフェース(172.18.0.1
)が追加されています。
1つめのコンテナを追加
sample
ネットワークにコンテナを追加します。
コンテナ名はcontainer1
にします。
host$ sudo docker run -itd --name container1 --net sample ubuntu:latest
ここまでを図にすると以下のようになります。
ホスト
のインターフェースを確認します。
host$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
inet 127.0.0.1/8 scope host lo
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc fq_codel state UP group default qlen 1000
inet 10.0.0.2/24 brd 10.0.0.255 scope global dynamic eth0
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
5: br-29faca36a6b4: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
inet 172.18.0.1/16 brd 172.18.255.255 scope global br-29faca36a6b4
7: veth7a0c3b1@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-29faca36a6b4 state UP group default
inet6 fe80::10b9:a1ff:feec:976/64 scope link
ホスト
にveth7a0c3b1@if6
インターフェースが追加されています。
このインターフェースは、ブリッジ(br-29faca36a6b4
)とコンテナ(container1
)を接続する仮想イーサネットのブリッジ側インターフェースです。
次にコンテナ
内のインターフェースを確認します。
host$ sudo docker exec -it container1 /bin/bash
container1# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
inet 127.0.0.1/8 scope host lo
6: eth0@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
inet 172.18.0.2/16 brd 172.18.255.255 scope global eth0
eth0@if7
インターフェースが作成されています。
eth0@if7
はブリッジ側のveth7a0c3b1@if6
と接続されています。
eth0@if7
の@if7
はホストの7
のインターフェースであるveth7a0c3b1
と接続しているという意味です。
逆にveth7a0c3b1@if6
はコンテナの6
のインターフェースeth0
と接続しているという意味です。
コンテナのルートテーブルも確認します。
ゲートウェイに172.18.0.1
が設定されています(172.18.0.1
は、ブリッジのbr-29faca36a6b4
インターフェースに付与されています)。
container1# route -ne
// FlagsのGがゲートウェイを表す
Kernel IP routing table
Destination Gateway Genmask Flags MSS Window irtt Iface
0.0.0.0 172.18.0.1 0.0.0.0 UG 0 0 0 eth0
172.18.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0
2つめのコンテナを追加
コンテナを追加します。
コンテナ名はcontainer2にします。
host$ sudo docker run -itd --name container2 --net sample ubuntu:latest
ここまでを図にすると以下のようになります。
ホスト
のインターフェースを確認します。
host$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
inet 127.0.0.1/8 scope host lo
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc fq_codel state UP group default qlen 1000
inet 10.0.0.2/24 brd 10.0.0.255 scope global dynamic eth0
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
5: br-29faca36a6b4: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
inet 172.18.0.1/16 brd 172.18.255.255 scope global br-29faca36a6b4
7: veth7a0c3b1@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-29faca36a6b4 state UP group default
inet6 fe80::10b9:a1ff:feec:976/64 scope link
9: veth2a5f404@if8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-29faca36a6b4 state UP group default
inet6 fe80::38aa:3bff:fee3:ed9b/64 scope link
ホスト
にveth2a5f404@if8
インターフェースが追加されています。
これはブリッジとコンテナ(container2)を接続する仮想イーサネットのブリッジ側インターフェースです。
次にコンテナ
内のインターフェースを確認します。
host$ sudo docker exec -it container2 /bin/bash
container2# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
inet 127.0.0.1/8 scope host lo
8: eth0@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
inet 172.18.0.3/16 brd 172.18.255.255 scope global eth0
コンテナにeth0@if9
インターフェースが作成されています。
eth0@if9
はブリッジ側のveth2a5f404@if8
と接続されています
コンテナのルートテーブルも確認します。
ゲートウェイに172.18.0.1
が設定されています(172.18.0.1
は、ブリッジのbr-29faca36a6b4
インターフェースに付与されています)。
container2# route -ne
Kernel IP routing table
Destination Gateway Genmask Flags MSS Window irtt Iface
0.0.0.0 172.18.0.1 0.0.0.0 UG 0 0 0 eth0
172.18.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0
コンテナ追加後のブリッジを確認
再掲になりますが、以下のようなユーザー定義ブリッジネットワークが作成されました。
コンテナを2つ追加した後のブリッジbr-29faca36a6b4
の状態を確認してみます。
host$ brctl show br-29faca36a6b4
bridge name bridge id STP enabled interfaces
br-29faca36a6b4 8000.0242c107547f no veth2a5f404
veth7a0c3b1
コンテナを2つ追加したのでブリッジとコンテナを繋ぐ2つの仮想イーサネットが作成されました。
interfaces
列に仮想イーサネットのブリッジ側インターフェースが追加されています(veth2a5f404
、veth7a0c3b1
)。
ここまでで、container1とcontainer2が通信できるようになります。
実際にインターフェースの通信をキャプチャしてデータの流れを確認します。
コンテナ間の通信を確認
コンテナ間の通信を確認するためにcontainer1(172.18.0.2
)からcontainer2(172.18.0.3
)にpingを発行してパケットの流れをみます。
今回は分かりやすさのためにIPアドレスを使用しますが、ユーザー定義ブリッジネットワークはDockerデーモンによって内蔵DNSサーバ機能が提供されています。
ですのでコンテナ名でも通信可能です。
pingによって発行されたICMP echo requestパッケットは以下経路でcontainer2に到達します。
- container1の
eth0@if7
インターフェース - ブリッジ側のcontainer1に対応する
veth7a0c3b1@if6
インターフェース - ブリッジの
br-29faca36a6b4
インターフェース - ブリッジ側のcontainer2に対応する
veth2a5f404@if8
インターフェース - container2の
eth0@if9
インターフェース
ICMP echo replyは逆の流れでcontainer2からcontainer1に到達します。
1. container1のeth0@if7
インターフェースをキャプチャ
container1# tcpdump -tnl -i eth0@if7
IP 172.18.0.2 > 172.18.0.3: ICMP echo request, id 1, seq 1, length 64
IP 172.18.0.3 > 172.18.0.2: ICMP echo reply, id 1, seq 1, length 64
2. ブリッジ側のveth7a0c3b1
インターフェースをキャプチャ
host$ sudo tcpdump -tnl -i veth7a0c3b1 icmp
IP 172.18.0.2 > 172.18.0.3: ICMP echo request, id 1, seq 1, length 64
IP 172.18.0.3 > 172.18.0.2: ICMP echo reply, id 1, seq 1, length 64
3. ブリッジのbr-29faca36a6b4
インターフェースをキャプチャ
host$ sudo tcpdump -tnl -i br-29faca36a6b4 icmp
IP 172.18.0.2 > 172.18.0.3: ICMP echo request, id 2, seq 1, length 64
IP 172.18.0.3 > 172.18.0.2: ICMP echo reply, id 2, seq 1, length 64
4. ブリッジ側のveth2a5f404
インターフェースをキャプチャ
host$ sudo tcpdump -tnl -i veth2a5f404 icmp
IP 172.18.0.2 > 172.18.0.3: ICMP echo request, id 1, seq 1, length 64
IP 172.18.0.3 > 172.18.0.2: ICMP echo reply, id 1, seq 1, length 64
5. container2のeth0@if9
インターフェースをキャプチャ
container2# tcpdump -tnl -i eth0@if9 icmp
IP 172.18.0.2 > 172.18.0.3: ICMP echo request, id 1, seq 1, length 64
IP 172.18.0.3 > 172.18.0.2: ICMP echo reply, id 1, seq 1, length 64
ブリッジを経由したcontainer1(172.18.0.2
)とcontainer2(172.18.0.3
)の通信を確認できました。
コンテナと外部の通信を確認
最後にブリッジネットワークと外部の通信について確認します。
外部との通信はホスト
が提供するiptables
のIPマスカレード(NAT)を使用して実現しています(IPマスカレードはNATのLinuxの実装です)。
コンテナから外部に送信される際にIPマスカレード
によって、ホスト
のIPアドレスに変換されてeth0
から送信されます。
IPマスカレードはDockerが自動でiptablesに設定します。
iptables
に設定されたIPマスカレードを確認します(必要な部分のみ掲載しています)。
host$ sudo iptables -nL -t nat
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- 172.18.0.0/16 0.0.0.0/0
上記のとおり172.18.0.0/16
から外部への通信にはMASQUERADEが設定されています。
外部との通信を確認
container1から93.184.216.34
(example.org)にpingを発行してsampleネットワークと外部との通信を確認します。
パケットは以下の経路を通ります
- container1の
eth0@if7
インターフェース - ブリッジ側のcontainer1に対応する
veth7a0c3b1@if6
インターフェース - ブリッジの
br-29faca36a6b4
インターフェース - ホストの
eth0
インターフェース
ICMP echo replyは逆の順序でcontainer1のeth0@if7
インターフェースに届きます。
tcpdump
でそれぞれのインターフェースの通信をキャプチャします。
1. container1のeth0@if7
インターフェースをキャプチャ
container1# tcpdump -tnl -i eth0 icmp
IP 172.18.0.2 > 93.184.216.34: ICMP echo request, id 5, seq 1, length 64
IP 93.184.216.34 > 172.18.0.2: ICMP echo reply, id 5, seq 1, length 64
2. ブリッジ側のcontainer1に対応するveth7a0c3b1@if6
インターフェースをキャプチャ
host$ sudo tcpdump -tnl -i veth7a0c3b1 icmp
IP 172.18.0.2 > 93.184.216.34: ICMP echo request, id 5, seq 1, length 64
IP 93.184.216.34 > 172.18.0.2: ICMP echo reply, id 5, seq 1, length 64
3. ブリッジのbr-29faca36a6b4
インターフェースをキャプチャ
host$ sudo tcpdump -tnl -i br-29faca36a6b4 icmp
IP 172.18.0.2 > 93.184.216.34: ICMP echo request, id 8, seq 1, length 64
IP 93.184.216.34 > 172.18.0.2: ICMP echo reply, id 8, seq 1, length 64
4. ホストのeth0インターフェースをキャプチャ
IPマスカレードによってIPが10.0.0.2に書き換えられて送信されています。
host$ sudo tcpdump -tnl -i eth0 icmp
IP 10.0.0.2 > 93.184.216.34: ICMP echo request, id 9, seq 1, length 64
IP 93.184.216.34 > 10.0.0.2: ICMP echo reply, id 9, seq 1, length 64
上記のとおりホストのeth0
インターフェースから、ホストのIPアドレス(10.0.0.2
)に変換されて送受信されています。
(今回はAmazon EC2で確認しているために、IPマスカレードによって変換されたアドレスが10.0.0.2
といプライベートアIPドレスになっていますが、
環境によっては、この時点でグローバルIPになっている場合もあります。)
以上、ざっとブリッジネットワークの通信を観察してきました。
まとめ
実際に作成されたリソースを確認することでネットワークに関する理解が深まりました。 参考になれば幸いです。