Docker Compose入門 (3) ~ネットワークの理解を深める~

前回(第2回)は、Apache httpdのCompose環境を例に、シンプルな開発環境の構築方法を学びました。今回取り上げるのは、Dockerに対応したアプリケーションを考える上で欠かせない、コンテナ間のDocker内部ネットワークと名前解決についてです。

名前空間(namespace)の分離とコンテナ間通信

通常のLinuxホスト上では、複数のプロセスがポートを重複して開くことはできません。たとえば、ApacheとNginxはデフォルトでTCPポート80番を使用しますが、それぞれが同じTCPポート80番を同時に使用できません。

一方で、複数のプロセスをコンテナとして動かせば、お互いの動作には何ら影響がありません。コンテナとして実行すると、PIDなどの名前空間が隔離(isolate)され、お互いのプロセスが見えない状態で動作するからです。それだけでなく、コンテナごとにネットワークも隔離されるため、コンテナ内で各々のプロセスがポートをリッスンしていたとしても、(ホスト側で使用するポート番号が重複しなければ)お互いに影響を与えず起動し続けられます。

これはDockerだけに限らず、Docker Composeを使う場合も同じです。Docker Composeを使えば、コンテナと同じように、1つのホスト上でも複数のアプリケーションを実行できます。Docker Composeでアプリケーションを実行すると、そのプロセスのPID等の名前空間分離は勿論、ネットワークやボリュームもアプリケーションごとに分離できるからです。

Docker Composeを使えば、docker-compose.yamlファイルに記述した、同じComposeのプロジェクト内に存在するコンテナ間(サービス)で通信可能なネットワークを作成します。これはユーザ定義・ブリッジ・ネットワークと呼ばれ、コンテナ間での通信を行うだけでなく、インターネットなどホスト上にあるネットワークとの通信もやりとり(ブリッジ)します。

それでは、Composeにおけるネットワークを触れる前に、コンテナのネットワーク機能について理解しておきましょう。

Dockerのネットワークモデル

改めて、Dockerコンテナは、デフォルトではコンテナ外部のネットワークと通信できません。コンテナの特徴の1つである隔離(isolate)する対象とは、プロセスやファイルシステムだけでなく、ネットワークに対しても同様です。そのため、「docker run」コマンドで複数のコンテナを実行しても相互に通信できない場合があります。意図した通りにコンテナ間で通信を行うには、適切な理解に基づく設定が必要です。

何も考えずに「docker run」コマンドを実行すると、通常は「bridge0」という名前のブリッジ・ネットワークに接続します。これがデフォルトのブリッジ・ネットワーク(bridge network)です。ブリッジとは、一般的なネットワークの機能であり、別々のネットワーク・セグメント間で通信できるようにする働きがあります。Dockerにおけるブリッジ・ネットワークも似たような概念で、同じブリッジ・ネットワーク上に接続しているコンテナは、お互いに通信できます。そのため、コンテナ起動時、コンテナが接続するネットワークを指定しないコンテナ間では、お互いにコンテナが持つIPアドレスを使っては通信できます。

しかしここで重要なのは、コンテナのIPアドレスが変わってしまった場合の対処です。せっかくコンテナを導入し、簡単にアプリケーションをスケールアウトしやすくなったのに、コンテナ間の通信でIPアドレスが必要であれば、これまでのサーバ上での利用と何ら変わらないでしょう。それどころか、アプリケーションのプロセス単位ごとに1コンテナという概念をも忠実に守るのであれば、通常のサーバ利用よりも管理や設定が大変になりがちです。むしろそれなら、コンテナを導入しないほうが楽でしょう。

そもそもDockerのコンセプトは、アプリケーションの開発・移動・実行を簡単にするためのソフトウェアです。そのために、Dockerにはコンテナ間で通信したり処理の負荷分散したりするため、ネットワーク内部で利用可能な「コンテナ名」「IPアドレス」の名前解決(サービス・ディスカバリ)を行う仕組みが標準で搭載されています。つまり、コンテナ間の通信は、コンテナのIPアドレスを指定するのではなく、通信先をコンテナ名で指定できるのです。そうしておけば、スケールできる環境においてもIPアドレスを意識する必要がなくなります。コンテナ間の通信や負荷分散が容易になるのは、コンテナ導入の大きなメリットです。

ただし、注意が必要なのはデフォルトの「bridge0」という名称のブリッジ・ネットワークでは、この名前解決機能がありません。コンテナ間の通信でサービス・ディスカバリを有効化するには、自分でブリッジ・ネットワークを作成する必用があります。これをDockerではUser Defined Bridge Network(ユーザ定義ブリッジ・ネットワーク)と呼びます。

また、Dockerコンテナが認識できるネットワークは、他にもあります。以降では、Dockerのネットワークモデルをみていきます。

