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でのネットワーク活用方法をみていきます。