ローカル環境にDockerのSwarmモードで13台のDocker Machineを立ち上げElastic Stackクラスタを構築

# はじめに
生産技術部で製品の検査工程を担当しているエンジニアです。Elastic Stackを実際に運用していくために、ローカル環境にElastic Stackのクラスタを構築し検証を行っています。

本記事では、Swarmモードを利用し、Docker Machineでクラスタを構築するための事前準備から、構築手順、動作検証用のログ生成、運用方法についてまとめましたので紹介します。

![portainer-swarm.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451049/d756ade1-9c6e-0ab2-4574-5af5ed6c1eeb.png)

go言語で記述したログ生成ツールを作成し、生成したログをElastic Stackに取り込み、Kibana上で正しく表示されていることを確認しましたので、合わせてツールも紹介します。

![visualized-log.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451049/db09f80a-090f-77ea-8309-02be1b07f361.png)

リポジトリに、作成したファイル一式を追加しました。
https://gitlab.com/iyama_yuki/elastic-stack-swarm

# 環境
```shell
$ docker -v
Docker version 20.10.5, build 55c4c88
$ docker-compose -v
docker-compose version 1.28.5, build c4eb3a1f
$ docker-machine -v
docker-machine version 0.16.2, build bd45ab13

$ docker-machine ls
NAME                      ACTIVE   DRIVER       STATE     URL                         SWARM   DOCKER      ERRORS
elasticsearch-data-01     -        virtualbox   Running   tcp://192.168.99.111:2376           v19.03.12   
elasticsearch-data-02     -        virtualbox   Running   tcp://192.168.99.112:2376           v19.03.12   
elasticsearch-data-03     -        virtualbox   Running   tcp://192.168.99.113:2376           v19.03.12   
elasticsearch-master-01   -        virtualbox   Running   tcp://192.168.99.106:2376           v19.03.12   
elasticsearch-master-02   -        virtualbox   Running   tcp://192.168.99.107:2376           v19.03.12   
elasticsearch-master-03   -        virtualbox   Running   tcp://192.168.99.108:2376           v19.03.12   
filebeat-01               -        virtualbox   Running   tcp://192.168.99.117:2376           v19.03.12   
filebeat-02               -        virtualbox   Running   tcp://192.168.99.118:2376           v19.03.12   
kibana                    -        virtualbox   Running   tcp://192.168.99.119:2376           v19.03.12   
local                     *        virtualbox   Running   tcp://192.168.99.105:2376           v19.03.12   
logstash-01               -        virtualbox   Running   tcp://192.168.99.114:2376           v19.03.12   
logstash-02               -        virtualbox   Running   tcp://192.168.99.115:2376           v19.03.12   
logstash-03               -        virtualbox   Running   tcp://192.168.99.116:2376           v19.03.12
```

* ハードウェアリソース

```
$ system_profiler SPHardwareDataType
Hardware:

    Hardware Overview:

      Model Name: MacBook Pro
      Model Identifier: MacBookPro15,1
      Processor Name: 8-Core Intel Core i9
      Processor Speed: 2.4 GHz
      Number of Processors: 1
      Total Number of Cores: 8
      L2 Cache (per Core): 256 KB
      L3 Cache: 16 MB
      Hyper-Threading Technology: Enabled
      Memory: 32 GB
```

# 事前準備
Swarmモードを利用してクラスタの構築を行っていきますが、まずはDocker Machineの準備と、Swarmクラスタへノードを参加させる必要があります。

