いまさらDockerでズンドコキヨシ

ちょっと前にズンドコルータというものが流行っていました(http://qiita.com/kooshin/items/2e00cdeb53cf9cf4c51d).
いいなー私もDockerでやりたいとか思ってましたが,Dockerの機能を使ったいい感じのやり方が思いつかなかったのでやってませんでした.

しかし! Docker 1.12からDockerにもロードバランサが!!これでズンドコキヨシできるやん!!
となりました.
以下思いついてやってみたものの記録です.
※今回結論から言うと成功してません.可能なはずですが何度も何度も繰り返す強い心が必要でした.

Docker ロードバランサ

以前の記事に書いた通りです.内部でIPVSのラウンドロビンで飛ばしてくれます.
今回はIngressのロードバランサのみでやってみます(簡単なので)

設計

まず最も簡単な実装を考えるためDocker LB配下のコンテナがhttpリクエストを受けるとそのコンテナIDによってzun, doko, kyoshiのいずれかを返します.
これでアクセスを繰り返せばいつかzun zun zun doko kiyoshiが完成するはず!という目論見です.
dockerzundoko

サーバ側

Dockerイメージはこんな感じ
https://github.com/YujiOshima/dockerfiles/tree/master/zundoko

Dockerfile

FROM python
ADD files /app
WORKDIR /app
ENTRYPOINT python run.py

run.py

from http.server import HTTPServer, SimpleHTTPRequestHandler
import socket

hostname = socket.gethostname()

class MyHandler(SimpleHTTPRequestHandler):
    def do_GET(self):
        uri = self.path
        body = ""
        if uri == "/zun":
            body = b"zun"
        elif uri == "/doko":
            body = b"doko"
        elif uri == "/kiyoshi":
            body = b"__kiyoshi__"
        else :
            body = judge_reponce(hostname).encode('utf-8')

        self.send_response(200)
        self.end_headers()
        self.send_header('Content-type', 'text/html; charset=utf-8')
        self.send_header('Content-length', len(body))
        self.wfile.write(body)

def judge_reponce(cid):
    intid = int(hostname, 16)
    if intid % 5 == 0:
        return "**kiyoshi**"
    elif intid % 5 == 1:
        return "doko"
    else:
        return "zun"

host = '0.0.0.0'
port = 8000
httpd = HTTPServer((host, port), MyHandler)
print('serving at port', port)
httpd.serve_forever()

めっちゃ簡単です.
これをDockerクラスタ上にデプロイします.

docker service create --name=zundokokiyoshi -p 9000:8000 --replicas=30 zundoko

クライアント側

アクセスする側も今回Dockerイメージにします.
https://github.com/YujiOshima/dockerfiles/tree/master/getzundoko

import sys
import http.client
import time

DESIRE = b"zun zun zun doko **kiyoshi** "

def main(server, port, loopnum):
    conn = http.client.HTTPConnection(server, port)
    for i in range(0, loopnum):
        result = b""
        for i in range(0, 5):
            conn.request("GET", "/")
            result += conn.getresponse().read() + b" "
            print(result)
            if result not in DESIRE:
                print("faild...")
                break
            time.sleep(0.01)
        if result == DESIRE:
            print("done!!")
            return 

if __name__ == '__main__':
    args = sys.argv
    server = "localhost"
    port = 80
    loopnum = 10
    if len(args) > 1:
        server = args[1]
    if len(args) > 2:
        port = args[2]
    if len(args) > 3:
        loopnum = int(args[3])
    print("connect "+server+":"+str(port))
    main(server, port, loopnum)

うん,これでアクセスを繰り返してzun zun zun doko kiyoshiが完成すれば終了するはず,

動作!

レッツプレイ!

docker run -it --rm getzundoko 52.196.60.122 9000 100
connect 52.196.60.122:9000
b'**kiyoshi** '
b'**kiyoshi** zun '
faild...
b'zun '
b'zun doko '
b'zun doko zun '
faild...
b'zun '
b'zun zun '
b'zun zun doko '
b'zun zun doko **kiyoshi** '
b'zun zun doko **kiyoshi** doko '
faild...
b'**kiyoshi** '
b'**kiyoshi** zun '
faild...
b'zun '
b'zun doko '
b'zun doko **kiyoshi** '
b'zun doko **kiyoshi** zun '
faild...
b'zun '
b'zun doko '
b'zun doko zun '
faild...
b'zun '
b'zun zun '
b'zun zun doko '
b'zun zun doko **kiyoshi** '
b'zun zun doko **kiyoshi** doko '
faild...
b'**kiyoshi** '
b'**kiyoshi** zun '
faild...
b'zun '
b'zun doko '
b'zun doko **kiyoshi** '
b'zun doko **kiyoshi** zun '
faild...

お!動いてるっぽい...!

結果

ダメでした.
正確には何度繰り返してもzun zun zun doko kiyoshiが完成しませんでした.
理由としてDockerLBの内部ではIPVSのラウンドロビンなのでアクセスが均等になるように割り振られるんですよね.
なので今回私一人しかアクセスしてないので着弾するコンテナの順番が固定になってしまうわけですね...もっと早く気付けよ...

もちろん何度もservice createでコンテナを作りなおす,複数プロセスからアクセスして適当に着弾順序を変えるとかすれば完成すると思いますが.面倒なのでやめました.
コンテナ間アクセスのLBとか使ってもうチョット真面目に考えなおそう.

広告

Docker 1.12のoverlayネットワーク

以前Docker 1.9でのマルチホストネットワークの作成について記事を書きましたが,Docker 1.12になって手順がだいぶ変わったので改めてまとめようと思います.ついでに色々してみました.

結論から言うとDocker 1.12のoverlayネットワークはserviceが定義されたコンテナのみ接続されることが想定されていますが回避も可能です.

まず前の記事を参考にDocker swarmのクラスタを作成します.

  • node1(master) : ip-172-31-1-218

  • node2 : ip-172-31-1-219

  • node3 : ip-172-31-1-217

$ docker node ls
ID                           NAME             MEMBERSHIP  STATUS  AVAILABILITY  MANAGER STATUS
71g9k9xcb78u90r8w6zcer4z0 *  ip-172-31-1-218  Accepted    Ready   Active        Leader
7gg6fa9a7frr3h66widewkjc2    ip-172-31-1-219  Accepted    Ready   Active
em6kz4ijedaj48tfssi7k26l3    ip-172-31-1-217  Accepted    Ready   Active

node1でoverlayNWを作成してみます.

$ docker network create -d overlay backend

$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
9lapeboarpux        backend             overlay             swarm
af3236af05f2        bridge              bridge              local
1592e8ed2c89        docker_gwbridge     bridge              local
950ee88d8b98        host                host                local
5jq39idymrkq        ingress             overlay             swarm
40051a135237        none                null                local

これでNWは作成されたはずですが,他のホストで確認して見るとこのNWはまだ見えません.

ubuntu@ip-172-31-1-217$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
1d3085970cb3        bridge              bridge              local
db8293d930ca        docker_gwbridge     bridge              local
d13db9be8a4c        host                host                local
5jq39idymrkq        ingress             overlay             swarm
a9f1cbd814fa        none                null                local

さらにネットワークを作成したnode1でこのNWに繋がるコンテナを作成しようとすると失敗します.

$ docker run -itd --net=backend busybox
docker: Error response from daemon: network backend not found.

この状態ではまだNWの実体がないんですね.inspectで確認するとサブネットなども設定されていないことがわかります.

$ docker network inspect backend
[
    {
        "Name": "backend",
        "Id": "10kza2wvjm6dz9nzt25jrdygl",
        "Scope": "swarm",
        "Driver": "overlay",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": []
        },
        "Internal": false,
        "Containers": null,
        "Options": {
            "com.docker.network.driver.overlay.vxlanid_list": "257"
        },
        "Labels": null
    }
]

