Medik8s + OpenShift Data Foundation で Ceph RBD の failover

## 概要
Rook-Ceph は、Kubernetes の Non-Graceful Node Shutdown 機能を前提とした failover の仕組みを備えている。前回の記事では、手動で障害の発生したノードに `out-of-service` Taints を付与し、StatefulSet (Pod + Ceph RBD) の failover が起こる様子を確認できた：

https://qiita.com/sawa2d2/items/41742227cfa4ad35a444

また、Medik8s と呼ばれる障害検知と Tainting を自動で行う OSS を併用することで [^medik8s]、先述のような障害発生時の Tainting も自動化する運用が考えられる。

本記事では、Medik8s と Rook-Ceph の組み合わせによる failover を検証し、ノードの電源を切るだけで StatefulSet (Pod + RBD PV) が自動的に別の正常な Node に移動する様子を確かめる。

## Failover のシナリオ
想定する Failover のシナリオは次の通り：

1. Ceph RBD の PV + Pod からなる StatefulSet を作成する
1. Node に障害を発生させる
1. Medik8s による Tainting を確認する
1. Kubernetes および Rook の作用で、Pod および RBD が終了することを確認する
1. 別ノードで Pod および RBD が再作成されることを確認する

![rook-failover.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/456b0d1f-a74f-8863-f074-4eb5d35e5b90.png)

