tkak's tech blog

This is my technological memo.

今更ですが、CoreOSでDocker Swarmクラスタを組んでマルチホストネットワーク機能を試しました(Proxy環境下)

Dockerを使って開発していると、本番環境でもDockerを使ってみたいなぁという気持ちになります。ただ、本番環境でDockerを使うのは、個人的にまだまだハードルが高そうな印象です。マルチホスト間のネットワークはどうするか?分散システムのクラスタリングはどうするか?コンテナのスケジューリングは?監視は?ログは?デプロイの仕組みは?負荷が高い時の対応はどうするのか?セキュリティは?など、運用面で色々と考えなければなりません。さらにDocker界隈のツールは様々なツールが存在していてどれから手をつけていいのか選択に困ります。最初はなるべく小さく始めたいところです。なので、今回、本番環境を意識したDocker環境をより少ないツールで作れるかどうか試してみました。ひとまず、クラスタ組んで、マルチホストネットワーキング機能が試せるところまで。使ったツールは以下になります。

今回試した環境のterraformファイルは、githubにあげてますので、よかったら参考にどうぞ。

なお、プライベートな環境下で構築を試したのでProxyの設定をもろもろしていますが、参考にする場合は適当に読み替えていただければと思います。

TerraformによるCoreOSのデプロイとetcdの設定

Dockerを本番環境で使う場合、複数ホストのコンテナ間通信をどうするかというのは気になる問題ですが、いくつか方法が存在します。

Dockerのバージョン1.9からマルチホストネットワーク機能が正式に導入されたので、今回はそれを使ってみます。マルチホストネットワーク機能を使うためには、以下のような条件が必要です。今回、極力カーネルコンパイルとかはしたくなかったので、条件を満たすためにCoreOSを選択しました。

  • DockerホストOSのカーネルが3.16かそれ以降
  • Dockerがサポートしている分散KVS(Consul, Etcd, ZooKeeper)が利用できること

まず、CoreOSサーバ3台を作りetcdクラスタを組みます。Dockerのドキュメントではdocker-machineを使った手順なんですが、普段からTerraformでインフラ管理してるので今回Terraformを使います。Terraformの設定ファイルは以下になります。個々のパラメータは適当に読み替えてください。

## main.tf
provider "openstack" {
    domain_name = "default"
    tenant_name = "project-1"
    auth_url = "<keystone endpoint>"
}

variable "servers" {
    default = "test-1,test-2,test-3"
}

resource "null_resource" "discovery_url_template" {
    provisioner "local-exec" {
        command = "curl -s 'https://discovery.etcd.io/new?size=${length(split(",", var.servers))}' > discovery_url"
    }
}

resource "template_file" "discovery_url" {
    template = "discovery_url"
    depends_on = [
        "null_resource.discovery_url_template"
    ]
}

resource "template_file" "cloud-init" {
    count = "${length(split(",", var.servers))}"
    template = "${file("cloud-config.yml.tpl")}"

    vars {
        hostname = "${element(split(",", var.servers), count.index)}"
        discovery_url = "${template_file.discovery_url.rendered}"
    }
}

resource "openstack_compute_instance_v2" "coreos" {
    count = "${length(split(",", var.servers))}"
    name = "${element(split(",", var.servers), count.index)}"
    image_name = "coreos-stable-899-3-0"
    flavor_name = "flavor-1"
    region = "region-1"
    network {
        name = "network-1"
    }
    key_pair = "core"
    security_groups = ["default"]
    user_data = "${element(template_file.cloud-init.*.rendered, count.index)}"
}

CoreOSは、OS初期設定をcloud-config.ymlで行うのが標準仕様になってます。OpenStack上でCoreOSを立てる時は、user_dataconfig_driveにcloud-configの設定を渡すことで、インスタンス起動時に初期設定が行われます。cloud-config.ymlを動的に設定したいので、Terraformのtemplate_fileリソースを使います。cloud-config.yml.tplファイルは以下のようになります。

#cloud-config

hostname: ${hostname}

