Docker swarm mode でDockerのオーケストレーションツール入門

この記事では、Dockerは触ったことあるけど、k8sみたいなオーケストレーションツールは使ったことないよって人向けに、Dockerのswarm modeでマルチホストなクラスタを構築し、いくつかの代表的な機能(スケールアウトとかローリングアップデートとか)を使うところまでをハンズオン形式で紹介します。

オーケストレーションツール触ってみたいけど、k8sはチョット敷居が高い・・・って人向けです。`swarm mode`、お手軽でいいよ


今回試す環境は以下のとおりです。

#### 環境

* Ubuntu 18.04 LTS 3台
* Docker version 18.09.7
* docker-compose version 1.24.0

## Docker swarm modeとは
Dockerのver 1.12以降で組み込まれたオーケストレーションツールです。

これを用いることで、Dockerの機能に加えて、ざっくり以下のようなことができるようになります。

* 複数ホスト間でのクラスタの構築
* コンテナレベルでのスケールアウト
* ローリングアップデートを用いた無停止デプロイ
* コンテナレベルでの迅速なロールバック

また、Swarmはdocker-composeとの相性が非常に良く、上記のような機能を`docker-compose.yml`ファイルベースで定義したコンテナ群に対して、一括で適用することができます。
([docker-composeのversion 3以降では、deployというswarm mode専用の記法が用意されているぐらいです](https://docs.docker.com/compose/compose-file/#deploy))
これが非常に強力で、普段からdocker-composeを使っている人であれば既存のワークフローを壊すことなく、簡単にクラスタ環境を手に入れることができます。

コンテナオーケストレーションツールとしては、ココ最近はkubernetesが一強になってしまった感はありますが、Docker swarm modeは、CE版のDockerにデフォルトで組み込まれているので、Dockerをインストールするだけで簡単に使い始めることができます。

クラスタの構築もコマンドを数行打つだけの簡単なものなので、コンテナオーケストレーションとはなんぞや？というのを理解する入り口として、とりあえず使って見る分にはとても良いのではないでしょうか。

ちなみにEnterPrise版のDockerでは、kubernetesがswarmと同じようにDockerに組み込まれたようなので、そちらが使える環境であればそちらを使ってみても良いかもしれません。

今回は[公式のswarmチュートリアル](http://docs.docker.jp/engine/swarm/swarm-tutorial/index.html)をベースに、わかりやすいよう多少内容を変えながらハンズオンを行っていきたいと思います。
詳しい人はそちらを見たほうがわかりやすいかもしれません。

## 環境準備

さて、今回は3台のLinuxサーバー(Ubuntu 18.04LTS)を使ってクラスタを組んでいきたいと思います。
なのでまずは、3台のLinuxマシンを用意しなければいけません。
AWSやAzureで仮想マシンを借りても良いのですが、今回はお試しなのでローカル環境にVirtualboxとVagrantを使って、３台の仮想マシンを立てちゃいたいと思います。

構成は以下のような感じです

![DockerSwarm_gram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93999/7d0cc44e-b291-dc6b-0899-7fbbbe92d7e2.png)

なお、今回仮想マシンを立てるホストマシンはmacです。Windows環境での動作は検証していないのでご了承ください。。まあ結局の所、仮想マシンを３台建てられさえすれば良いので、方法は何でも良いと思います。

#### 1. Virtual boxとVagrantのインストール

公式サイトからそれぞれインストーラーを落としてきてインストールします

* [Virtual box](https://www.virtualbox.org/wiki/Downloads)
* [Vagrant](https://www.vagrantup.com/)

#### 2. 仮想マシンの作成

インストールが終わったら、仮想マシンを作成します。まず、Vagrant用のディレクトリを仮想マシンの台数分作ります。ここでは`node1``node2``node3`と名付けることにします

``` ディレクトリ構成.
.
├── node1
│   └── data        // VMとホストの共有フォルダ docker-compose.ymlなどの構築資材を置く
├── node2
│   └── data
└── node3
    └── data
```

それぞれのnodeディレクトリの直下に移動し、以下のコマンドを実行します。

``` console.
$ cd ./node1
$ vagrant init ubuntu/bionic64
```

すると以下のような`Vagrantfile`が作成されると思うので、それぞれのnodeにIPアドレスを割り振るために以下のように書き換えます。IPアドレスはnodeごとに別のものを設定するようにしてください

```.rb
# -*- mode: ruby -*-
# vi: set ft=ruby :

# All Vagrant configuration is done below. The "2" in Vagrant.configure
# configures the configuration version (we support older styles for
# backwards compatibility). Please don't change it unless you know what
# you're doing.
Vagrant.configure("2") do |config|
  # The most common configuration options are documented and commented below.
  # For a complete reference, please see the online documentation at
  # https://docs.vagrantup.com.

  # Every Vagrant development environment requires a box. You can search for
  # boxes at https://vagrantcloud.com/search.
  config.vm.box = "ubuntu/bionic64"

  # Disable automatic box update checking. If you disable this, then
  # boxes will only be checked for updates when the user runs
  # `vagrant box outdated`. This is not recommended.
  # config.vm.box_check_update = false

  # Create a forwarded port mapping which allows access to a specific port
  # within the machine from a port on the host machine. In the example below,
  # accessing "localhost:8080" will access port 80 on the guest machine.
  # NOTE: This will enable public access to the opened port
  # config.vm.network "forwarded_port", guest: 80, host: 8080

  # Create a forwarded port mapping which allows access to a specific port
  # within the machine from a port on the host machine and only allow access
  # via 127.0.0.1 to disable public access
  # config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1"

  # Create a private network, which allows host-only access to the machine
  # using a specific IP.
  config.vm.network "private_network", ip: "192.168.33.10"    ## ここのコメントアウトを外し、一意なipアドレスを各nodeに割り振る

  # Create a public network, which generally matched to bridged network.
  # Bridged networks make the machine appear as another physical device on
  # your network.
  # config.vm.network "public_network"

  # Share an additional folder to the guest VM. The first argument is
  # the path on the host to the actual folder. The second argument is
  # the path on the guest to mount the folder. And the optional third
  # argument is a set of non-required options.
  # dataディレクトリをVMにマウントする
  config.vm.synced_folder "./data", "/home/vagrant/vagrant_data"

  # Provider-specific configuration so you can fine-tune various
  # backing providers for Vagrant. These expose provider-specific options.
  # Example for VirtualBox:
  #
  # config.vm.provider "virtualbox" do |vb|
  #   # Display the VirtualBox GUI when booting the machine
  #   vb.gui = true
  #
  #   # Customize the amount of memory on the VM:
  #   vb.memory = "1024"
  # end
  #
  # View the documentation for the provider you are using for more
  # information on available options.

  # Enable provisioning with a shell script. Additional provisioners such as
  # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
  # documentation for more information about their specific syntax and use.
  # config.vm.provision "shell", inline: <<-SHELL
  #   apt-get update
  #   apt-get install -y apache2
  # SHELL
end

```

最後に以下を実行することで仮想マシンの作成と立ち上げを行います。

```
$ vagrant up
```

立ち上げた仮想マシンには以下のコマンドでssh接続することができます

```
$ vagrant ssh
```

サーバーに接続できたら、わかりやすいように各Nodeサーバーのhostnameを`node1` `node2` `node3`に変更しておきましょう

```
$ sudo hostnamectl set-hostname ubuntu-node1    // node1の場合
$ hostname
ubuntu-node1           // 変わったことを確認
```

以上の操作を3つのNode全てに行ってください。最終的なディレクトリ構成は以下のような感じになると思います。

```
.
├── node1
│   ├── data
│   ├── Vagrantfile
│   └── ubuntu-bionic-18.04-cloudimg-console.log
├── node2
│   ├── data
│   ├── Vagrantfile
│   └── ubuntu-bionic-18.04-cloudimg-console.log
└── node3
    ├── data
    ├── Vagrantfile
    └── ubuntu-bionic-18.04-cloudimg-console.log
```

## DockerとDockerComposeのインストール

swarmを動作させるために各NodeにDockerとdocker-composeをインストールします。
docker-composeは入れなくても使えるのですが、Dockerのswarm modeではdocker-composeのyaml記法で定義されたコンテナ群をまとめてデプロイできるので、あったほうがいろいろ便利です。

#### Dockerのインストール

基本的には[公式サイト](https://docs.docker.com/install/linux/docker-ce/ubuntu/)に従ってインストールするのが一番間違いがないです。一応現時点(2019/07)でのインストール方法をメモしておきます

* インストールされている古いDockerの削除

```
$ sudo apt-get remove docker docker-engine docker.io containerd runc
```

* Dockerのインストール

```
$ sudo apt-get update
$ sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg-agent \
    software-properties-common
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo apt-key fingerprint 0EBFCD88
$ sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"
$ sudo apt-get update
$ sudo apt-get install docker-ce docker-ce-cli containerd.io
```

最後に現在のユーザー(vagrant)をdocker groupに所属させ、`docker`コマンドをsudoなしで実行できるようにします。

```
# dockerグループがなければ作る
$ sudo groupadd docker

# 現行ユーザをdockerグループに所属させる
$ sudo gpasswd -a $USER docker

# dockerをrestert
$ sudo service docker restart

# 一度ログアウトして入り直す
$ exit 
$ vagrant ssh

# dockerコマンドが実行できることを確認
$ docker ps -a
```

#### Docker composeのインストール
こちらも[公式サイト](https://docs.docker.com/compose/install/)のやり方に従うのが一番良いでしょう。

一応現時点(2019/07)でのインストール方法をメモしておきます。

```
$ sudo curl -L "https://github.com/docker/compose/releases/download/1.24.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose
$ sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
```

上記の作業を３つのNode全てに行います。

## Docker Swarmの初期設定とクラスタの構築

まずはNode1をManagerNodeとしてswarmに登録する必要があります。
Node1にsshでログインし、以下のコマンドを実行しましょう。

``` Node1で実行.
$ docker swarm init --advertise-addr="192.168.33.10" --listen-addr=0.0.0.0:2377

Swarm initialized: current node (ql8aefbw7rg8nfmznejnzfeus) is now a manager.
To add a worker to this swarm, run the following command:
docker swarm join --token SWMTKN-1-66qe5cyf31fd8rktwvjbrtpd63l32789aqpbfheauwgfh38g8wrf2ey-bob77wfdnrkpcmqtxfgkiu47a 192.168.33.10:2377

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

`--advertise-addr`には自分自身のIPアドレス。`--listen-addr`にはクラスタ間の通信に使用するport番号を指定しましょう。このポートでクラスタ間の通信を行うので、事前にVM上で、各クラスタに対してこのポートを開けておく必要があります。

これでSwarmのクラスタが作成されます。今後は、上記で作ったクラスタに他のNodeをjoinさせることで、クラスタをどんどん広げていくことができます。

クラスタに参加しているnodeの一覧を見るには以下を実行します。
今は自分自身だけですね

```
$ docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
ql8aefbw7rg8nfmznejnzfeus *   ubuntu-node1       Ready               Active              Leader              18.09.7

```

さっそく`Node2`と`Node3`をworker nodeとしてjoinさせてみましょう。

swarm init時に発行された`token`を使ってクラスタにjoinすることができます。

``` Node2,3で実行.
$ docker swarm join --token SWMTKN-1-66qe5cyf31fd8rktwvjbrtpd63l32789aqpbfheauwgfh38g8wrf2ey-bob77wfdnrkpcmqtxfgkiu47a 192.168.33.10:2377
```

もしもtokenを忘れてしまった場合は以下のコマンドで見ることができます。

```
$ docker swarm join-token -q worker
```

もう一度`docker node ls`を実行すると、クラスタに参加しているNodeが増えていることがわかります。

```
$ docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
ql8aefbw7rg8nfmznejnzfeus *   ubuntu-node1        Ready               Active              Leader              18.09.7
uusz619ly0y6zp3zsdh0br31y     ubuntu-node2        Ready               Active                                  18.09.7
pgouhk6cndlxddjhll1vo1rfv     ubuntu-node3        Ready               Active                                  18.09.7

```

これで、manager node１個とworker node2個の計３個のVMでクラスタを組むことができました。

### （補足）　manager nodeとworker nodeについて

Swarmでは、クラスタを構成するDocker Engineインスタンスのことをノードと呼びます。

今までnode = LinuxVMみたいな書き方をしてきましたが、正確にはLinuxの上で動いているDocker Engineでクラスタを組んでいる。という表現が正しいです。

このクラスタを構成するnodeには、manager nodeとworker nodeの２種類が存在し、それぞれ以下のような役割を担います。

* maneger nodeは一つのリーダー(leader node)を選出し、コンテナのオーケストレーションや管理指示をworker nodeに送信する。デフォルトではmanager nodeもworker nodeとして動作する
* worker nodeはmanager nodeからの指示を受け取り、コンテナをデプロイしたり削除したりする。

詳しくは以下を読んでみてください

[Swarm モードの重要な概念 - ノード](http://docs.docker.jp/engine/swarm/key-concepts.html#swarm-concept-node)

## Portainerの構築

Swarmの構築には必要ないのですが、コンテナの状態をGUI上から監視できる[Portainer](https://github.com/portainer/portainer)というツールがあるのでこれを各Nodeに構築しておきます。 portainerというディレクトリを作り、以下の`docker-compose.yml`を作ります。

```
.
├── node1
│   ├── Vagrantfile
│   └── data
│       └── portainer
│           └── docker-compose.yml      //　追加
├── node2
│   ├── Vagrantfile
│   └── data
│       └── portainer
│           └── docker-compose.yml      // 追加
└── node3
    ├── Vagrantfile
    └── data
        └── portainer
            └── docker-compose.yml      // 追加
```

``` docker-compose.yml
version: '3'

services:
  portainer:
    container_name: my-portainer
    hostname: my_portainer
    image: portainer/portainer
    ports:
      - "9000:9000"
    command: -H unix:///var/run/docker.sock
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - data:/data
    restart: always

volumes:
  data:
```

最後に以下のコマンドを実行して、Portainerのコンテナを立ち上げましょう

```
$ cd ./<Portainerのdocker-composeファイルをおいたディレクトリ>
$ docker-compose up -d
```

以下のURLにアクセスするとPortainerにログインできます。
`http://<NodeのIP>:9000`

## コンテナのデプロイ

さて、では先ほど作成したクラスタ上でコンテナをデプロイしましょう。コンテナのデプロイは`docker service`コマンドを用いて行う事ができますが、複雑な構成のコンテナをデプロイする場合は、コマンドが長くなってしまって使いにくいです。

swarmはdocker-composeの`yaml`形式で定義されたファイルを使ってコンテナのデプロイを行うことができるので、こちらを使うようにしましょう。

まず、Node1の配下に以下のような`docker-compose.yml`ファイルを用意します

```docker-compose.yml
version: '3.7'

services:
  nginx:
    image: nginx
    ports:
    - target: 80
      published: 8080
      protocol: tcp
      mode: ingress
```

```
.
├── node1
│   ├── Vagrantfile
│   └── data
│       ├── nginx
│       │   └── docker-compose.yml  // 追加
│       └── portainer
│           └── docker-compose.yml
├── node2
│   ├── Vagrantfile
│   └── data
│       └── portainer
│           └── docker-compose.yml
└── node3
    ├── Vagrantfile
    └── data
        └── portainer
            └── docker-compose.yml
```

Docker composeを使い慣れた人にとっては見慣れた形式のファイルだと思います。
今回は簡単のために空の`nginx`コンテナを使って動作確認することにします。

以下のコマンドをNode1上で実行しましょう。

```
$ cd <docker-compose.ymlを用意した場所>
$ docker stack deploy nginx --compose-file  ./docker-compose.yml
```

以下にアクセスしてコンテナが起動したことを確認します。nginxのデフォルトの画面が表示されれば成功です。

`http://<Node1のIP>:8080` (上記の手順に従ってNodeServerを作った場合は http://192.168.33.10:8080 のハズ)

さて今回は`ingress`モードでコンテナのポートを公開しました。この場合、コンテナが立ち上がっているhost(今回はNode1)以外のNodeからでもこのコンテナにアクセスできます。

以下のURLにも接続してみて、ページが表示されることを確かめてみましょう。

`http://<Node2のIP>:8080`
`http://<Node3のIP>:8080`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93999/682510da-874b-3b6b-12fd-02e58c6abe1c.png)

Portainerを使うと、swarmクラスタ上でどのようにコンテナがデプロイされているかを見ることができます。

サイドバーから「swarm」を選び、「Go to cluster visualizer」のリンクをクリックしてみましょう

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93999/0c615e11-d50c-1fdf-32ef-74b2e20aca8b.png)

今はコンテナをひとつだけデプロイしたので以下のような感じですね

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93999/d7a23bce-f178-32f8-20c2-75a82a546181.png)

ちなみに今回は、簡単のためにServiceを１種類しか定義しませんでしたが、swarmでは`docker-compose.yml`で使える記法はよほど特殊なものでない限りすべてサポートしているので、serviceを複数定義すれば、複数のコンテナを連携させてデプロイすることももちろん可能です。

既存の`docker-compose.yml`ファイルがそのまま使えるのは嬉しいですね


## スケールアウト

現在、１個だけコンテナが立ち上がっているわけですが、実際のサービスでは、利用状況によってコンテナをスケールアウトしたくなる場合があります。

その場合は以下のコマンド打つことで簡単スケールアウトすることができます。

まずはスケールアウトしたいService(docker-composeで定義したService)の名前を見つけます

```
$ docker stack ps nginx
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE            ERROR               PORTS
p1syqxgwanpy        nginx_nginx.1       nginx:latest        ubuntu-node1        Running             Running 27 minutes ago       
```

docker-composeを使ってコンテナをデプロした場合、各Serviceの名前は`composefileのあるディレクトリ名_composeで定義したservice名`になります。今回の場合は`nginx`ディレクトリの下に`docker-compose.yml`を作っちゃったので`nginx_nginx`ですね。（なんでこの名前にしちゃったんだろ？）

上のコマンドでは`.1`が末尾についていますが、これはServiceがスケールアウトして同じコンテナが複数立った場合に見分けるためのものです。なので実際のサービス名は`nginx_nginx`ですね

このサービスをスケールアウトするには以下の`service scale`コマンドをNode1で実行します。

```
$ docker service scale nginx_nginx=5   // コンテナの数を5個にスケールアウトする
nginx_nginx 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 
```

Portainerで確認すると、コンテナが５つに増えていることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93999/48697c31-bc94-d506-d4ea-e80f4e3d0b2d.png)


### composefileを使ったスケールアウト

上記の方法でもスケールアウトはできるのですが、compose fileに予め、作成するコンテナ数を定義する事もできます。`docker-compose.yml`を以下のように書き換えてみましょう

``` docker-compose.yml
version: '3.7'

services:
  nginx:
    image: nginx
    ports:
    - target: 80
      published: 8080
      protocol: tcp
      mode: ingress
    deploy:
      mode: replicated
      replicas: 3       ## コンテナを3つ作成する
```

docker-composeを書き換えた後、もう一度`stack deploy`を実行することで、`docker-compose.yml`の差分を自動的に検知して、その状態にコンテナをデプロイし直してくれます。

宣言的で良い感じですね

```
$ cd <docker-compose.ymlを用意した場所>
$ docker stack deploy nginx --compose-file  ./docker-compose.yml
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93999/d045864c-c021-b615-a08f-7ec3677f4e9d.png)

## リカバリー

さて、これで同じコンテナを複数個立てて、ロードバランシングすることができました。
じゃあこの状態で、コンテナやNodeを落とすとどうなってしまうのでしょう？

ちょっとやってみたいと思います。

### コンテナを削除する

まず削除対象のコンテナを探します

```
$ docker ps
CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS                    NAMES
7e504778aab7        nginx:latest          "nginx -g 'daemon of…"   About an hour ago   Up About an hour    80/tcp                   nginx_nginx.1.p1syqxgwanpys3c64kx9dlwsl  
```

今回はnode1のコンテナを削除してみましょう

```
$ docker rm 7e504778aab7
```

すると、以下のように削除したはずのコンテナが復活します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93999/10c165de-c5d9-b1ab-afe2-c6e4f35e272a.png)

実はDocker swarmは`docker-compose.yml`の`replicas`で指定した数のコンテナが起動するように常に維持しようとします。なので、もしもコンテナが止まったり、削除されたりした場合は、新しいコンテナを作成してこれを補おうとします。上では一度コンテナの作成に失敗しているようですが、最終的には起動しているコンテナが3つになるように常に調整してくれます。

### Worker Nodeを減らす

では今度はworker nodeを減らして挙動を見てみましょう。この状態で`Node3`のVMの電源を落とします

```Node3.
$ sudo init 0    // Node3の電源を落とす
```

すると以下のような状態になります。Node3がdown状態になって、Node1にもう一つコンテナが立ち上がっているのがわかります。

このようにNodeがダウンすると、Swarmはそこにデプロイされていたコンテナを別のNodeで起動して数を調整します。賢いですね

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93999/2b5a4784-b8fb-9a72-ac3f-62aebfc1e58c.png)

この状態でもう一度Node3を起動すると以下のような状態になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93999/4cef380c-a3e6-9030-fd68-2121560b1ffc.png)

あれ、Node3にデプロイされていたコンテナがshatdownされちゃいましたね。なんだかNode1にデプロイ先が偏っていて微妙な感じです。うーん。。。

SwarmがNodeの再接続を検知しても、現在のコンテナの状態が正しい場合は、既存のコンテナを移動してrebalanceするようなことはしてくれないみたいですね。

なのでもしも偏っちゃったコンテナをrelocateしたい場合は以下のコマンドを実行してください。

```
$ docker service update --force nginx_nginx
```

でもこのコマンドって、既存のコンテナを順番に破棄して、全て新しく入れ替えるコマンドなんですよね。主にあとで紹介するローリングアップデートとかに使うやつです。なので停止したコンテナが大量にできちゃってますね

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93999/e70dc8bc-2eca-2bfe-0e5d-b0ab0ad282de.png)

### Manager Nodeを落とす

ではworker nodeではなくmanager nodeを落とすとどうなってしまうのでしょう？

Swarmでは現在リーダーとなっているノード(manager nodeの中から一つ選ばれる)が落ちると、残りのmanager nodeの中から一つを選出し、そのnodeがleader node(Swarmをコントロールするノード)になります。

現在はmanager node1、 worker node2の環境なので、まずはmanager nodeの数を増やします。
このとき、manager nodeの数は最低でも3つないと新しいリーダーを選出できません。なのでまずは今回作ったNode2とNode3を両方共manager nodeに昇格させます。

昇格させる場合は、Node1で以下を実行します。

```
$ docker node promote ubuntu-node2          // node2をManagerに昇格
$ docker node promote ubuntu-node3          // node3をManagerに昇格
```

`docker node ls`を実行すると、MANAGER STATUSが`Reachable`になっているのがわかるでしょうか？

```
$ docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
ql8aefbw7rg8nfmznejnzfeus *   ubuntu-node1        Ready               Active              Leader              18.09.7
uusz619ly0y6zp3zsdh0br31y     ubuntu-node2        Ready               Active              Reachable           18.09.7
pgouhk6cndlxddjhll1vo1rfv     ubuntu-node3        Ready               Active              Reachable           18.09.7

```

この状態でNode1を落としてみます。

Node2の上で`docker node ls`を実行してみると`node3`がLeaderに昇格していることがわかります。

```
$ docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
ql8aefbw7rg8nfmznejnzfeus     ubuntu-node1        Unknown             Active              Unreachable         18.09.7
uusz619ly0y6zp3zsdh0br31y *   ubuntu-node2        Ready               Active              Reachable           18.09.7
pgouhk6cndlxddjhll1vo1rfv     ubuntu-node3        Ready               Active              Leader              18.09.7

```

Node3のPortainerを見てみると、以下のようになっています。
ダウンしたNode1の代わりに、Node3が適切にコンテナを起動して、コンテナの数を維持していることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93999/5cf5b62b-11b1-a84d-4752-4f72e9923e7a.png)

### (補足)　Leader Node選出ロジックについて

Swarmでは、Swarmを管理するleader nodeが落ちてしまった場合、新しいleader nodeを[Raft](https://raft.github.io/raft.pdf)と呼ばれるロジックに従って選出します。

この場合、ロスト可能なNodeの最大数は、manager nodeの数をNとして`(N-1)/2`で計算されます。
3台の場合は`(3-1)/2 = 1`なので１台までなら落ちても大丈夫です。

ただし多ければ良いというものでもないみたいで、主にパフォーマンス上の理由から、Docker swarmではmanager nodeの数は最大でも7台までに抑えることが推奨されています。

詳しくは以下を参照して見てください
https://docs.docker.com/engine/swarm/how-swarm-mode-works/nodes/

> ・A three-manager swarm tolerates a maximum loss of one manager.
> ・A five-manager swarm tolerates a maximum simultaneous loss of two manager nodes.
> ・An N manager cluster tolerates the loss of at most (N-1)/2 managers.
> ・Docker recommends a maximum of seven manager nodes for a swarm.


## ローリングアップデート

さて、個人的にSwarmの目玉機能だと思っているローリングアップデートを紹介したいと思います。今まで見てきたように、Swarmでは同じイメージのコンテナを複数立ててロードバランシングすることができます。

そして複数コンテナを立てている環境では、デプロイされているコンテナを順番に更新していくことで、無停止デプロイを実現することができます。俗に言うローリングアップデートと言うやつですね。

さっそく試してみましょう`docker-compose.yml`を以下のように書き換えます

``` docker-compose.yml
version: '3.7'

services:
  nginx:
    image: httpd     ## => イメージをnginxからApacheに変更
    ports:
    - target: 80
      published: 8080
      protocol: tcp
      mode: ingress
    deploy:
      mode: replicated
      replicas: 3
```

そして再び`docker stack deploy` を実行しましょう。これだけでローリングアップデートによって、３つのコンテナが順番にApache serverに差し替わります。

```
$ cd <docker-compose.ymlを用意した場所>
$ docker stack deploy nginx --compose-file  ./docker-compose.yml
```

実行した後、今までnginxが動いていたURLを更新してみてください。どこかのタイミングでnginxがhttpdに差し替わっているのが確認できると思います。

`http://<NodeのIP>:8080`

httpdの初期画面

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93999/3a0f527c-6164-9605-c03a-070544e4bdcf.png)


ちなみに`docker stack deploy`は、実行時に`docker-compose.yml`の差分を検知して、差分のあったserviceだけを`docker service update`するような動きになっています。
なので実際に実行されているのは`docker service update`ですね。

## ロールバック

上で無事`nginx`を`httpd`に「アップデート」できました。

しかし世は大nginx時代、皆さんの中にはこのアップデートを快く思わない人もいるでしょう。今すぐ、Apacheを窓から投げ捨て、サーバーのアップデートをなかったコトにして、もとのnginxに戻したくなるかもしれません

そんなときでもSwarmなら安心です。SwarmにはService単位でのrollback機能が実装されています。
以下のコマンドを実行してみてください。

アップデートはなかったことにされ、再びnginxの初期画面を目にすることができるでしょう

```
$ docker service rollback nginx_nginx
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93999/682510da-874b-3b6b-12fd-02e58c6abe1c.png)

## スタックの削除

最後にデプロイしたスタックをまるごと削除します。以下を実行してみましょう

```
$ docker stack rm nginx
Removing service nginx_nginx
Removing network nginx_default
```

デプロイされたコンテナがすべて削除され、`docker-stack deploy` を行う前の状態に戻った事がわかるかと思います。Swarmの場合は、このように、デプロイしているサービス群を、`docker-compose.yml`で定義された単位でまるごと削除することができます。docker-composeで言うところの、`$ docker-compose down` と同じようなものだと思えばよいかと思います。

こういう取り回しの良さも`docker swarm`の魅力です。

## 余談

### 作成されるコンテナの数を制限したい

ローリングアップデートを繰り返していると、過去のイメージで作られた停止済みコンテナが大量にできてしまいます。
それが嫌な場合は、swarmのoptionでこれを制限することができます。

```
$ docker swarm update --task-history-limit 5    // 一つのServiceについてコンテナを5世代前まで残す
```

この設定が効果を発揮するのは次回の`docker service update`時であることに注意してください。
つまりこの設定を有効にしただけでは古いコンテナは削除されません。

もしも古いコンテナを削除したいのであれば、以下のコマンドを実行してみてください。
現在存在する古いコンテナが削除され、現時点のコンテナだけになることがわかると思います

```
$ docker swarm update --task-history-limit 0    // コンテナ履歴を残さない
$ docker service update --force nginx_nginx
```

ただし、停止されたコンテナが大量にできたからと言って、大量のストレージが消費されているわけではありません。

以下のコマンドを実行して、停止されているコンテナのストレージ容量を見てみてください

```
$ docker ps -a -s
CONTAINER ID        IMAGE                 COMMAND                  CREATED              STATUS                      PORTS                    NAMES                                     SIZE
3bb6e4eac2a7        nginx:latest          "nginx -g 'daemon of…"   42 seconds ago       Up 35 seconds               80/tcp                   nginx_nginx.2.nd8v1m97djhykqhe1jd7xoopb   2B (virtual 109MB)
cf9d34de148f        nginx:latest          "nginx -g 'daemon of…"   About a minute ago   Exited (0) 38 seconds ago                            nginx_nginx.2.z7h14vx534milfsml9zddyvu7   0B (virtual 109MB)
1369522e3aca        portainer/portainer   "/portainer -H unix:…"   30 hours ago         Up About a minute           0.0.0.0:9000->9000/tcp   my-portainer                              0B (virtual 75.4MB)

```

`size 0B (virtual 109MB)`となっているのがわかるでしょうか？

実はDockerのコンテナのストレージは元となったcontainer imageとの差分のみを保持する仕組みになっています。なので同じイメージのコンテナがいくら増えようが、使われる容量は0Bです。

むしろDockerを運用するときにはconatiner imageの方を気にするようにしましょう。特に自分が作っているアプリのconatiner imageがエンハンスによって増えていくと、新たなイメージをデプロイするたびにどんどんストレージ容量が使われている可能性があります。
その場合は、定期的にストレージに保存されている古いイメージを削除するなどしたほうが良いでしょう。

## まとめ

以上でDocker Swarm modeのハンズオンは終わりです。

ここで紹介した以外にも、様々な機能がswarmには備わっているので興味がある人は調べてみてください。

ちなみにここでは紹介しませんでしたが、DockerのSwarm modeはシングルノードでも実行できます。
その場合は、引数なしで`docker swarm init`と打つだけで、単一のサーバーの上でローリングアップデートやロールバックなどの魅力的な機能を利用することができます。

クラスタ組むほどじゃないんだけど、ローリングアップデートはしたいんだよな〜ってときはとてもお手軽で便利です。([Single node docker swarm でお手軽 rolling update](https://blog.1q77.com/2018/10/rolling-update-on-single-node-docker-swarm/)なんかは参考になります)
ココらへんはk8sにはない利点なんじゃないでしょうか？

以上

## 参考URL

* [docker swarm - 公式リファレンス](https://docs.docker.com/engine/reference/commandline/swarm/)
* [docker stack - 公式リファレンス](https://docs.docker.com/v17.09/engine/reference/commandline/stack/)
* [docker node - 公式リファレンス](https://docs.docker.com/v17.09/engine/reference/commandline/node/)
* [docker compose deploy - 公式リファレンス](https://docs.docker.com/compose/compose-file/#deploy)