いまさら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 libnetworkデザイン

最近Dockerネットワークの機能がいろいろ増えたりしてるけどあんまり使われてないように思います.

Docker enginのドキュメントだとDockerが叩くAPIからの説明しか無いので,機能の実装側であるlibnetworkについて自分の勉強も兼ねてまとめていこうと思います.まずはlibnetworkについて根本思想となるContainer Network Modelについて理解するために公式のdesign.mdを和訳してみました.
思想とlibnetworkの実装が混ざっていてちょっとわかりづらい部分も有るが,やはり全体を把握するには一番いい資料ですので.今後各ドライバなどについても詳解するつもりで,その中でわかってくることもあるかと思います.

Design

libnetworkのバージョンやゴールはroadmapにハイライトされている.
このドキュメントはlibnetworkが如何にデザインされて今の形になっているかを説明する.
特定のリリースでの情報はProject Pageを参照のこと.

多くのデザインの方向性はDocker v1.6のネットワークデザインを参考にしている.
Docker v1.6のネットワークデザインについてはDocker v1.6 Designを参照のこと.

Goal

libnetworkプロジェクトはdeveloping small, highly modular and composable tools that works well independentlyの哲学に従っている.
libnetworkはコンテナのネットワークに必要な機能を満足させることを目的とする.

The Container Network Model

LibnetworkはContainer Network Model (CNM)の実装という位置付けである.
CNMはコンテナに対し様々なネットワーク機能を提供するための抽象化の枠組みで,主に以下の3コンポーネントから成る.

Sandbox

Sandboxはコンテナのネットワークスタックを定義する.
コンテナのインターフェースやルーティングテーブル,DNSの設定などである.
実装としてはLinuxのネットワークネームスペース,FreeBSDのJailなどがこれに相当する.
Sandboxは複数のendpointを持ち,そのEndpointはそれぞれ異なるnetworkに所属することが可能である.

Endpoint

EndpointはSandboxをネットワークに参加させる役割を持つ.
実装としてはvethやOpen vSwitchのinternal portなどがある.
Endpointは必ず1つのネットワークに所属し,1つの以下のSnadboxに所属することができる.

Network

NetworkはEndpointのグループであり,それぞれのEndpointが直接通信するが可能である.
実装としてはLinux bridgeやVLANなどである.
当然,ネットワークは複数のEndpointから成る.

CNM Objects

NetworkController
NetworkController はDockerにおけるDocker daemonのようにAPIを提供し,Networkを管理する.
libnetworkはinbuilt・remote問わず複数のdriverが同時に動作することをサポートしている.

Driver
Driver は実際にネットワークを作成する実装となる.
libnetworkではDriver特有のoption, labelをDriverへ透過的に渡すためのAPIが提供されている.
DriverにはBridgeやHost, None, overlay といったのinbuiltのものとサードパーティの提供するremoteがある.
Driverが提供するネットワークに対し責任を持ち,管理する.
将来的に多くのネットワーク機能を管理できるように幾つかのDriverを用意する予定である.

Network
NetworkオブジェクトはCNMモデルにおけるNetworkの実装となる.
NetworkControllerNetworkオブジェクトを作成,管理するAPIを提供する.
Networkが作成されたり,情報の更新があった際にDriverへイベントの通知が送られる.
libnetworkでは同一Networkに所属するendpoint間での疎通を可能にし,他のNetworkと隔離するというレベルの抽象化を行っている,
実際にはDriverが疎通と隔離を管理する必要がある.
このNetworkがグローバルスコープである場合には,同一ホスト内及び複数ホスト間の両方で疎通が可能である.

Endpoint
Endpointはサービスのエンドポイントであり,あるコンテナのサービスがネットワーク内の別コンテナのサービスへ疎通可能となる.
NetworkオブジェクトがEndpointの作成及び管理のAPIを提供します.あるEndpointは1つのNetworkにアタッチされる.
Endpointの作成は,対応するSandboxのリソース管理を行うDriverオブジェクトによって行われる.
Endpointはコンテナに特有である必要はなく,クラスタ内でグローバルスコープを持つのが好ましい.
(
補足:クラスタ内で一意であり,クラスタを跨いだ別のSandboxにアタッチなどが可能)

Sandbox
Sandboxオブジェクトはコンテナ内のネットワーク設定,例えばIPアドレスやMACアドレス,ルーティングテーブル,DNSエントリなどを表す.
SandboxオブジェクトはユーザがNetworkに対しEndpointの作成を要求したタイミングで作成される.
そのNetworkを管理するDriverはIPアドレスなどのリソースを割り当て,libnetwrokへSandboxInfoを返す.
libnetworkはネットワーク情報を格納するSandboxの実装としてOSごとに適した構成を用いている(例えばLinuxではnetns).
Sansboxは異なるnetwrokに属する複数のendpointをアタッチすることが可能である.
Sandboxはあるホストの特定のコンテナに紐づくため,そのコンテナが存在するホストに閉じたもの(ローカルスコープ)となる.

