sagantaf

メモレベルの技術記事を書くブログ。

Dockerのネットワークの仕組み

はじめに

前回は、Docker/Kubernetesを扱う上で必要なネットワークの基礎知識ということで、

について解説しました。

リンクは以下です。

sagantaf.hatenablog.com

sagantaf.hatenablog.com


上記の項目を理解しておくことで、Dockerのネットワークの仕組みをスムーズに理解できると思います。特にiptablesとnetwork namespaceの理解は必須です。

本記事では、上記の内容を元に、Dockerのネットワークの仕組みを紐解いていきます。




Dockerのネットワークの概要

Dockerではコンテナが通常のホストマシンと同じようにネットワーク通信ができるように、vethとnamespace、iptablesを使って仮想的なネットワークを1つのホストマシン内部に構築しています。

具体的には、コンテナ1つに対してvethペアを作成し、片方をホストのブリッジとリンクさせ、もう片方をコンテナのnamespaceのインターフェースとすることで、ブリッジを介してコンテナ同士やホストと通信します。

f:id:sagantaf:20191218225457p:plain

では、具体的にDockerのネットワーク構成を確認していきます。


デフォルト設定

Dockerをインストールした直後は、デフォルトで3種類(bridge, host, none)のネットワークが作成されています。何も指定せずにコンテナを起動すると、自動的にbridgeのネットワークが利用されるようになっています。ネットワーク一覧を表示することで確認できます。

$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
5a5c97732d37        bridge              bridge              local
fe0ef46114b8        host                host                local
477421ac0904        none                null                local

上記のようにdocker network コマンドを使うことで、Dockerのネットワーク情報を表示したり、作成したりといった操作ができます。

$ docker network --help
    
Usage:        docker network COMMAND
    
Manage networks

Commands:
  connect     Connect a container to a network
  create      Create a network
  disconnect  Disconnect a container from a network
  inspect     Display detailed information on one or more networks
  ls          List networks
  prune       Remove all unused networks
  rm          Remove one or more networks
    
Run 'docker network COMMAND --help' for more information on a command.


bridgeネットワークの詳細

例えばbridgeネットワークの詳細は以下のようになっています。

$ docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "5a5c97732d3723d6d8d2a69073d5d7f7f03b67554459a08e708c79db517b2046",
        "Created": "2019-12-17T09:14:08.464805258Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

この詳細情報から、サブネットが172.17.0.0/16であることや、オプションとしてIPマスカレードやMTUの設定が適用されていること、ブリッジ名がdocker0として設定されていることがわかります。

ではブリッジdocker0の詳細をifconfigコマンドおよびipコマンドで確認してみます。

$ ifconfig docker0
docker0   Link encap:Ethernet  HWaddr 02:42:89:90:7e:b1
          inet addr:172.17.0.1  Bcast:172.17.255.255  Mask:255.255.0.0
          UP BROADCAST MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

docker0に172.17.0.1IPアドレスが割り当てられていることが分かります。

$ ip link show docker0
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default
    link/ether 02:42:89:90:7e:b1 brd ff:ff:ff:ff:ff:ff


$ ip address show docker0
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    link/ether 02:42:89:90:7e:b1 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever

ipコマンドでは、割り当てられているIPアドレスに加え、そのインターフェースのステータスがDOWNになっていることも分かります。

ここまでのイメージ図は下記です。

f:id:sagantaf:20191218225846p:plain


コンテナ起動時のネットワーク構成

コンテナを起動してホストのネットワーク構成の変化を確認する

では、ここでサンプルでコンテナを起動させます。(test1という名前をつけています。また今回はコンテナの中身は関係ないため軽量なalpineイメージを使っています。)

$ sudo docker run -d --name test1 alpine sleep 6000
1004a5da26cec98fc3c861cb18db921cc7d806efcde9880bbca4169d4cc7a3d2

$ sudo docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
1004a5da26ce        alpine              "sleep 6000"        3 seconds ago       Up 2 seconds                            test1

すると、docker0のステータスがUPに変わり、vethのインターフェースが作成されます。

$ ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc mq state UP mode DEFAULT group default qlen 1000
    link/ether 0e:52:22:18:99:ca brd ff:ff:ff:ff:ff:ff
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
    link/ether 02:42:89:90:7e:b1 brd ff:ff:ff:ff:ff:ff
33: veth3d3f000@if32: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default
    link/ether 8e:35:fe:12:13:a9 brd ff:ff:ff:ff:ff:ff link-netnsid 0

vethの設定の中にはmaster docker0という記載もあるため、docker0とリンクされていることも分かります。このリンクされている情報は、ブリッジの情報を出力してくれるbrctlコマンドからも確認できます。

$ brctl show
bridge name        bridge id                STP enabled        interfaces
docker0                8000.024289907eb1        no                veth3d3f000

