Elasticsearch Cluster on Linux Container

# 概要

Linuxコンテナ（systemd-nspawn）を使って、複数ホストにまたがるElasticsearchクラスタを構築してみました。
私はコンテナ歴12年くらいですが、ほとんどSolarisコンテナ（Zones）ばかり使っていて、Linuxコンテナはどんなものかよくわかっていないので、おかしなことを言っていましたらご指摘ください。


# 背景

以下のような感じの要件で、基盤のアーキテクチャを考えていました。

1. 50～100ノード規模のElasticsearchクラスタを作りたい
2. オンプレ環境で
3. 一度起動したら年単位で動き続ける
4. 商用ライセンスはあまり使いたくないけど、人柱になって自力でなんとかする気概はあり

なんとなく以下のような実現方式を考えました。本音はSolaris Zonesを使いたくてたまりませんが今回はLinux縛りです。

|カテゴリ|方式|評価（主観）|
|----|----|----|
|物理系|物理サーバ50台|× ラックが足りない。運用嫌だ。|
|    |物理サーバ＋マルチプロセス|△ 運用嫌だ。|
|ハイパーバイザ系|VMware|△ ライセンス必要。オーバーヘッド嫌だ。|
|    |KVM, Xen|△ オーバーヘッド嫌だ。|
|コンテナ系|Docker|△ どうも合わない。k8s, Swarmとかは大がかりすぎ。|
|    |OpenVZ|△ まだあるのだろうか？|
|    |LXD|○？ 良さそう。使うならUbuntuが良さそうか。|
|    |systemd-nspawn|○？ ほぼOS標準の機能！|


Dockerは少し検証してみたのですが、動くには動くのでしょうが、ホストまたぎのネットワークを構成するのが面倒で、KVSを立ち上げて、VXLANでオーバーレイネットワークとかになるのでやる気がなくなりました。
そのためだけにKubernetesとかSwarmを使うのも大がかりすぎに思えました。

