失敗は一時の恥

パッケージソフト開発をしているプログラマが気の赴くままに何かを投稿するブログ.

Docker swarm チュートリアル

Docker swarm チュートリアル

この記事について

Docker swarm を公式ドキュメントのチュートリアルに沿って試してみた内容をまとめています.

使うもの

Set up に書いてある通り, Docker が動く互いにネットワーク接続できるマシン (仮想でもよい) が 3 台必要になります.

僕は macOS 上で docker-machine コマンドと VirtualBox を利用して……

$ docker-machine create manager1
$ docker-machine create worker1
$ docker-machine create worker2

仮想マシンを用意しました.

f:id:snobutaka:20180401092232p:plain
チュートリアルで使用する仮想マシン

Docker swarm is 何?

Docker swarm は,複数のマシンからなる Docker のクラスターを構築するための仕組みです.

swarm の基本概念は Swarm mode key concepts にまとめられています. 大まかにかいつまむと,次のようなキーワードが登場します.

  • Node: swarm のクラスターを構成する各マシン (Docker engine) のことを Node と呼びます.Node には manager と worker の 2 通りがあり,
    • manager はクラスターの状態管理を行います.また,worker にタスクをアサインします.manager 自身も worker と同様にタスクを実行することもできます.
    • worker は manager からアサインされたタスクを実行します.
  • Service と Task:
    • Task は Docker コンテナとそこで実行するコマンドのことです.
    • Service はクラスターで実行するタスクの定義で,レプリカ数の設定に応じた数の Task が実行されるように指定すること等ができます.
  • Load Balancing: クラスターのノード外へサービスを公開する際に,クラスター内で負荷分散が行われるようになります.クラスターを構成しているいずれのノードにリクエストしても (そのノードでは Task が実行されていなくても),サービスにアクセスすることが可能です.

コンテナのオーケストレーションといえば Kubernetes が有名ですが,swarm はもともと Docker の一部として提供されているのもあり,より簡易にトライできそうな雰囲気です.

swarm を構成する

manager のセットアップ

まず,manager として稼働させるマシンに接続します

$ docker-machine ssh manager1

manager として Docker swarm を初期化します.

docker@manager1:~$ docker swarm init --advertise-addr 192.168.99.100
Swarm initialized: current node (tul5yq5nqs72u8mkvvd947w9m) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-20egj6uc7b1q03t93updrwlfi2m7082nsddxvc9sgeywqvqd4l-14kopj72ycqvysdv2lsy6epm5 192.168.99.100:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

出力されるメッセージの中に,今初期化した swarm に対して新たに他の Node を参加させるコマンドも記述されています.

  • worker を追加する: docker swarm join --token SWMTKN-1-20egj6uc7b1q03t93updrwlfi2m7082nsddxvc9sgeywqvqd4l-14kopj72ycqvysdv2lsy6epm5 192.168.99.100:2377
  • manager を追加する: docker swarm join-token manager を実行し,案内に従う.

swarm に参加している Node を確認するには,docker node コマンドを使用します.

docker@manager1:~$ docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
tul5yq5nqs72u8mkvvd947w9m *   manager1            Ready               Active              Leader              18.03.0-ce

ID の隣に * 印がついているのは,現在コマンドを実行している Node 自身を表しています.

worker の追加

先ほど manager を初期化した際に出力されたコマンドを使って,worker1 を swarm に参加させます.

ssh 接続をしてから……

$ docker-machine ssh worker1

docker swarm join コマンドを実行します

docker@worker1 $ docker swarm join --token SWMTKN-1-20egj6uc7b1q03t93updrwlfi2m7082nsddxvc9sgeywqvqd4l-14kopj72ycqvysdv2lsy6epm5 192.168.99.100:2377

同じことを worker2 でも行います.

そして,manager1 から docker node ls を実行すると 3 台の Node が swarm に参加できたことがわかるでしょう.

docker@manager1:~$ docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
tul5yq5nqs72u8mkvvd947w9m *   manager1            Ready               Active              Leader              18.03.0-ce
py07mcf6fk9ino9hctipnwpyj     worker1             Ready               Active                                  18.03.0-ce
mf9qqqgr8uk212lz5a837zccf     worker2             Ready               Active                                  18.03.0-ce

