Kubernetes: Volume Health Monitorの動作検証

# はじめに

Kubernetes v1.19まではボリュームがPodにマウントされた後、ボリュームのステータスを監視する方法がありませんでした。
そこで、KubernetesのSIG Storageでは、[KEP-1432](https://github.com/kubernetes/enhancements/tree/master/keps/sig-storage/1432-volume-health-monitor)にてVolume Health Monitorが提案され、Kubernetes v1.19からAlpha機能として追加、v1.21でSecond Alphaとしてアップデートされています。CSIの仕様としては[CSI v1.3.0](https://github.com/container-storage-interface/spec/releases/tag/v1.3.0)から登場しています。
ボリュームを提供するストレージやネットワークやノード自体の障害により、KubernetesでPodがマウントしているにもボリュームが利用できなくなっている場合があります。
このような場合、Kubernetesでボリュームを示すリソースであるPV(PersistentVolume)およびPVC(PersistentVolumeClaim)のステータスでは判断できず、Podの中のコンテナで動作するアプリケーションにてRead/Writeを実行したことで始めて障害に気がつくことがあります。
もしかすると、頻繁にRead/Writeが発生するアプリケーションであれば、すぐに障害に気がつくかもしれません。しかし、稀にしかRead/Writeが発生しないアプリケーションの場合は、障害に気がつかないままとなっている場合があります。
そこで、ボリュームの状態を定期的に監視するための機能としてVolume Health Monitorが開発されています。
2021/4時点のVolume Health Monitorでは、イベントとログでの報告のみですが、将来的には自動復旧する方法も目指しているとのことです。

[障害の例]

- Kubernetesの外部でボリュームが誤って削除される可能性があります。
- ボリュームが存在するディスクは、メンテナンスのために一時的に取り外すことができます。
- ボリュームが存在するディスクに障害が発生する可能性があります。
- ボリュームの状態に影響を与える、基盤となるストレージシステムの構成の問題がある可能性があります。
- ボリュームが容量不足になっている可能性があります。
- ディスクが劣化している可能性があり、パフォーマンスに影響します。
- 読み取り/書き込みI/Oエラーが発生する可能性があります。
- ボリューム上のファイルシステムが破損している可能性があります。
- ファイルシステムの容量が不足している可能性があります。
- Kubernetesの外部で誤ってボリュームがアンマウントされる可能性があります。

# アーキテクチャ
 
Volume Health Monitorは、[kubernetes-csi/external-health-monitor](https://github.com/kubernetes-csi/external-health-monitor)の[external-health-monitor-controller](https://kubernetes-csi.github.io/docs/external-health-monitor-controller.html)と[external-health-monitor-agent](https://kubernetes-csi.github.io/docs/external-health-monitor-agent.html)の2つをサイドカーコンテナとしてCSI Driverとあわせてデプロイします。

## external-health-monitor-controller

CSI controller driverのサイドカーコンテナとしてデプロイします。
CSI controller driverのListVolumes, ControllerGetVolumeを定期的に呼び出し、ボリュームの状態をチェックします。
各チェックの実装は以下を参照下さい。

- ListVolumes
  - [CheckControllerListVolumeStatuses()](https://github.com/kubernetes-csi/external-health-monitor/blob/2279993958327310e4e62f46e5289513e8586a51/pkg/csi-handler/pv_checker.go#L73) -> [ControllerListVolumeConditions()](https://github.com/kubernetes-csi/external-health-monitor/blob/2279993958327310e4e62f46e5289513e8586a51/pkg/csi-handler/pv_handler.go#L55)
- ControllerGetVolume
  - [CheckControllerVolumeStatus()](https://github.com/kubernetes-csi/external-health-monitor/blob/2279993958327310e4e62f46e5289513e8586a51/pkg/csi-handler/pv_checker.go#L137) -> [ControllerGetVolume()](https://github.com/kubernetes-csi/external-health-monitor/blob/2279993958327310e4e62f46e5289513e8586a51/pkg/csi-handler/pv_handler.go#L82)

## external-health-monitor-agent

CSI node driverのサイドカーコンテナとしてデプロイします。
CSI node driverのNodeGetVolumeStatsを定期的に呼び出し、ボリュームの状態をチェックします。
各チェックの実装は以下を参照ください。

- NodeGetVolumeStats
  - [CheckNodeVolumeStatus()](https://github.com/kubernetes-csi/external-health-monitor/blob/2279993958327310e4e62f46e5289513e8586a51/pkg/csi-handler/pv_checker.go#L182) -> [NodeGetVolumeCondition()](https://github.com/kubernetes-csi/external-health-monitor/blob/2279993958327310e4e62f46e5289513e8586a51/pkg/csi-handler/pv_handler.go#L100)

## CSI Driver(csi-driver-host-path)におけるボリュームの状態チェック

上記に述べたようにexternal-health-monitorの2つのコンテナにより、CSI DriverのListVolumes, ControllerGetVolume, NodeGetVolumeStatsのAPIが定期的に呼び出されボリュームの状態がチェックされます。
これらのAPIでのボリュームの状態チェック方法やチェック項目については、各CSI Driverによって異なります。
以下に、本検証で利用するCSI Driverのサンプル実装として参照されることの多い[csi-driver-host-path](https://github.com/kubernetes-csi/csi-driver-host-path)での実装箇所を以下に記します。どの項目をどのようにチェックしているかは実装を参照してください。

- [ListVolumes()](https://github.com/kubernetes-csi/csi-driver-host-path/blob/e4d72e308439144cdd996cbb7e22f8ca0a965474/pkg/hostpath/controllerserver.go#L395) -> [doHealthCheckInControllerSide()](https://github.com/kubernetes-csi/csi-driver-host-path/blob/e4d72e308439144cdd996cbb7e22f8ca0a965474/pkg/hostpath/healthcheck.go#L162)
- [ControllerGetVolume()](https://github.com/kubernetes-csi/csi-driver-host-path/blob/e4d72e308439144cdd996cbb7e22f8ca0a965474/pkg/hostpath/controllerserver.go#L450) ->[doHealthCheckInControllerSide()](https://github.com/kubernetes-csi/csi-driver-host-path/blob/e4d72e308439144cdd996cbb7e22f8ca0a965474/pkg/hostpath/healthcheck.go#L162)
- [NodeGetVolumeStats()](https://github.com/kubernetes-csi/csi-driver-host-path/blob/e4d72e308439144cdd996cbb7e22f8ca0a965474/pkg/hostpath/nodeserver.go#L360) -> [doHealthCheckInNodeSide](https://github.com/kubernetes-csi/csi-driver-host-path/blob/e4d72e308439144cdd996cbb7e22f8ca0a965474/pkg/hostpath/healthcheck.go#L193)

external-health-monitor-controllerコンテナから呼び出されるListVolumesとControllerGetVolumeは同じチェックとなっています。

# 動作検証

Volume Health Monitorの動作検証を行います。

## 検証環境

- minikube v1.19.0
  - Kubernetes v1.21.0
- CSI Driver: csi-driver-host-path

## 検証内容

minikubeを使いKubernetes v1.21.0を`--feature-gates=CSIVolumeHealth=true ` を指定し立ち上げます。 
[Feature GatesでCSIVolumeHealthを有効](https://github.com/kubernetes/kubernetes/pull/99284)にすることで、Volume Health Monitorでチェックされたボリュームの状態をイベントとして報告されるようになります。

```
$ minikube start --vm-driver=virtualbox --kubernetes-version=1.21.0 --feature-gates=CSIVolumeHealth=true 
```

minikubeによりKubernetes v1.21.0が起動したことを確認します。
次に、Volume Health Monitorに対応しているcsi-driver-host-pathをセットアップします。 

```
$ git clone git@github.com:kubernetes-csi/csi-driver-host-path.git
$ cd csi-driver-host-path/
$ deploy/kubernetes-latest/deploy.sh
...
csidriver.storage.k8s.io/hostpath.csi.k8s.io created
   /Users/ysakashi/code/golang/src/github.com/kubernetes-csi/csi-driver-host-path/deploy/kubernetes-latest/hostpath/csi-hostpath-plugin.yaml
        using           image: k8s.gcr.io/sig-storage/csi-external-health-monitor-agent:v0.2.0
        using           image: k8s.gcr.io/sig-storage/csi-external-health-monitor-controller:v0.2.0
...

error: unable to recognize "/Users/ysakashi/code/golang/src/github.com/kubernetes-csi/csi-driver-host-path/deploy/kubernetes-latest/snapshotter/csi-hostpath-snapshotclass.yaml": no matches for kind "VolumeSnapshotClass" in version "snapshot.storage.k8s.io/v1"
```

VolumeSnapshotClassでエラーが出ますが、今回はVolumeSnapshot関連のリソースをデプロイしていないため、無視します。
上記のデプロイ時の出力をみると、`csi-external-health-monitor-agent:v0.2.0`と`csi-external-health-monitor-controller:v0.2.0`がcsi-hostpath-pluginのサイドカーコンテナとしてデプロイされていることがわかります。(csi-hostpath-pluginはCSI controller driverとCSI node driverの両方を備え持つStatefulSetです)

csi-driver-host-pathのPodが起動していることを確認します。

```
$ kubectl get pod
NAME                         READY   STATUS    RESTARTS   AGE
csi-hostpath-attacher-0      1/1     Running   0          3m20s
csi-hostpath-provisioner-0   1/1     Running   0          3m17s
csi-hostpath-resizer-0       1/1     Running   0          3m16s
csi-hostpath-snapshotter-0   1/1     Running   0          3m15s
csi-hostpath-socat-0         1/1     Running   0          3m15s
csi-hostpathplugin-0         5/5     Running   0          3m18s
```

次に、StorageClassを作成します。

```
$ kubectl apply -f examples/csi-storageclass.yaml 
storageclass.storage.k8s.io/csi-hostpath-sc created 

$ kubectl get sc
NAME                 PROVISIONER                RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
csi-hostpath-sc      hostpath.csi.k8s.io        Delete          Immediate           true                   3m32s
standard (default)   k8s.io/minikube-hostpath   Delete          Immediate           false                  24m
```

PVC/PVを作成しPodにマウントします。
ここでは、exampleディレクトリ以下に用意されているサンプルを利用します。

```
$ kubectl apply -f examples/csi-pvc.yaml -f examples/csi-app.yaml 
persistentvolumeclaim/csi-pvc created
pod/my-csi-app created

$ kubectl get pod,pvc,pv
NAME                             READY   STATUS    RESTARTS   AGE
...
pod/my-csi-app                   1/1     Running   0          16s

NAME                            STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS      AGE
persistentvolumeclaim/csi-pvc   Bound    pvc-63fd7293-c543-4c83-8f65-01aacb098ab3   1Gi        RWO            csi-hostpath-sc   16s

NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM             STORAGECLASS      REASON   AGE
persistentvolume/pvc-63fd7293-c543-4c83-8f65-01aacb098ab3   1Gi        RWO            Delete           Bound    default/csi-pvc   csi-hostpath-sc            16s
```

Pod,PVC,PVが正しく起動していることを確認した後、`kubectl describe`でPodのイベントを確認します。

```
$ kubectl describe pod my-csi-app 
...
Events:
  Type     Reason                   Age                 From                                      Message
  ----     ------                   ----                ----                                      -------
...
  Normal   VolumeConditionNormal    16s (x10 over 16s)  csi-pv-monitor-agent-hostpath.csi.k8s.io  The Volume returns to the healthy state
```

すると、Volume Health Monitorによりチェックされたボリュームの状態のイベントがあがっているのを確認できます。
このボリュームの状態のイベントのTypeは`Normal`, Reasonは`VolumeConditionNormal`, Messageは`The Volume returns to the healthy state`となっており、ボリュームの状態が健全であることがわかります。

では、ここでボリュームに障害を発生させます。
今回は、 csi-driver-host-pathのNodeGetVolumeStatsにて[実装](https://github.com/kubernetes-csi/csi-driver-host-path/blob/e4d72e308439144cdd996cbb7e22f8ca0a965474/pkg/hostpath/healthcheck.go#L193)されているケースを試します。本ケースは上記で述べた「ボリュームが存在するディスクは、メンテナンスのために一時的に取り外すことができます。」「Kubernetesの外部で誤ってボリュームがアンマウントされる可能性があります。」を想定したチェック項目になります。

ボリュームのマウントを外すため、ノードにログインし作業します。

```
$ minikube ssh
                         _             _            
            _         _ ( )           ( )           
  ___ ___  (_)  ___  (_)| |/')  _   _ | |_      __  
/' _ ` _ `\| |/' _ `\| || , <  ( ) ( )| '_`\  /'__`\
| ( ) ( ) || || ( ) || || |\`\ | (_) || |_) )(  ___/
(_) (_) (_)(_)(_) (_)(_)(_) (_)`\___/'(_,__/'`\____)


# rootで作業します
$ sudo bash    

# マウントパスを調べます
$ mount |grep pvc
tmpfs on /var/lib/kubelet/pods/0ed3b369-d5a6-4b99-b489-1bb651710c60/volumes/kubernetes.io~csi/pvc-63fd7293-c543-4c83-8f65-01aacb098ab3/mount type tmpfs (rw,relatime,size=5357156k)
tmpfs on /mnt/sda1/var/lib/kubelet/pods/0ed3b369-d5a6-4b99-b489-1bb651710c60/volumes/kubernetes.io~csi/pvc-63fd7293-c543-4c83-8f65-01aacb098ab3/mount type tmpfs (rw,relatime,size=5357156k)

# マウントを外します
$ umount /mnt/sda1/var/lib/kubelet/pods/0ed3b369-d5a6-4b99-b489-1bb651710c60/volumes/kubernetes.io~csi/pvc-63fd7293-c543-4c83-8f65-01aacb098ab3/mount

# ノードからログアウトします
$ exit
exit
$ exit
logout
```

ノードにて、Podからマウントされているボリュームのマウントを外しました。
次に、PodにてVolume Health Monitorによるイベントを確認します。

```
$ kubectl describe pod my-csi-app 
...
Events:
  Type     Reason                   Age                    From                                      Message
  ----     ------                   ----                   ----                                      -------
  ...
  Normal   VolumeConditionNormal    5m53s (x141 over 19m)  csi-pv-monitor-agent-hostpath.csi.k8s.io  The Volume returns to the healthy state
  Warning  VolumeConditionAbnormal  53s (x11 over 20m)     csi-pv-monitor-agent-hostpath.csi.k8s.io  The volume isn't mounted
```

その結果、`Warning`, `VolumeConditionAbnormal`,`The volume isn't mounted`のボリュームの異常を示すイベントがあがっているのが確認できました。
ログからも確認します。
csi-driver-host-pathにて、csi-external-health-monitorをサイドカーで持つPod(`csi-hostpathplugin-0`)のログを確認します。

```
$ kubectl logs csi-hostpathplugin-0 csi-external-health-monitor-agent 
...
I0420 09:39:53.317402       1 connection.go:182] GRPC call: /csi.v1.Node/NodeGetVolumeStats
I0420 09:39:53.317429       1 connection.go:183] GRPC request: {"staging_target_path":"/var/lib/kubelet/plugins/kubernetes.io/csi/pv/pvc-63fd7293-c543-4c83-8f65-01aacb098ab3/globalmount","volume_id":"47f67155-a1b9-11eb-8b74-0242ac110003","volume_path":"/var/lib/kubelet/pods/0ed3b369-d5a6-4b99-b489-1bb651710c60/volumes/kubernetes.io~csi/pvc-63fd7293-c543-4c83-8f65-01aacb098ab3/mount"}
I0420 09:39:53.320345       1 connection.go:185] GRPC response: {"usage":[{"available":5485727744,"total":706768896,"unit":1,"used":4778958848}],"volume_condition":{"abnormal":true,"message":"The volume isn't mounted"}}
I0420 09:39:53.320410       1 connection.go:186] GRPC error: <nil>
```

csi-hostpathplugin-0 のサイドカーコンテナcsi-external-health-monitor-agentからNodeGetVolumeStatsが呼ばれ、`volume_condition":{"abnormal":true,"message":"The volume isn't mounted"` が返っていることが確認できます。

# 感想

Volume Health Monitorは、Kubernetes v1.21時点ではAlpha機能ですが、PVC/PVを安心・安全に運用するために重要なポジションになる機能かと睨んでいます。Volume Health Monitorでは、Kubernetesのノード内のボリュームの状態を定期的に監視することで、障害を未然に防ぐことができるポテンシャルを備えています。まだ、2021/4時点の機能では、イベントやログにボリュームの状態が出力されるだけです。そのため、ボリュームの異常を示す`VolumeConditionAbnormal`のイベントをピックアップしアラートを生成するなどの設定を行い検知できるようにすると共に、障害発生時には管理者が手動で対応する必要があります。ただし、KEP-1432でも一言だけですが述べられているように、将来的にはボリュームの異常状態を検知し自動復旧するための、最初の一歩になる重要な機能です。ノードでのボリュームに関する障害について自動復旧できるようになり、ストレージに自信のない管理者でも安心・安全に運用できる世界が来ることを予見させる機能であり、今後のVolume Health Monitorの成長が楽しみです。

# 参考情報
- [KEP-1432: Volume Health Monitor](https://github.com/kubernetes/enhancements/tree/master/keps/sig-storage/1432-volume-health-monitor)
- https://github.com/kubernetes-csi/external-health-monitor
- https://github.com/kubernetes-csi/csi-driver-host-path 
- Kubernetes CSI Developer Documentation
  - [CSI external-health-monitor-controller](https://kubernetes-csi.github.io/docs/external-health-monitor-controller.html)
  - [CSI external-health-monitor-agent](https://kubernetes-csi.github.io/docs/external-health-monitor-agent.html)