CNM Attributes

Options
Options はDriver固有の設定を行う共通的かつ柔軟な枠組みである.
Optionsはkey-valueのペアでkeyは文字列,valueは一般的なオブジェクトである(例えばgo言語のinterface{}など).
libnetworkはkeynet-labelsパッケージに定義されたLabelに一致するときのみOptionsを受け付ける.
Optionsは下記で説明するLabelsの概念を包含する.
エンドユーザーがUIからOptionsを確認することはできないがLabaelsは可能である.

Labels
LabelsOptionsと非常に似ており,実質的にOptionsの一部である.
Labelsはエンドユーザーから確認することが可能であり,--labelsオプションを指定することで明示的にUIで指定可能である.
UIからDriverへと渡され,Driverはこれを使用し,そのDriver固有の操作を行うことができる(例えばネットワークにIPアドレスを割り当てたりなど).

CNM Lifecycle

CNMの利用者,たとえばDockerはCNMオブジェクトおよびそのAPIを介して操作すべきコンテナのネットワークをやり取りを行う.

  1. DriversNetworkControllerを登録する.Build-inのドライバはlibnetworkの内部から,remoteドライバはPluginメカニズムを介して行う(Pluginメカニズムについては今後).各driverは特定のnetworkTypeを持つ.
  2. NetworkControllerオブジェクトはlibnetwork.New()APIを用いてネットワークの割当やDockerへのOptionsを用いてDriverへの設定を行う.
  3. NetworkはコントローラのNewNetwork()APIによってnamenetworkTypeを与えられる.
    networkTypeNetwork作成時にどのDriverを選択すべきかを示す.
    これ以降,Networkに対する操作は全てDriverから行われる.
  4. controller.NewNetwork() APIはoptionsパラメータを取ることができる.
    これはドライバ固有のオプションやLabelsを与えるものである.
  5. network.CreateEndpoint() は新しいEndpointをネットワークが作成する際に呼ばれる.
    このAPIはoptionsパラメータを取ることができる.
    これらのoptionsは定義されたラベルかつドライバ固有のラベルを指定できる.
    そしてドライバがdriver.CreateEndpointによって呼ばれ,NetworkEndpointが作成された時に予約されたIPv4/IPv6アドレス群の中からアドレスが選ばれる.
    このIP/IPv6アドレスはそのエンドポイントが待ち受けるポートと共にサービスのエンドポイントとして定義されなければならない.
    なぜならサービスのエンドポイントの本質はそのコンテナが待ち受けているネットワークアドレスとポート番号に他ならないからである.
  6. endpoint.Join()はコンテナにEndpointをアタッチする際に用いられる.
    このJoinはそのコンテナのためのSandboxが未だ作成されていなければ作成する.
    ドライバはSandboxのIDを複数のエンドポイントが同じコンテナにアタッチされるために利用することができる.
    このAPIはまたoptionsパラメータを指定可能である.
  7. libnetworkの直接的なデザインの問題ではないがDockerのようにendpoint.Join()が呼ばれるのは,コンテナが作成が呼び出されるStart() ライフライクルであることが強く推奨される.
  8. エンドポイントのjoin()APIについてのFAQにおいて,エンドポイントのcreateとjoinを別のAPIにする必要があるのかというものがあった.
    • これに対する答えはエンドポイントはサービスを表現しており,それはコンテナとは独立である.エンドポイントが作成された時,リソースが確保されいかなるコンテナもそのエンドポイントにあとからアタッチすることが可能であり,一貫したネットワークの挙動を示す.
  9. endpoint.Leave() はコンテナがストップした際に呼ばれる.
    DriverJoin()の時に確保したものを削除する.
    libnetworkはSandboxの削除を,それを参照する最後のエンドポイントがleaveした時に行う.
    しかしlibnetworkはエンドポイントが持つIPアドレスを持ち続け,コンテナ(同じか別かに関わらず)再びjoinした時にそのアドレスを使用する.
    これはコンテナのリソースがストップし,再スタートしても変わらないことを保証する.
  10. endpoint.Delete() はエンドポイントをネットワークから削除するときに用いられる.
    エンドポイントが削除されるとsandbox.Infoからも取り除かれる.
  11. network.Delete() はネットワークを削除するときに用いられる.
    libnetworkはネットワークに所属するエンドポイントが存在する場合にはそのネットワークを削除する事はできない.