docker node ls は,manager からしか実行できないので注意が必要です.

swarm にサービスをデプロイする

Node 3 台からなる swarm のクラスターを構成できたので,サービスをデプロイしてみます.

サービスをデプロイするには,manager からdocker service create コマンドを実行します.

docker@manager1:~$ docker service create --replicas 1 --name helloworld alpine ping docker.com

docker run とかなり似ています. 見慣れないオプションの "--replicas 1" は,読んで字のごとく docker プロセスを (レプリカを) 1 つ走らせるということです.

起動しているサービスは,docker service ls コマンドで確認します.

docker@manager1:~$ docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE               PORTS
lljs26w9vi5x        helloworld          replicated          1/1                 alpine:latest       

docker service inspect コマンドを使えば,サービスの使用するイメージや実行するコマンドといった詳細を得ることができます.

docker@manager1:~$ docker service inspect --pretty helloworld

ID:     lljs26w9vi5xhhy7jgr1kjx6w
Name:       helloworld
Service Mode:   Replicated
 Replicas:  1
Placement:
UpdateConfig:
 Parallelism:   1
 On failure:    pause
 Monitoring Period: 5s
 Max failure ratio: 0
 Update order:      stop-first
RollbackConfig:
 Parallelism:   1
 On failure:    pause
 Monitoring Period: 5s
 Max failure ratio: 0
 Rollback order:    stop-first
ContainerSpec:
 Image:     alpine:latest@sha256:7df6db5aa61ae9480f52f0b3a06a140ab98d427f86d8d5de0bedab9b8df6b1c0
 Args:      ping docker.com 
Resources:
Endpoint Mode:  vip

(--pretty オプションを指定しない場合は,JSON 形式で出力されます)

サービスとして動いている docker プロセスは,docker service ps コマンドで取得できます.

docker@manager1:~$ docker service ps helloworld
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE           ERROR               PORTS
rtvl9wo07nwn        helloworld.1        alpine:latest       worker2             Running             Running 7 minutes ago                       

どの Node で何のイメージが実行されているのかが分かります.

サービスをスケールする

サービスをデプロイできたので,今度はサービスをスケールしてみましょう. クラスターらしいですね.

サービスのスケールとは,サービスを構成するタスク (稼働するコンテナ) の数を増減することです.次のようなコマンドを実行します.

docker@manager1:~$ docker service scale helloworld=5
helloworld scaled to 5
overall progress: 5 out of 5 tasks 
1/5: running   [==================================================>] 
2/5: running   [==================================================>] 
3/5: running   [==================================================>] 
4/5: running   [==================================================>] 
5/5: running   [==================================================>] 
verify: Service converged 

実際にタスクが増えていることを確認しましょう.

docker@manager1:~$ docker service ps helloworld
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE            ERROR               PORTS
rtvl9wo07nwn        helloworld.1        alpine:latest       worker2             Running             Running 14 minutes ago                       
syl5rksdy779        helloworld.2        alpine:latest       manager1            Running             Running 34 seconds ago                       
o3c3oa99a5te        helloworld.3        alpine:latest       manager1            Running             Running 34 seconds ago                       
szf1qy7flz8j        helloworld.4        alpine:latest       worker2             Running             Running 39 seconds ago                       
5xyffmmtgahj        helloworld.5        alpine:latest       worker1             Running             Running 34 seconds ago                       

タスクが 3 ノードに適当に分散されていることも分かります. なぜか manager1 が 2 タスク,worker1 が 1 タスクという割り振りになっていますが,まぁそういうこともあるのでしょう……

サービスを削除する

helloworld サービスの役目はここで終わりになります.サービスを停止するには docker service rm を実行します.削除できたことは docker service ls や,各ノードで docker ps を実行すれば確認できます.

docker@manager1:~$ docker service rm helloworld
helloworld
docker@manager1:~$ docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE               PORTS
# 何もなし!
docker@manager1:~$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
# 何もなし!すべてのコンテナが消えるのには何秒かかかります.

サービスをローリングアップデートする

サービスを構成するタスクを,全て一斉にではなく一部ずつアップデートしていくことをローリングアップデートといいます.

アップデートに伴って問題が発生した場合に,影響範囲を絞ることができるといったメリットがあります.