なのでまずこのNWに紐づくserviceを作成します.これでようやくNWのサブネットやGWが割り当てられ,NWの実体の作成とコンテナのアタッチが行われます.

$ docker service create --name=backend --replicas=5 -p 8000:80/tcp --network=backend redis

$ docker network inspect backend
[
    {
        "Name": "backend",
        "Id": "10kza2wvjm6dz9nzt25jrdygl",
        "Scope": "swarm",
        "Driver": "overlay",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "10.0.0.0/24",
                    "Gateway": "10.0.0.1"
                }
            ]
        },
        "Internal": false,
        "Containers": {
            "53b2c41b5b957f5926b48b9517f67b7af833c2c33d1f91023fd80486f83bcd02": {
                "Name": "anotherbackend.1.caomy5tse6gaow0cv6u6vy6rb",
                "EndpointID": "f09a12d8c937b32e9b8707a7e042168ef806372708c4d03cf26152e55b4f0a83",
                "MacAddress": "02:42:0a:00:00:0b",
                "IPv4Address": "10.0.0.11/24",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.driver.overlay.vxlanid_list": "257"
        },
        "Labels": {}
    }
]

これで別のホスト上でもNWが作成されます.

ubuntu@ip-172-31-1-217:~$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
1ul9idog376w        backend             overlay             swarm
1d3085970cb3        bridge              bridge              local
db8293d930ca        docker_gwbridge     bridge              local
d13db9be8a4c        host                host                local
5jq39idymrkq        ingress             overlay             swarm
a9f1cbd814fa        none                null                local

しかしこの状態でもこのNWに繋がるコンテナをserviceの定義なしに作ることはできません.

$ docker run -itd --net=backend busybox
docker: Error response from daemon: swarm-scoped network (backend) is not compatible with `docker create` or `docker run`. This network can be only used docker service.
See 'docker run --help'.

あくまでoverlay NWに繋がるコンテナの作成はserviceの作成とセットになります.

$ docker service create --name=anotherbackend --replicas=5 --network=backend postgres