Implementation Details

Networks & Endpoints

libnetworkのネットワークとエンドポイントのAPIは基本的にCNMで示されるオブジェクトと抽象化のレベルに一致させている.
CNMで示したように本質的な実装はドライバで行われている.参考:the drivers section

Sandbox

libnetworkは複数のOSでSandboxを実装するためのフレームワークを提供している.
現状Linuxではnamespace_linux.goconfigure_linux.gosandboxパッケージじ含まれる.
これはファイルシステムにおいて唯一のパスを持つネットワークネームスペースをそれぞれのサンドボックスごとに持つ.
Netlinkがグローバルネットワークからサンドボックスのネームスペースにインターフェースを移動するために用いられる.
Netlinkはネットワーク内でのルーティングテーブルの操作も行う.

Drivers

API

ドライバはlibnetworkを拡張し,上で述べたlibnetworkのAPIを実装している部分となる.
そのため全てのNetworkEndpoint の以下のAPIに1対1で対応するものを持つ.

  • driver.Config
  • driver.CreateNetwork
  • driver.DeleteNetwork
  • driver.CreateEndpoint
  • driver.DeleteEndpoint
  • driver.Join
  • driver.Leave

ドライバはユニークなID (networkid,endpointid,…) を持つ.
これはユーザがAPIを操作する際に目にする名前に対応している.

APIは未だ過渡的なものであり,特にマルチホストネットワークを実現する際には変わり得るものである.

Driver semantics

  • Driver.CreateEndpoint

このメソッドはInterfaceAddInterfaceメソッドによってEndpointInfoインターフェースを渡す.
Interface の返り値がnon-nilの場合,ドライバはそこに含まれるインターフェースの情報を利用(例えばアドレスは静的に与えられているものとして扱う)し,それが不可能な場合はerrorを返さなくてはならない.
返り値がnilの場合にはドライバが新しいインターフェースを割り当てなければならないを意味し,AddInterfaceによってそれらを登録するか,不可能な場合にはerrorを返す.

AddInterfaceInterfacenon-nilの場合には使用できない.

Implementations

libnetworkは以下のドライバを持つ.

  • null
  • bridge
  • overlay
  • remote

Null

nullドライバはAPIとしてnoopの実装となっており,ネットワーキングを必要としない時にのみ利用される.
これはDockerの --net=noneオプションのバックワードコンパチビリティのために用意されている.

Bridge

bridgeドライバはLinux Bridgeをベースとするブリッジング接続を提供する.
より詳細はブリッジドライバの章で説明する.

Overlay

Overlayドライバの実装はVXLAN等のカプセリングによるオーバレイネットワークによりマルチホストネットワークを実現する.
より詳細はオーバレイドライバの章で説明する.

Remote

remoteパッケージはドライバを提供しておらず,リモート接続のドライバをサポートする方法が示されている.
貴方は好きな言語でドライバを実装することができる.
より詳細はリモートドライバの章で説明する.

 

Raspberry Piで「簡単に」コンテナ!(Raspberry PiでRunC)


 
Raspberry Piでコンテナを動かしたい!
と思うことが最近よくあります.
普段サーバではコンテナでアプリをデプロイしてるのでベアメタルに直接いろいろアプリを入れるのやだなーと.
でもRaspberry PiでDockerって結構面倒なんですよね.それ専用のイメージにしたり...
そもそもDockerのイメージ管理とかあればいいけど必須ではない.
aufsとか使うとSDカードだとパフォーマンスめっちゃ落ちる...

