パラボラアンテナと星の日記

あることないこと

EC2で複数ホストでdocker swarm します、swarm managerも冗長化するよ

f:id:hoppie:20150928031455p:plain

この記事でやること

こういう感じのクラスタをつくります

f:id:hoppie:20150929064649p:plain

ポイントとしては、

  • EC2インスタンスを2台動かし、両方で
    • swarm manager を動かす
    • swarm agent を動かす ということをやります。

agentを複数ホストでやるやつは、ググれば結構出てきますが、

今回はmanagerも複数ホストでやってみます。

今回利用した各バージョンとかは以下のとおり。


  • AMI: ami-f2338ff2 CoreOS-stable-723.3.0-hvm
    • CoreOS: 723.3.0
      • docker: 1.6.2
      • etcd: 2.0.12
  • docker swarm: 0.4.0

作ろう

以下、cloud-config を作っていきます

あ、先に断っておきますが、 Docker Swarm によると現在まだ

Note: Swarm is currently in BETA, so things are likely to change. We don’t recommend you use it in production yet.

な感じなので、将来的にこのコマンドで動くかどうかはわかりません。

etcd

サービスディスカバリーの部分です。

本来、swarm create でdocker hub のトークンを得るのが楽で良いのですが、

複数マネージャーの立て方を紹介したページ High availability in Docker Swarm によると、複数managerを動かす場合は token:// が使えず、現在のところ etcd://zk://consul:// の3つでしか動かないとのことです。

今回は、CoreOSなので、etcd を使ってみました。

以下のサンプルを元にやりました。

(サンプルに合った不要な記述を少し削除しましたが)簡単に動きました。

Customize with Cloud-Config

こんな感じ。

#cloud-config

coreos:
  etcd2:
    # <https://discovery.etcd.io/new?size=Xでトークンを得る。今回はX=1>
    discovery: https://discovery.etcd.io/tokenxxxxxxxxxxxxxxxxxxxxxxxxxxx
    advertise-client-urls: http://$private_ipv4:2379
    initial-advertise-peer-urls: http://$private_ipv4:2380
    listen-client-urls: http://0.0.0.0:2379
    listen-peer-urls: http://$private_ipv4:2380
  units:
    - name: etcd2.service
      command: start

Docker Remote APIを、TCP経由で使えるように準備

Create a swarm for development によると、

swarm agentは、Docker daemonに対して外からTCP経由での接続が必要なようです。

ここで大事なこととして、

Caution : Only use this set up if your network environment is secured by a firewall or other measures.

とありますので、注意が必要です(私はセキュリティグループで 該当ポートはVPC内での接続しか受け付けないようにしました)。

今回はCoreOSのこちらのページ を参考に、DockerのRemote APIを、TCP 2375番で晒しました。

coreos:
  units:
    - 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

ここで、もしうまくいっていれば、以下の様に docker ps コマンドが、(エラーにならずに)実行できるはずです

# docker daemonが2375ポートで動いている場合
$ docker -H :2375 ps; echo $?
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS               NAMES
0

# docker daemonが2375ポートで動いていない場合(うまくいっていない場合)
$ docker -H :2375 ps; echo $?
FATA[0000] Cannot connect to the Docker daemon. Is 'docker -d' running on this host?
1

swarm agentを動かす

Create a swarm for development を参考に。

コマンドの形は docker run [runのoption] swarm(=イメージ名) join [swarm-joinのoption] argment みたいな感じです。

最後の引数は、サービスディスカバリーの情報です。さっき作った自ホストのetcdを指定します。

シェルからならこんな感じ。

$ docker run -d --name=swarm-join \
    swarm join \
      --advertise=ホストのプライベートIPアドレス:2375 \
      etcd://ホストのプライベートIPアドレス::2379/swarm # ここは0.0.0.0でもイケたかも、試してない

cloud-configにした場合、こんな感じ。$private_ipv4 という、変数的なものが使えます。

coreos:
  units:
    - name: swarm-join.service
      command: start
      content: |
        [Unit]
        Description=swarm-join Container
        After=docker.service
        Requires=docker.service

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

        [Install]
        WantedBy=multi-user.target

swarm managerを動かす

https://docs.docker.com/swarm/multi-manager-setup/#create-the-primary-manager を参考にやります。

シェルからならこんな感じ。

$ docker run -d --net=host --name=swarm-manage \
    swarm manage \
      -H :4000 \
      --replication \
      --strategy=binpack \
      --advertise 自ホストのローカルIPアドレス:4000 \
      etcd://自ホストのローカルIPアドレス:2379/swarm

cloud-configにするとこんな形になりました。

coreos:
  units:
    - name: swarm-manage.service
      command: start
      content: |
        [Unit]
        Description=swarm-manage Container
        After=docker.service
        Requires=docker.service

        [Service]
        TimeoutStartSec=0
        Restart=always
        ExecStartPre=-/usr/bin/docker stop swarm-manage
        ExecStartPre=-/usr/bin/docker rm swarm-manage
        ExecStartPre=-/usr/bin/docker pull swarm
        ExecStart=/usr/bin/docker run --net=host --name=swarm-manage swarm manage -H :4000 --replication --strategy=binpack --advertise $private_ipv4:4000 etcd://$private_ipv4:2379/swarm

        [Install]
        WantedBy=multi-user.target