$ docker network inspect backend
[
    {
        "Name": "backend",
        "Id": "10kza2wvjm6dz9nzt25jrdygl",
        "Scope": "swarm",
        "Driver": "overlay",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "10.0.0.0/24",
                    "Gateway": "10.0.0.1"
                }
            ]
        },
        "Internal": false,
        "Containers": {
            "02d3b4a24d99720295539c4c56157e559ec2592c193e1f1ec82c79a265dfd655": {
                "Name": "backend.4.1wbj6bo0ruq87nvdkuswt9xy5",
                "EndpointID": "a248c65b18386464afb2baf4279c6541a9252432b153d7519cdc3fc6abc06654",
                "MacAddress": "02:42:0a:00:00:06",
                "IPv4Address": "10.0.0.6/24",
                "IPv6Address": ""
            },
            "06d638c77e2c3c12178ff6e93a453a3f67f9b7b43ce2d407753acb43e18b5d0e": {
                "Name": "backend.3.9387bpsqnvvyhnugj433ufx32",
                "EndpointID": "47c9969877337f965e742a08809a5065b339f543943cee1241af3f845cf94386",
                "MacAddress": "02:42:0a:00:00:05",
                "IPv4Address": "10.0.0.5/24",
                "IPv6Address": ""
            },
            "53b2c41b5b957f5926b48b9517f67b7af833c2c33d1f91023fd80486f83bcd02": {
                "Name": "anotherbackend.1.caomy5tse6gaow0cv6u6vy6rb",
                "EndpointID": "f09a12d8c937b32e9b8707a7e042168ef806372708c4d03cf26152e55b4f0a83",
                "MacAddress": "02:42:0a:00:00:0b",
                "IPv4Address": "10.0.0.11/24",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.driver.overlay.vxlanid_list": "257"
        },
        "Labels": {}
    }
]

もしもserviceに紐付かないコンテナをNWにどうしても接続したい場合は先にコンテナを作成してからNWにconnectしましょう.

$ docker run -itd busybox
2fda7c0bc0b7f70879c38461b9588f35880be76752ca594a6cd52a220a9ca077

$ docker network connect backend 2fda7c0bc0b7

何故作成時には許されなくて作成後は許されているのかはよく分かりませんが今のところはこれが可能です.
ちなみにdiscponnectも可能です.

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
2fda7c0bc0b7        busybox             "sh"                     4 minutes ago       Up 4 minutes                            angry_roentgen
53b2c41b5b95        postgres:latest     "/docker-entrypoint.s"   About an hour ago   Up About an hour    5432/tcp            anotherbackend.1.caomy5tse6gaow0cv6u6vy6rb
06d638c77e2c        redis:latest        "docker-entrypoint.sh"   About an hour ago   Up About an hour    6379/tcp            backend.3.9387bpsqnvvyhnugj433ufx32
02d3b4a24d99        redis:latest        "docker-entrypoint.sh"   About an hour ago   Up About an hour    6379/tcp            backend.4.1wbj6bo0ruq87nvdkuswt9xy5

$ docker network disconnect backend 06d638c77e2c

これをすると表面上サービスはscale=5で正しく動いているように見えて実はネットワーク不通のコンテナが発生することになります.
おいおいこれはまずいだろと思わなくもないですが,まあ新機能なのでこれから変わるのでしょう.

$ docker service tasks backend
          ID                         NAME       SERVICE  IMAGE  LAST STATE             DESIRED STATE  NODE
          euwtk7ck34ghknvggea6i1fg2  backend.1  backend  redis  Running About an hour  Running        ip-172-31-1-217
          24z71bl2u2fwebaa04dk2o186  backend.2  backend  redis  Running About an hour  Running        ip-172-31-1-219
実は不通-> 9387bpsqnvvyhnugj433ufx32  backend.3  backend  redis  Running About an hour  Running        ip-172-31-1-218 
          1wbj6bo0ruq87nvdkuswt9xy5  backend.4  backend  redis  Running About an hour  Running        ip-172-31-1-218
          1nt960d8v1vqkl69790yrborb  backend.5  backend  redis  Running About an hour  Running        ip-172-31-1-217

AWS上での悲しい出来事

これでできるはずなんですが,今回AWS上でテストしていることが原因(?)でDNSの名前解決したうえでの疎通がうまく行きませんでした.

$ docker exec -it 53b2c41b5b95 ping -c 3 backend
PING backend (10.0.0.2): 56 data bytes
92 bytes from 53b2c41b5b95 (10.0.0.11): Destination Host Unreachable
92 bytes from 53b2c41b5b95 (10.0.0.11): Destination Host Unreachable
92 bytes from 53b2c41b5b95 (10.0.0.11): Destination Host Unreachable
--- backend ping statistics ---
3 packets transmitted, 0 packets received, 100% packet loss

$ docker exec -it 53b2c41b5b95 ping -c 3 10.0.0.11
PING 10.0.0.11 (10.0.0.11): 56 data bytes
64 bytes from 10.0.0.11: icmp_seq=0 ttl=64 time=0.060 ms
64 bytes from 10.0.0.11: icmp_seq=1 ttl=64 time=0.076 ms
64 bytes from 10.0.0.11: icmp_seq=2 ttl=64 time=0.057 ms
--- 10.0.0.11 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.057/0.064/0.076/0.000 ms

あれ!?っと思ったけどそういやなんかissue見たことあるなーと思ったら案の定こちらで報告されてました.
まあそのうち治るでしょう.以前もAWS上でNWの問題があったのでなにか相性が悪いことがあるようです.

Docker ロードバランサ内部実装

先日のDockerCon16でDocker 1.12RCが発表されまして,主な機能追加として

  • SwarmのDocker engineへの統合とそれに伴うクラスタ構築の簡略化

  • Service機能の追加

  • Load Balancer機能の追加

