freeeの開発情報ポータルサイト

祝k8s on AWS!ということで一番基本的なDockerネットワークについて調べてみた

おはようございます。会計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

詳しくは 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のプロダクト開発も同様です。様々な技術を組み合わせて、日本中・世界中のスモールビジネスに関わる人々に価値を届けるプロダクトづくりを行っていますので、そんな価値観に共感いただける方はぜひぜひ一緒に働きましょう!

www.wantedly.com

jobs.freee.co.jp

次回

同じチームで会計freeeの開発をやっている him0 さんです。ご期待下さい!