Single node docker swarm でお手軽 rolling update

https://kazeburo.hatenablog.com/entry/2018/10/09/174111

という記事をみて、次のようなブコメを書いたのですが実際には試したことがなかったのでやってみることにしました。

実行するアプリの準備

次のような構成のものを構築します

構成

構成

Global mode は kubernetes での DaemonSet のようなモードで、各ノードで起動されます。今回は1ノードしか用意しませんが、あとからノードを追加した場合にそのまま LB や DNS Round robin に追加できます。

Docker swarm のセットアップ

セットアップと言ってもシングルノードなので docker がインストールされていれば次の1コマンドで完了です。node 追加の予定がないので listen-addr を 127.0.0.1 にしてあります。

docker swarm init --advertise-addr 127.0.0.1 --listen-addr 127.0.0.1:2377

docker info コマンドで swarm モードが有効になっているかどうかを確認できます

# docker info | grep ^Swarm
Swarm: active

docker swarm init

docker-compse でテスト

Swarm の stack, servicedocker-compose.yml からも作れるので、まず、docker-compose で動く状態にしてみます。コードは https://github.com/yteraoka/single-node-swarm-test に置いてあります。

git clone https://github.com/yteraoka/single-node-swarm-test.git
cd single-node-swarm-test
docker-compose up
# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                  PORTS                NAMES
d7f2b3a76c26        myapp-app:1.0.0     "./app"                  2 minutes ago       Up 2 minutes (healthy)                        single-node-swarm-test_app_1
ef520dbf910e        myapp-web:1.0.0     "nginx -g 'daemon of…"   2 minutes ago       Up 2 minutes (healthy)   0.0.0.0:80->80/tcp   single-node-swarm-test_web_1

起動したらブラウザで port 80 の / にアクセスすると

Version: 1.0.0
Hostname: d7f2b3a76c26

というのが返ってくるはずです。Hostname は app の Container ID です。/color にアクセスすると背景が緑色になります。あとでイメージの更新をする際にわかりやすいように色をつけてみました。

動作確認できたら Ctrl-C で停止して docker-compose down で不要なコンテナを削除します

Stack の作成

service では build 済みのイメージファイルが必要です。今回は先程 docker-compose up を実行した際に build されているのでそれが使えます。なにか書き換えた場合は docker-compose build を実行することでイメージが作成されます

docker stack deploy --compose-file docker-compose.yml myapp

docker ps で起動コンテナを確認

# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                           PORTS                NAMES
ea742c69bd61        myapp-web:1.0.0     "nginx -g 'daemon of…"   4 seconds ago       Up 3 seconds (health: starting)   0.0.0.0:80->80/tcp   myapp_web.2jqq8twdy8x549tyl0i2yokst.kenbu7e7519nk54tkeld20s4u
7fa3f88bb36f        myapp-app:1.0.0     "./app"                  8 seconds ago       Up 7 seconds (health: starting)                        myapp_app.2.6tmaj4j37srfn5lxqu836h0w1
4777a3a0d76d        myapp-app:1.0.0     "./app"                  8 seconds ago       Up 7 seconds (health: starting)                        myapp_app.1.h08cosodbi870jjt5555bart7

▼ stack の一覧確認

# docker stack ls
NAME                SERVICES            ORCHESTRATOR
myapp               2                   Swarm

▼ stack のコンテナを確認(nginx コンテナの HEALTHCHECK を見直す余地があるかな)

# docker stack ps myapp
ID                  NAME                                      IMAGE               NODE               DESIRED STATE       CURRENT STATE            ERROR                       PORTS
3thdglvcpbt5        myapp_web.2jqq8twdy8x549tyl0i2yokst       myapp-web:1.0.0     swarm              Running             Starting 2 seconds ago
rd3gxvhbvpuk         \_ myapp_web.2jqq8twdy8x549tyl0i2yokst   myapp-web:1.0.0     swarm              Shutdown            Failed 7 seconds ago     "task: non-zero exit (1)"
yia37uy212na         \_ myapp_web.2jqq8twdy8x549tyl0i2yokst   myapp-web:1.0.0     swarm              Shutdown            Failed 18 seconds ago    "task: non-zero exit (1)"
kenbu7e7519n         \_ myapp_web.2jqq8twdy8x549tyl0i2yokst   myapp-web:1.0.0     swarm              Shutdown            Failed 29 seconds ago    "task: non-zero exit (1)"
h08cosodbi87        myapp_app.1                               myapp-app:1.0.0     swarm              Running             Running 7 seconds ago
6tmaj4j37srf        myapp_app.2                               myapp-app:1.0.0     swarm              Running             Running 7 seconds ago

▼ stack 内の service の一覧確認

# docker stack services myapp
ID                  NAME                MODE                REPLICAS            IMAGE              PORTS
oup9x86rr54q        myapp_app           replicated          2/2                 myapp-app:1.0.0
qmk60m0sytxn        myapp_web           global              1/1                 myapp-web:1.0.0

▼ stack を限定しない service の一覧確認

# docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE              PORTS
oup9x86rr54q        myapp_app           replicated          2/2                 myapp-app:1.0.0
qmk60m0sytxn        myapp_web           global              1/1                 myapp-web:1.0.0

新しい docker image の作成

docker-compose.yml を書き換えて新しいイメージファイルをビルドします。 app イメージのバージョンを 1.0.1 に書き換え、先程の背景色を緑から赤に変更します。

diff --git a/docker-compose.yml b/docker-compose.yml
index bc8bba3..5199cfe 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -15,13 +15,13 @@ services:
         protocol: tcp
         mode: host
   app:
-    image: myapp-app:1.0.0
+    image: myapp-app:1.0.1
     build:
       context: ./app
       dockerfile: Dockerfile
       args:
-        VERSION: 1.0.0
-        COLOR: green
+        VERSION: 1.0.1
+        COLOR: red
     networks:
       - overlay
     deploy:

▼ 書き換えたら docker-compose build でイメージをビルドします。(build-arg で渡す変数を go の compile 時に指定する変数として使ってアプリにバージョンとか色を埋め込んでます)

# docker-compose build
...
Successfully built 9cb764797c20
Successfully tagged myapp-app:1.0.1
# docker image ls | grep myapp-app
myapp-app           1.0.1               9cb764797c20        23 seconds ago      10.8MB
myapp-app           1.0.0               ca6303e343e2        About an hour ago   10.8MB

余談:今回、テストアプリを Go で書いたので、イメージを小さくするために multistage-build を試してみました。scratch じゃなくて alpine をベースにしましたけど

イメージの入れ替え

docker service update でイメージを入れ替えます

docker service update myapp_app --image myapp-app:1.0.1

WARNING が出ていますが、これは本来 swarm は複数 node で実行されるものなので、image は registry にアップしてねということですね (docker service create --name registry --publish published=5000,target=5000 registry:2 で swarm 内に registry サービスを作って、docker-compose push でプッシュすることも可能 docker-compose.yml はイジる必要があります)

# docker service update myapp_app --image myapp-app:1.0.1
image myapp-app:1.0.1 could not be accessed on a registry to record
its digest. Each node will access myapp-app:1.0.1 independently,
possibly leading to different nodes running different
versions of the image.

myapp_app
overall progress: 0 out of 2 tasks
1/2: starting
2/2:

順に入れ替えられていきます

myapp_app
overall progress: 2 out of 2 tasks
1/2: running
2/2: running
verify: Service converged

docker service ps で更新などの履歴が確認できます

# docker service ps myapp_app
ID                  NAME                IMAGE               NODE                DESIRED STATE      CURRENT STATE            ERROR               PORTS
iu6nmiijnxby        myapp_app.1         myapp-app:1.0.1     swarm               Running            Running 2 minutes ago
h08cosodbi87         \_ myapp_app.1     myapp-app:1.0.0     swarm               Shutdown           Shutdown 2 minutes ago
896sinhjy4r4        myapp_app.2         myapp-app:1.0.1     swarm               Running            Running 2 minutes ago
6tmaj4j37srf         \_ myapp_app.2     myapp-app:1.0.0     swarm               Shutdown           Shutdown 3 minutes ago

これでまた port 80 の /color にアクセスすると背景が赤色になっています。バージョンの部分も 1.0.1 になっています

Version: 1.0.1
Hostname: 0597b98c6ff4

しかし、Chrome でアクセスすると2つの app コンテナが起動しているのに毎回同じ Hostname (Container ID) しか表示されません。これは毎回 /favicon.ico へもアクセスしていて、2 アクセスずつしているからでした

docker service update には他にもたくさんオプションがあります

stack を使っているので docker-compose.yml の image を書き換える、もしくは環境変数で指定することにして、再度 docker stack deploy を実行することでも image の更新が可能です。さらに、image だけじゃなくて replica 数だったり、メモリのリミットとか各種設定の更新にも使えます。deploy.update_config.order を start-first にしておくと replicas が 1 の場合にも downtime をなくせます。

rollback

新しいバージョンがそもそも起動しないとかであれば自動で rollback させたりもできるようですが、そうではないなんらかの不具合があって一つ前のバージョンに戻したいという場合は

docker service rollback myapp_app

とするだけで戻せます。ただし、一つ前に戻すだけで、rollback 後の一つ前は戻す必要のあった問題のあるバージョンになるので2度続けて実行するのは危険です。イメージ入れ替えだけだから前のバージョンがわかっているなら (docker service ps myapp_app すればわかる) そのイメージを指定するのが良いかも

実行コンテナ数を増減させる

service で実行するコンテナの数は docker service scale で変更することができます。2つだった app コンテナを 3 つにしてみます