Docker標準の3つのネットワークモデル

Dockerは初期状態で3つのネットワークを持っています。Dockerのネットワークを確認するコマンドは「docker network ls」です。こちらを実行しましょう。

# docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
e5dd457767fd        bridge              bridge              local
8836c0e5c268        host                host                local
acdbf891b515        none                null                local

表示結果の「NAME」列がネットワーク名で、「DRIVER」列はネットワーク・ドライバ、「SCOPE」列は対象ネットワークの適用範囲です(localは自分のホスト上のみが対象)。

  • 「bridge」はLinuxのブリッジ機能を使うブリッジ・ネットワークで、コンテナ内からの通信はインターネット側にルーティングされます。
  • 「host」はコンテナがホスト側のネットワーク・インターフェースを共有するものです。ネットワークが隔離しないため、コンテナ内でプログラムがポートをリッスンすると、そのままインターネット側と通信可能になるモードです。
  • 「none」はコンテナにネットワーク・インターフェースを持たせない機能です。そのため、コンテナは内外の通信ができません。

コンテナ起動時、どのネットワークに接続するかは「--net」オプションで指定します。たとえば「host」ネットワークに接続するには、次のように実行します。

docker run -itd --net host --name hostnginx nginx:alpine

通常のnginxのコンテナであれば、ホスト外からの通信をするには「-p」オプションの指定が必須でした。ですが、hostネットワークを使っていますので、コンテナとして実行しているnginxがリッスンしているポート80が、特に何の制限なく、ホスト外からも通信できます。

hostネットワークに接続しているコンテナは、起動時に-pオプションや-Pオプションでポートの割り当てをしなくても通信可能です。ブラウザでサーバのIPアドレスを開くと、Nginxの初期画面を表示できます。

同様に、「none」ネットワークを使ってコンテナを起動しましょう。

# docker run -it --net none --rm nginx:alpine ip a
(省略)
1: lo:  mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever

ブリッジネットワークやホストネットワークではeth0という仮想的なインターフェースが表示されますが、noneネットワークでは外部と通信するインターフェースそのものが表示できません。

また、pingを実行しようとしても、外部との通信はできません。コンテナ内で「1.1.1.1」への通信を試みても、次のように"ネットワークに疎通しない"という応答しかありません。

# docker run -it --net none nginx:alpine ping -c 3 1.1.1.1
PING 1.1.1.1 (1.1.1.1): 56 data bytes
ping: sendto: Network unreachable

このように3つのネットワークを、コンテナの必要性に応じて使い分けできます。

ブリッジ・ネットワークの挙動をコマンドを実行して理解しよう

それでは、コンテナ内でコマンドを実行しながら挙動を理解しましょう。

まずはじめに、デフォルトのbridge0ブリッジ・ネットワークの挙動を確認します。Alpine Linuxのコンテナを実行しましょう。

docker run -itd --name alpine1 alpine /bin/sh
docker run -itd --name alpine2 alpine /bin/sh

この状態でdocker psコマンドを実行すると、次のように2つのAlpine Linuxを使ったコンテナが実行中と分かります。

# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
cc96f77523e8        alpine              "/bin/sh"           2 seconds ago       Up 1 second                             alpine2
66b2da8c974e        alpine              "/bin/sh"           3 minutes ago       Up 3 minutes                            alpine1

この時、各コンテナのIPアドレスを確認するには「docker network inspect」コマンドを使います。ブリッジ・ネットワークのネットワーク情報を確認するには、次のように実行します。

# docker network inspect bridge
(省略)
        "Containers": {
            "66b2da8c974eae0788c0d404a5032fe07f8639b820cf5840c7b8236211cc2bae": {
                "Name": "alpine1",
                "EndpointID": "3a22242a0e490290f5904561144c22192bd4c2b0b0103bd970f1036b4ca78445",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            },
            "cc96f77523e83694324c11442f188820b4f6c1d5eaae7b9d8f41fc993acf8211": {
                "Name": "alpine2",
                "EndpointID": "7f1e50e69c7fcdb4381d27245d3e4e4cf8a712001833ddc9181f2652bf612f0c",
                "MacAddress": "02:42:ac:11:00:03",
                "IPv4Address": "172.17.0.3/16",
                "IPv6Address": ""
            }
        },
(省略)

この結果から、「alpine1」コンテナのIPアドレスは172.17.0.2、「alpine2」コンテナのIPアドレスは172.17.0.3と分かります。コンテナの中でip addrコマンドを実行し、インターフェースのIPアドレスを確認します。また、routeコマンドを実行してゲートウェイアドレスも確認します。

# docker exec -it alpine1 /bin/sh
/ # ip addr
1: lo:  mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
35: eth0@if36:  mtu 1500 qdisc noqueue state UP
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever
/ # route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         172.17.0.1      0.0.0.0         UG    0      0        0 eth0
172.17.0.0      *               255.255.0.0     U     0      0        0 eth0