が発表されました.
今回はSwarmクラスタの構築~Serviceの定義まで行って,ロードバランサの内部実装を詳しく追ってみます.

Docker 1.12RCのインストール

これは既にget dockerで問題なく可能です.
今回はAWS上にクラスタを構築します.

  • node1(master) : ip-172-31-1-218

  • node2 : ip-172-31-1-219

  • node3 : ip-172-31-1-217

これら3台のホスト上でそれぞれ以下のコマンドでDocker 1.12をインストールします.

$ wget -qO- https://experimental.docker.com/ | sh

Swarm クラスタの構築

node1上で以下のコマンドを実行し,Swarmクラスタのmasterとして定義します.

$ docker swarm init
Swarm initialized: current node (71g9k9xcb78u90r8w6zcer4z0) is now a manager.

次にnode2, node3それぞれで以下のコマンドを実行し,Swarmノードへ追加します.

$ docker swarm join 172.31.1.218:2377
This node joined a Swarm as a worker.

これで3台のSwarmクラスタが構築されました.
node1で以下のコマンドを実行し,クラスタが構築されていることを確認します.

$ docker node ls
ID NAME MEMBERSHIP STATUS AVAILABILITY MANAGER STATUS
71g9k9xcb78u90r8w6zcer4z0 * ip-172-31-1-218 Accepted Ready Active Leader
7gg6fa9a7frr3h66widewkjc2 ip-172-31-1-219 Accepted Ready Active
em6kz4ijedaj48tfssi7k26l3 ip-172-31-1-217 Accepted Ready Active

これまでのポートを開放したりSwarmコンテナを構築したりといった手順がなくなり手軽に構築できました.

Dockerサービス定義

次は1.12から追加されたサービス機能です.
composeではサービスの定義はコンテナのラベルで管理されていましたが,正式にserviceというリソースが定義されdockerコマンドで管理できるようになりました.
と言っても難しいことは特にないです.docker runの延長の様な感じです.

node1上で以下のコマンドを実行します.

$ docker service create --name vote -p 8080:80 instavote/vote
4g17854r60v68gcti1gjbvjqx

これでvoteサービスが定義され,instavote/voteイメージのコンテナが1台配備されます.

現在定義されているサービスを確認

$ docker service ls
ID NAME REPLICAS IMAGE COMMAND
4g17854r60v6 vote 1/1 instavote/vote

更にそのサービス上で動作しているコンテナを確認

$ docker service tasks vote
ID NAME SERVICE IMAGE LAST STATE DESIRED STATE NODE
c003owls6tcdfnfwpn7o89g32 vote.1 vote instavote/vote Running 57 seconds Running ip-172-31-1-218

node1上でinstavote/voteイメージのコンテナが1台動作していることがわかります.
composeと同様にscaleコマンドも存在します.

$ docker service scale vote=6
vote scaled to 6

$ docker service tasks vote
ID NAME SERVICE IMAGE LAST STATE DESIRED STATE NODE
c003owls6tcdfnfwpn7o89g32 vote.1 vote instavote/vote Running About a minute Running ip-172-31-1-218
aff60et0v925cylsiw62docss vote.2 vote instavote/vote Preparing 1 seconds Running ip-172-31-1-217
chpnwvc3cv3fuq9yorzfz6wj3 vote.3 vote instavote/vote Preparing 1 seconds Running ip-172-31-1-219
4jsnmbn9fne9pouh8ffxuo6no vote.4 vote instavote/vote Preparing 1 seconds Running ip-172-31-1-219
7safag59ght9u4pobgw47ae04 vote.5 vote instavote/vote Preparing 1 seconds Running ip-172-31-1-218
4yyo054kzasvupckynhi9dx7b vote.6 vote instavote/vote Preparing 1 seconds Running ip-172-31-1-217

それぞれのホストに6台のコンテナが分散されて配備されたのがわかります.
これで実はサービスは8080番のポートで外部に公開されており,どのホストに8080番でアクセスしてもこれら6台のコンテナにロードバランスされます.
このinstavote/voteはアクセスするとコンテナIDを表示しているのでリロードする度着弾するコンテナが変わるのがわかりやすいです.

LBwebUI

ロードバランサの内部実装

これで使う分にはOKですが,内部がどうなっているか一応理解しておかないと気持ち悪いのでもう少し深追いしてみます.
まず,8080番のポートはDockerらしくiptablesでコンテナ内の80番にNATされています.

$ sudo iptables-save
...
-A DOCKER-INGRESS -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.18.0.2:8080
...

さて,このIP 172.18.0.2はどのコンテナにも該当しません.
例えばnode1上のコンテナでは

$ docker exec -it vote.1.c003owls6tcdfnfwpn7o89g32 ip address|grep -A 1 -B 1 link/eth
18: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP
link/ether 02:42:0a:ff:00:07 brd ff:ff:ff:ff:ff:ff
inet 10.255.0.7/16 scope global eth0
--
20: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:12:00:03 brd ff:ff:ff:ff:ff:ff
inet 172.18.0.3/16 scope global eth1

