TL; DR
- ECK やるには PersistentVolume が必要だけどオンプレ K8s だとこれがツラい
- 他のサーバからどうアクセスさせるか(External IP )もちょっとツラい
- よく考えたら外部公開の予定も無いのに ECK 頑張る意味はあまりなかった
「頑張ってなんとかしてみたけど、頑張ってみた結果頑張る意味無かったな、と気づいた」という内容のポエムです。
背景
今構築されているオンプレ Elasticsearch は ansible-elasticsearch で構築されたもののようだが、担当者がこれといった資料も残さないで退職してしまい、正直どうやって更新したものかわからなくなっている。
加えて、ansible-elasticsearch 自体も更新を止め、7.17 で打ち止めになってしまったため、今後のことを考えると頑張ってこれの運用を続けるメリットも無い。
これは困ったなぁ、どうするかなぁと悩み、Elasticsearch 本家をググったら Elastic Cloud on Kubernetes (ECK) なんてものがあるではないか。
ちょうど Kubernetes クラスタは別件で用意してあったし、これからは Kubernetes で Cloud Native の時代や!と軽い気持ちで ECK の導入に踏み切った。
それが茨の道とも知らずに・・・。
最初のつまづき、PersistentVolume
公式の Quickstart や先達の書いた Qiita を参考にすればとりあえず動くと思っていた。
(実際、データの永続性がなくてよければ Quickstart ですぐに動く。なぞるだけなのでここではあえて書かない。)
が、当然だが PersistentVolume を用意しないとデータの永続性がなくなってしまう。
AWS などを利用しているのなら gp2 とか使えば簡単に実現できるようだが、今回のターゲットはオンプレで構築した Kubernetes クラスタなのでそうもいかない。
色々調べてみると glusterfs が PersistentVolume として使えるらしい。
別件で GlusterFS の運用実績はあったため、これ幸いと飛びつき、余っている VM リソースを使って GlusterFS Volume を構築し、試すことにした。
※この記事を書いている時点では、Kubernetes v1.25 から glusterfs は deprecated であることが明記されているが、調査中は書いてなかった・・・はず
Volume と PersistentVolume の違いに挫折
いくつかのつまづきはあったが、Kubernetes node 全てに GlusterFS node の hosts を記載し、glusterfs-client をインストールしておくことで Pod も Gluster Volume をマウントできるようになることを知った。
最終的に↓のような感じで Volume をマウント、書き込みができることまで確認した。
pod の作成
# 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
書き込みのテスト
# kc exec -it glusterfs -- /bin/sh
# ls /mnt/glusterfs
# echo "hoge" >> /mnt/glusterfs/hoge
# cat /mnt/glusterfs/hoge
hoge
#
Gluster Server 側で書き込まれた内容の確認
$ ls /var/spool/kubevol/
hoge
$ cat /var/spool/kubevol/hoge
hoge
どうやらちゃんとマウントされて書き込まれているようだ。
Pod を削除してもデータが残るか確認。
# kc delete pod glusterfs
pod "glusterfs" deleted
$ cat /var/spool/kubevol/hoge
hoge
問題なさそう。
公式に沿って ECK を deploy した後に、Elasticsearch Cluster を作ってみた。
※ ここで PersistentVolume を理解せず、pod の volume を使っていることが誤り
# 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 になった。
kubectl get elasticsearch -n testeck
NAME HEALTH NODES VERSION PHASE AGE
ecksandbox green 6 7.17.5 Ready 109s
先達の与えてくれた知恵に沿ってデータ投入などしてみて、動いてそうだった。
# 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 を見ても何も書き込まれていなかった。
# 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) じゃないとデータを永続化できないことを知る。(先達もちゃんと書いてくれているが、理解できていなかった)
仕方ないので、GlusterFS を PersistentVolume として扱う方法を調べたが、どう頑張っても Heketi を使って REST API のインターフェースを噛ませる方法しか出てこなかった。
(詳しく調べられたわけではないが、恐らく glusterfs の provisioner が Heketi を使うように作られているのだと思う。)
しかし、肝心の Heketi はもう critical な bugfix しか行わない事が明記されていたため、GlusterFS をこのまま PersistentVolume として扱うことは諦めた。
なんとか Gluster を PersistentVolume として使えないか試行錯誤
Heketi は諦めたが、NFS Ganesha を使って GlusterFS をエクスポートすれば、NFS の PersistentVolume として扱えるのではないか?と考え、色々試行錯誤した。
が、どうにもこうにも HA 構成の Ganesha を構築するハードルが高すぎて諦めてしまった。
(ログを貼り付けるだけで長くなるので割愛、きっと Ganesha/Gluster の構成は RHEL の素敵なストレージを使う人向けのものなんだ・・・。)
もう既にこのあたりで思考が迷走しているのだが、紆余曲折を経て「各 Kubernetes node に Gluster volume をマウントしたうえで local StorageClass を使って PV を作れば永続化できるではないか」と閃いた。(全然ひらめきでもなんでもないが・・・。)
ここでもまた、PV と PVC は 1:1 の関係にないといけないらしい、といった数々の失敗を乗り越えて
- Kubernetes node 1 つにつき Gluster Volume 1 つをマウント
- node 毎のマウントパスは同じにする
- PV を Kubernetes node 分それぞれ作る
という荒業で PV を作ることに成功した。
各 Kubernetes が Gluster Volume をマウントしている状況(のイメージ)
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
# 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 の構成ファイル
# 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 使えばいいよ、みたいなことが書かれているのでやってみた。
が、pending から進まない。
# 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 を明記することで解決した。
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 クラスタ全体を停止 に追い込んでしまった。
本番環境でやったら泣いちゃうどころじゃ済まない致命的なミスを検証環境でやれておいて本当によかった。
最終的にこのような形に。
# 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 に強かったりネットワークに強かったりオンプレに強かったり目的を見失わずに仕事ができるインフラエンジニアを強く求めています。
インフラエンジニアじゃなくてもぜひ一度覗いてみてください。