K8s v1.24 の non-graceful node shutdown を kube-fencing で自動化する

※この記事は３部作の３番目です。前編は以下の２つです。
* [Kubespray+Vagrant で かんたん Kubernetes デプロイ](https://qiita.com/yosshy/items/9a6e10958f50e7e90266)
* [K8s v1.24 の non-graceful node shutdown を試す](https://qiita.com/yosshy/items/868d709e9e8d99634d90)

## はじめに

前回、K8s v1.24 上で障害ノードに `node.kubernetes.io/out-of-service` taint を**手動で**付与する事により StatefulSet の PV 付 Pod が他ノードに移動する事を確認しました。しかし、何故手動なのでしょうか？これではセルフヒーリングとは言えません。
これにはやむを得ない理由があります。例えば、ノードが NotReady になる理由は色々あります。

* ノードの電源が OFF になる
* ノードの OS がフリーズする
* ワーカーノード上の kubelet が止まる
* ワーカーノードとマスターノード間のネットワークが不通になる
* マスターノード上の apiserver のサーバ証明書の更新がワーカーノード上に反映されていない

ここに挙げたのはほんの一例で、他にも色々理由が考えられますが、これらの中には Pod が継続稼働可能なケースもあります。
そうすると、k8s 上でノードの状態が NotReady になっただけではワーカーノード上の Pod が本当に動作を停止したかどうか分かりません。**もしも障害ノード上で PV 付 Pod が稼働し続けており、PV 上のデータを更新し続けた場合、それを放置したまま別ノードで同じ PV をアタッチした代わりの Pod を作成すると、PV上のデータを破壊しかねません**。この事態を回避する為には、

1. Kubernetes クラスタ外部の運用監視システムが障害ノードを強制停止する仕組みを作る (あるいは運用者が手動で停止する）
2. Pacemaker の STONITH デバイスのように、障害ノードを Kubernetes 自身が強制停止（又はシステムから完全分離）する機能（Fencing）を実装する

のいずれかを行って**障害ノードを一度完全に停止し、PV の更新が絶対に起こらない状態になっている事を保証する**必要があります。
[KEP 2268: Non-graceful node shutdown](https://github.com/kubernetes/enhancements/tree/master/keps/sig-storage/2268-non-graceful-shutdown) では、Kubernetes 自体への Fencing 機能を実装せず、システム管理者または外部のシステムに任せる事にしました（＝上記1）。なお、KEP 中の代替案として [Node Fencing](https://github.com/kubernetes/enhancements/tree/master/keps/sig-storage/2268-non-graceful-shutdown#node-fencing) （＝上記2）が挙げられており、本 KEP でこのアプローチを取らなかった理由が書かれています。

> This approach seems too intrusive as a general in-tree solution. Since the scope of this KEP is narrowed down to handle a real node shutdown scenario, we do not need the node fencing approach. I think what has been proposed in this KEP also does not prevent us from supporting the node partitioning case in the future. We can reconsider this approach after the proposed solution is implemented.
> このアプローチは汎用の in-tree solution（訳注：Kubernetes 自体の機能）としては押し付けがましすぎるように見えます。本 KEP の対象範囲が実際のノードシャットダウンシナリオの取り扱いに絞られたため、ノードの fencing アプローチが必要なくなりました。私はまた、本KEPで提案された内容が将来のノード分断ケースの対応を妨げるとは思っていません。本提案ソリューションの実装後、我々は本アプローチを再検討できます。

そこで、Kubernetes の代わりに NotReady になったノードを Fencing する OSS を探したところ、以下のものを見つけました。

* [kube-fencing](https://github.com/kvaps/kube-fencing)
    * Pacemaker でも使われている [fence-agents](https://github.com/ClusterLabs/fence-agents) パッケージを使っています。物理サーバや各種ハイパーバイザ上の VM を強制的に停止・再起動できます。
* [node-fencing](https://github.com/rootfs/node-fencing)

個人的には kube-fencing が丁度良さそうに見えたので、[自分のアカウントに kube-fencing をフォークし](https://github.com/yosshy/kube-fencing)、[ノード強制停止後に `node.kubernetes.io/out-of-service` taint を付与する機能](https://github.com/yosshy/kube-fencing/commit/eb03b37935170e34de5d45d85dd25f51bba674db)を実装しました。

## セットアップ

まずは改造版 kube-fencing を取ってきます。
```
yosshy@nuc2:~$ git clone https://github.com/yosshy/kube-fencing.git
yosshy@nuc2:~$ cd kube-fencing
```

次に、kube-fencing 関連のリソースを作成するマニフェストを適用します。ネームスペースは `kube-fencing` になっていますので、必要なら helm で別のネームスペースを指定するなどして下さい。
```
yosshy@nuc2:~/kube-fencing$ kubectl apply -f deploy/kube-fencing.yaml
namespace/kube-fencing created
serviceaccount/fencing-controller created
serviceaccount/fencing-switcher created
clusterrole.rbac.authorization.k8s.io/fencing-controller created
clusterrole.rbac.authorization.k8s.io/fencing-switcher created
clusterrolebinding.rbac.authorization.k8s.io/fencing-controller created
clusterrolebinding.rbac.authorization.k8s.io/fencing-switcher created
role.rbac.authorization.k8s.io/fencing-controller created
rolebinding.rbac.authorization.k8s.io/fencing-controller created
daemonset.apps/fencing-switcher created
deployment.apps/fencing-controller created
yosshy@nuc2:~/kube-fencing$
```

kube-fencing-agent コンテナ用 PodTemplate マニフェスト `deploy/examples/virtualbox.yaml ` を見てみましょう。
```
apiVersion: v1
kind: PodTemplate
metadata:
  name: fencing
template:
  spec:
    restartPolicy: OnFailure
    containers:
    - name: fence
      image: ghcr.io/yosshy/kube-fencing/kube-fencing-agent:latest
      command: ["fence_vbox", "-a", "172.18.8.1", "-l", "$(FENCING_USER)", "-p", "$(FENCING_PASSWORD)", "-n", "$(FENCING_ID)", "-o", "off"]
      env:
      - name: FENCING_ID
        valueFrom:
          fieldRef:
            fieldPath: metadata.annotations['fencing/id']
      - name: FENCING_USER
        valueFrom:
          secretKeyRef:
            name: fencing-user
            key: username
      - name: FENCING_PASSWORD
        valueFrom:
          secretKeyRef:
            name: fencing-user
            key: password
```
kube-fencing では、Fencing Job コンテナ中に fence-agents パッケージ中の様々な fencing コマンドをインストールしてあり、そのうち１つをコンテナ起動時のコマンドラインで実行するようになっています。VirtualBox 用の本 PodTemplate では、fence_vbox コマンドを実行します。
また、ここではホストの IP アドレスを 172.18.8.1 で決め打ちしています。必要であれば適切な IP アドレスに変更して下さい。
Fencing Job コンテナからホストにリモートログインするためのアカウント名とパスワードは後述する secret で指定するようにしています。
ノード毎に異なる VM 名は、ノードの `fencing/id` annotation で指定するようにしています。
Fencing のモードは off（強制停止）にしてあります。ここを reboot にすれば VM は再起動しますが、Pod を全削除する前に障害ノードが再起動してきて Ready 状態になるのは安全性の観点から好ましくありませんので、ここでは一旦 off にしています（障害ノードの停止・再起動はシステムの障害復旧ポリシー次第です）。

kube-fencing-agent コンテナ用の PodTemplate を登録します。
```
yosshy@nuc2:~/kube-fencing$ kubectl -n kube-fencing apply -f deploy/examples/virtualbox.yaml 
podtemplate/fencing created
yosshy@nuc2:~/kube-fencing$ 
```

PodTemplate 内で使用する secret を設定します。
|secret名|説明|
|---|---|
|`username`|ホスト上で vagrant up を実行したユーザのアカウント名|
|`password`|同パスワード|
```
yosshy@nuc2:~/kube-fencing$ kubectl -n kube-fencing create secret generic fencing-user --from-literal='username=yosshy' --from-literal='password=P@ssw0rd'
secret/fencing-user created
yosshy@nuc2:~/kube-fencing$ 
```

ワーカーノード毎に kube-fencing 関連の annotation を登録していきます。
既存の annotation の説明は[こちら](https://github.com/kvaps/kube-fencing#configuration-parameters)にありますが、今回使用するものは以下のものです。

| annotation名 | 設定値 | 説明 |
|---|---|---|
|`fencing/enabled`|`true`|ノードを kube-fencing の対象にするかどうか。既定値 `true`|
|`fencing/id`|各ノードの VM 名|vboxmanage コマンドの引数に指定するVM名|
|`fencing/mode`|`taint`|ノード fencing 後の Pod, PV の扱い。|

これらを k8s-2, k8s-3 ノードに設定していきます。

```
yosshy@nuc2:~/kube-fencing$ vboxmanage list vms
"generic-ubuntu1804-virtualbox_1655701800174_67684" {7e7d8758-7362-41f7-9c04-21d09c879c44}
"kubespray_k8s-1_1665804234747_39746" {a3512cf1-2cd1-492b-9ec9-5b5bf517b583}
"kubespray_k8s-2_1665804309774_65270" {28725bfd-7387-4489-9b09-57c5b8522f10}
"kubespray_k8s-3_1665804382129_19421" {a9f75c2a-33b7-4a32-9b5e-f3a996ad3f2c}
yosshy@nuc2:~/kube-fencing$ kubectl annotate nodes k8s-2 fencing/id=kubespray_k8s-2_1665804309774_65270
node/k8s-2 annotated
yosshy@nuc2:~/kube-fencing$ kubectl annotate nodes k8s-2 fencing/mode=taint
node/k8s-2 annotated
yosshy@nuc2:~/kube-fencing$ kubectl annotate nodes k8s-3 fencing/id=kubespray_k8s-3_1665804382129_19421
node/k8s-3 annotated
yosshy@nuc2:~/kube-fencing$ kubectl annotate nodes k8s-3 fencing/mode=taint
node/k8s-3 annotated
yosshy@nuc2:~/kube-fencing$ 
```

これで準備完了です。

## 試してみる

前回はノードの疑似障害を VM pause で行っていましたが、今回は kube-fencing に VM off を させたいので、kubelet 停止で擬似障害としてみます。

```
yosshy@nuc2:~/kube-fencing$ cd ~/csi-driver-nfs
yosshy@nuc2:~/csi-driver-nfs$ kubectl create ns kep2268
namespace/kep2268 created
yosshy@nuc2:~/csi-driver-nfs$ kubectl -n kep2268 apply -f deploy/example/statefulset.yaml 
statefulset.apps/statefulset-nfs created
yosshy@nuc2:~/csi-driver-nfs$ kubectl get pods -n kep2268 -o wide 
NAME                READY   STATUS    RESTARTS   AGE   IP             NODE    NOMINATED NODE   READINESS GATES
statefulset-nfs-0   1/1     Running   0          64s   10.233.66.9    k8s-3   <none>           <none>
statefulset-nfs-1   1/1     Running   0          58s   10.233.65.14   k8s-2   <none>           <none>
statefulset-nfs-2   1/1     Running   0          51s   10.233.64.8    k8s-1   <none>           <none>
statefulset-nfs-3   1/1     Running   0          44s   10.233.66.10   k8s-3   <none>           <none>
statefulset-nfs-4   1/1     Running   0          38s   10.233.65.15   k8s-2   <none>           <none>
statefulset-nfs-5   1/1     Running   0          33s   10.233.64.9    k8s-1   <none>           <none>
statefulset-nfs-6   1/1     Running   0          27s   10.233.66.11   k8s-3   <none>           <none>
statefulset-nfs-7   1/1     Running   0          20s   10.233.65.16   k8s-2   <none>           <none>
yosshy@nuc2:~/csi-driver-nfs$ 
```

k8s-3 の kubelet を停止する事にします。
最初にノードの詳細情報を確認します。
```
yosshy@nuc2:~/csi-driver-nfs$ kubectl describe node k8s-3 
Name:               k8s-3
Roles:              <none>
Labels:             beta.kubernetes.io/arch=amd64
                    beta.kubernetes.io/os=linux
                    kubernetes.io/arch=amd64
                    kubernetes.io/hostname=k8s-3
                    kubernetes.io/os=linux
Annotations:        csi.volume.kubernetes.io/nodeid: {"nfs.csi.k8s.io":"k8s-3"}
                    fencing/enabled: true
                    fencing/id: kubespray_k8s-3_1665804382129_19421
                    fencing/mode: taint
                    flannel.alpha.coreos.com/backend-data: {"VNI":1,"VtepMAC":"aa:d2:74:8e:8f:7b"}
                    flannel.alpha.coreos.com/backend-type: vxlan
                    flannel.alpha.coreos.com/kube-subnet-manager: true
                    flannel.alpha.coreos.com/public-ip: 172.18.8.103
                    kubeadm.alpha.kubernetes.io/cri-socket: unix:////var/run/containerd/containerd.sock
                    node.alpha.kubernetes.io/ttl: 0
                    volumes.kubernetes.io/controller-managed-attach-detach: true
CreationTimestamp:  Sat, 15 Oct 2022 12:46:47 +0900
Taints:             <none>
（中略）
```
ターミナルをもう１つ開いて、そちらで k8s-3 ノードに SSH 接続し、kubelet サービスを停止します。
```
yosshy@nuc2:~$ cd kubespray
yosshy@nuc2:~/kubespray$ vagrant ssh k8s-3
Last login: Mon Oct 17 04:01:26 2022 from 10.0.2.2
-bash: warning: setlocale: LC_ALL: cannot change locale (ja_JP.UTF-8)
vagrant@k8s-3:~$ sudo systemctl stop kubelet
vagrant@k8s-3:~$ top
```

最初のターミナル上で k8s-3 ノードの状態変化をモニタします。
```
yosshy@nuc2:~/csi-driver-nfs$ kubectl get nodes -w
NAME    STATUS   ROLES           AGE   VERSION
k8s-1   Ready    control-plane   2d    v1.24.6
k8s-2   Ready    <none>          2d    v1.24.6
k8s-3   Ready    <none>          2d    v1.24.6
（中略）
k8s-3   NotReady   <none>          2d    v1.24.6
k8s-3   NotReady   <none>          2d    v1.24.6
（^c で中断）
```

ノードの状態が NotReady に変化後、すかさず k8s-3 の詳細情報をチェックします（下記）。

ノードが停止したことで `node.kubernetes.io/unreachable:NoExecute`, `node.kubernetes.io/unreachable:NoSchedule` taint が自動的に付与されています（従来の K8s の仕様。
また、kube-fencing-controller により `fencing/state: started` annotation が追加されました。これは PodTemplate から Fencing Job が作成された事を示しています。
```
yosshy@nuc2:~/csi-driver-nfs$ watch kubectl describe node k8s-3 
Every 2.0s: kubectl describe node k8s-3                                  nuc2: Mon Oct 17 13:03:47 2022

Name:               k8s-3
Roles:              <none>
Labels:             beta.kubernetes.io/arch=amd64
                    beta.kubernetes.io/os=linux
                    kubernetes.io/arch=amd64
                    kubernetes.io/hostname=k8s-3
                    kubernetes.io/os=linux
Annotations:        csi.volume.kubernetes.io/nodeid: {"nfs.csi.k8s.io":"k8s-3"}
                    fencing/enabled: true
                    fencing/id: kubespray_k8s-3_1665804382129_19421
                    fencing/mode: taint
                    fencing/state: started
                    flannel.alpha.coreos.com/backend-data: {"VNI":1,"VtepMAC":"02:eb:a3:af:b2:d1"}
                    flannel.alpha.coreos.com/backend-type: vxlan
                    flannel.alpha.coreos.com/kube-subnet-manager: true
                    flannel.alpha.coreos.com/public-ip: 172.18.8.103
                    kubeadm.alpha.kubernetes.io/cri-socket: unix:////var/run/containerd/containerd.sock
                    node.alpha.kubernetes.io/ttl: 0
                    volumes.kubernetes.io/controller-managed-attach-detach: true
CreationTimestamp:  Sat, 15 Oct 2022 12:46:47 +0900
Taints:             node.kubernetes.io/unreachable:NoExecute
                    node.kubernetes.io/unreachable:NoSchedule
Unschedulable:      false
Lease:
```

Fencing Job が正常終了すると、`fencing/state: fenced` に変化します（下記）。この時点で後から開いたターミナルの SSH ログインは終了してて元ホストに戻ってきています。
同時に、（改造された）kube-fencing-controller によって `node.kubernetes.io/out-of-service=nodeshutdown:NoExecute` taint が自動的に追加されています。
```
Name:               k8s-3
Roles:              <none>
Labels:             beta.kubernetes.io/arch=amd64
                    beta.kubernetes.io/os=linux
                    kubernetes.io/arch=amd64
                    kubernetes.io/hostname=k8s-3
                    kubernetes.io/os=linux
Annotations:        csi.volume.kubernetes.io/nodeid: {"nfs.csi.k8s.io":"k8s-3"}
                    fencing/enabled: true
                    fencing/id: kubespray_k8s-3_1665804382129_19421
                    fencing/mode: taint
                    fencing/state: fenced
                    flannel.alpha.coreos.com/backend-data: {"VNI":1,"VtepMAC":"02:eb:a3:af:b2:d1"}
                    flannel.alpha.coreos.com/backend-type: vxlan
                    flannel.alpha.coreos.com/kube-subnet-manager: true
                    flannel.alpha.coreos.com/public-ip: 172.18.8.103
                    kubeadm.alpha.kubernetes.io/cri-socket: unix:////var/run/containerd/containerd.sock
                    node.alpha.kubernetes.io/ttl: 0
                    volumes.kubernetes.io/controller-managed-attach-detach: true
CreationTimestamp:  Sat, 15 Oct 2022 12:46:47 +0900
Taints:             node.kubernetes.io/out-of-service=nodeshutdown:NoExecute
                    node.kubernetes.io/unreachable:NoExecute
                    node.kubernetes.io/unreachable:NoSchedule
Unschedulable:      false
Lease:
```

Pod が移動したかどうか確認しましょう。
```
（^c で中断）
yosshy@nuc2:~/csi-driver-nfs$ kubectl get pods -n kep2268 -o wide 
NAME                READY   STATUS    RESTARTS   AGE     IP             NODE    NOMINATED NODE   READINESS GATES
statefulset-nfs-0   1/1     Running   0          12s     10.233.64.10   k8s-1   <none>           <none>
statefulset-nfs-1   1/1     Running   0          5m53s   10.233.65.14   k8s-2   <none>           <none>
statefulset-nfs-2   1/1     Running   0          5m46s   10.233.64.8    k8s-1   <none>           <none>
statefulset-nfs-3   1/1     Running   0          9s      10.233.65.19   k8s-2   <none>           <none>
statefulset-nfs-4   1/1     Running   0          5m33s   10.233.65.15   k8s-2   <none>           <none>
statefulset-nfs-5   1/1     Running   0          5m28s   10.233.64.9    k8s-1   <none>           <none>
statefulset-nfs-6   1/1     Running   0          5s      10.233.64.11   k8s-1   <none>           <none>
statefulset-nfs-7   1/1     Running   0          5m15s   10.233.65.16   k8s-2   <none>           <none>
yosshy@nuc2:~/csi-driver-nfs$ 
```
期待通り k8s-3 上にあった Pod が別ノードに移動しています。

では、後始末しましょう。
```
yosshy@nuc2:~/csi-driver-nfs$ vboxmanage list vms
"generic-ubuntu1804-virtualbox_1655701800174_67684" {7e7d8758-7362-41f7-9c04-21d09c879c44}
"kubespray_k8s-1_1665804234747_39746" {a3512cf1-2cd1-492b-9ec9-5b5bf517b583}
"kubespray_k8s-2_1665804309774_65270" {28725bfd-7387-4489-9b09-57c5b8522f10}
"kubespray_k8s-3_1665804382129_19421" {a9f75c2a-33b7-4a32-9b5e-f3a996ad3f2c}
yosshy@nuc2:~/csi-driver-nfs$ vboxmanage startvm kubespray_k8s-3_1665804382129_19421 --type headless
Waiting for VM "kubespray_k8s-3_1665804382129_19421" to power on...
VM "kubespray_k8s-3_1665804382129_19421" has been successfully started.
yosshy@nuc2:~/csi-driver-nfs$ kubectl get nodes -w
NAME    STATUS     ROLES           AGE   VERSION
k8s-1   Ready      control-plane   2d    v1.24.6
k8s-2   Ready      <none>          2d    v1.24.6
k8s-3   NotReady   <none>          2d    v1.24.6
（中略）
k8s-3   Ready      <none>          2d    v1.24.6
k8s-3   Ready      <none>          2d    v1.24.6
（^C で中断）
yosshy@nuc2:~/csi-driver-nfs$ kubectl  taint nodes k8s-3 node.kubernetes.io/out-of-service=nodeshutdown:NoExecute-
node/k8s-3 untainted
yosshy@nuc2:~/csi-driver-nfs$ kubectl describe node k8s-3 
Name:               k8s-3
Roles:              <none>
Labels:             beta.kubernetes.io/arch=amd64
                    beta.kubernetes.io/os=linux
                    kubernetes.io/arch=amd64
                    kubernetes.io/hostname=k8s-3
                    kubernetes.io/os=linux
Annotations:        csi.volume.kubernetes.io/nodeid: {"nfs.csi.k8s.io":"k8s-3"}
                    fencing/enabled: true
                    fencing/id: kubespray_k8s-3_1665804382129_19421
                    fencing/mode: taint
                    flannel.alpha.coreos.com/backend-data: {"VNI":1,"VtepMAC":"aa:d2:74:8e:8f:7b"}
                    flannel.alpha.coreos.com/backend-type: vxlan
                    flannel.alpha.coreos.com/kube-subnet-manager: true
                    flannel.alpha.coreos.com/public-ip: 172.18.8.103
                    kubeadm.alpha.kubernetes.io/cri-socket: unix:////var/run/containerd/containerd.sock
                    node.alpha.kubernetes.io/ttl: 0
                    volumes.kubernetes.io/controller-managed-attach-detach: true
CreationTimestamp:  Sat, 15 Oct 2022 12:46:47 +0900
Taints:             <none>
（中略）
yosshy@nuc2:~/csi-driver-nfs$ kubectl delete ns kep2268
namespace "kep2268" deleted
yosshy@nuc2:~/csi-driver-nfs$ 
```
taint が全て削除され、kube-fencing が自動付与した `fencing/state` annotation も削除されました。これで元通りです。

## まとめ

今回、non-graceful node shutdown において管理者（または運用管理システム）の責任とされた

* NotReady になった障害ノードが電源 OFF である事を保証
* 電源 OFF の障害ノードに `node.kubernetes.io/out-of-service` taint を付与

する操作を、改造版の kube-fencing によって自動化可能である事を実証しました。
しかし、果たしてこの機能はどういうユースケースで有用なのでしょうか？

何らかの IaaS 上に Kubernetes クラスタが存在する場合、IaaS 側の機能として障害ノード VM の検出・削除・再作成を行うのは比較的簡単ですし、AWS EKS 等で既に実現されています。こうしたケースでは non-graceful node shutdown のアプローチは正直なところ必要ないでしょう。
一方、IaaS を用いないオンプレミス環境や、エッジコンピューティングのエッジ部分、組み込み用途など比較的小規模なサーバ環境上の Kubernetes では、こうした障害ノードの再作成は簡単ではありません。こうしたケースでは現状、kube-fencing のような add-on ソリューションを活用して Kubernetes クラスタの障害ノードのセルフヒーリングを実現する必要があります。
また、Kubernetes 自体に kube-fencing 相当の機能を実装し、Kubernetes 単体で完全なセルフヒーリングを実現する方向性もありえます。現在の non-graceful node shutdown はその最初の一歩と考える事もできるでしょう。