coreos:
  etcd2:
    discovery: ${discovery_url}
    discovery-proxy: http://proxy.example.com:1234 <- Discovery URLにアクセスするためのproxyの設定
    advertise-client-urls: http://$private_ipv4:2379,http://$private_ipv4:4001
    initial-advertise-peer-urls: http://$private_ipv4:2380
    listen-client-urls: http://0.0.0.0:2379,http://0.0.0.0:4001
    listen-peer-urls: http://$private_ipv4:2380
  units :
    - name: etcd2.service
      command: start
    - name: docker-tcp.socket
      command: start
      enable: true
      content: |
        [Unit]
        Description=Docker Socket for the API

        [Socket]
        ListenStream=2375
        BindIPv6Only=both
        Service=docker.service

        [Install]
        WantedBy=sockets.target
    - name: docker.service
      drop-ins:
        - name: 20-http-proxy.conf
          content: |
            [Service]
            Environment="HTTP_PROXY=http://proxy.example.com:1234"
      command: restart

etcdクラスタを組む際、各サーバで動かすetcd同士が同じクラスタに所属することを識別させるための情報(discovery token)が必要になります。手動でやる場合、以下のURLにアクセスしdiscovery tokenを取得してetcdの設定に使用するんですが、今回はTerraformのnull_resourceを使ってtokenを取得し、cloud-config.ymlを動的に生成するようにしています。

$ curl -s 'https://discovery.etcd.io/new?size=3'

以上のファイルの準備が整ったらterraform applyを実行します。インスタンスが立ち、etcdクラスタが作られます。etcdクラスタが組めているか、以下のコマンドで確認します。

$ etcdctl cluster-health
member 39da9b2c59694a93 is healthy: got healthy result from http://xxx.xxx.xxx.xxx:2379
member 53d999311a07d120 is healthy: got healthy result from http://yyy.yyy.yyy.yyy:2379
member d4822f0cf45608fd is healthy: got healthy result from http://zzz.zzz.zzz.zzz:2379
cluster is healthy

Docker Swarmクラスタを組む

次にDocker Swarmクラスタを組みます。Docker Swarmは、Dockerホストをクラスタリングし、Dockerコンテナを起動する時にどのホストで起動させるのかをスケジュールするツールです。Docker社が提供してます。

Docker Swarmは、AgentとManagerの2つの構成になっていて、それぞれDockerコンテナとして動かします。また、Swarm Managerは1台あればSwarmクラスタが組めますが、Swarm Managerが落ちるとDocker Swarm自体が使えなくなってしまうので、Swarm Managerを複数(プライマリとレプリカの構成で)動かします。その構成の場合、サービスディスカバリーとしてConsul, Etcd, ZooKeeperのどれかを使う必要があります。今回は、CoreOSに標準で組み込まれているEtcdを使います。

f:id:keepkeptkept:20160123163205p:plain

以下のコマンドで、Docker Swarm Agentを起動します。

$ docker run -d --name=swarm-agent swarm join --addr=$(cut -d'=' -f2 /etc/environment):2375 etcd://$(cut -d'=' -f2 /etc/environment):2379/nodes

次に、Docker Swarm Managerを起動します。4000番ポートをDocker Swarm Managerのポートにしています。

$ docker run -d -p $(cut -d'=' -f2 /etc/environment):4000:4000 --name=swarm-manager swarm manage -H :4000 --replication --advertise $(cut -d'=' -f2 /etc/environment):4000 etcd://$(cut -d'=' -f2 /etc/environment):2379/nodes

4000番ポートでdocker infoを実行するとクラスタの情報がわかります。

## Host A
$ docker -H $(cut -d'=' -f2 /etc/environment):4000 info
Containers: 6
Images: 3
Role: primary
Strategy: spread
Filters: health, port, dependency, affinity, constraint
Nodes: 3
 xxx: xxx.xxx.xxx.xxx:2375
  └ Status: Healthy
  └ Containers: 2
  └ Reserved CPUs: 0 / 4
  └ Reserved Memory: 0 B / 16.46 GiB
  └ Labels: executiondriver=native-0.2, kernelversion=4.3.3-coreos-r1, operatingsystem=CoreOS 899.3.0, storagedriver=overlay
 yyy: yyy.yyy.yyy.yyy:2375
  └ Status: Healthy
  └ Containers: 2
  └ Reserved CPUs: 0 / 4
  └ Reserved Memory: 0 B / 16.46 GiB
  └ Labels: executiondriver=native-0.2, kernelversion=4.3.3-coreos-r1, operatingsystem=CoreOS 899.3.0, storagedriver=overlay
 zzz: zzz.zzz.zzz.zzz:2375
  └ Status: Healthy
  └ Containers: 2
  └ Reserved CPUs: 0 / 4
  └ Reserved Memory: 0 B / 16.46 GiB
  └ Labels: executiondriver=native-0.2, kernelversion=4.3.3-coreos-r1, operatingsystem=CoreOS 899.3.0, storagedriver=overlay
