社内オンプレ Kubernetes の条件になんとかマッチする ECK を構築したが運用は止めた話

# TL; DR

- ECK やるには PersistentVolume が必要だけどオンプレ K8s だとこれがツラい
    - 他のサーバからどうアクセスさせるか（External IP ）もちょっとツラい
- よく考えたら外部公開の予定も無いのに ECK 頑張る意味はあまりなかった

「頑張ってなんとかしてみたけど、頑張ってみた結果頑張る意味無かったな、と気づいた」という内容のポエムです。


# 背景

今構築されているオンプレ Elasticsearch は [ansible-elasticsearch](https://github.com/elastic/ansible-elasticsearch) で構築されたもののようだが、担当者がこれといった資料も残さないで退職してしまい、正直どうやって更新したものかわからなくなっている。
加えて、ansible-elasticsearch 自体も更新を止め、7.17 で打ち止めになってしまったため、今後のことを考えると頑張ってこれの運用を続けるメリットも無い。
これは困ったなぁ、どうするかなぁと悩み、Elasticsearch 本家をググったら [Elastic Cloud on Kubernetes (ECK)](https://www.elastic.co/jp/elastic-cloud-kubernetes) なんてものがあるではないか。
ちょうど Kubernetes クラスタは別件で用意してあったし、これからは Kubernetes で Cloud Native の時代や！と軽い気持ちで ECK の導入に踏み切った。
それが茨の道とも知らずに・・・。


# 最初のつまづき、PersistentVolume

[公式の Quickstart](https://www.elastic.co/guide/en/cloud-on-k8s/current/k8s-quickstart.html) や[先達の書いた Qiita](https://qiita.com/yokawasa/items/31742bb9b6a1898fb450) を参考にすればとりあえず動くと思っていた。
（実際、データの永続性がなくてよければ Quickstart ですぐに動く。なぞるだけなのでここではあえて書かない。）
が、当然だが PersistentVolume を用意しないとデータの永続性がなくなってしまう。

AWS などを利用しているのなら gp2 とか使えば簡単に実現できるようだが、今回のターゲットはオンプレで構築した Kubernetes クラスタなのでそうもいかない。
色々調べてみると [glusterfs が PersistentVolume として使える](https://kubernetes.io/docs/concepts/storage/volumes/#glusterfs)らしい。
別件で GlusterFS の運用実績はあったため、これ幸いと飛びつき、余っている VM リソースを使って GlusterFS Volume を構築し、試すことにした。
※この記事を書いている時点では、Kubernetes v1.25 から glusterfs は deprecated であることが明記されているが、調査中は書いてなかった・・・はず

## Volume と PersistentVolume の違いに挫折

いくつかのつまづきはあったが、Kubernetes node 全てに GlusterFS node の hosts を記載し、glusterfs-client をインストールしておくことで Pod も Gluster Volume をマウントできるようになることを知った。

最終的に↓のような感じで Volume をマウント、書き込みができることまで確認した。

pod の作成

```bash:k8s-master
# cat glusterfs-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: glusterfs
spec:
  containers:
  - name: glusterfs
    image: nginx
    volumeMounts:
    - mountPath: "/mnt/glusterfs"
      name: glusterfsvol
  volumes:
  - name: glusterfsvol
    glusterfs:
      endpoints: glusterfs-cluster
      path: kubevol
      readOnly: false
# kc create -f glusterfs-pod.yaml
pod/glusterfs created
```

書き込みのテスト

```bash:k8s-master
# kc exec -it glusterfs -- /bin/sh
# ls /mnt/glusterfs
# echo "hoge" >> /mnt/glusterfs/hoge
# cat /mnt/glusterfs/hoge
hoge
#
```

Gluster Server 側で書き込まれた内容の確認

```bash:gluster-server
$ ls /var/spool/kubevol/
hoge
$ cat /var/spool/kubevol/hoge
hoge
```

どうやらちゃんとマウントされて書き込まれているようだ。
Pod を削除してもデータが残るか確認。

```bash:k8s-master
# kc delete pod glusterfs
pod "glusterfs" deleted
```

```bash:gluster-server
$ cat /var/spool/kubevol/hoge
hoge
```

問題なさそう。
[公式に沿って ECK を deploy](https://www.elastic.co/guide/en/cloud-on-k8s/current/k8s-deploy-eck.html) した後に、Elasticsearch Cluster を作ってみた。
**※ ここで PersistentVolume を理解せず、pod の volume を使っていることが誤り**

```bash:k8s-master
# cat eck.yaml
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: ecksandbox
spec:
  version: 7.17.5
  nodeSets:
  - name: master-nodes
    count: 3
    config:
      node.store.allow_mmap: false
      node.roles: ["master"]
    podTemplate:
      spec:
        volumes:
        - name: elasticsearch-data
          glusterfs:
            endpoints: glusterfs-cluster
            path: kubevol
            readOnly: false
  - name: data-nodes
    count: 3
    config:
      node.store.allow_mmap: false
      node.roles: ["data", "ingest"]
    podTemplate:
      spec:
        volumes:
        - name: elasticsearch-data
          glusterfs:
            endpoints: glusterfs-cluster
            path: kubevol
            readOnly: false
# kc apply -f eck.yaml -n testeck
elasticsearch.elasticsearch.k8s.elastic.co/ecksandbox created
```

しばらく待つと green になった。

```bash:k8s-master
 kubectl get elasticsearch -n testeck
NAME         HEALTH   NODES   VERSION   PHASE   AGE
ecksandbox   green    6       7.17.5    Ready   109s
```

[先達の与えてくれた知恵](https://qiita.com/yokawasa/items/31742bb9b6a1898fb450#elasticsearch%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%BF%E3%81%AB%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9)に沿ってデータ投入などしてみて、動いてそうだった。

```bash:k8s-servers
# curl -u "elastic:$PASSWORD" -k -H "Content-Type: application/json" -XPOST "https://localhost:9200/bank/_bulk?pretty&refresh" --data-binary "@accounts.json"
{
  "took" : 2050,
  "errors" : false,
  "items" : [
    {
（略）
        "_seq_no" : 999,
        "_primary_term" : 1,
        "status" : 201
      }
    }
  ]
}
```

しかし、Gluster を見ても何も書き込まれていなかった。

```bash:gluster-server
# ls -la /var/spool/kubevol/
合計 16
drwxr-xr-x    4 root root  107  7月 15 18:12 .
drwxr-xr-x.   9 root root  102  7月 15 16:09 ..
drw-------  262 root root 8192  7月 15 16:11 .glusterfs
drwxr-xr-x    2 root root    6  7月 15 16:10 .glusterfs-anonymous-inode-90bb008b-f2b5-43f6-9f74-b3d088d48266
-rw-r--r--    2 root root    5  7月 15 18:12 hoge
```

この後、Pod を削除したらデータが消えることを確認したり色々と無駄な調査をしたが、ECK では StatefulSet を使う都合上、PersistentVolume (PersistentVolumeClaim) じゃないとデータを永続化できないことを知る。[（先達もちゃんと書いてくれているが、理解できていなかった）](https://qiita.com/yokawasa/items/31742bb9b6a1898fb450#%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%881-statefulset%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E5%90%84pod%E3%81%B8%E3%81%AE%E5%80%8B%E5%88%A5%E3%81%AEpvcpv%E3%81%AE%E5%89%B2%E5%BD%93%E3%81%A6)

仕方ないので、GlusterFS を PersistentVolume として扱う方法を調べたが、どう頑張っても Heketi を使って REST API のインターフェースを噛ませる方法しか出てこなかった。
（詳しく調べられたわけではないが、恐らく glusterfs の provisioner が Heketi を使うように作られているのだと思う。）
[しかし、肝心の Heketi はもう critical な bugfix しか行わない事が明記されていた](https://github.com/heketi/heketi)ため、GlusterFS をこのまま PersistentVolume として扱うことは諦めた。

## なんとか Gluster を PersistentVolume として使えないか試行錯誤

Heketi は諦めたが、NFS Ganesha を使って GlusterFS をエクスポートすれば、NFS の PersistentVolume として扱えるのではないか？と考え、色々試行錯誤した。
が、どうにもこうにも HA 構成の Ganesha を構築するハードルが高すぎて諦めてしまった。
（ログを貼り付けるだけで長くなるので割愛、きっと Ganesha/Gluster の構成は RHEL の素敵なストレージを使う人向けのものなんだ・・・。）

もう既にこのあたりで思考が迷走しているのだが、紆余曲折を経て「各 Kubernetes node に Gluster volume をマウントしたうえで [local StorageClass](https://kubernetes.io/docs/concepts/storage/volumes/#local) を使って PV を作れば永続化できるではないか」と閃いた。（全然ひらめきでもなんでもないが・・・。）

ここでもまた、[PV と PVC は 1:1 の関係にないといけないらしい](https://stackoverflow.com/questions/60988966/local-persistentvolumeclaim-says-no-volume-plugin-matched)、といった数々の失敗を乗り越えて

- Kubernetes node 1 つにつき Gluster Volume 1 つをマウント
    - node 毎のマウントパスは同じにする
- PV を Kubernetes node 分それぞれ作る

という荒業で PV を作ることに成功した。

各 Kubernetes が Gluster Volume をマウントしている状況（のイメージ）

```bash
k8s-server-1 | CHANGED | rc=0 >>
ファイルシス        1K-ブロック    使用    使用可 使用% マウント位置
gluster-server:/kubevol001   132485936 5455620 127030316    5% /var/spool/kubevol
k8s-server-2 | CHANGED | rc=0 >>
ファイルシス        1K-ブロック    使用    使用可 使用% マウント位置
gluster-server:/kubevol002   132485936 5455620 127030316    5% /var/spool/kubevol
k8s-server-3 | CHANGED | rc=0 >>
ファイルシス        1K-ブロック    使用    使用可 使用% マウント位置
gluster-server:/kubevol003   132485936 5455620 127030316    5% /var/spool/kubevol
k8s-server-4 | CHANGED | rc=0 >>
ファイルシス        1K-ブロック    使用    使用可 使用% マウント位置
gluster-server:/kubevol004   132485936 5455620 127030316    5% /var/spool/kubevol
k8s-server-5 | CHANGED | rc=0 >>
ファイルシス        1K-ブロック    使用    使用可 使用% マウント位置
gluster-server:/kubevol005   132485936 5455620 127030316    5% /var/spool/kubevol
```

無理やり作成された PV

```bash:k8s-master
# cat sc-lv.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: kubevol
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
# cat pv-lv.yaml
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: kubevol-pv1
spec:
  capacity:
    storage: 10Gi
  volumeMode: Filesystem
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: kubevol
  local:
    path: /var/spool/kubevol
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - k8s-server-1
（以下略）

# kc get pv
NAME          CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                                                     STORAGECLASS   REASON   AGE
kubevol-pv1   10Gi       RWO            Retain           Bound    testeck/elasticsearch-data-ecksandbox-es-master-nodes-2   kubevol                 4h49m
kubevol-pv2   10Gi       RWO            Retain           Bound    testeck/elasticsearch-data-ecksandbox-es-master-nodes-1   kubevol                 4h49m
kubevol-pv3   10Gi       RWO            Retain           Bound    testeck/elasticsearch-data-ecksandbox-es-master-nodes-0   kubevol                 4h49m
kubevol-pv4   10Gi       RWO            Retain           Bound    testeck/elasticsearch-data-ecksandbox-es-data-nodes-0     kubevol                 4h49m
kubevol-pv5   10Gi       RWO            Retain           Bound    testeck/elasticsearch-data-ecksandbox-es-data-nodes-1     kubevol                 4h49m
```

PersistentVolume として local を使うように修正された Elasticsearch cluster の構成ファイル

```bash:k8s-master
# cat old_eck.yaml
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: ecksandbox
spec:
  version: 7.17.4
  nodeSets:
  - name: master-nodes
    count: 3
    podTemplate:
      spec:
        affinity:
          nodeAffinity:
            requiredDuringSchedulingIgnoredDuringExecution:
              nodeSelectorTerms:
              - matchExpressions:
                - key: kubernetes.io/hostname
                  operator: In
                  values:
                  - k8s-server-1
                  - k8s-server-2
                  - k8s-server-3
    config:
      node.store.allow_mmap: false
      node.roles: ["master"]
    volumeClaimTemplates:
    - metadata:
        name: elasticsearch-data
      spec:
        accessModes:
        - ReadWriteOnce
        resources:
          requests:
            storage: 5Gi
        storageClassName: kubevol
  - name: data-nodes
    count: 2
    podTemplate:
      spec:
        affinity:
          nodeAffinity:
            requiredDuringSchedulingIgnoredDuringExecution:
              nodeSelectorTerms:
              - matchExpressions:
                - key: kubernetes.io/hostname
                  operator: In
                  values:
                  - k8s-server-4
                  - k8s-server-5
    config:
      node.store.allow_mmap: false
      node.roles: ["data", "ingest"]
    volumeClaimTemplates:
    - metadata:
        name: elasticsearch-data
      spec:
        accessModes:
        - ReadWriteOnce
        resources:
          requests:
            storage: 5Gi
        storageClassName: kubevol
```

こうすることで、Elasticsearch Cluster を動かすことができた。

```
# kc get elasticsearch -n testeck
NAME         HEALTH   NODES   VERSION   PHASE   AGE
ecksandbox   green    5       7.17.4    Ready   3h32m
```

### 余談

local 系の PersistentVolume についてはこちらの Qiita が大変参考になりました。
https://qiita.com/ysakashita/items/67a452e76260b1211920

# 次のつまづき、External IP

ここまでで Elasticsearch 自体は動かせたが、まだこのままではポートフォワードして localhost でアクセスするしかなく、他のサーバから Elasticsearch にアクセスができない。
Service に対して ingress-nginx 使うという手もあるが、今回の要件としては特に外部公開をする意味はないので、LAN 内からアクセスできればそれでいい。

さくっとググると、[公式では LoadBalancer type 使えばいいよ](https://www.elastic.co/guide/en/cloud-on-k8s/master/k8s-services.html)、みたいなことが書かれているのでやってみた。
が、pending から進まない。

```bash:k8s-master
# kc get all -n testeck
NAME                               READY   STATUS    RESTARTS   AGE
pod/ecksandbox-es-data-nodes-0     1/1     Running   0          63m
pod/ecksandbox-es-data-nodes-1     1/1     Running   0          63m
pod/ecksandbox-es-master-nodes-0   1/1     Running   0          63m
pod/ecksandbox-es-master-nodes-1   1/1     Running   0          63m
pod/ecksandbox-es-master-nodes-2   1/1     Running   0          63m

NAME                                  TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
service/ecksandbox-es-data-nodes      ClusterIP      None           <none>        9200/TCP         63m
service/ecksandbox-es-http            LoadBalancer   10.233.7.95    <pending>     9200:31494/TCP   45m
service/ecksandbox-es-internal-http   ClusterIP      10.233.29.62   <none>        9200/TCP         63m
service/ecksandbox-es-master-nodes    ClusterIP      None           <none>        9200/TCP         63m
service/ecksandbox-es-transport       ClusterIP      None           <none>        9300/TCP         63m

NAME                                          READY   AGE
statefulset.apps/ecksandbox-es-data-nodes     2/2     63m
statefulset.apps/ecksandbox-es-master-nodes   3/3     63m
```

これについては externalIPs を明記することで解決した。

```yaml
  1 apiVersion: elasticsearch.k8s.elastic.co/v1
  2 kind: Elasticsearch
  3 metadata:
  4   name: ecksandbox
  5 spec:
  6   version: 7.17.4
  7   http:　#←このブロック追記
  8     service:
  9       spec:
 10         type: LoadBalancer
 11         externalIPs:
 12         - 192.168.1.3
 13         - 192.168.1.4
 14         - 192.168.1.5
```

なお、このときに最初 **誤って「K8s master node の management に利用されている IP」を指定してしまい、K8s クラスタ全体を停止** に追い込んでしまった。
本番環境でやったら泣いちゃうどころじゃ済まない致命的なミスを検証環境でやれておいて本当によかった。
最終的にこのような形に。

```bash:k8s-master
# kc get svc -n testeck
NAME                          TYPE           CLUSTER-IP      EXTERNAL-IP                                       PORT(S)          AGE
ecksandbox-es-data-nodes      ClusterIP      None            <none>                                            9200/TCP         34m
ecksandbox-es-http            LoadBalancer   10.233.62.129   192.168.1.3,192.168.1.4,192.168.1.115   9200:30748/TCP   16m
ecksandbox-es-internal-http   ClusterIP      10.233.57.6     <none>                                            9200/TCP         34m
ecksandbox-es-master-nodes    ClusterIP      None            <none>                                            9200/TCP         34m
ecksandbox-es-transport       ClusterIP      None            <none>                                            9300/TCP         16m
```


# ここまでやってきて気づいた

ECK であるなら、きっとこの状態から運用していっても簡単に upgrade できたり、簡単に外部公開することができるだろう。
しかし、求めていたものは「オンプレで、データが（ある程度）保全され、（そこそこの）安定性があり、LAN 内からアクセス可能な Elasticsearch クラスタ」であり、Kubernetes の上で動かなければいけない必要はないし、そのデータ保全のために無茶な構成にして運用を難しくしたりスケールしづらくしてしまっていては本末転倒である。
最初から「誰かの作った ansible は諦めて自分たち用の elasticsearch 構築 ansible タスクを作る」べきだったのである。

PersistentVolume や StatefulSet の勉強にはなったし、K8s の永続化ボリュームはやはり AWS などのほうが扱いやすい（が、それでもやはり分離しておいたほうがよさそうだけど）という学びを得たが、目的を見失うとすぐに無駄なことをしてしまうなぁと改めて思い知った事案だった。


# 最後に

会社がどういう方針かはあまり聞いてないですが、私個人としては Kubernetes に強かったりネットワークに強かったりオンプレに強かったり目的を見失わずに仕事ができるインフラエンジニアを強く求めています。
インフラエンジニアじゃなくてもぜひ一度覗いてみてください。

https://qiita.com/organizations/nekojarashi