## 前提条件
* Kubernetes Cluster として 3-node 構成の [OKD4](https://www.okd.io/) cluster を用いる。各 Node は master と worker 両方の role を持つ。
* Rook-Ceph 環境として　OpenShift Data Foundation を用いる。OpenShift Data Foundation 導入手順の例については次の記事も参照：

https://qiita.com/sawa2d2/items/f8c2b37340b59b4ee579


## 事前準備
### Node Health Check Operator のインストール

Operator Hub を開き、"Medik8s" などで検索し、Node Health Check Operator を見つける：

![medik8s_operator.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/1dd07383-ef6d-ec43-e98f-e838de415b54.png)


Node Health Check Operator をインストールする。


完了すると、"Node Health Check Operator"、および "Self Node Remediation Operator" がインストールされる。

![Screenshot from 2024-06-27 12-12-47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/6eea5d2c-6dd2-7fb9-3f7b-94b57690f937.png)

### SelfNodeRemediationTemplate カスタムリソースの作成
Self Node Remediation Operator の Web コンソールを開き、SelfNodeRemediatorTemplate リソースの設定画面を開く。
画面右側の "SelfNodeRemediatorTemplate の作成" と書かれた青いボタンを押す。

![Screenshot from 2024-06-27 01-32-30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/6911ac76-f7ab-3052-4aac-9b4213ae8f95.png)

各項目を次のように入力し、「作成」をクリックする：
- Name は `selfnoderemediationtemplate-outofservicetaint` 
- Template -> spec -> remediationStrategy: "OutOfServiceTaint"

![Screenshot from 2024-06-26 22-50-56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/d959cc9d-756f-e99b-6ee6-e5d4b15eab38.png)


### NodeHealthCheck カスタムリソースの作成
Node Health Check Operator の Web コンソールを開き、NodeHealthCheck リソースの設定画面を開く。
画面右側の "NodeHealthCheck の作成" と書かれた青いボタンを押す。

![Screenshot from 2024-06-27 01-33-08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/29e1b4e1-eb15-544d-b08e-f3fa3e5f62fd.png)

各項目を次のように入力し、「作成」をクリックする：

* Name は `nodehealthcheck-outofservice` とする
* Remediation template の欄を設定する
    * "Other" ラジオボックスを選択する
    * Api version, Kind, Name, Namespace は、さきほど作成した SelfNodeRemediationTemplate を指定する。具体的な値は次の表の通り：

| Key | Value |
|-----|-------|
| Api version | `self-node-remediation.medik8s.io/v1alpha1` |
| Kind | `SelfNodeRemediationTemplate` |
| Name | `selfnoderemediationtemplate-outofservicetaint` |
| Namespace | `openshift-workload-availability` |

* Node selection は Template を適用したいノードのラベルを指定する。本記事では ODF node を指定したいので、`cluster.ocs.openshift.io/openshift-storage` を指定する

* Unhealthy Condition はデフォルトのままでよい。Duration は異常を検知してから障害発生と認定するまでの時間で、デフォルトでは `300s` になっている。今回は実験目的なので、`20s` 程度の適当な短い値を設定する

![nhc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/6c6d5ef0-8726-5f73-b621-146aa4a4a200.png)

### StatefulSet の作成

まずは次の manifest より StatefulSet を作成する：
```test-sts.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: test-sts
spec:
  serviceName: "nginx"
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 1Gi
      storageClassName: "ocs-storagecluster-ceph-rbd"
```

リソースを作成する：
```
$ oc apply -f test-sts.yaml 
statefulset.apps/test-sts created
```

StatefulSet が作成されたことを確認する：
```
$ oc get sts
NAME       READY   AGE
test-sts   1/1     2m16s
```

Pod が稼働していることを確認する：

```
$ oc get pod -o wide          
NAME         READY   STATUS    RESTARTS   AGE   IP            NODE      NOMINATED NODE   READINESS GATES          
test-sts-0   1/1     Running   0          36s   10.130.0.28   master0   <none>           <none>      
```

PVC がBound されていることを確認する：
```
$ oc get pvc
NAME             STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS                  AGE
www-test-sts-0   Bound    pvc-1e714430-b9ce-435e-93e4-f4338d1eeaed   1Gi        RWO            ocs-storagecluster-ceph-rbd   2m27s
```

作成された PV を確認する：

```
$ oc get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                                                      STORAGECLASS                  REASON   AGE
pvc-1e714430-b9ce-435e-93e4-f4338d1eeaed   1Gi        RWO            Delete           Bound    default/www-test-sts-0                                     ocs-storagecluster-ceph-rbd            2m28s
```

### RBD の確認
次に、PV に対応する RBD がどのNode 上に存在するか確認する。
これには、次のスクリプト：`find_rbd.sh` を作成すると便利である。
これは、PV 名を与えると RBD デバイスが存在する Node 名を表示するものである：
```bash:find_rbd.sh
#!/bin/bash

usage()
{
  echo "Usage:"
  echo "  ./find_rbd.sh <pvname>"
}

# Parse arguments
if [ "$#" -ne 1 ]; then
    usage
    exit 1
fi

pvname=$1
nodes=$(oc get nodes --no-headers)

while read -r line; do
  node=$(echo "$line" | awk '{print $1}')
  status=$(echo "$line" | awk '{print $2}')
  if [ "$status" != "Ready" ]; then
    continue
  fi

  lsblk_result=$(oc debug node/${node} -- chroot /host lsblk -d 2> /dev/null)
  echo "${lsblk_result}" | grep "/${pvname}/" > /dev/null
  if [[ $? -eq 0 ]]; then
    echo "An RBD found on $node:"
    # Output until the indentation ends
    echo "${lsblk_result}" | awk -v pvname="${pvname}" '
      {
        if (found) {
          if ($0 ~ /^ {32}/) {
            print $0
          } else {
            found = 0
          }
        }
        if ($0 ~ pvname) {
          print $0
          found = 1
        }
      }'
    exit 0
  fi
done <<< "$nodes"

echo RBD not found.
exit 1
```

これを利用して、PV に対応する RBD が存在する Node を特定する：
```
$ ./find_rbd.sh pvc-1e714430-b9ce-435e-93e4-f4338d1eeaed
An RBD found on master2:
rbd0  251:0    0     1G  0 disk /var/lib/kubelet/pods/f0cb6e7d-78b2-4bfd-8ffd-55627a990c36/volumes/kubernetes.io~csi/pvc-1e714430-b9ce-435e-93e4-f4338d1eeaed/mount
                                /var/lib/kubelet/plugins/kubernetes.io/csi/openshift-storage.rbd.csi.ceph.com/4005d25cbb5fbd3d05a867378d413b2564df4455349e0458fe26e66ece694fa8/globalmount/0001-0011-open
    shift-storage-0000000000000002-5f231f68-9dd5-457a-a5f4-61f1bae8fd9b
```

Node `master2` 上に `/dev/rbd0` として存在していることが確認できた。

### Node に taints がないことの確認
すべての Nodes において、 `taints : null` になっていることを確認する。

```
$ oc get nodes -o json | jq '.items[] | {name: .metadata.name, taints: .spec.taints}'
{
  "name": "master0",
  "taints": null
}
{
  "name": "master1",
  "taints": null
}
{
  "name": "master2",
  "taints": null
}
```

## Failover の動作確認

準備段階で対象の RBD が `master2` 上にあることがわかったので、この Node を強制的に電源OFFにする。

しばらくすると OFF にした Node にTaints が付与される。


:::note
待ち時間は、NodeHealthCheck 作成時に設定した Duration によるが、`20s` に設定した場合は1分くらいで Tainting されると思われる。
:::


Medik8s によって Taints が付与されているようだが、`out-of-service` taints ではない。
```
$ oc get nodes -o json | jq '.items[] | {name: .metadata.name, taints: .spec.taints}'
{
  "name": "master0",
  "taints": null
}
{
  "name": "master1",
  "taints": null
}
{
  "name": "master2",
  "taints": [
    {
      "effect": "NoSchedule",
      "key": "node.kubernetes.io/unreachable",
      "timeAdded": "2024-06-28T01:21:27Z"
    },
    {
      "effect": "NoExecute",
      "key": "node.kubernetes.io/unreachable",
      "timeAdded": "2024-06-28T01:21:33Z"
    },
    {
      "effect": "NoExecute",
      "key": "medik8s.io/remediation",
      "timeAdded": "2024-06-28T01:21:48Z",
      "value": "self-node-remediation"
    },
    {
      "effect": "NoSchedule",
      "key": "node.kubernetes.io/unschedulable",
      "timeAdded": "2024-06-28T01:21:48Z"
    }
  ]
}
```

少し待つと Pod が Terminating に変化する。
```
$ date
Fri Jun 28 10:20:46 AM JST 2024
$ oc get pods -o wide
NAME         READY   STATUS    RESTARTS   AGE     IP             NODE      NOMINATED NODE   READINESS GATES
test-sts-0   1/1     Running   0          2m43s   10.129.0.243   master2   <none>           <none>

$ date
Fri Jun 28 10:21:57 AM JST 2024
$ oc get pods -o wide
NAME         READY   STATUS        RESTARTS   AGE     IP             NODE      NOMINATED NODE   READINESS GATES
test-sts-0   1/1     Terminating   0          3m54s   10.129.0.243   master2   <none>           <none>

...
```

Terminating 状態が3分ほど続いたあと、一瞬だけ `out-of-service` Taints が master2 に付与される。

```
$ oc get nodes -o json | jq '.items[] | {name: .metadata.name, taints: .spec.taints}'
{
  "name": "master0",
  "taints": null
}
{
  "name": "master1",
  "taints": null
}
{
  "name": "master2",
  "taints": [
    {
      "effect": "NoSchedule",
      "key": "node.kubernetes.io/unreachable",
      "timeAdded": "2024-06-28T01:21:27Z"
    },
    {
      "effect": "NoExecute",
      "key": "node.kubernetes.io/unreachable",
      "timeAdded": "2024-06-28T01:21:33Z"
    },
    {
      "effect": "NoExecute",
      "key": "medik8s.io/remediation",
      "timeAdded": "2024-06-28T01:21:48Z",
      "value": "self-node-remediation"
    },
    {
      "effect": "NoSchedule",
      "key": "node.kubernetes.io/unschedulable",
      "timeAdded": "2024-06-28T01:21:48Z"
    },
    {
      "effect": "NoExecute",
      "key": "node.kubernetes.io/out-of-service",
      "timeAdded": "2024-06-28T01:24:50Z",
      "value": "nodeshutdown"
    }
  ]
}

```


:::note
Medik8s において、`out-of-service` taint は付与しっぱなしではなく、リソースが削除されるまでの間だけ存在し、Pod や RBD の detach 処理が完了したら速やかに taint が削除される [^untaint]。なのでtaint を1秒間隔で出力とかしてないと `out-of-service` tains が付与される瞬間は見れないかもしれない。

`out-of-service` Taints が付与されたことを後から確認するには、`self-node-remediation-controller-manager` Pod の logs を見ればよい：

```
$ oc logs -n openshift-workload-availability $(oc get pod -n openshift-workload-availability -o custom-columns=:metadata.name | grep self-node-remediation-controller-manager)
...
2024-06-27T02:04:47.033077449Z  INFO    controllers.SelfNodeRemediation out-of-service taint added      {"selfnoderemediation": "openshift-workload-availability/master2", "new taints": [{"key":"node.kubernetes.io/unreachable","effect":"NoSchedule","timeAdded":"2024-06-27T02:01:14Z"},{"key":"node.kubernetes.io/unreachable","effect":"NoExecute","timeAdded":"2024-06-27T02:01:20Z"},{"key":"medik8s.io/remediation","value":"self-node-remediation","effect":"NoExecute","timeAdded":"2024-06-27T02:01:45Z"},{"key":"node.kubernetes.io/unschedulable","effect":"NoSchedule","timeAdded":"2024-06-27T02:01:45Z"},{"key":"node.kubernetes.io/out-of-service","value":"nodeshutdown","effect":"NoExecute","timeAdded":"2024-06-27T02:04:47Z"}]}
2024-06-27T02:04:47.03358787Z   DEBUG   events  [remediation] Remediation process - add out-of-service taint to unhealthy node  {"type": "Normal", "object": {"kind":"Node","name":"master2","uid":"7c4e06b6-dd7b-484d-9554-369515eb9b2d","apiVersion":"v1","resourceVersion":"454433"}, "reason": "AddOutOfService"}
2024-06-27T02:04:47.037005111Z  INFO    controllers.SelfNodeRemediation waiting for terminating pod     {"selfnoderemediation": "openshift-workload-availability/master2", "pod name": "route-controller-manager-7c8f598cc4-fcfdq", "phase": "Running"}
2024-06-27T02:04:52.049149508Z  INFO    controllers.SelfNodeRemediation waiting for terminating pod     {"selfnoderemediation": "openshift-workload-availability/master2", "pod name": "controller-manager-646bc75c48-grgct", "phase": "Running"}
2024-06-27T02:04:54.332206174Z  INFO    controllers.SelfNodeRemediationConfig   Syncing certs
2024-06-27T02:04:54.332443056Z  INFO    controllers.SelfNodeRemediationConfig   Cert secret already exists
2024-06-27T02:04:54.332453562Z  INFO    controllers.SelfNodeRemediationConfig.syncConfigDaemonset       Start to sync config daemonset
2024-06-27T02:04:54.339036147Z  INFO    controllers.SelfNodeRemediationConfig   Updating DS tolerations
2024/06/27 02:04:54 reconciling (apps/v1, Kind=DaemonSet) openshift-workload-availability/self-node-remediation-ds
2024/06/27 02:04:54 update was successful
2024-06-27T02:04:57.192184753Z  INFO    controllers.SelfNodeRemediation out-of-service taint removed    {"selfnoderemediation": "openshift-workload-availability/master2", "new taints": [{"key":"node.kubernetes.io/unreachable","effect":"NoSchedule","timeAdded":"2024-06-27T02:01:14Z"},{"key":"node.kubernetes.io/unreachable","effect":"NoExecute","timeAdded":"2024-06-27T02:01:20Z"},{"key":"medik8s.io/remediation","value":"self-node-remediation","effect":"NoExecute","timeAdded":"2024-06-27T02:01:45Z"},{"key":"node.kubernetes.io/unschedulable","effect":"NoSchedule","timeAdded":"2024-06-27T02:01:45Z"}]}
2024-06-27T02:04:57.192542562Z  DEBUG   events  [remediation] Remediation process - remove out-of-service taint from node       {"type": "Normal", "object": {"kind":"Node","name":"master2","uid":"7c4e06b6-dd7b-484d-9554-369515eb9b2d","apiVersion":"v1","resourceVersion":"454661"}, "reason": "RemoveOutOfService"}
2024-06-27T02:04:57.192888654Z  DEBUG   events  [remediation] Remediation process - finished deleting unhealthy node resources  {"type": "Normal", "object": {"kind":"Node","name":"master2","uid":"7c4e06b6-dd7b-484d-9554-369515eb9b2d","apiVersion":"v1","resourceVersion":"454661"}, "reason": "DeleteResources"}
```
:::

`out-of-service` Taints の付与とほぼ同時に、別ノード `master1` で ContainerCreating が開始される。
```
...

$ date
Fri Jun 28 10:24:48 AM JST 2024
$ oc get pods -o wide
NAME         READY   STATUS        RESTARTS   AGE     IP             NODE      NOMINATED NODE   READINESS GATES
test-sts-0   1/1     Terminating   0          6m45s   10.129.0.243   master2   <none>           <none>

$ date
Fri Jun 28 10:24:58 AM JST 2024
$ oc get pods -o wide
NAME         READY   STATUS              RESTARTS   AGE   IP       NODE      NOMINATED NODE   READINESS GATES
test-sts-0   0/1     ContainerCreating   0          1s    <none>   master0   <none>           <none>

$ date
Fri Jun 28 10:25:08 AM JST 2024
$ oc get pods -o wide
NAME         READY   STATUS    RESTARTS   AGE   IP             NODE      NOMINATED NODE   READINESS GATES
test-sts-0   1/1     Running   0          11s   10.130.0.125   master0   <none>           <none>
```

failover に要した時間（Pod が Terminating に変化してから ContainerCreating になるまで）は3分程度のようだ。（ただし今回は RBD に何も入れていないので、そこは考慮する必要がある）。

RBD も `master2` から `master0` に移っていることが確認できる：

```
$ ./find_rbd.sh pvc-1e714430-b9ce-435e-93e4-f4338d1eeaed
An RBD found on master0:
rbd0  251:0    0     1G  0 disk /var/lib/kubelet/pods/9676b428-cb30-45bd-a24a-788420133951/volumes/kubernetes.io~csi/pvc-1e714430-b9ce-435e-93e4-f4338d1eeaed/mount
                                /var/lib/kubelet/plugins/kubernetes.io/csi/openshift-storage.rbd.csi.ceph.com/4005d25cbb5fbd3d05a867378d413b2564df4455349e0458fe26e66ece694fa8/globalmount/0001-0011-openshift-storage-0000000000000002-5f231f68-9dd5-457a-a5f4-61f1bae8fd9b
```

## Medik8s 参考資料

- [OpenShift.Run-2024-Medik8s - ノード障害時にStatefulSet も自動回復したい！そんなときはmediks.io - Manabu Ori - RedHat - Speaker Deck](https://speakerdeck.com/orimanabu/openshift-dot-run-2024-medik8s)
- [第6章 ノードヘルスチェックを使用したノードの修復 | Red Hat Product Documentation](https://docs.redhat.com/ja/documentation/workload_availability_for_red_hat_openshift/23.3/html/remediation_fencing_and_maintenance/node-health-check-operator#node-health-check-operator)
- [Node Health Check Operator - Red Hat Blog](https://www.redhat.com/en/blog/node-health-check-operator)
- [Getting Started | Medik8s](https://www.medik8s.io/GettingStarted/)


[^untaint]:Rook-Ceph による `out-of-service` Taint の付与・除去を行うコード箇所のリンク：[self-node-remediation/controllers/selfnoderemediation_controller.go at 4b91e5c9f79bc4dfc0c47c5027164e877709617e · medik8s/self-node-remediation](https://github.com/medik8s/self-node-remediation/blob/4b91e5c9f79bc4dfc0c47c5027164e877709617e/controllers/selfnoderemediation_controller.go#L360-L381)

[^medik8s]:[Kubernetes フェイルオーバーの改善: Non-Graceful Node Shutdown (Open Source Summit Japan 2023) : OSS 貢献活動 | NEC](https://jpn.nec.com/oss/community/blog/kubernetes-failover-improvement-non-graceful-node-shutdown.html)