まずは redis:3.0.6 の 3 タスクからなるサービスを作成します.

docker@manager1:~$ docker service create --replicas 3 --name redis --update-delay 10s redis:3.0.6

--update-delay は,タスクのアップデート同士の間隔を設定します.

デフォルトでは 1 度にアップデートされるタスクは 1 つですが,--update-parallelism オプションにより同時にアップデートを行うタスクの最大数を設定することもできます.

もしもアップデートが失敗した場合の動作は,--update-failure-action で指定が可能です.デフォルトでは,アップデートを中断します.

それでは,アップデートを実行しましょう.

docker@manager1:~$ docker service update --image redis:3.0.7 redis
redis
overall progress: 3 out of 3 tasks 
1/3: running   [==================================================>] 
2/3: running   [==================================================>] 
3/3: running   [==================================================>] 
verify: Service converged 

無事にコマンドが完了したら,アップデートされていることを確認しましょう.

docker@manager1:~$ docker service inspect --pretty redis

ID:     ms5ooar390takwiill06y6ey8
Name:       redis
Service Mode:   Replicated
 Replicas:  3
UpdateStatus:
 State:     completed
 Started:   2 minutes ago
 Completed: About a minute ago
 Message:   update completed
Placement:
UpdateConfig:
 Parallelism:   1
 Delay:     10s
 On failure:    pause
 Monitoring Period: 5s
 Max failure ratio: 0
 Update order:      stop-first
RollbackConfig:
 Parallelism:   1
 On failure:    pause
 Monitoring Period: 5s
 Max failure ratio: 0
 Rollback order:    stop-first
ContainerSpec:
 Image:     redis:3.0.7@sha256:730b765df9fe96af414da64a2b67f3a5f70b8fd13a31e5096fee4807ed802e20
Resources:
Endpoint Mode:  vip
docker@manager1:~$ docker service ps redis
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE            ERROR               PORTS
ofy9j61ayidp        redis.1             redis:3.0.7         worker1             Running             Running 3 minutes ago                        
6nq293pf2jtz         \_ redis.1         redis:3.0.6         worker1             Shutdown            Shutdown 3 minutes ago                       
y9kxv1cwsxpe        redis.2             redis:3.0.7         worker2             Running             Running 2 minutes ago                        
ifuzzfy80igc         \_ redis.2         redis:3.0.6         worker2             Shutdown            Shutdown 2 minutes ago                       
wmybv3j8w5jd        redis.3             redis:3.0.7         manager1            Running             Running 2 minutes ago                        
yfmnr47k8wt0         \_ redis.3         redis:3.0.6         manager1            Shutdown            Shutdown 2 minutes ago                       

イメージが redis:3.0.6 から redis:3.0.7 に切り替わっていることが分かります.

ノードを停止する

サービスを稼働中でも,計画メンテナンスなどでマシンを停止させる必要性は発生します. swarm のノードを "drain" ステータスにすることで,その間はタスクの割り当てなどを受け付けないようにするということができます.

(この "drain" というステータスはあくまでも swarm のステータスであり,docker run コマンドなどで起動されたコンテナの管理については関知しません.)

いま,worker1 をメンテナンスのため停止したいものとします. 稼働しているタスクはこのようになっているとします.

docker@manager1:~$ docker service ps redis
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE           ERROR               PORTS
uw92a8l1w1sq        redis.1             redis:3.0.6         manager1            Running             Running 3 minutes ago                       
pv988a7ecmdt        redis.2             redis:3.0.6         worker1             Running             Running 3 minutes ago                       
ec4z7huzr27h        redis.3             redis:3.0.6         worker2             Running             Running 3 minutes ago                       

manager1 から次のようなコマンドを実行することで,worker1 を停止させることができます.

docker@manager1:~$ docker node update --availability drain worker1

dokcer node コマンドを確認してみましょう.

docker@manager1:~$ docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
o67397jwrzch9ccxkhre3sgrh *   manager1            Ready               Active              Leader              18.03.0-ce
xpeqk27zg8x2rnf1qpcail6de     worker1             Ready               Drain                                   18.03.0-ce
9i8v2ked45bm572ti34oyu3r5     worker2             Ready               Active                                  18.03.0-ce