## Swarmモードとは
SwarmモードはDocker Engine[^1] 1.12以降に組み込まれたクラスタ管理及びオーケストレーション機能のことで、オーケストレーション・ソフトウェアを別途インストールする必要はありません。Swarmモードで動作する複数のDockerホストによって、Swarm（クラスタ）を構成し、各ノードはManagerもしくはWorkerの役割を持ちます。Managerとして設定したノードはWorkerノードの管理を行い、Workerとして設定したノードはサービス[^2]を実行します。今回は、検証のためにMasterノードが１台で構成しましたが複数台用意する必要があります。Managerノードが故障した場合に、サービスは稼働し続ける事ができますが再度クラスタを作成する必要があります。３つのManagerノードを用意しておく事で１つのManagerノードの故障に耐える事ができます。`N`個のノードでは、`(N-1)/2`個のManagerノードの故障に耐える事ができます。

[^1]: 一般に、Dockerと言うとDocker Engineの事を指します。
[^2]: Swarmクラスタに参加しているDocker Engineの事をノードと呼び、MasterノードやWorkerノードで実行されるタスクをサービスと呼んでいます。


## Docker Machineの作成
Docker Machineは、仮想マシン上にDocker Engineをインストールするツールであり、複数のDockerホストの起動、停止、再起動等を行う事が出来ます。ローカル、クラウドに関わらずDockerが利用できるホストであれば、同様に管理する事ができます。

https://docs.docker.jp/machine/overview.html

以下のコマンドは、virtualboxをドライバとして利用し、`elasticsearch-data-01`という名称のホストを用意します。ディストリビューションはDocker Engineを実行するための最小の機能を有する`boot2docker`が利用されています。今回は１３台のマシンを利用するため、１３台分作成し１台はlocalと言う名前のManagerノードとしました。実運用では耐故障性を向上させるために、前述の通り３台以上のManagerノードを追加する必要性があります。

```shell
$ docker-machine create --driver virtualbox elasticsearch-data-01 # workerノード
```

追加したマシンの一覧を確認することができます。

```
$ docker-machine ls
NAME                      ACTIVE   DRIVER       STATE     URL                         SWARM   DOCKER      ERRORS
elasticsearch-data-01     -        virtualbox   Running   tcp://192.168.99.111:2376           v19.03.12   
```

作成したマシンに対して、Docker CLIを利用するためには、envを設定する必要があります。設定することでACTIVEの項目が`*`となり、elasticsearch-data-01に対してDocker CLIが実行できるようになります。

```shell
$ eval "$(docker-machine env elasticsearch-data-01)"
$ docker-machine ls
NAME                      ACTIVE   DRIVER       STATE     URL                         SWARM   DOCKER      ERRORS
elasticsearch-data-01     *        virtualbox   Running   tcp://192.168.99.111:2376           v19.03.12   

```

## Swarmクラスタへの参加
以下のコマンドを実行する事で、直近に作成したクラスタ上のManagerノードになることが出来ます。

```shell
$ eval "$(docker-machine env local)"
$ docker swarm init
```

Managerノードで発行したトークンを設定することで、SwarmクラスタにWorkerノードとして参加することが出来ます。

```shell
# Managerノードでトークンを確認
$ eval "$(docker-machine env local)"
$ docker swarm join-token worker # workerをmanagerとすることで、managerノードとして参加出来ます。
To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-1xopvt9ri58lpbp9poi9bg3du70om35k6mwfctn7cnodjnmbne-cgz0z3783of0o3ftslt4pdbn3 192.168.99.105:2377

# Workerノードに切り替える
$ eval "$(docker-machine env elasticsearch-data-01)"
$ docker swarm join --token SWMTKN-1-1xopvt9ri58lpbp9poi9bg3du70om35k6mwfctn7cnodjnmbne-cgz0z3783of0o3ftslt4pdbn3 192.168.99.105:2377
```

Managerノードから、参加しているノード一覧を確認する事が出来ます。MANAGER STATUSの`Leader`はManagerノードである事を意味します。

```shell
$ eval "$(docker-machine env local)"
% docker node ls                    
ID                            HOSTNAME                  STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
x1bdzf22dk9rarffbplaimeyn     elasticsearch-data-01     Ready     Active                          19.03.12
y201dtu6z4eb722e04xidm9ol *   local                     Ready     Active         Leader           19.03.12
```