というわけでRunC(https://runc.io/)が使えないかなと.
これなら普通のRaspbianですぐ使えるし,パフォーマンスの劣化も少なそう(未確認)
早速やってみました.

準備

Go言語インストール
こことか参考にGo langをraspbianにインストール!

$ sudo apt-get install -y mercurial gcc libc6-dev
$ hg clone https://code.google.com/p/go/ $HOME/go
$ cd $HOME/go/src  
$ ./make.bash

うむうむ
ただこれだと$HOME以下にGoのバイナリが入っちゃうので(別にいいなら良し)Goのバイナリを/usr/local以下に移動
$GOPATHになるディレクトリを作成

$ sudo mv $HOME/go /usr/local
$ mkdir $HOME/go

でGoに関わる環境変数を~/.bashrcに追加

export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin
export GOROOT=/usr/local/go
export GOPATH=$HOME/go

を追記

$ go version

でちゃんとGoのバージョンが表示されればOK

RunCビルド

次にRunCをビルドします.
go getで普通に取ってこれます

$ go get github.com/opencontainers/runc

多分エラーが出ますが無視して改めてビルドします.

$ cd go/src/github.com/opencontainers/runc/
$ make BUILDTAGS=""
$ sudo make install

ここでBUILDTAGS=""を指定しておかないとエラーになります.
Raspbianがseccompに未対応だからだそうです.

これでrunc -vとかするとRunCのバージョンが表示されるはずです.

コンテナのファイルシステム作成

これでRunCがインストールされました.
一応すぐにもコンテナは作成可能なのですが,先に言ったようにRunCはアプリ実行環境のパッケージング機能がないので実行環境を別途用意して上げる必要があります.
それもarm用の(!)
ああ面倒くさい.これがネックでした.いっそraspbianのシステムファイル全部コピーでもしようかなと...

ですがよく考えるとarmアーキテクチャのVMを作ったりする必要すらありませんでした.
方法は至極簡単.普通のx86アーキテクチャのDockerホストでraspi用のDockerイメージをpullしてexportするだけ.
boot2dockerでもなんでも使いましょう.

*x86 Dockerホスト上
$ docker pull sdhibit/rpi-raspbian
$ docker run --name=rsptest sdhibit/rpi-raspbian /bin/bash

ちなみにraspi用のイメージが慣習的に頭にrpi-がついています.
当然x86では実行できないと起こられますが,気にせず

*x86 Dockerホスト上
$ docker export rsptest > raspbian.tar

でraspbian.tarにイメージのファイルシステムをexportします.
これでarm用のraspbianのファイルシステムがtarで固められた状態で手にはいります.
もちろん他のDockerイメージ(Raspberry Pi用)でもOK.
このraspbian.tarを先ほどRunCを入れたraspberry piに送って,rootfsというディレクトリに展開します.

$ mkdir ~/rpi-raspbian
$ mkdir ~/rpi-raspbian/rootfs
$ sudo tar xvf raspbian.tar -C ~/rpi-raspbian/rootfs

そしてrootfsと同階層にRunCのコンフィグファイルを生成します.

$ sudo runc spec
$ ls
config.json  rootfs  runtime.json

ここで注意.今回RunCのビルド時にseccompを無効化していますのでconfigファイルでもその設定を行う必要があります.
210行目付近の

 "seccomp": {
     "defaultAction": "SCMP_ACT_ALLOW",
     "architectures": null,
     "syscalls": []
 },

を削除します.
これで

sudo runc start

とするとコンテナが立ち上がりまっさらなraspbianが立ち上がります.
ただip linkとかするとわかりますがこのままではネットワークネームスペースも分離されていますので,ほとんど何もできません...
今回私はファイルシステムだけ分離されていればよかったのでネットワークネームスペースは分離させないようconfig.jsonを変更します.
runtime.jsonの130行目付近の

"type": "network",
    "path": ""
},

を削除します.ちなみにその付近のpidやらがそれぞれのネームスペースなので隔離しないものを削除してください.
あとconfig.jsonの最後に

                "capabilities": [
                        "CAP_NET_RAW",
                        "CAP_NET_BIND_SERVICE",
                        "CAP_AUDIT_WRITE",
                        "CAP_DAC_OVERRIDE",
                        "CAP_SETFCAP",
                        "CAP_SETPCAP",
                        "CAP_SETGID",
                        "CAP_SETUID",
                        "CAP_MKNOD",
                        "CAP_CHOWN",
                        "CAP_FOWNER",
                        "CAP_FSETID",
                        "CAP_KILL",
                        "CAP_SYS_CHROOT",
                        "CAP_NET_BROADCAST",
                        "CAP_SYS_MODULE",
                        "CAP_SYS_RAWIO",
                        "CAP_SYS_RAWIO",
                        "CAP_SYS_ADMIN",
                        "CAP_SYS_NICE",
                        "CAP_SYS_RESOURCE",
                        "CAP_SYS_TIME",
                        "CAP_SYS_TTY_CONFIG",
                        "CAP_AUDIT_CONTROL",
                        "CAP_MAC_OVERRIDE",
                        "CAP_MAC_ADMIN",
                        "CAP_NET_ADMIN",
                        "CAP_SYSLOG",
                        "CAP_DAC_READ_SEARCH",
                        "CAP_LINUX_IMMUTABLE",
                        "CAP_IPC_LOCK",
                        "CAP_IPC_OWNER",
                        "CAP_SYS_PTRACE",
                        "CAP_SYS_BOOT",
                        "CAP_LEASE",
                        "CAP_WAKE_ALARM"
                ]

とか書いとけばDockerで言うところのprivilegeになります.
Dockerが簡単に動けばそっちの方が楽かもしれませんが軽量な分raspiなんかには調度良いかなと思います!

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ですね.

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

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