docker service scale myapp_app=3
# docker service ps myapp_app
ID                  NAME                IMAGE               NODE                DESIRED STATE      CURRENT STATE             ERROR               PORTS
iu6nmiijnxby        myapp_app.1         myapp-app:1.0.1     swarm               Running            Running 9 minutes ago
h08cosodbi87         \_ myapp_app.1     myapp-app:1.0.0     swarm               Shutdown           Shutdown 9 minutes ago
896sinhjy4r4        myapp_app.2         myapp-app:1.0.1     swarm               Running            Running 9 minutes ago
6tmaj4j37srf         \_ myapp_app.2     myapp-app:1.0.0     swarm               Shutdown           Shutdown 10 minutes ago
bi3v0wo0m1fi        myapp_app.3         myapp-app:1.0.1     swarm               Running            Running 13 seconds ago

コンテナが3つになったので、Chrome でもアクセスの度に Hostname (Container ID) が変わることが確認できます

更新は順番に行われますが、同時にいくつのコンテナを入れ替えるのか(--update-parallelism)、更新間隔(--update-delay)をどうするかなども調整可能です。 エラーが続く場合に自動でロールバックさせる機能もあります(--update-failure-actionなど)。

イメージ更新時に downtime はある?

while : ; do
  curl -so /dev/null -w "%{http_code} %{time_total}\n" -m 5 http://localhost/
done

を実行しながらイメージの更新をしてみてもダウンタイムはありませんでした。 が、nginx コンテナの HEALTHCHECK 実行時になんかレスポンスが悪くなるのが気になるな。 docker events で見てると、このログが出るときにレスポンスが1秒超えることがある。なんでだろ?

2018-10-13T04:31:04.542378009Z container exec_create: /bin/sh -c curl -f http://127.0.0.1/healthcheck || exit 1 fc4105bc721c89956d358d078d8f689110e68cd0b02d3e48fd4074cb7d0f635f (com.docker.stack.namespace=myapp, com.docker.swarm.node.id=2jqq8twdy8x549tyl0i2yokst, com.docker.swarm.service.id=qmk60m0sytxnqlpgwz483hq58, com.docker.swarm.service.name=myapp_web, com.docker.swarm.task=, com.docker.swarm.task.id=3thdglvcpbt5i5bac38k8nrcq, com.docker.swarm.task.name=myapp_web.2jqq8twdy8x549tyl0i2yokst.3thdglvcpbt5i5bac38k8nrcq, execID=3387082ab0c7e6c1b8f9ff0f1b73003e975c4ccb83dfb3d99144e9951d88d65b, image=myapp-web:1.0.0, maintainer=NGINX Docker Maintainers , name=myapp_web.2jqq8twdy8x549tyl0i2yokst.3thdglvcpbt5i5bac38k8nrcq)
2018-10-13T04:31:04.542511350Z container exec_start: /bin/sh -c curl -f http://127.0.0.1/healthcheck || exit 1 fc4105bc721c89956d358d078d8f689110e68cd0b02d3e48fd4074cb7d0f635f (com.docker.stack.namespace=myapp, com.docker.swarm.node.id=2jqq8twdy8x549tyl0i2yokst, com.docker.swarm.service.id=qmk60m0sytxnqlpgwz483hq58, com.docker.swarm.service.name=myapp_web, com.docker.swarm.task=, com.docker.swarm.task.id=3thdglvcpbt5i5bac38k8nrcq, com.docker.swarm.task.name=myapp_web.2jqq8twdy8x549tyl0i2yokst.3thdglvcpbt5i5bac38k8nrcq, execID=3387082ab0c7e6c1b8f9ff0f1b73003e975c4ccb83dfb3d99144e9951d88d65b, image=myapp-web:1.0.0, maintainer=NGINX Docker Maintainers , name=myapp_web.2jqq8twdy8x549tyl0i2yokst.3thdglvcpbt5i5bac38k8nrcq)
2018-10-13T04:31:04.644207191Z container exec_die fc4105bc721c89956d358d078d8f689110e68cd0b02d3e48fd4074cb7d0f635f (com.docker.stack.namespace=myapp, com.docker.swarm.node.id=2jqq8twdy8x549tyl0i2yokst, com.docker.swarm.service.id=qmk60m0sytxnqlpgwz483hq58, com.docker.swarm.service.name=myapp_web, com.docker.swarm.task=, com.docker.swarm.task.id=3thdglvcpbt5i5bac38k8nrcq, com.docker.swarm.task.name=myapp_web.2jqq8twdy8x549tyl0i2yokst.3thdglvcpbt5i5bac38k8nrcq, execID=3387082ab0c7e6c1b8f9ff0f1b73003e975c4ccb83dfb3d99144e9951d88d65b, exitCode=0, image=myapp-web:1.0.0, maintainer=NGINX Docker Maintainers , name=myapp_web.2jqq8twdy8x549tyl0i2yokst.3thdglvcpbt5i5bac38k8nrcq) 

さらに

swarm config を使うと nginx の設定はイメージを変更したり、ホスト側のファイルをマウントしなくても docker service update で更新できそう Store configuration data using Docker Configs

しばらく見ていなかったけど swarm も意外と使えるのかな? Sidecar とかが使えないのを割り切れば。

Built with Hugo
テーマ StackJimmy によって設計されています。