[マルチホスト上での、dockerコンテナ間ネットワーク環境整備メモ（Overlay Networking w/ Etcd） \- Qiita](https://qiita.com/ttsubo/items/1257fe914744d00b4ed8)

DockerをDisる気はなく、用途がマッチすれば良いと思いますが、仮想マシンのようなことがやりたいならまったく不向きで、無理矢理使っても悲惨な運用になるのが落ちという感じがします。
長年Solarisコンテナを使ってきたのもあって、なるべくシンプルでほぼOS標準の機能で動作するということから systemd-nspawn にたどり着き、検証してみようと思いました。

なお、クラウド前提なら、Amazon Elasticsearch Service を検討するのもいいと思います。

[料金 \- Amazon Elasticsearch Service \| AWS](https://aws.amazon.com/jp/elasticsearch-service/pricing/)


# シナリオ

今回構築する環境は以下のようなイメージです。物理マシンがあれば使ってもいいですが、私はVirtualBox上に構築することにしました。
![container.png](https://qiita-image-store.s3.amazonaws.com/0/317213/0b5c39be-2678-0e52-badf-d60f4d52c4c2.png)


構築は以下のような流れで進めました。

- ホスト構築編
  - CentOS 7 をインストールする
  - systemd-networkd をインストールする
  - ZFS をインストールする（任意）
- コンテナ構築編
  - コンテナイメージをインストールする
  - コンテナを起動する
  - Elasticsearchをインストールする
  - コンテナをコピーする


# 構築

## ホスト構築編

### CentOS 7 をインストールする

OSはユーザが多いであろうCentOSにしました。バージョンは現時点で最新の7.6にしました。

インストールはできましたが、グラフィカルインストーラだとマウスでボタンをクリックできないことがある謎の事象に遭遇したので、テキストインストーラを使う方がいいような気がします。

[CentOS7 \(1804\) のテキストベースインストール手順 \- Qiita](https://qiita.com/mattsun/items/a1359abdacc14317ad9b)

今回はホスト間で通信できる必要があるので、VirtualBoxのネットワークの設定でブリッジアダプターを使うとよいと思います。また、プロミスキャスモードを許可する設定も必要です。
![virtualbox_network.png](https://qiita-image-store.s3.amazonaws.com/0/317213/674ddab2-272e-2aed-c5f0-90f2eb7e3b60.png)

また、OSの領域とコンテナの領域を分けたいので、ストレージの設定画面でディスクイメージを1つ追加しました。

SELinuxは無効にしました。

```
$ sudo vi /etc/sysconfig/selinux
...
SELINUX=disabled
...
```

### systemd-networkd をインストールする

今回はホスト間で通信できるようにネットワークブリッジを使うので、ネットワークの管理をsystemd-networkdで行うのがよいようです。以下のページを参考にしました。

[Use systemd\-networkd under CentOS 7 « Rene Diepstraten's Blog](https://renediepstraten.nl/?p=56)

以下のページも参考にしました。systemd関連のドキュメントはArch Linuxのものがわかりやすい感じがします。

[systemd\-networkd \- ArchWiki](https://wiki.archlinux.jp/index.php/Systemd-networkd)

systemd-networkd と systemd-resolved をインストールします。

```
$ sudo yum install systemd-networkd systemd-resolved
```

設定ファイルを作成します。内容は環境に合わせて適当に変えてください。Redhat標準の /etc/sysconfig/network-scripts/ 以下のファイルは使われなくなります。

```
/etc/systemd/network/br0.netdev
----
[NetDev]
Name=br0
Kind=bridge
```

```
/etc/systemd/network/enp0s3.network
----
[Match]
Name=enp0s3

[Network]
Bridge=br0
```

```
/etc/systemd/network/br0.network
----
[Match]
Name=br0

[Network]
Address=192.168.0.10
Gateway=192.168.0.254
DNS=192.168.0.254
```

systemd-networkd と systemd-resolved を有効にします。

```
$ sudo systemctl disable network NetworkManager
$ sudo systemctl enable systemd-networkd systemd-resolved
$ sudo reboot
```

resolve.conf を入れ替えておきます。

```
$ sudo ln -s /run/systemd/resolve/resolv.conf /etc/resolv.conf
```

networkctl コマンドで、IPアドレスなどを確認することが出来ます。

```
$ networkctl status
●      State: routable
     Address: 192.168.0.10 on br0
     Gateway: 192.168.0.254 (XXX) on br0
         DNS: 192.168.0.254
```


### ZFS をインストールする（任意）

ZFSは必須ではありませんが、コンテナの領域を管理するのに都合が良さそうなので使ってみたいと思います。
systemd-nspawn はBtrfsと連携する機能があるようなのですが、ここは慣れ親しんだZFSにします。

ZFSをCentOSで使用するには、カーネルモジュールを追加する必要があります。ZFS on Linux の公式の手順に従ってインストールします。

[RHEL and CentOS · zfsonlinux/zfs Wiki](https://github.com/zfsonlinux/zfs/wiki/RHEL-and-CentOS)

ZFSのYumリポジトリを使えるようにします。

```
$ sudo yum install http://download.zfsonlinux.org/epel/zfs-release.el7_6.noarch.rpm
```

zfs-kmod のほうを有効にします。

```
$ sudo vi /etc/yum.repos.d/zfs.repo
----
 [zfs]
 name=ZFS on Linux for EL 7 - dkms
 baseurl=http://download.zfsonlinux.org/epel/7/$basearch/
-enabled=1
+enabled=0
 metadata_expire=7d
 gpgcheck=1
 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-zfsonlinux
@@ -9,7 +9,7 @@
 [zfs-kmod]
 name=ZFS on Linux for EL 7 - kmod
 baseurl=http://download.zfsonlinux.org/epel/7/kmod/$basearch/
-enabled=0
+enabled=1
 metadata_expire=7d
 gpgcheck=1
 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-zfsonlinux
```

ZFS をインストールします。

```
$ sudo yum install zfs
```

ARCのサイズを制限しておきます。

```
$ sudo vi /etc/modprobe.d/zfs.conf
----
options zfs zfs_arc_max=1073741824
```

ストレージプールを作成します。ZFS on Linux では、ls -l /dev/disk/by-id で表示されるデバイスIDを使用することが推奨されているようです。

```
$ sudo zpool create tank ata-VBOX_HARDDISK_XXXXXX-XXXXXX
```

tank という名前でストレージプールができました。

```
$ zpool list
NAME   SIZE  ALLOC   FREE  EXPANDSZ   FRAG    CAP  DEDUP  HEALTH  ALTROOT
tank  39.8G   291K  39.7G         -     0%     0%  1.00x  ONLINE  -
```

コンテナ用の領域（データセット）を作成します。compressionの設定はお好みに合わせてという感じです。近年はCPUが高速なので、圧縮した方が Disk I/O が減って高速になるケースもあります。

```
$ sudo zfs create -o compression=lz4 -o mountpoint=/var/lib/machines tank/machines
$ sudo zfs create tank/machines/es11
$ sudo zfs create tank/machines/es12
```

こんな感じでデータセットができました。

```
$ zfs list
NAME                 USED   AVAIL  REFER  MOUNTPOINT
tank                 162K   38.5G  25.5K  /tank
tank/machines         73K   38.5G    25K  /var/lib/machines
tank/machines/es11    24K   38.5G    24K  /var/lib/machines/es11
tank/machines/es12    24K   38.5G    24K  /var/lib/machines/es12
```

systemd-nspawnでは、 /var/lib/machines/ 配下のディレクトリ名がコンテナのhostnameになるように想定されているようです。長さの制限は一見ないように見えるのですが、systemd-networkd がインタフェース名を自動生成するときに先頭11文字で切り落としてしまうようで、先頭11文字がユニークでないと名前が衝突するようです。そのうち改善するのかもしれませんが、ディレクトリ名（コンテナ名）は11文字以下にするのが無難なようです。

[nspawn containers with too long names conflict regarding veth name if \`\-\-network\-veth\` is used · Issue \#10721 · systemd/systemd](https://github.com/systemd/systemd/issues/10721)

## コンテナ構築編

### コンテナイメージをインストールする

systemd-nspawn では、コンテナイメージをインストールする方法は主に3種類くらいあるようです。

1. yum install --installroot でインストールする。chroot環境を作るのと似た感じ。
2. ビルド済みのイメージを使う。Dockerのイメージも使えるようだ。
3. すでに作成済みのコンテナからコピーする。

ひとまずオーソドックスな1.でやってみることにします。3.はあとからやります。

ZFSで作成した領域に最低限のパッケージをインストールします。

```
$ sudo yum -y --nogpg --releasever=7 --installroot=/var/lib/machines/es11 install systemd systemd-networkd passwd yum vim-minimal
```

lsで見ると、OSのイメージぽく見えます。

```
$ ls -l /var/lib/machines/es11/
total 17
lrwxrwxrwx.  1 root root   7 Apr  6 23:24 bin -> usr/bin
dr-xr-xr-x.  2 root root   2 Apr 11  2018 boot
drwxr-xr-x.  2 root root   3 Apr  6 23:24 dev
drwxr-xr-x. 47 root root 113 Apr  6 23:25 etc
drwxr-xr-x.  2 root root   2 Apr 11  2018 home
lrwxrwxrwx.  1 root root   7 Apr  6 23:24 lib -> usr/lib
lrwxrwxrwx.  1 root root   9 Apr  6 23:24 lib64 -> usr/lib64
drwxr-xr-x.  2 root root   2 Apr 11  2018 media
drwxr-xr-x.  2 root root   2 Apr 11  2018 mnt
drwxr-xr-x.  2 root root   2 Apr 11  2018 opt
dr-xr-xr-x.  2 root root   2 Apr 11  2018 proc
dr-xr-x---.  2 root root   2 Apr 11  2018 root
drwxr-xr-x. 12 root root  12 Apr  6 23:25 run
lrwxrwxrwx.  1 root root   8 Apr  6 23:24 sbin -> usr/sbin
drwxr-xr-x.  2 root root   2 Apr 11  2018 srv
dr-xr-xr-x.  2 root root   2 Apr 11  2018 sys
drwxrwxrwt.  7 root root   7 Apr  6 23:25 tmp
drwxr-xr-x. 13 root root  14 Apr  6 23:24 usr
drwxr-xr-x. 18 root root  21 Apr  6 23:24 var
```

ちなみに、圧縮率を見てみると2倍以上になっていてなかなかいい感じです。lz4は圧縮率と速度のバランスが良く、ZFSを使うならおすすめです。

```
$ zfs get compressratio tank/machines/es11
NAME                PROPERTY       VALUE  SOURCE
tank/machines/es11  compressratio  2.03x  -
```


### コンテナを起動する

systemd-nspawn -Dオプションで、イメージをインストールしたディレクトリを指定してコンテナを起動し、rootのパスワードを設定します。

```
$ sudo systemd-nspawn -D /var/lib/machines/es11
-bash-4.2# passwd
...
-bash-4.2# exit
```

SELinuxを適切に設定するか無効にしないとパスワード変更で↓のようなエラーになるようなのでご注意ください。私は恥ずかしながらSELinuxをまともに使ったことがありません。

```
passwd: Authentication token manipulation error
```

コンテナをデーモンモードで起動します。

```
$ sudo systemd-nspawn -b -D /var/lib/machines/es11
```

起動シーケンスが走って、ログインプロンプトが出てくるのでrootでログインします。

```
Spawning container es11 on /var/lib/machines/es11.
Press ^] three times within 1s to kill container.
systemd 219 running in system mode. (+PAM +AUDIT +SELINUX +IMA -APPARMOR +SMACK +SYSVINIT +UTMP +LIBCRYPTSETUP +GCRYPT +GNUTLS +ACL +XZ +LZ4 -SECCOMP +BLKID +ELFUTILS +KMOD +IDN)
Detected virtualization systemd-nspawn.
Detected architecture x86-64.

Welcome to CentOS Linux 7 (Core)!

...
CentOS Linux 7 (Core)
Kernel 3.10.0-957.el7.x86_64 on an x86_64

es11 login: root
Password:
Last login: Sat Apr  6 23:43:37 on console
```

ps -ef で見ると、最低限のプロセスが起動しているのがわかります。

```
container# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  1 23:45 ?        00:00:00 /usr/lib/systemd/systemd
root        14     1  0 23:45 ?        00:00:00 /usr/lib/systemd/systemd-journald
dbus        19     1  0 23:45 ?        00:00:00 /usr/bin/dbus-daemon --system --address=systemd: --nofork --nopidfil
root        21     1  0 23:45 ?        00:00:00 /usr/lib/systemd/systemd-logind
root        23     1  0 23:45 ?        00:00:00 login -- root
root        25    23  0 23:45 console  00:00:00 -bash
root        37    25  0 23:45 console  00:00:00 ps -ef
```

デフォルトでは、ホストとネットワークを共有しています。

```
container# networkctl status
●      State: n/a
     Address: 192.168.0.10 on br0
     Gateway: 192.168.0.254 (JStream Technologies Inc.) on br0
```

Ctrl+] を3回入力するとコンテナが終了します。

```
Container es11 terminated by signal KILL.
```


ネットワークの設定を追加します。コンテナの中身はただのディレクトリなので、ホスト側からも見えます。

```
$ cd /var/lib/machines/es11
$ cd etc/systemd
$ sudo mkdir network
$ cd network
```

systemd-nspawnではhost0という仮想NICが自動的に作られるので、以下のように設定ファイルを作成します。

```
$ sudo vi host0.network
----
[Match]
Name=host0

[Network]
Address=192.168.0.11/24
Gateway=192.168.0.254
DNS=192.168.0.254
```

今度は、--network-bridge オプションを追加して起動します。

```
$ sudo systemd-nspawn -b -D /var/lib/machines/es11 --network-bridge=br0
```

host0にIPアドレスが設定されていることがわかります。これで、コンテナに独自のIPアドレスを割り当てて外部と通信できるようになりました。

```
container# networkctl status
●      State: routable
     Address: 192.168.0.11 on host0
     Gateway: 192.168.0.254 on host0
         DNS: 192.168.0.254
```

machinectl login でrootでログインできるように、/etc/securettyにpts/0を追記しておきます。

```
container# echo pts/0 >> /etc/securetty
```

いったん Ctrl+] を3回入力してコンテナを終了します。

```
Container es11 terminated by signal KILL.
```

ここからは、machinectlコマンドでコンテナを操作します。

ネットワークブリッジを使うように起動オプションを書き換えます。また、network.targetより後に起動するようにします。

```
$ sudo vi /usr/lib/systemd/system/systemd-nspawn\@.service
> After=network.target
...
< ExecStart=/usr/bin/systemd-nspawn --quiet --keep-unit --boot --link-journal=try-guest --network-veth --machine=%I
---
> ExecStart=/usr/bin/systemd-nspawn --quiet --keep-unit --boot --link-journal=try-guest --network-bridge=br0 --machine=%I
```

machines.targetを有効にします。

```
$ systemctl enable machines.target
```

machinectl enable でコンテナが自動起動するようにします。

```
$ sudo machinectl enable es11
Created symlink from /etc/systemd/system/machines.target.wants/systemd-nspawn@es11.service to /usr/lib/systemd/system/systemd-nspawn@.service.
```

machinectl start コマンドで、コンテナを起動できます。

```
$ sudo machinectl start es11
```

machinectl login コマンドで、コンテナにログインできます。

```
$ sudo machinectl login es11
```

ここまでで、コンテナを仮想サーバと同じような感覚で扱えるようになりました。

systemd-nspawn の概要は、例によって Arch Linux のページがわかりやすいと思います。

[systemd\-nspawn \- ArchWiki](https://wiki.archlinux.jp/index.php/Systemd-nspawn)


### Elasticsearchをインストールする

ホスト側で、vm.max_map_countの値を増やしておきます。

```
$ sudo vi /etc/sysctl.d/10-vm.conf
---
vm.max_map_count=262144

$ sudo sysctl -p
```

Elasticsearchのyumリポジトリを追加します。

```
container# cd /etc/yum.repos.d
container# vi elasticsearch.repo
---
[elasticsearch-6.x]
name=Elasticsearch repository for 6.x packages
baseurl=https://artifacts.elastic.co/packages/6.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=1
autorefresh=1
type=rpm-md
```

Javaをインストールします。

```
container# yum install java-11-openjdk
```

Elasticsearchをインストールします。

```
container# yum install elasticsearch
```

設定を変更します。

```
container# cd /etc/elasticsearch
container# vi elasticsearch.yml
---
...
node.name: ${HOSTNAME}

network.host: 0.0.0.0

discovery.zen.ping.unicast.hosts: ["192.168.0.11", "192.168.0.12","192.168.0.21", "192.168.0.22"]
```

本番で使うなら Shard allocation awareness も考慮した方がよいと思います。

[Shard allocation awareness \| Elasticsearch Reference \[master\] \| Elastic](https://www.elastic.co/guide/en/elasticsearch/reference/master/allocation-awareness.html)

サービスを有効にします。

```
container# systemctl daemon-reload
container# systemctl enable elasticsearch.service
container# systemctl start elasticsearch.service
```

動作確認してみます。ひとまず単一ノードで無事に起動しているようです。

```
container# curl http://localhost:9200/
{
  "name" : "es11",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "Rnvu_GRyQoapvhnodWHb4A",
  "version" : {
    "number" : "6.7.1",
    "build_flavor" : "default",
    "build_type" : "rpm",
    "build_hash" : "2f32220",
    "build_date" : "2019-04-02T15:59:27.961366Z",
    "build_snapshot" : false,
    "lucene_version" : "7.7.0",
    "minimum_wire_compatibility_version" : "5.6.0",
    "minimum_index_compatibility_version" : "5.0.0"
  },
  "tagline" : "You Know, for Search"
}
```

### コンテナをコピーする

いったんコンテナを停止します。

```
$ sudo machinectl poweroff es11
```

machinectlコマンドには、export-tarというオプションがあるようなのですが、CentOS 7 に付属のものはバージョンが古いのかそのオプションがありません。

[machinectl\(1\) — Arch manual pages](https://jlk.fjfi.cvut.cz/arch/manpages/man/machinectl.1.html)


仕方がないので、rsyncでコピーすることにします。

```
$ cd /var/lib/machines/
$ sudo rsync -av es11/ es12
...
```

Node IDが重複しないように消しておきます。

```
container# rm -rf /var/lib/elasticsearch/nodes/0/
```

ネットワークの設定を書き換えます。

```
$ cd es12
$ cd etc/systemd/network
$ sudo vi host0.network
----
[Match]
Name=host0

[Network]
Address=192.168.0.12/24
Gateway=192.168.0.254
DNS=192.168.0.254
```

machinectl enable でコンテナが自動起動するようにします。

```
$ sudo machinectl enable es12
Created symlink from /etc/systemd/system/machines.target.wants/systemd-nspawn@es12.service to /usr/lib/systemd/system/systemd-nspawn@.service.
```

machinectl start コマンドで、コンテナを起動します。

```
$ sudo machinectl start es11
$ sudo machinectl start es12
```

2台でクラスタ構成を組めていることを確認できました。

```
$ curl http://192.168.0.11:9200/_cat/health?v
epoch      timestamp cluster       status node.total node.data shards pri relo init unassign pending_tasks max_task_wait_time active_shards_percent
1554574589 18:16:29  elasticsearch green           2         2      0   0    0    0        0             0                  -                100.0%
```


## ホストの追加

もう一式ホストを追加します。私はVirtualBoxのクローン機能を使って作りました。以下のあたりに気をつければ良いと思います。

- ホスト
  - ホスト名変更: hostnamectl set-hostname ...
  - machine-id変更: rm /etc/machine-id; systemd-machine-id-setup
  - IPアドレス変更: /etc/systemd/network/ 以下
  - ブリッジのMACアドレス変更
  - zpoolのimport: zpool import -f tank
- コンテナ
  - コンテナ名変更: zfs rename
  - IPアドレス変更
  - ElasticseachのID情報削除


クラスタの状態を確認すると、無事に4台でクラスタ構成を組めていることを確認できました。

```
$ curl http://192.168.0.11:9200/_cat/health?v
epoch      timestamp cluster       status node.total node.data shards pri relo init unassign pending_tasks max_task_wait_time active_shards_percent
1554575745 18:35:45  elasticsearch green           4         4      0   0    0    0        0             0                  -                100.0%
```


# まとめ

Linuxコンテナ（systemd-nspawn）でホストまたぎのElasticsearchクラスタを構築してみました。 Docker+Kubernetes よりはるかにシンプルにできたと思います。
今回は私の趣味で ZFS on Linux を使っていますが、ほぼOS標準の機能だけでもできるので、運用の観点でも安心感があるのではないでしょうか。

今回の検証で、Linuxコンテナを完全に理解しました（※チュートリアルを終えたレベルという意味）。なお、Solarisコンテナについては何もわかりません（※10年以上キャリアグレードの商用環境で使いまくったという意味）。