docker exec -it vote.5.7safag59ght9u4pobgw47ae04 ip address|grep -A 1 -B 1 link/eth
22: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP
link/ether 02:42:0a:ff:00:0b brd ff:ff:ff:ff:ff:ff
inet 10.255.0.11/16 scope global eth0
--
24: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:12:00:04 brd ff:ff:ff:ff:ff:ff
inet 172.18.0.4/16 scope global eth1

実は,コンテナに紐付かないingress用のnetwork namespaceが作られています.
今回その中を覗いてみます.

まずnode1上のコンテナを改めて確認.

$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a060a2cdfa46 instavote/vote:latest "gunicorn app:app -b " About a minute ago Up About a minute 80/tcp vote.5.7safag59ght9u4pobgw47ae04
c423eeebc7f4 instavote/vote:latest "gunicorn app:app -b " 2 minutes ago Up 2 minutes 80/tcp vote.1.c003owls6tcdfnfwpn7o89g32

これらのコンテナのnetwork namespaceのIDを確認.

$ docker inspect a060a2cdfa46|grep Sandbox
"SandboxID": "567239b6a8b4d9116210e16eb6419a140d510296b090667bf2e45a6a9340d4fa",
"SandboxKey": "/var/run/docker/netns/567239b6a8b4",

$ docker inspect c423eeebc7f4|grep Sandbox
"SandboxID": "df78773b11ec2d07feffdc3b557d1971f4955d7bf5fca66cc9eb882e9d6bbd76",
"SandboxKey": "/var/run/docker/netns/df78773b11ec",

ここでのSandboxというのがdockerのCNMの用語でLinuxでのnetwork namespaceに相当します.
それらの実体はSandboxKeyに記述されているファイルです.
実際に/var/run/docker/netnsを見てみると

$ sudo ls /var/run/docker/netns
1-5jq39idymr 567239b6a8b4 c47f7eacb1bc df78773b11ec

コンテナは2台しかないはずなのにSandbox (network namespace)は4つ作られています.
のうち,567239b6a8b4df78773b11ecはコンテナのSandboxでc47f7eacb1bcはIngress用のSandbox,1-5jq39idymrはコンテナ間をつなぐoverlay networkのvtepが入っているSandboxになります.
これらのSandboxは/var/run/netns以下にシンボリックリンクを貼れば中を除くことが可能です.

$ sudo ln -s /var/run/docker/netns/1-5jq39idymr /var/run/netns/vtep

$ sudo ln -s /var/run/docker/netns/c47f7eacb1bc /var/run/netns/lbingress

まずIngress のnetwork namespaceの中は

$ sudo ip netns exec lbingress ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
14: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
link/ether 02:42:0a:ff:00:03 brd ff:ff:ff:ff:ff:ff
inet 10.255.0.3/16 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:aff:feff:3/64 scope link
valid_lft forever preferred_lft forever
16: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.18.0.2/16 scope global eth1
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe12:2/64 scope link
valid_lft forever preferred_lft forever

ここに,iptablesのNAT先の172.18.0.2があることがわかります.
そしてこのnetwork namespace内のipvsの設定を確認します.

$ sudo ip netns exec lbingress ipvsadm -L
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
FWM 256 rr
-> ip-10-255-0-7.ap-northeast-1 Masq 1 0 0
-> ip-10-255-0-8.ap-northeast-1 Masq 1 0 0
-> ip-10-255-0-9.ap-northeast-1 Masq 1 0 0
-> ip-10-255-0-10.ap-northeast- Masq 1 0 1
-> ip-10-255-0-11.ap-northeast- Masq 1 0 0
-> ip-10-255-0-12.ap-northeast- Masq 1 0 0

これで外からのパケットが10.255.0.0/16のネットワークにラウンドロビンで転送されることがわかります.
次にvtepの存在するnetwork namespaceを確認します.

$ sudo ip netns exec vtep ip -d link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 promiscuity 0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 06:4b:04:c2:0e:15 brd ff:ff:ff:ff:ff:ff promiscuity 0
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
link/ether 02:42:e7:e2:42:9c brd ff:ff:ff:ff:ff:ff promiscuity 0
bridge
9: docker_gwbridge: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
link/ether 02:42:bd:90:69:18 brd ff:ff:ff:ff:ff:ff promiscuity 0
bridge
12: ov-000100-5jq39: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP mode DEFAULT group default
link/ether ae:17:c3:4a:f7:a7 brd ff:ff:ff:ff:ff:ff promiscuity 0
bridge
13: vx-000100-5jq39: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master ov-000100-5jq39 state UNKNOWN mode DEFAULT group default
link/ether ce:8d:fa:16:ca:ab brd ff:ff:ff:ff:ff:ff promiscuity 1
vxlan id 256 port 32768 61000 proxy l2miss l3miss ageing 300
15: vethddbc8d5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master ov-000100-5jq39 state UP mode DEFAULT group default
link/ether b2:a9:a5:49:f0:b4 brd ff:ff:ff:ff:ff:ff promiscuity 1
veth
17: veth75b6707: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker_gwbridge state UP mode DEFAULT group default
link/ether 2e:59:0d:5c:68:27 brd ff:ff:ff:ff:ff:ff promiscuity 1
veth
19: veth8b7c018: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master ov-000100-5jq39 state UP mode DEFAULT group default
link/ether d2:61:21:33:e4:b7 brd ff:ff:ff:ff:ff:ff promiscuity 1
veth
21: veth18584b0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker_gwbridge state UP mode DEFAULT group default
link/ether 72:4d:09:77:76:41 brd ff:ff:ff:ff:ff:ff promiscuity 1
veth
23: vetha2a1ee3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master ov-000100-5jq39 state UP mode DEFAULT group default
link/ether ae:17:c3:4a:f7:a7 brd ff:ff:ff:ff:ff:ff promiscuity 1
veth
25: veth3589589: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker_gwbridge state UP mode DEFAULT group default
link/ether a6:a9:a6:c5:ca:da brd ff:ff:ff:ff:ff:ff promiscuity 1
veth
27: veth2d99f87: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default
link/ether 66:3e:a3:35:55:f1 brd ff:ff:ff:ff:ff:ff promiscuity 1
veth
29: vethd1e34b5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default
link/ether 06:07:8a:d0:d5:df brd ff:ff:ff:ff:ff:ff promiscuity 1
veth