# クラスタ構築
必要なノードを追加が完了すると、サービスをデプロイする事が可能な状態になります。早速、Composeファイルを利用し、サービスをデプロイします。
## Composeファイルを利用
サービスをデプロイする方法の一つとして、Composeファイルを利用する方法があります。この方法を用いることによって、一台のホストマシンにデプロイするのと同じように、複数台のホストマシンに対してデプロイする事が可能になります。これは、Docker ComposeとDocker Swarmは完全な統合を目指しているからです。ただし、現状はいくつかの制約があり、完全には統合されていません。

* まず、Composeファイルのバージョンによって利用できる設定が異なります。
* Dockerfileを使ったイメージの構築は出来ません。
* さらに以下のドキュメントで、「swarm」を検索すると複数の制約が確認できます。
* https://docs.docker.com/compose/compose-file/compose-file-v3/

デプロイ方法について各種設定をします。

* endpoint_mode
  * vip(デフォルト)、dnsrrから選択します。
  * https://matsuand.github.io/docs.docker.jp.onthefly/compose/compose-file/compose-file-v3/#endpoint_mode
  * vipはサービスに対して仮想IPを割り当てます。これは、外部のクライアントからアクセスするためのフロントエンドとして機能します。
  * dnsrrはDNSラウンドロビンの事です。外部のロードバランサを利用する場合に利用される方式で、単一の仮想IPを扱いません。代わりに、Dockerはサービスに対するDNSエントリを準備し、サービス名のDNSクエリに対してIPアドレスのリストを返し、クライアントが直接そのIPアドレスにアクセスできるようにします。

* mode
  * global、replicatedから選択します。
  * globalは、ノードごとに１つのコンテナをサービスとしてデプロイします。
  * replicatedは、指定した数のサービスをデプロイします。replicasに複製する数を設定します。

* resources
  * 利用するハードウェアリソースの制限を設定する事ができます。cpus:"0.50"とすると、CPUの使用率を50%に制限できます。

* placement
  * max_replicas_par_nodeは、ノードごとに複製するサービスの数の最大値を設定します。
  * constraintsは、デプロイする先のノードを指定したり、デプロイ先を制限します。ノードのラベルを利用して制限をかける場合には、ノードに対して予めラベルを設定しておく必要があります。以下の例では、デプロイ対象のノードにesというキーとtrueというバリューの組みを追加しています。

```yml
    deploy:
      endpoint_mode: dnsrr
      mode: replicated
      replicas: 3
      resources:
        limits:
          cpus: "0.50"
          memory: 512M
      placement:
        max_replicas_per_node: 1
        constraints: [node.labels.es == true]
```

## サービスのデプロイおよび削除
サービスは１つずつデプロイすることもできますが、スタックを利用すれば一斉にデプロイすることが出来ます。この方法では、Composeファイルを作成しサービスのデプロイを行います。どのマシンに対してデプロイするかは、ComposeファイルのConstraintsを設定することで決定します。ノードに対してラベルを設定しておく事でホストマシンを指定します。複数のノードに同一のラベルを設定することも可能です。

```shell
# デプロイ
$ docker stack deploy --compose-file docker-compose.yml mystack
# スタックの削除
$ docker stack rm mystack
```

デプロイされたサービスの一覧を確認することができます。REPLICASの数字が複製された数を示し、`max 1 per node`は１つのノードに対して最大１つまでという制約が設定されていることを確認できます。