ポイントとしては、他のmanagerから来る問い合わせに使うポートを、-H 4000--advertise $private_ipv4:4000 で、指定しています。

このあたり、--net=host としないとうまく動かず… うまい書き方があったら教えてほしいところです。

準備完了っす!

ぜんぶくっつけた全体像はこんな感じ。

#cloud-config

hostname: node1
coreos:
  etcd2:
    # <https://discovery.etcd.io/new?size=1でトークンを得る>
    discovery: https://discovery.etcd.io/tokenxxxxxxxxxxxxxxxxxxxxxxxxxxx
    advertise-client-urls: http://$private_ipv4:2379
    initial-advertise-peer-urls: http://$private_ipv4:2380
    listen-client-urls: http://0.0.0.0:2379
    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: swarm-join.service
      command: start
      content: |
        [Unit]
        Description=swarm-join Container
        After=docker.service
        Requires=docker.service

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

        [Install]
        WantedBy=multi-user.target

    - name: swarm-manage.service
      command: start
      content: |
        [Unit]
        Description=swarm-manage Container
        After=docker.service
        Requires=docker.service

        [Service]
        TimeoutStartSec=0
        Restart=always
        ExecStartPre=-/usr/bin/docker stop swarm-manage
        ExecStartPre=-/usr/bin/docker rm swarm-manage
        ExecStartPre=-/usr/bin/docker pull swarm
        ExecStart=/usr/bin/docker run --net=host --name=swarm-manage swarm manage -H :4000 --replication --strategy=binpack --advertise $private_ipv4:4000 etcd://$private_ipv4:2379/swarm

        [Install]
        WantedBy=multi-user.target

以上のyamlをユーザーデータとして利用し、CoreOSのAMIを選択し、最初からEC2を起動します(ユーザーデータについては説明割愛します)。

2台目を起動するときは、今回は hostname: node1 の部分を hostname: node2 に書き換えて起動します(ここだけ超手動)

動いた!

EC2が起動したら node1でコマンド発行してみます。

# 普通にdocker ps。これも一応動く。managerとagentの2つが動いています。
core@node1 ~ $ docker ps
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS               NAMES
1153b3bc311d        swarm:latest        "/swarm manage -H :4   5 minutes ago       Up 5 minutes                            swarm-manage        
bb475b187a5e        swarm:latest        "/swarm join --adver   5 minutes ago       Up 5 minutes        2375/tcp            swarm-join    

# 自ノードのポート指定してdocker ps。 ポートが利用されていることが確認できる。
core@node1 ~ $ docker -H:2375 ps
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS               NAMES
1153b3bc311d        swarm:latest        "/swarm manage -H :4   5 minutes ago       Up 5 minutes                            swarm-manage        
bb475b187a5e        swarm:latest        "/swarm join --adver   5 minutes ago       Up 5 minutes        2375/tcp            swarm-join    

# node2のポート指定してdocker ps。node2.innerという名前は、別途手で、2台めのIPに対して付与しています。。
# CONTAINER IDがnode1のものと違うので、別のコンテナであることがわかります
core@node1 ~ $ docker -H node2.inner:2375 ps
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS               NAMES
3c6a0a41d3e6        swarm:latest        "/swarm manage -H :4   5 minutes ago       Up 5 minutes                            swarm-manage        
1f340aef1237        swarm:latest        "/swarm join --adver   5 minutes ago       Up 5 minutes        2375/tcp            swarm-join  
core@node1 ~ $

よさ気です。つぎに、 manager である4000番ポートも調べてみます。

# 普通にdocker psするとなにも表示されない。
core@node1 ~ $ docker -H :4000 ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

# -aオプションつけると、managerやagentも表示される。NAMES列でホスト名が確認できる。
core@node1 ~ $ docker -H :4000 ps -a
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS               NAMES
3c6a0a41d3e6        swarm:latest        "/swarm manage -H :4   6 minutes ago       Up 6 minutes                            node2/swarm-manage
1f340aef1237        swarm:latest        "/swarm join --adver   6 minutes ago       Up 6 minutes        2375/tcp            node2/swarm-join
1153b3bc311d        swarm:latest        "/swarm manage -H :4   6 minutes ago       Up 6 minutes                            node1/swarm-manage
bb475b187a5e        swarm:latest        "/swarm join --adver   6 minutes ago       Up 6 minutes        2375/tcp            node1/swarm-join