さらに docker node inspect コマンドを使えば,worker1 のより詳細な情報を取得できます.

docker@manager1:~$ docker node inspect --pretty worker1
ID:         xpeqk27zg8x2rnf1qpcail6de
Hostname:               worker1
Joined at:              2018-03-31 11:27:28.770241361 +0000 utc
Status:
 State:         Ready
 Availability:          Drain
... (以下省略) ...

たしかに worker1 が Drain ステータスになっています. このとき,稼働していたタスクはどうなっているのでしょうか? docker service ps コマンドで確認してみましょう.

docker@manager1:~$ docker service ps redis
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE           ERROR               PORTS
uw92a8l1w1sq        redis.1             redis:3.0.6         manager1            Running             Running 10 hours ago                        
31vg14mao63n        redis.2             redis:3.0.6         manager1            Running             Running 10 hours ago                        
pv988a7ecmdt         \_ redis.2         redis:3.0.6         worker1             Shutdown            Shutdown 10 hours ago                       
ec4z7huzr27h        redis.3             redis:3.0.6         worker2             Running             Running 10 hours ago                        

worker1 でう退いていたタスク redis.2 が,manager1 に再割り当てされていることが分かります.

worker1 のメンテナンスが完了したら,元に戻すことができます.

$ docker node update --availability active worker1

ステータスを確認してみましょう.

docker@manager1:~$ docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
o67397jwrzch9ccxkhre3sgrh *   manager1            Ready               Active              Leader              18.03.0-ce
xpeqk27zg8x2rnf1qpcail6de     worker1             Ready               Active                                  18.03.0-ce
9i8v2ked45bm572ti34oyu3r5     worker2             Ready               Active                                  18.03.0-ce

ステータスが active に戻っており,worker1 は再びタスクを実行できるようになります.

注意として,ステータスが active に戻ったとしても,drain 時に他ノードへ再割り当てされたタスクはそのままとなります. つまり,タスク redis.2 はまだ manager1 で実行されています.

ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE           ERROR               PORTS
uw92a8l1w1sq        redis.1             redis:3.0.6         manager1            Running             Running 10 hours ago                        
31vg14mao63n        redis.2             redis:3.0.6         manager1            Running             Running 10 hours ago                        
pv988a7ecmdt         \_ redis.2         redis:3.0.6         worker1             Shutdown            Shutdown 10 hours ago                       
ec4z7huzr27h        redis.3             redis:3.0.6         worker2             Running             Running 10 hours ago                        

サービスを公開する

swarm のスラスターにはルーティンメッシュが構築されており,外部から任意のノードに送られたリクエストは,swarm 内で動作する適切なタスクへと振り分けられます.

公式チュートリアルの図が分かり易いです.

service ingress image

サービスを外部に公開するには,--publish オプションを指定します. 試しに,nginx をポート 8080 で公開するようにしてみましょう.

docker@manager1:~$ docker service create --name my-web --replicas 2 --publish published=8080,target=80 nginx

サービスの公開状況は,docker service inspect コマンドで確認することができます.

docker@manager1:~$ docker service inspect --format="{{json .Endpoint.Spec.Ports}}" my-web
[{"Protocol":"tcp","TargetPort":80,"PublishedPort":8080,"PublishMode":"ingress"}]

swarm の外部 (ここでは,swarm のノードを VirtualBox で動作させている macOS) からアクセスしてみます.

f:id:snobutaka:20180401092228p:plain
外部からのサービスへのアクセス

ポイントは,swarm の全てのノード (192.168.99.100, 192.168.99.101, 192.168.99.102) からサービスにアクセスできるということです. このサービスは,--replicas 2 で起動しているため,実際にタスクが動作しているのは 2 ノードのみとなっています. しかし,swarm 内部のロードバランサーによって,タスクを実行していないノードからでもサービスへのアクセスができるようになっているのです.

おわり

swarm のチュートリアルをさらっとやってみましたが,かなり面白い内容でした. ロードバランサーを勝手に立ち上げて良きに計らってくれるところとか,かなり良いですね.

いまのところ個人的には swarm を使って実際の製品・サービスを提供するというよりかは,クラスター化したテスト環境の作成などに利用してみたいと思っています.

いずれ Kubernetes とかより本格的 (?) なオーケストレーションも試してみたいです.