Docker Swarmで実現するローリングアップデート

# はじめに
本記事はDocker Swarmで実現するローリングアップデートについて記載しています。

Kubernetesではアプリケーションのローリングアップデートを実行することで、ダウンタイムなしのアップデートができます。

従来のオンプレミスのシステムにおけるメンテナンス時の運用では、sorryサーバを用意してLBから1台ずつ切り離してアップデートを行い、アップデート終了後にまた組み込みを行う運用が一つのデザインパターンでした。

クラウドネィティブ時代ではローリングアップデートの恩恵は大きく、今後もデファクトスタンダードになるでしょう。本記事ではDocker Swarm環境における極力ダウンタイムを減らすための運用方法について解説します。

## ローリングアップデート
本記事ではDocker Swarmによるクラスタ環境を構築し、ローカルでDockerレジストリをセットアップしていることを前提としています。

Docker Swarmによるクラスタ環境構築については、以前書いた[Docker Swarmで学ぶサービスメッシュ](https://qiita.com/Brutus/items/b3dfe5957294caa82669)を参照。

### ビルド作業
例えば、ローリングアップデートを行わないでコンテナのビルドを行う場合はポートが重複するため、一度コンテナを停止して再度ビルドを行う必要があります。そのため、シングル構成の場合はサービスのダウンタイムを避けることができません。

しかし、ローリングアップデートの場合は先にビルドができるため、アプリケーションリリースで時間の短縮ができます。

まずは、アプリケーションのアップデートを行うサーバに対して資産を配備します。

**ローリングアップデートを行うときのポイントは、Docker-composeファイルでイメージのバージョンを変更します。**

以下のDocker-composeファイル抜粋の例では、**image**で指定するタグ名を変更しています。

- アップデート前

```dockerfile
version: '3'

networks:
  MyApp:
  
services:
  app:
    deploy:
      replicas: 2
    image: 127.0.0.1:5000/stackdemo
```

- アップデート時

```dockerfile
version: '3'

networks:
  MyApp:
  
services:
  app:
    deploy:
      replicas: 2
    image: 127.0.0.1:5000/stackdemo_v2
```

アップデート前に`docker images`コマンドを実行した結果は以下になります。

```console
REPOSITORY                 TAG                 IMAGE ID            CREATED             SIZE
127.0.0.1:5000/stackdemo   latest              1f25d780708a        4 minutes ago       878MB
redis                      latest              4cdbec704e47        2 weeks ago         98.2MB
registry                   <none>              708bc6af7e5e        2 months ago        25.8MB
node                       12.2-alpine         f391dabf9dce        11 months ago       77.7MB
```

以下のコマンドを実行してビルドします。

`# docker-compose build`

```console
WARNING: Some services (app, redis) use the 'deploy' key, which will be ignored. Compose does not support 'deploy' configuration - use `docker stack deploy` to deploy to a swarm.
redis uses an image, skipping
Building app
Step 1/10 : FROM node:12.2-alpine
 ---> f391dabf9dce

（略）

Successfully built f03dc959d289
Successfully tagged 127.0.0.1:5000/stackdemo_v2:lates
```

`docker images`コマンドを実行し、新しいイメージが作成されていることを確認します。

```console
REPOSITORY                    TAG                 IMAGE ID            CREATED             SIZE
127.0.0.1:5000/stackdemo_v2   latest              f03dc959d289        3 minutes ago       879MB
127.0.0.1:5000/stackdemo      latest              1f25d780708a        10 minutes ago      878MB
redis                         latest              4cdbec704e47        2 weeks ago         98.2MB
registry                      <none>              708bc6af7e5e        2 months ago        25.8MB
node                          12.2-alpine         f391dabf9dce        11 months ago       77.7MB
```

ローカルのDockerレジストリにプッシュします。

`# docker-compose push`

```console
WARNING: Some services (app, redis) use the 'deploy' key, which will be ignored. Compose does not support 'deploy' configuration - use `docker stack deploy` to deploy to a swarm.
Pushing app (127.0.0.1:5000/stackdemo_v2:latest)...
The push refers to repository [127.0.0.1:5000/stackdemo_v2]
6b65a73c07f0: Pushed
647209689cbb: Pushed
a5bda7b4ef65: Pushed
80f6c9bc1a2c: Pushed
76e5dca151d7: Mounted from stackdemo
917da41f96aa: Mounted from stackdemo
7d6e2801765d: Mounted from stackdemo
f1b5933fe4b5: Mounted from stackdemo
latest: digest: sha256:84ee56656dc9a9e87d4058bba2c98ebdd3615629258f13c4ac81ff56def69b11 size: 2002
```

### serviceによるデプロイ
`docker service update`コマンドでデプロイする場合は以下のコマンドを実行します。`--update-delay`などのオプションで指定している引数は例になります。

`# docker service update --update-delay 5s --update-parallelism 1 --image 127.0.0.1:5000/stackdemo_v2  stackdemo_app`

```console
overall progress: 2 out of 2 tasks 
1/2: running   [==================================================>] 
2/2: running   [==================================================>] 
verify: Service converged 
```

### stackによるデプロイ
`docker stack`コマンドでデプロイする場合の手順は以下のコマンドを実行します。通常時のstackによるデプロイと同じです。

`# docker stack deploy --compose-file docker-compose.yml stackdemo`

## アプリケーションのダウングレード
元の資産があるディレクトリに移動し、`docker service update`コマンドまたは、`docker stack`コマンドでデプロイします。以下は`docker stack`コマンドの例です。

`# docker stack deploy --compose-file docker-compose.yml stackdemo`

## ナレッジ
最小構成のシステムを基準に考えた場合は、アップデートする前の`Docker-compose.yml`ファイルで指定する**replicas**の数は2以上が望ましいと考えます。

`docker service update`コマンドの`--update-delay 60s`のオプションを指定することで、アップデートの間隔を設けることができます。1サーバに対して2コンテナ以上稼働する様に設定することでダウンダイムの影響を局所化することができます。

![docker_swarm_update.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221759/874dfe59-13db-3d23-b099-7cd945baaa03.png)


以下は`docker service update`コマンドでローリングアップデート実行時の動きです。
本記事の構成はMaster1台とNode1台になります。`docker-compose.yml`ファイルで指定する**replicas**の数を2としているので、それぞれのホストでアプリケーションコンテナは2個ずつ作成されます。

ローリングアップデート実行後、コンテナ1個毎にアプリケーションのアップデートが始まります。

```console
stackdemo_app
overall progress: 1 out of 4 tasks 
1/4: running   [==================================================>] 
2/4:   
3/4:   
4/4:   
```

`docker service update`コマンドの`--update-delay 60s`のオプションにより、アプリケーションのアップデートは60秒の間隔で実行されます。

```console
stackdemo_app
overall progress: 2 out of 4 tasks 
1/4: running   [==================================================>] 
2/4: running   [==================================================>] 
3/4:   
4/4:   
```

例としてこのアプリケーションのAPIはバージョン情報をレスポンスとして返します。以下はLBのDNS名にアクセスしたレスポンスとして、`{"version":"1.0"}`と`{"version":"1.1"}`が交互に返ってくることから、順次アプリケーションのアップデートが行われているのが確認できます。

```console
{"version":"1.0"}
{"version":"1.1"}
{"version":"1.0"}
{"version":"1.1"}
{"version":"1.0"}
{"version":"1.1"}
```

```console
overall progress: 3 out of 4 tasks 
1/4: running   [==================================================>] 
2/4: running   [==================================================>] 
3/4: running   [==================================================>] 
4/4: 
```

全てのコンテナに対するローリングアップデートが終了しました。

```console
overall progress: 4 out of 4 tasks 
1/4: running   [==================================================>] 
2/4: running   [==================================================>] 
3/4: running   [==================================================>] 
4/4: running   [==================================================>] 
verify: Service converged 
```

`docker container ps -a`コマンドを実行すると、古いアプリケーションが稼働していたコンテナは停止し、新しいアプリケーションのコンテナが作成されていることが確認できます。なお、ローリングアップデート時の動きとしては、まず古いアプリケーションのコンテナが停止し、新しいコンテナが生成されます。

```console
CONTAINER ID        IMAGE                                COMMAND                  CREATED             STATUS                      PORTS               NAMES
52db94935164        127.0.0.1:5000/stackdemo_v2:latest   "/bin/sh -c 'yarn st…"   38 minutes ago      Up 38 minutes               3001/tcp            stackdemo_app.4.j5vgh0f7azp88zal6rd7gveny
8ea68352da6c        127.0.0.1:5000/stackdemo_v2:latest   "/bin/sh -c 'yarn st…"   41 minutes ago      Up 41 minutes               3001/tcp            stackdemo_app.2.zc2pzhd2xkbc8obvc4apvg7qh
4733dd99b9f4        127.0.0.1:5000/stackdemo:latest      "/bin/sh -c 'yarn st…"   43 minutes ago      Exited (1) 41 minutes ago                       stackdemo_app.2.s0lq2qx6zkj5v51mdnfrl4ygr
fee3bf216379        127.0.0.1:5000/stackdemo:latest      "/bin/sh -c 'yarn st…"   43 minutes ago      Exited (1) 38 minutes ago                       stackdemo_app.4.uymqss60i5ac4ozxa05xfgaw4
4ef0610778f0        redis:latest                         "docker-entrypoint.s…"   43 minutes ago      Up 43 minutes               6379/tcp            stackdemo_redis.2.ls88b5nb1h5qeo0todl6lc7dn
867d0cadeeea        registry:2                           "/entrypoint.sh /etc…"   About an hour ago   Up About an hour            5000/tcp            registry.1.wspi67ubshjo47af8b55kvz10
```

`Docker-compose.yml`ファイルで指定する**replicas**の数を増やした場合、Dockerで出力されるログはコンテナ分増量します。

Docker Swarm設計のポイントとして、ダウンタイムを考慮したけれどもストレージを考慮していなかったために、Dockerのログでディスクが枯渇したら本末転倒です。その場合はDockerで使用するディスク領域を別にするなど工夫が必要になります。

# おわりに
Docker Swarmは十分本番環境で使用できる技術ですが、Dockerが出力するログの考慮や、挙動が不安定なときもあるのでリスクヘッジを抑えてからの運用を行うのが望ましいです。

## 参考
- [Apply rolling updates to a service](https://docs.docker.com/engine/swarm/swarm-tutorial/rolling-update/)