interfacesの項目にveth名が記載されています。

なお、ifconfigコマンドでもvethインターフェースを確認できます。

$ ifconfig veth3d3f000
veth3d3f000 Link encap:Ethernet  HWaddr 8e:35:fe:12:13:a9
          inet6 addr: fe80::8c35:feff:fe12:13a9/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:10 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:0 (0.0 B)  TX bytes:828 (828.0 B)

ここまでで分かっている部分のイメージ図です。

f:id:sagantaf:20191218225915p:plain


さて最初の概要にて、 「コンテナ1つに対してvethペアを作成し、片方をホストのブリッジとリンクさせ、もう片方をコンテナのnamespaceのインターフェースとすることで、ブリッジを介してコンテナ同士やホストと通信します。」 と書きました。

つまり、もう1つのvethインターフェースはコンテナのnamespaceにて確認できるはずです。

しかし、network namespaceを確認できるip netnsコマンドを実行しても、何も表示されません。

$ ip netns
(何も表示されない)

これは、デフォルトではipコマンドで見えないところでコンテナのnetns情報が管理されているためです。

ipコマンドの管理下にコンテナのnetns情報をリンクすることで、扱えるようになります。

コンテナのnetnsをipコマンドの管理下にリンクする

まずは、test1のプロセスIDを取得します。

$ docker inspect test1 --format '{{.State.Pid}}'
15329

※環境によってプロセスIDは異なります。

確認したプロセスIDのディレクトリからネットワーク識別子を確認します。

$ sudo ls -la /proc/15329/ns/net
lrwxrwxrwx 1 root root 0 Dec 17 12:29 /proc/15329/ns/net -> net:[4026532283]

このファイルをipコマンドの支配下である/var/run/netnsにリンクさせることで、ipコマンドで扱えるようになります。

$ sudo mkdir -p /var/run/netns
$ sudo ln -s /proc/15329/ns/net /var/run/netns/test1-ns

再度ipコマンドで確認すると表示されます。

$ ip netns
test1-ns

これでtest1コンテナのnetwork namespaceを確認できるようになりました。

コンテナ内部のネットワーク構成を確認する

実際に確認してみます。

確認するためのコマンドは ip net ns exec <netns名> <実行したいipコマンド> です。

$ sudo ip netns exec test1-ns ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
24: eth0@if25: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0

$ sudo ip netns exec test1-ns ip addr show eth0
24: eth0@if25: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

eth0がインターフェースとして存在し、172.17.0.2/16が割り当てられていることが分かります。

また、下記のコマンドから、コンテナのデフォルトゲートウェイ172.17.0.1に設定されていることも分かります。

$ sudo ip netns exec test1-ns ip route
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0  proto kernel  scope link  src 172.17.0.2

ここまでで、コンテナを起動した時に、bridgeネットワークを使ってどのようにネットワークが構築されるのかが分かりました。

f:id:sagantaf:20191218231858p:plain

コンテナ同士の通信を確認する

続いてはコンテナをもう1つ立ち上げ、コンテナ同士で通信ができるかどうかを確認します。

test2のコンテナを起動します。

$ sudo docker run -d --name test2 alpine sleep 6000
e5981078e932e6a038d7b0ecde7dcc972a4f93aefd967b745f9bd2036c6142d1

$ sudo docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED              STATUS              PORTS               NAMES
e5981078e932        alpine              "sleep 6000"        6 seconds ago        Up 5 seconds                            test2
1004a5da26ce        alpine              "sleep 6000"        About a minute ago   Up About a minute                       test1

それぞれのコンテナのIPアドレスを確認します。

$ sudo docker inspect --format "{{.NetworkSettings.Networks.bridge.IPAddress}}" test1
172.17.0.2
$ sudo docker inspect --format "{{.NetworkSettings.Networks.bridge.IPAddress}}" test2
172.17.0.3

上記で確認したtest2のIPアドレスを使って、test1のコンテナからtest2に対して、pingが通るか確認します。

$ sudo docker exec -ti test1 ping -c 3 172.17.0.3
PING 172.17.0.3 (172.17.0.3): 56 data bytes
64 bytes from 172.17.0.3: seq=0 ttl=64 time=0.120 ms
64 bytes from 172.17.0.3: seq=1 ttl=64 time=0.079 ms
64 bytes from 172.17.0.3: seq=2 ttl=64 time=0.077 ms

--- 172.17.0.3 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.077/0.092/0.120 ms

0%のパケットロスで疎通できており、正常に通信できることが分かりました。

ここまでで分かったことをまとめると下図のようになります。

f:id:sagantaf:20191218230044p:plain

外部と通信する時のネットワーク構成

ではホストとコンテナをポートフォワードし、外部から接続できるようにしてみます。