たくさんインターフェースがありますが,これは同一ovelay networkに繋がるホスト上のコンテナ全てがこのnetwork namespace上でブリッジングされているためです.
そしてvx-000100-5jq39がvtepでov-000100-5jq39に接続されてます.
中のfdbとルーティングテーブルはこんな感じ

$ sudo ip netns exec vtep bridge fdb show dev vx-000100-5jq39
ce:8d:fa:16:ca:ab vlan 0 permanent
02:42:0a:ff:00:04 vlan 0
02:42:0a:ff:00:04 dst 172.31.1.219 self permanent
02:42:0a:ff:00:05 dst 172.31.1.217 self permanent
02:42:0a:ff:00:08 dst 172.31.1.217 self permanent
02:42:0a:ff:00:09 dst 172.31.1.219 self permanent
02:42:0a:ff:00:0a dst 172.31.1.219 self permanent
02:42:0a:ff:00:0c dst 172.31.1.217 self permanent

$ sudo ip netns exec vtep ip route
default via 172.31.0.1 dev eth0
10.255.0.0/16 dev ov-000100-5jq39 proto kernel scope link src 10.255.0.1
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
172.18.0.0/16 dev docker_gwbridge proto kernel scope link src 172.18.0.1
172.31.0.0/20 dev eth0 proto kernel scope link src 172.31.1.218

ingressからこのnetwork namespaceに飛ばされて,vtepでカプセリングされてそれぞれのコンテナに飛ばされることがわかりました.
ingress

ちなみに,これは外からのアクセスの場合.
内部でのコンテナ間通信でも実はDNSのラウンドロビンでロードバランサは実装されています.
Dockerコンテナ間はDockerのembededDNSサーバが名前解決することでサービス名で疎通することが可能で,その際にラウンドロビンでロードバランスされます.
inner

Docker 1.12のswarmはサービスのオートヒーリングの機能も実装されて,ようやくDockerクラスタの機能が揃った感じがあります.
kubernetesと比較して簡単にクラスタやロードバランサの設定ができるのは魅力ですが,スケーラビリティとかそれぞれの機能比較は今後したい感じです.

Docker 1.9とDocker Swarmのマルチホストネットワーク体験

 

Docker SwarmDocker 1.9からdocker networkコマンドがExperimentalでなくてもサポートされるようになり,Docker Swarmもv1.0となりnetworkコマンドに対応しました(参考).
公式ページにあるようにSwarmクラスタでマルチホストネットワークの構築が楽になりました.
早速標準サポートされたマルチホストネットワークを試してみようと思います.

準備

Kernel 3.16以上のlinux : Dockerでマルチホストネットワークを構築するためのlibnetworkのoverlayドライバはlinux kernel 3.16以上のみサポートなのでそれ以降のlinuxを用意します.
e.g. Ubuntu 14.04.1とか15.04など

ubuntu@awesome-song:~$ uname -a
Linux awesome-song 3.19.0-15-generic #15-Ubuntu SMP Thu Apr 16 23:32:37 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux

Docker 1.9 : 以下でインストール

curl -sSL https://get.docker.com/ | sh

クラスタ構成

今回は以下の構成で行います

  • Manager : IP 192.168.100.1 ホスト名 MAAS-server
  • Node1 : IP 192.168.100.11 ホスト名 awesome-song
  • Node2 : IP 192.168.100.12 ホスト名 worthy-rule

ホスト名はMAASで自動生成されたものでなんでもいいのですがswarmのバックエンドやネットワーク情報の交換にconsulを使うため同一ホスト名だと怒られます.