# node2のポートも見られることを確認。
core@node1 ~ $ docker -H node2.inner:4000 ps -a
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS               NAMES
3c6a0a41d3e6        swarm:latest        "/swarm manage -H :4   6 minutes ago       Up 6 minutes                            node2/swarm-manage
1f340aef1237        swarm:latest        "/swarm join --adver   6 minutes ago       Up 6 minutes        2375/tcp            node2/swarm-join
1153b3bc311d        swarm:latest        "/swarm manage -H :4   7 minutes ago       Up 7 minutes                            node1/swarm-manage
bb475b187a5e        swarm:latest        "/swarm join --adver   7 minutes ago       Up 7 minutes        2375/tcp            node1/swarm-join

以上のようにmanagerに対してdocker psすると、4個のコンテナ((manageer + agent) * 2台)がrunしていることがわかります。

managerの動きを調べる

つぎに、managerに対して docker info してみます。

core@node1 ~ $ docker -H :4000 info
Containers: 4
Images: 4
Storage Driver:
Role: primary
Strategy: binpack
Filters: affinity, health, constraint, port, dependency
Nodes: 2
 node1: 172.31.14.196:2375
  └ Containers: 2
  └ Reserved CPUs: 0 / 1
  └ Reserved Memory: 0 B / 1.022 GiB
  └ Labels: executiondriver=native-0.2, kernelversion=4.0.5, operatingsystem=CoreOS 723.3.0, storagedriver=overlay
 node2: 172.31.25.198:2375
  └ Containers: 2
  └ Reserved CPUs: 0 / 1
  └ Reserved Memory: 0 B / 1.022 GiB
  └ Labels: executiondriver=native-0.2, kernelversion=4.0.5, operatingsystem=CoreOS 723.3.0, storagedriver=overlay
Execution Driver:
Kernel Version:
Operating System:
CPUs: 2
Total Memory: 2.043 GiB
Name: node1
ID:
Http Proxy:
Https Proxy:
No Proxy:

Nodeを2つ認識しており、RolePrimaryであることがわかります。

対して、node2 に対して実行すると

core@node1 ~ $ docker -H node2.inner:4000 info
Containers: 4
Images: 4
Storage Driver:
Role: replica
Primary: 172.31.14.196:4000
Strategy: binpack
…以下同じなので省略…

Rolereplicaであることがわかります。

今回はたまたま、node1(172.31.14.196)がPrimaryに選出されたようですね。

選挙の様子は、それぞれのmanagerに対してdocker logsを実行すれば見られます。

core@node1 ~ $ docker -H :4000 logs node1/swarm-manage
time="2015-09-29T21:45:30Z" level=info msg="Listening for HTTP" addr=":4000" proto=tcp
time="2015-09-29T21:45:30Z" level=info msg="Leader Election: Cluster leadership lost"
time="2015-09-29T21:45:30Z" level=error msg="Leader Election: watch leader channel closed, the store may be unavailable..."
time="2015-09-29T21:45:30Z" level=info msg="Leader Election: Cluster leadership acquired"
time="2015-09-29T21:45:30Z" level=info msg="Registered Engine node1 at 172.31.14.196:2375"
time="2015-09-29T21:45:49Z" level=info msg="Registered Engine node2 at 172.31.25.198:2375"
core@node1 ~ $ docker -H :4000 logs node2/swarm-manage
time="2015-09-29T21:45:50Z" level=info msg="Listening for HTTP" addr=":4000" proto=tcp
time="2015-09-29T21:45:50Z" level=info msg="Leader Election: Cluster leadership lost"
time="2015-09-29T21:45:50Z" level=info msg="New leader elected: 172.31.14.196:4000"
time="2015-09-29T21:45:50Z" level=info msg="Registered Engine node2 at 172.31.25.198:2375"
time="2015-09-29T21:45:51Z" level=info msg="Registered Engine node1 at 172.31.14.196:2375"

managerのうち、1台を止めてみる

止めてみます。

sudo systemctl disable swarm-manage
sudo systemctl stop swarm-manage

このあとnode2のmanagerのlogを見てみると、自分がNew leader であると主張しています!

$ docker -H node2.inner:4000 logs swarm-manage 
…()
time="2015-09-30T10:28:41Z" level=info msg="Leader Election: Cluster leadership acquired"

docker infoして確認しても、今度はnode2のほうがprimaryだと主張していることがわかります。

core@node1 ~ $ docker -H node2.jnner:4000 info
Containers: 4
Images: 4
Storage Driver:
Role: primary
...()

フェイルオーバーっぽいことができていることがわかります。

まぁ、docker -H $(etcdctl get /swarm/docker/swarm/leader) ps 的なことが、1台壊れるぐらいなら大丈夫な感じみたいな。感じ感じ

あと、etcdは安定的に動かすには3台以上無いとダメっぽいです

まとめ

  • managerの冗長化も、etcd使ってなんとかなる
    • しかも割と簡単
  • docker-machineとかdocker-composeとかvagrantとか使わんでも、なんとかなる!
  • EC2のオートスケーリングとかと組み合わせれば夢が広がりそう

次回(いつか)は、このクラスタでswarmのbinpackストラテジーを体験できることを書きたいと思います。