$ sudo docker run -d --name test3 -p 12345:8888 alpine sleep 6000
8d3b2bb5b4f87c6fd4d02c237f219f0ed8765411679972dd93188a593c20d056

$ sudo docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                     NAMES
8d3b2bb5b4f8        alpine              "sleep 6000"        11 seconds ago      Up 9 seconds        0.0.0.0:12345->8888/tcp   test3
e5981078e932        alpine              "sleep 6000"        12 minutes ago      Up 12 minutes                                 test2
1004a5da26ce        alpine              "sleep 6000"        14 minutes ago      Up 14 minutes                                 test1

このポートフォワードの設定は、iptablesのNAT変換およびパケットフィルタリングにて実現されています。

ということで、iptablesの設定を確認します。iptalblesは最初にnatの設定が適用され、その後filter(パケットフィルタリング)の設定が適用されます。

まずはnatテーブルを確認します。

$ sudo iptables -t nat -L DOCKER
Chain DOCKER (2 references)
target     prot opt source               destination
RETURN     all  --  anywhere             anywhere
DNAT       tcp  --  anywhere             anywhere             tcp dpt:12345 to:172.17.0.4:8888

設定通り、ホストの12345ポートに届いた通信は、DNATとして 172.17.0.4:8888 への通信に変換されることが分かります。

次にfilterテーブルです。

$ sudo iptables -L DOCKER
Chain DOCKER (1 references)
target     prot opt source               destination
ACCEPT     tcp  --  anywhere             172.17.0.4  tcp dpt:8888

こちらも設定通り、172.17.0.4のポート8888に対する通信がACCEPTになっています。

ここまでの構成を改めてまとめると下図のようになります。

f:id:sagantaf:20191218231924p:plain

これでDockerのデフォルトのネットワーク構成に対する理解が深まったかと思います。


独自ネットワークを作成

次は、独自のネットワークを作成する方法を見ていきます。 デフォルトのdocker0のネットワークは明示的にIPアドレスを指定できず、Docker側で自動的に決められてしまいます。もしIPアドレスを指定したい場合は、自分でネットワークを作成する必要があります。

docker networkコマンドでサブネットと名前を指定して作成できます。 ここではtestnet1という名前をつけて作成しています。

$ docker network create --subnet 172.21.0.0/16 --attachable testnet1

--attachableのオプションをつけることで、後から作成したコンテナに紐付けることができるようになります。細かい使い方は公式ページにまとまっています。

docker network create | Docker Documentation

では作成されたかどうかを確認します。

$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
659876731c81        bridge              bridge              local
01c446104fae        host                host                local
41c4e357056e        none                null                local
cae34612a700        testnet1            bridge              local

testnet1というネットワークが追加されていることがわかりました。

このネットワークを使うには、コンテナ起動時に--network--ipのオプションを付ける必要があります。

$ docker run -d --network testnet1 --ip 172.21.0.2 --name test alpine sleep 6000
$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED              STATUS              PORTS               NAMES
c888cb905dcf        alpine              "sleep 6000"        About a minute ago   Up About a minute                       test

$ docker exec -ti test hostname -i
172.21.0.2

ただし、作成したネットワークのコンテナが、他のネットワークやマシンの外のネットワークと通信をするためには、bridgeネットワークのdocker0のようにブリッジなどの設定が必要になります。


マルチホストによるネットワーク構築

コンテナを起動するときに-pオプションを使ってポートフォワードの設定をすることで、iptablesが設定され、外部と通信できることを確認しました。

ただ、毎回コンテナを起動するときにポートフォワードを設定することは、数多くのコンテナを立ち上げたり、自動でコンテナを起動させたい場合などに、設定や管理が煩雑になります。

そこで、サードパーティ製のネットワーク構築OSSを利用することで、ポート管理をせずに、自動でネットワークの作成削除など管理をしてくれます。

サードパーティ製のネットワーク構築OSSは数多く存在しており、flannelやcalico、weave netが有名です。例えば、flannelでは、VXLANを利用してオーバレイネットワークを実現し、etcd(KVS)を使ってネットワーク設定を紐付けられたホスト間で共有することで、マルチホスト間でも重複しないネットワーク設定を使って通信を可能にしています。

自動でネットワークを管理できることは、Kubernetesのネットワークの肝になります。Kubernetesを使っていくのであれば、しっかりと理解しておく必要があります。そのため、詳しい実装に関しては別途まとめていきます。


おわりに

今回はDockerのネットワークの仕組みについてまとめました。まだまだ奥深い部分はあるとは思いますが、ブリッジがあって、vethペアがあって、iptablesがあって、といった基本的な仕組みは把握できたかと思います。

最後の方にも書きましたが、次はKubernetesのネットワークの仕組みを理解してまとめたいと思っています。

参考文献

Docker/Kubernetes 実践コンテナ開発入門

Docker/Kubernetes 実践コンテナ開発入門

Docker

Docker