ちなみにネットワークに関する情報を交換する仕組みについては簡単にですがここ(http://www.slideshare.net/Oshima0x3fd/docker-51844008)で書きました.

Swarmクラスタ構築

Manager, Nodeそれぞれで以下を行います.

@Manager

$docker run -d --net=host progrium/consul -server -bootstrap-expect=1 node=cumulus1 -data-dir=/tmp/consul -client=0.0.0.0 -ui-dir=/opt/consul/webui/
$docker run -d --net=host swarm manage -H tcp://0.0.0.0:2375 consul://127.0.0.1:8500/swarm

@Nodes

各ノードでまずtcp:2376でDocker APIを受け付けるようにし,ネットワーク情報をcunsulで交換すること及びホスト間通信をする際のvxlanデバイスが紐づくインターフェースを規定する必要があります.

/etc/default/dockerを編集してサービスを再起動するか,直接コマンドでデーモンを起動する貸してください.

ubuntu 15.04からは/etc/default/dockerが使えなくなったので(参考)今回は以下のコマンドで直接デーモンを起動しました.

$sudo service docker stop
$docker daemon -D -H unix:// -H tcp://0.0.0.0:2376 --cluster-store=consul://<Manager のIPアドレス>:8500 --cluster-advertise=<vxlanの紐づくデバイス名 例:eth0>:0

次にSwarmノードの登録を行います.

$sudo docker run -d --net=host --privileged --name consul progrium/consul -join <Manager のIPアドレス>
$sudo docker run -d --net=host -p 2376:2376 --name swarm swarm join --advertise=<Node のIPアドレス>:2376 consul://127.0.0.1:8500/swarm

確認

Managerで以下のように出力されれば成功です.

$docker -H 127.0.0.1:2375 info
Containers: 4
Images: 6
Role: primary
Strategy: spread
Filters: health, port, dependency, affinity, constraint
Nodes: 2
 awesome-song: 192.168.100.11:2376
  └ Containers: 2
  └ Reserved CPUs: 0 / 1
  └ Reserved Memory: 0 B / 513.6 MiB
  └ Labels: executiondriver=native-0.2, kernelversion=3.19.0-15-generic, operatingsystem=Ubuntu 15.04, storagedriver=aufs
 worthy-rule: 192.168.100.12:2376
  └ Containers: 2
  └ Reserved CPUs: 0 / 1
  └ Reserved Memory: 0 B / 513.6 MiB
  └ Labels: executiondriver=native-0.2, kernelversion=3.19.0-15-generic, operatingsystem=Ubuntu 15.04, storagedriver=aufs
CPUs: 2
Total Memory: 1.003 GiB
Name: MAAS-server

ちなみに

http:// Manager のIPアドレス :8500

でcunsulのノードや交換されている情報が覗けるのでここでもクラスタが組めているかが確認できます.

Screen Shot 2015-11-19 at 23.30.46

できてそうですね.

Swarmクラスタのマルチホストネットワーク

ではついにネットワークの作成です.
とは言え難しいことはほとんどありません.
まず現状のネットワーク状況の確認から

以下は全てMaster上での作業です.-H 127.0.0.1:2375で明示的にホストを指定していますが,export DOCKER_HOST=127.0.0.1:2375としておけば省略可能です.

$docker -H 127.0.0.1:2375 network ls
NETWORK ID          NAME                           DRIVER
a07a12ff3d7a        awesome-song/none              null
cb252e2f7d0a        awesome-song/host              host
2e6e98f302dd        awesome-song/bridge            bridge
56ea118e07ec        awesome-song/docker_gwbridge   bridge
d89042992a70        worthy-rule/none               null
32561a25f5fb        worthy-rule/host               host
84ff1cbe3e67        worthy-rule/bridge             bridge
f84f9b47cc09        worthy-rule/docker_gwbridge    bridge

順番は前後しますがだいたい同じはずです.

それぞれどのホストのネットワークなのかNAMEを見ればすぐわかると思います.

host, none, bridgeは昔ながらのdockerネットワークのホストモード,none,docker0ブリッジです.

ではここでマルチホストネットワークを作成します.

$docker -H 127.0.0.1:2375 network create backend
$docker -H 127.0.0.1:2375 network ls
NETWORK ID          NAME                           DRIVER
f84f9b47cc09        worthy-rule/docker_gwbridge    bridge
dede253b1863        worthy-rule/none               null
44d2707f300f        worthy-rule/host               host
563e8ac0486a        awesome-song/bridge            bridge
56ea118e07ec        awesome-song/docker_gwbridge   bridge
8322a02d1fd3        awesome-song/none              null
5a4c5085ca9a        backend                        overlay
6cb857289a36        worthy-rule/bridge             bridge
6a8851d55453        awesome-song/host              host

さて,新しいネットワークbackendができました.これはいずれのホストにも属していないネットワークになります.

Swarmマネージャに対してnetwork createを実行するとデフォルトでoverlayドライバが選択されます.

ドライバを指定したい場合にはnetwork create -d bridgeなどを行います.

次にbackendに属したコンテナを作成します.

$docker -H 127.0.0.1:2375 pull ubuntu
$docker -H 127.0.0.1:2375 run -itd --net=backend --name=app1 ubuntu /bin/bash
$docker -H 127.0.0.1:2375 exec -it app1 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
    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
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
13: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
    link/ether 02:42:0a:00:00:02 brd ff:ff:ff:ff:ff:ff
    inet 10.0.0.2/24 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:aff:fe00:2/64 scope link
       valid_lft forever preferred_lft forever
20: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.2/16 scope global eth1
       valid_lft forever preferred_lft forever
    inet6 fe80::42:acff:fe12:2/64 scope link
       valid_lft forever preferred_lft forever

2つのインターフェースを持ったコンテナが作成されます.

ここでeth0がコンテナ間通信用,eth1が外部との通信用です.

なのでこのままでも外とNATで通信することが可能です.

$docker -H 127.0.0.1:2375 exec -it app1 ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=59 time=9.45 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=59 time=6.46 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=59 time=6.69 ms
^C
--- 8.8.8.8 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 6.467/7.538/9.454/1.361 ms

更にもう一台コンテナを作成し,通信してみます

$docker -H 127.0.0.1:2375 run -itd --net=backend --name=app2 ubuntu /bin/bash
$docker -H 127.0.0.1:2375 ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED                  STATUS                  PORTS               NAMES
12047321581a        ubuntu              "/bin/bash"              Less than a second ago   Up Less than a second                       awesome-song/app1
374aeb62fa75        ubuntu              "/bin/bash"              Less than a second ago   Up Less than a second                       worthy-rule/app2
78f4bcfde082        swarm               "/swarm join --advert"   Less than a second ago   Up Less than a second                       worthy-rule/swarm
2f2dda9e7dcd        progrium/consul     "/bin/start -join 192"   Less than a second ago   Up Less than a second                       worthy-rule/consul
1547d49fe9a6        swarm               "/swarm join --advert"   Less than a second ago   Up Less than a second                       awesome-song/swarm
22b4cab4fa4b        progrium/consul     "/bin/start -join 192"   Less than a second ago   Up Less than a second                       awesome-song/consul
$docker -H 127.0.0.1:2375 exec -it app1 ping app2
PING app2 (10.0.0.3) 56(84) bytes of data.
64 bytes from app2 (10.0.0.3): icmp_seq=1 ttl=64 time=0.526 ms
64 bytes from app2 (10.0.0.3): icmp_seq=2 ttl=64 time=0.539 ms
64 bytes from app2 (10.0.0.3): icmp_seq=3 ttl=64 time=0.500 ms
64 bytes from app2 (10.0.0.3): icmp_seq=4 ttl=64 time=0.466 ms
^C
--- app2 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 2999ms
rtt min/avg/max/mdev = 0.466/0.507/0.539/0.039 ms

異なるホスト間のコンテナ同士で疎通できていることが確認できました.

また同一ネットワークに属したコンテナならコンテナ名で疎通可能です.

これはコンテナ作成時にコンテナ内の/etc/hostsが更新されるためです.

$docker -H 127.0.0.1:2375 exec -it app1 cat /etc/hosts
10.0.0.2        12047321581a
127.0.0.1       localhost
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
10.0.0.3        app2
10.0.0.3        app2.backend

じゃあ今度は既存のNWに新しいコンテナを接続してみましょう.

まず新しいコンテナを作ります.

$docker -H 127.0.0.1:2375 run -itd --name=app3 ubuntu /bin/bash
$docker -H 127.0.0.1:2375 exec app3 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
    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
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
22: eth0: <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
    inet 172.17.0.2/16 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:acff:fe11:2/64 scope link
       valid_lft forever preferred_lft forever

デフォルトのネットワーク(NATされるブリッジ)に接続されている事がわかりました.

ではbackendに接続します.

$docker -H 127.0.0.1:2375 network connect backend app3
$docker -H 127.0.0.1:2375 exec app3 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
    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
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
22: eth0: <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
    inet 172.17.0.2/16 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:acff:fe11:2/64 scope link
       valid_lft forever preferred_lft forever
24: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
    link/ether 02:42:0a:00:00:04 brd ff:ff:ff:ff:ff:ff
    inet 10.0.0.4/24 scope global eth1
       valid_lft forever preferred_lft forever
    inet6 fe80::42:aff:fe00:4/64 scope link
       valid_lft forever preferred_lft forever
$docker -H 127.0.0.1:2375 exec app3 ping app1
PING app1 (10.0.0.2) 56(84) bytes of data.
64 bytes from app1 (10.0.0.2): icmp_seq=1 ttl=64 time=0.079 ms
64 bytes from app1 (10.0.0.2): icmp_seq=2 ttl=64 time=0.062 ms
64 bytes from app1 (10.0.0.2): icmp_seq=3 ttl=64 time=0.059 ms

最後に複数ネットワークに接続を試してみます.

まずbackend2ネットワークを作成してapp3コンテナを接続...

$docker -H 127.0.0.1:2375 network create --subnet=192.168.10.0/24 backend2
$docker -H 127.0.0.1:2375 run -itd --net=backend2 --name=app4 ubuntu /bin/bash
$docker -H 127.0.0.1:2375 network connect backend2 app3
Error response from daemon: invalid container <nil> : nosuchcontainer: no such id: app3

あれ?
ちなみにNode2のdockerデーモンのログ

ERRO[161920] Handler for POST /v1.21/networks/3f1df16f133f58d46e0c864ca6c7f9f95281ce61af6f864d6578e98ffeb787ec/connect returned error: invalid container <nil> : nosuchcontainer: no such id: app3
ERRO[161920] HTTP Error                                    err=invalid container <nil> : nosuchcontainer: no such id: app3 statusCode=404

うーん,複数ネットワークに所属可能なまずなんですが...

ちなみに交換されているネットワーク情報もconsulの中身を見ることで(http:// Manager のIPアドレス /ui/#/dc1/kv/docker/network/v1.0/)知ることができます.
consulnetwork
swarmにはweb UIはありませんが,そこはついこないだ発表されたDocker Universal Control Planeですね.

こちらについてはまた後日.

とりあず今回はここまで.