CPUs: 12
Total Memory: 49.38 GiB
Name: be07ab661423


## Host B
$ docker -H $(cut -d'=' -f2 /etc/environment):4000 info
Containers: 6
Images: 3
Role: replica
Primary: xxx.xxx.xxx.xxx:4000
Strategy: spread
Filters: health, port, dependency, affinity, constraint
Nodes: 3
...
...

無事にDocker Swarmが組めているのがわかります。試しにSwarm Managerを落としてみると、primaryが別なホストに移るのがわかります。

## Host A
$ docker stop swarm-manager

## Host B
$ docker -H $(cut -d'=' -f2 /etc/environment):4000 info
Containers: 6
Images: 3
Role: primary
Strategy: spread
Filters: health, port, dependency, affinity, constraint
Nodes: 3
...
...

ちなみに、環境変数DOCKER_HOSTを設定しておくとオプションを省略できて、シングルホストの時のように使えます。

$ export DOCKER_HOST=$(cut -d'=' -f2 /etc/environment):4000
$ docker info
Containers: 6
Images: 3
Role: primary
Strategy: spread
Filters: health, port, dependency, affinity, constraint
Nodes: 3
...
...

また、Dockerコンテナをsystemdから起動できるようにする場合は、以下のような記述をcloud-config.yml.tplに追記します。

    - name: swarm-agent.service
      command: start
      content: |
        [Unit]
        Description=Docker Swarm Agent Container
        After=docker.service
        Requires=docker.service

        [Service]
        TimeoutStartSec=0
        Restart=always
        ExecStartPre=-/usr/bin/docker stop swarm-agent
        ExecStartPre=-/usr/bin/docker rm -f swarm-agent
        ExecStartPre=-/usr/bin/docker pull swarm
        ExecStart=/usr/bin/docker run --name=swarm-agent swarm join --addr=$private_ipv4:2375 etcd://$private_ipv4:2379/nodes

        [Install]
        WantedBy=multi-user.target
    - name: swarm-manager.service
      command: start
      content: |
        [Unit]
        Description=Docker Swarm Manager Container
        After=docker.service
        Requires=docker.service

        [Service]
        TimeoutStartSec=0
        Restart=always
        ExecStartPre=-/usr/bin/docker stop swarm-manager
        ExecStartPre=-/usr/bin/docker rm -f swarm-manager
        ExecStartPre=-/usr/bin/docker pull swarm
        ExecStart=/usr/bin/docker run -p $private_ipv4:4000:4000 --name=swarm-manager swarm manage -H :4000 --replication --advertise $private_ipv4:4000 etcd://$private_ipv4:2379/nodes

        [Install]
        WantedBy=multi-user.target 

こうすることでterraform applyだけで、Docker Swarmクラスタまで組めるようになります。

マルチホストネットワーク機能を試す

Docker Swarmクラスタが組めたので(ようやく)、次にマルチホストネットワーク機能を試します。

マルチホストネットワーク機能は、overlayドライバを使うことで仮想ネットワークを作ることができます。以下のコマンドでoverlayネットワークを作ります。Docker Swarmを使ってる場合、overlayドライバがデフォルトなので--driverオプションは省略可能です。

$ docker network create --driver overlay backend

ここで以下のようなメッセージが出る場合は、Dockerのクラスタ設定--cluster-advertise--cluster-storeが抜けているので、設定を追加します。

$ docker network create backend
Error response from daemon: 500 Internal Server Error: failed to parse pool request for address space "GlobalDefault" pool "" subpool "": cannot find address space GlobalDefault (most likely the backing datastore is not configured)

以下のファイルから、Dockerオプションを設定することができます。ファイルを作ったらdocker.serviceを再起動します。