```shell
$ docker service ls
ID             NAME                       MODE         REPLICAS               IMAGE                                                  PORTS
vu39p5h1ciu8   mystack_es-data            replicated   3/3 (max 1 per node)   docker.elastic.co/elasticsearch/elasticsearch:7.12.0   
9l06iajhebor   mystack_es-master          replicated   3/3 (max 1 per node)   docker.elastic.co/elasticsearch/elasticsearch:7.12.0   
iv446tehe3oc   mystack_kibana             replicated   1/1                    docker.elastic.co/kibana/kibana:7.12.0                 *:5601->5601/tcp
tjaen6ecqdmc   mystack_logstash           replicated   3/3 (max 1 per node)   docker.elastic.co/logstash/logstash:7.12.0             
vkse7irszqmw   mystack_metricbeat         replicated   1/1                    docker.elastic.co/beats/metricbeat:7.12.0              
lslw1vi4poae   mystack_portainer_master   global       1/1                    portainer/portainer:latest
```

## Elasticsearchのメモリ設定
Elasticsearchは、コンテナの起動前にmemlockを設定する必要があります。公式では、Composeファイルに設定しコンテナごとに制御していますが、この方法はSwarmで利用する事ができません。そこで、Docker Machineで起動したシステムに対して設定します。Docker Machineのディストリビューションであるboot2dockerに設定します。

```shell
$ vi /var/lib/boot2docker/profile

EXTRA_ARGS='
--label provider=virtualbox
--default-ulimit memlock=-1
 
'                                 
CACERT=/var/lib/boot2docker/ca.pem 
DOCKER_HOST='-H tcp://0.0.0.0:2376'
DOCKER_STORAGE=overlay2
DOCKER_TLS=auto                              
SERVERKEY=/var/lib/boot2docker/server-key.pem
SERVERCERT=/var/lib/boot2docker/server.pem
```

また、Elasticsearchは、`vm.max_map_count`の値も変更する必要があります。Docker Machineに対して設定する場合、sshを利用します。設定したホスト名を指定しsshを実行する事で、マシンにリモートログインします。値を変更しリモートログインから抜けた後に、再起動を実施することで反映されます。

```shell
$ docker-machine ssh elasticsearch-data-01
worker $ sudo vi /var/lib/boot2docker/profile
worker $ sudo sysctl -w vm.max_map_count=262144
$ exit
$ docker-machine restart elasticsearch-data-01
```

# 動作検証用のログを生成
## go言語でログジェネレータを作成
ログを正しく追加できていることを確認するために、ログを定期的に生成するツール「[log-generator](https://gitlab.com/iyama_yuki/elastic-stack-swarm/-/blob/master/beats/filebeat/helper/log-generator.go)」をgo言語で作成しました。log-generatorは、ログの生成を継続する期間と、周期を選択する事ができます。使い方は以下の通りです。

```shell
$ cd beats/filebeat/helper
$ go run log-generator.go -h
Usage of log-generator:
  -d int
        duration (default 60)
  -p int
        period (default 1)
```

実行すると、動いていることを確認できるログが標準出力されます。

```shell
$ cd beats/filebeat/helper 
$ go run log-generator.go -d 10    
[2021-03-28 20:34:33]create log.
[2021-03-28 20:34:34]create log.
[2021-03-28 20:34:35]create log.
[2021-03-28 20:34:36]create log.
[2021-03-28 20:34:37]create log.
[2021-03-28 20:34:38]create log.
[2021-03-28 20:34:39]create log.
[2021-03-28 20:34:40]create log.
[2021-03-28 20:34:41]create log.
[2021-03-28 20:34:42]create log.
```


# 運用方法
## Nodeの管理ツール
[Portainer](https://github.com/portainer/portainer)を利用しました。コンテナの管理だけでなく、swarmの管理にも対応しています。管理画面からノードのラベルを編集したり、コンテナのスケールの調整、イメージを指定して、新たにサービスを追加することも可能です。実際に管理画面から、elasticsearchのmasterノードをスケールし、3台から2台に変更し、2台から3台に戻した時に正常に復帰する事を確認しました。

# 最後に
DockerのSwarmモードを利用し、初めて複数台の仮想マシンを使ったクラスタの構築を行いました。Docker Composeは普段から利用しており、スムーズに構築する事ができました。今後は、AWSを使ったクラスタの構築やk8sを試していきたいと思っています。