この状態で、alpine2コンテナへの通信を、IPアドレスとホスト名で試しましょう。

/ # 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.269 ms
64 bytes from 172.17.0.3: seq=1 ttl=64 time=0.134 ms
64 bytes from 172.17.0.3: seq=2 ttl=64 time=0.122 ms
--- 172.17.0.3 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.122/0.175/0.269 ms
/ # ping -c 3 alpine2
ping: bad address 'alpine2'
/ #

pingはIPアドレスでは疎通しますが、ホスト名では疎通できないことが分かります。次は、自分でブリッジ・ネットワークを作成しましょう。

/ # [root@docker ~]# docker network create mybridge
bc51faf6c0b7dccbb897c538e5af444597a8366b238edd65b4711121cccac0f2
[root@docker ~]# docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
e5dd457767fd        bridge              bridge              local
8836c0e5c268        host                host                local
bc51faf6c0b7        mybridge            bridge              local
acdbf891b515        none                null                local

それから、先ほど同様alpine3、alpine4のコンテナを作成します。ただし、接続先のネットワークは今回使ったmybridgeネットワークに接続します。

[root@docker ~]# docker run -itd --net mybridge --name alpine3 alpine  /bin/sh
135a4fdd1f9f04a90d656431da7871b025fc734eaabab6e9098db26792949764
[root@docker ~]# docker run -itd --net mybridge --name alpine4 alpine  /bin/sh
2f892246b6ecddc1dcd1ba3e0b68ad0330b84458e76d90f04849a97f8eb2214d
[root@docker ~]#

この状態で「docker network inspect mybridge」を実行します。先ほどとはネットワークのアドレスブロックが違うのが分かります。また、コンテナに割り当てられるIPアドレスも標準のブリッジ・ネットワークとは異なります。

[root@docker ~]# docker exec -it alpine3 /bin/sh
/ # ip a
1: lo:  mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
40: eth0@if41:  mtu 1500 qdisc noqueue state UP
    link/ether 02:42:ac:19:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.25.0.2/16 brd 172.25.255.255 scope global eth0
       valid_lft forever preferred_lft forever
/ # route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         172.25.0.1      0.0.0.0         UG    0      0        0 eth0
172.25.0.0      *               255.255.0.0     U     0      0        0 eth0

ところで、この自分で作成したブリッジ・ネットワークに接続した状態では、先ほどのalpine1コンテナには通信できません。なぜなら、接続しているネットワークが違うからです。そのため、pingを時刻しても応答はありません。

/ # ping 172.17.0.2
PING 172.17.0.2 (172.17.0.2): 56 data bytes
^C
--- 172.17.0.2 ping statistics ---
2 packets transmitted, 0 packets received, 100% packet loss
/ # ping 172.17.0.3
PING 172.17.0.3 (172.17.0.3): 56 data bytes
^C
--- 172.17.0.3 ping statistics ---
2 packets transmitted, 0 packets received, 100% packet loss

ただし、同じネットワーク内にあるalpine3からalpine4への通信は可能です。

/ # ping -c 3 172.25.0.3
PING 172.25.0.3 (172.25.0.3): 56 data bytes
64 bytes from 172.25.0.3: seq=0 ttl=64 time=0.119 ms
64 bytes from 172.25.0.3: seq=1 ttl=64 time=0.127 ms
64 bytes from 172.25.0.3: seq=2 ttl=64 time=0.132 ms
--- 172.25.0.3 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.119/0.126/0.132 ms

そして、自分で作成したブリッジ・ネットワーク内ではIPアドレスだけでなく、「コンテナ名」でも疎通可能になります。

/ # ping -c 3 alpine4
PING alpine4 (172.25.0.3): 56 data bytes
64 bytes from 172.25.0.3: seq=0 ttl=64 time=0.138 ms
64 bytes from 172.25.0.3: seq=1 ttl=64 time=0.109 ms
64 bytes from 172.25.0.3: seq=2 ttl=64 time=0.184 ms
--- alpine4 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.109/0.143/0.184 ms
/ #

このように、自分で定義したネットワークであればIPアドレスだけでなく、コンテナ名で通信可能となります。

振り返り

Dockerには独自のネットワーキング・モデルがあり、複数のコンテナ間で通信をするには、このネットワーキング・モデルの理解が欠かせません。Dockerコンテナ間でサービス名(コンテナ名)で名前解決するためにはブリッジ・ネットワークが必要です。Docker Composeは、プロジェクト単位で内部通信用のブリッジ・ネットワークを持ち、IPアドレスとコンテナ名の名前解決が可能になっています。次回は、このネットワークの特性を理解した上で、Docker Composeでのネットワーク活用方法をみていきます。