$ sudo vi /etc/systemd/system/docker.service.d/30-custom.conf
[Service]
Environment="DOCKER_OPTS=--cluster-advertise eth0:2375 --cluster-store etcd://xxx.xxx.xxx.xxx:2379"

$ sudo systemctl daemon-reload
$ sudo systemctl restart docker

が、再度試したところ、また違うエラーが・・・。

$ docker network create backend
Error response from daemon: 500 Internal Server Error: error getting pools config from store during init: could not get pools config from store: client: response is invalid json. The endpoint is probably not valid etcd cluster endpoint.

今回、だいぶここでハマったんですが、Docker APIの通信がProxyを介してしまってうまくいかなかったようです。NO_PROXYにDockerホストのIPを全部並べてDockerホスト間はProxyさせないようにします。

$ cat /etc/systemd/system/docker.service.d/20-http-proxy.conf
[Service]
Environment="HTTP_PROXY=http://proxy.example.com:1234"
Environment="NO_PROXY=localhost,127.0.0.1,xxx.xxx.xxx.xxx,yyy.yyy.yyy.yyy,zzz.zzz.zzz.zzz"

再度docker.serviceを再起動させて、無事にネットワークができました。

$ docker network create backend
856e8f470224627390abaaa31db868bd01ba7423e0bf7ea5b020f7e8e0336b34
$ docker network ls
NETWORK ID          NAME                DRIVER
856e8f470224        backend             overlay
f52c2ae81a2a        xxx/bridge     bridge
f9c89af96fad        yyy/none       null
...
...

では、コンテナを2つ立ち上げて疎通が確認できるか試してみます。

$ docker run -itd --net=backend --name test1 --hostname test1 ubuntu /bin/bash
$ docker run -itd --net=backend --name test2 --hostname test2 ubuntu /bin/bash

$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
c1b2cd585175        ubuntu              "/bin/bash"         5 minutes ago       Up 49 seconds                           xxx/test2
01f8c3067398        ubuntu              "/bin/bash"         5 minutes ago       Up 5 minutes                            zzz/test1

docker psコマンドから、異なるホストでコンテナ2つが起動してるのがわかります。

$ docker exec -it zzz/test1 /bin/bash
root@test1:/# ip a
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
27: eth0@if28: <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
29: eth1@if30: <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

eth0についている10.0.0.0/24のネットワークがoverlayネットワークです。

root@test1:/# ping test2
PING test2 (10.0.0.3) 56(84) bytes of data.
64 bytes from test2 (10.0.0.3): icmp_seq=1 ttl=64 time=0.950 ms
64 bytes from test2 (10.0.0.3): icmp_seq=2 ttl=64 time=0.544 ms
^C
--- test2 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 0.544/0.747/0.950/0.203 ms
root@test1:/#

おぉ、疎通も無事確認できました!

ところで、コンテナ同士の名前解決はどうやってるんでしょうか。/etc/resolv.confなどをみてみましたが、どうやら/etc/hostsでやっているようです。

root@test1:/# cat /etc/hosts
10.0.0.2        test1
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        test2
10.0.0.3        test2.backend
root@test1:/# 

試しに、新しくtest3を作ってみると/etc/hostsに新しい設定が追加されてました。

root@test1:/# cat /etc/hosts
10.0.0.2        test1
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        test2
10.0.0.3        test2.backend
10.0.0.4        test3
10.0.0.4        test3.backend
root@test1:/# 

まとめ

Dockerを本番環境で使うために、CoreOSでDocker Swarmクラスタを組んでマルチホストネットワーク機能を試してみました。DockerをProxy環境下で動かす場合は、いろんな場所にproxy設定を記述しないといけないのがアレですね。ここまで動かすのにProxyのおかげでだいぶ消耗しました・・・。あと、コンテナを立ち上げるのにdocker runコマンド経由なのが少し煩わしく感じました。その辺はスクリプトにするか、もう少し宣言的に出来ないかと思うのでdocker-composeやnomadなどを検討する必要がありそうです。でも、まぁ、ひとまず、環境ができてよかったです。

プログラマのためのDocker教科書 インフラの基礎知識&コードによる環境構築の自動化

プログラマのためのDocker教科書 インフラの基礎知識&コードによる環境構築の自動化

Docker 実践ガイド (impress top gear)

Docker 実践ガイド (impress top gear)