おはようございます。会計freeeの開発をしている bananaumai と言います。
ついにEKSでAWSにKubernetesがやってきましたね!
また、Fargate の登場により、ECS自体の利用もとても気軽になりました。いい時代来てます。
オメデタイことが続くので?、Dockerのネットワークを調べてみようとふと思いました。
しかし、ネットワーク苦手エンジニアの僕からするとあまりにも壮大な壁でした。
ということで、freee Developers Advent Calendar の11日目、一番基本的なDockerネットワークを調べてお茶を濁すことにします。k8s関係ありません。すみません。
※ EKSについての説明は同僚のmumoshuさんの記事がわかりやすいのでご参照下さい。
それではやっていきます。
とりあえず見てみましょう
コンテナと外の世界との通信は特に意識しなくてもできますね。
host:$ docker -ti ubuntu ubuntu:$ apt-get update Get:1 http://archive.ubuntu.com/ubuntu xenial InRelease [247 kB] Get:2 http://archive.ubuntu.com/ubuntu xenial-updates InRelease [102 kB] Get:3 http://archive.ubuntu.com/ubuntu xenial-security InRelease [102 kB] Get:4 http://archive.ubuntu.com/ubuntu xenial/main Sources [1103 kB] 12% [4 Sources 844 kB/1103 kB 77%] ...snip...
逆に、コンテナの外の世界からコンテナへの通信も簡単ですよね。
host1:$ docker run --detach -publish 8000:80 nginx host2: $ curl 192.168.99.100:8000 <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> <style> body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } </style> </head> <body> <h1>Welcome to nginx!</h1> ...snip...
コンテナ間の通信も同様。
host:$ docker run -dti --name container1 busybox 52401dd3be458d10bf6411c536ab8c2b768e35496624e735c2490285c90c5423 host:$ docker run -dti --name container2 --link container1 busybox 5b0e5014cdd03804b3b005aa469ed1446a4a7c7614ec1e274a002f1a5b0d4a67 host:$ docker attach container2 $ ping container1 PING container1 (172.17.0.2): 56 data bytes 64 bytes from 172.17.0.2: seq=0 ttl=64 time=0.077 ms 64 bytes from 172.17.0.2: seq=1 ttl=64 time=0.063 ms 64 bytes from 172.17.0.2: seq=2 ttl=64 time=0.077 ms ...snip...
Dockerコンテナのネットワーク設定
host:$ docker network ls NETWORK ID NAME DRIVER SCOPE 6d7d22a9f3b8 bridge bridge local 0dc7b3541aed host host local b94db4ad8431 none null local
- デフォルトで3種類のネットワークが用意されています。
- bridge
- host
- none
docker run
するとデフォルトでbridge
ネットワークが利用されます。docker run --network
オプションを指定することで任意のネットワークを指定できます。- ネットワークはデフォルト以外のものを作成することができます。
- ネットワークを作成する際に、ネットワークドライバを指定できて・・・
- デフォルトで以下の5種類が用意されています。
- bridge
- host
- none
- overlay
- macvlan
- デフォルトで以下の5種類が用意されています。
詳しくは https://docs.docker.com/engine/userguide/networking/ を参照して下さい。
このなかでデフォルトネットワークに用意されているbridgeが一番基本的なネットワーク機能になるのでそこを深掘りしていきます。
bridgeドライバのネットワーク詳細
以下のLinuxの技術を使ってネットワーキングを実現しています。
- iptables : パケットのフィルタリングとかNATをやるやつ (上述)
- Linux bridge : Linuxプロセス上で動作する仮想的ネットワークスイッチ
- veth : 物理的なネットワークケーブル的なものを実現
- netns: ネットワークリソースの隔離
これらの技術を使ってどのようにネットワークが作られていくのかを簡単にまとめてみます。
- まず、Linuxでdockerを起動すると
docker0
というLinux Bridgeが出来あがります。- デフォルトのbridgeネットワークが使うbridgeインスタンス。
- このbridgeには接続されるdockerインスタンスのデフォルトゲートウェイとなるipアドレスが割り振られます。(e.g. 172.17.0.1)
- dockerを起動すると
docker0
に対してveth
のペアが作られてdocker0ブリッジに接続されます。 - dockerd(Docker Engineのデーモン)を起動する時にhostにはoutbound用のiptablesが設定されており、docker0のセグメントからのトラフィックがeth0にフォワードされるようになります。
図にすると以下のような感じに仕上がります。
routeを見てみると以下のようにみえます。
host:$ route -n Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 10.0.2.2 0.0.0.0 UG 1 0 0 eth0 10.0.2.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0 127.0.0.1 0.0.0.0 255.255.255.255 UH 0 0 0 lo 172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0 192.168.99.0 0.0.0.0 255.255.255.0 U 0 0 0 eth1
iptablesはどうでしょうか?
$ sudo iptables -L -t nat Chain PREROUTING (policy ACCEPT) target prot opt source destination DOCKER all -- anywhere anywhere ADDRTYPE match dst-type LOCAL Chain INPUT (policy ACCEPT) target prot opt source destination Chain OUTPUT (policy ACCEPT) target prot opt source destination DOCKER all -- anywhere !127.0.0.0/8 ADDRTYPE match dst-type LOCAL Chain POSTROUTING (policy ACCEPT) target prot opt source destination MASQUERADE all -- 172.17.0.0/16 anywhere Chain DOCKER (2 references) target prot opt source destination RETURN all -- anywhere anywhere
また、-pオプションで8000番ポートにコンテナの80番ポートを晒した場合、以下のようになります。
$ sudo iptables -L -t nat ...snip... Chain DOCKER (2 references) target prot opt source destination RETURN all -- anywhere anywhere DNAT tcp -- anywhere anywhere tcp dpt:8000 to:172.17.0.3:80
DNAT(送信先アドレスの変換)が追加されていますね。
bridgeドライバを使うネットワークを作ってみる
最後に、ここまで勉強してきたことを踏まえつつ、自分でDockerのbirdgeドライバがやっていることを再現してみました。
id:enakai00さんのこちらの記事 が秀逸でしたので、そちらをほぼほぼ踏襲させて頂いています。
それではネットワークをやっていきます。今回はDocker for Macでやりますが、Docker for Macは少しLinuxのDockerと異なるので、docker-machineで作業用のVMを作ります。
mac: $ docker-machine create -d drive hoge-docker ...snip... mac: $ docker-machine ssh hoge-docker
そして、適当なdockerコンテナを立ち上げます。
host: $ docker run -dti --name busybox
さて、ここまでやった状態では以下の図のような状態になっています。
デフォルトのネットワークであるdocker0というbridgeドライバのネットワークにつながっています。
では、自分でbr0というLinux Bridge作ってみます。ipアドレスもふっておきます。
host:$ sudo ip link add name br0 type bridge host:$ sudo ip link set dev br0 up host:$ sudo ip addr add 192.168.200.1/24 dev br0
仮想的なether interfaceができました。ですが、この状態だと(物理の世界で言えば)ネットワークケーブルがつながっていない状態です。
ということで(仮想的な)ネットワークケーブルを繋いでいきます。
host:$ sudo ip link add name veth-host type veth peer name veth-guest host:$ sudo ip link set veth-host up host:$ sudo ip link set dev veth-host master br0
ここからが本題(今更!?)です。Dockerっぽいです。ネットネームスペースの登場です。
まずはコンテナのプロセスIDを調べます。
host:$ pstree -p init(1)-+-VBoxService(2293) |-acpid(2328) |-crond(2276) |-dockerd(2632)-+-docker-containe(2641)---docker-containe(17407)---busybox(17418)---sh(17437) | `-docker-proxy(17403) |-forgiving-getty(2765)---sleep(17441) |-forgiving-getty(2767)---sleep(17443) |-forgiving-getty(2768)---sleep(17440) |-forgiving-getty(2770)---sleep(17444) |-ntpd(2762) |-sh(2763) |-sshd(2324)-+-sshd(9457)---sshd(9459)---sh(9460)---pstree(17445) | `-sshd(11522)---sshd(11524)---sh(11525) |-udevd(205)-+-udevd(971) | `-udevd(972) |-udhcpc(2334) `-udhcpc(2347)
プロセスIDがわかったので、ネームスペースを隔離していきます。
host:$ sudo mkdir /var/run/netns host:$ sudo ln -s /proc/17437/ns/net /var/run/netns/test host:$ sudo ip link set veth-guest netns test
ネームスペースが分離されたのでメインプロセスからはvguest-hostが見えなくなります。
host:$ ifconfig veth-guest ifconfig: veth-guest: error fetching interface information: Device not found
この時の状態は以下のようになっています。
ネットワークが繋がれるまでもう少しです。
host:$ sudo ip netns exec test ip link set veth-guest name eth1 host:$ sudo ip netns exec test ip addr add 192.168.200.102/24 dev eth1 host:$ sudo ip netns exec test ip link set eth1 up host:$ sudo ip netns exec test ip route delete default host:$ sudo ip netns exec test ip route add default via 192.168.200.1
できました。pingもホスト側から通ります。
host:$ ping 192.168.200.102 PING 192.168.200.102 (192.168.200.102): 56 data bytes 64 bytes from 192.168.200.102: icmp_seq=0 ttl=64 time=3.216 ms 64 bytes from 192.168.200.102: icmp_seq=1 ttl=64 time=4.187 ms 64 bytes from 192.168.200.102: icmp_seq=2 ttl=64 time=4.771 ms 64 bytes from 192.168.200.102: icmp_seq=3 ttl=64 time=3.089 ms
ネットワークが繋がった状態が以下のような感じ。
この後NATの設定しないと外との通信ができないのですが、Dockerを支える技術のコアな部分であるネームスペースの技術を使っている部分について理解できると思うので、ここまでとしておきます。
終わりに
ここまで長々と読んで頂いた皆様、ありがとうございました。
Docker自体様々な既存技術の組み合わせでできているとよく言われますが、ネットワークも同様でiptables、Linux bridge、veth、netnsなどの技術の組み合わせでできていることがわかりました。技術の組み合わせで新しい価値を作ることはソフトウェアエンジニアの醍醐味の1つですね。
freeeのプロダクト開発も同様です。様々な技術を組み合わせて、日本中・世界中のスモールビジネスに関わる人々に価値を届けるプロダクトづくりを行っていますので、そんな価値観に共感いただける方はぜひぜひ一緒に働きましょう!
次回
同じチームで会計freeeの開発をやっている him0 さんです。ご期待下さい!