VolumeをReadWriteOnceからReadWriteOncePodへ移行する

手元のクラスタに`ReadWriteOnce`のPVがあったので、これを`ReadWriteOncePod`に変更したので、その時に調査した内容を紹介します。

`ReadWriteOncePod`はKubernetes1.22でAlpha機能ととして登場し、1.27からBetaとなり、最新の1.29でGAした機能です。

基本的な動作については以下の記事が詳しいです。

https://qiita.com/ysakashita/items/ad1f13e2af99969c8e9e

さて、今回はすでにデプロイしてしまっているPVについて、1つのPodからしか利用していないので`ReadWriteOncePod`に変更することをやってみました。

# なぜ実施するのか？

`ReadWriteOnce`に設定したVolumeは同じNodeに配置された他のPodからもマウントすることが出来ますが、多くの場合そのようなマウントができることは不要です。

セキュリティ文脈における最小権限の原則からも、不要なことは出来ないようにしておくことが望ましいです。

# PVをeditする

まず変更実施すべきはPVです。
実体として1つのPodしかVolumeをマウントしていないケースにおいては単純にPVをeditして`AccessMode`を`ReadWriteOncePod`に変更することで、これを実現できます。

:::note alert
#### すでに複数のPodがマウントしているPVを`ReadWriteOncePod`に変更したらどうなる？

ちょっと気になったのでやってみたところ、変更することができ、妙な状況となりました。


```mermaid
graph TD
PVC["PVC"]
PV["PV
accessMode = ReadWriteOncePod
"]
Pod1 --mount--> PVC
Pod2 --mount--> PVC
PVC --bind--> PV
```

この状態は、矛盾を抱えており、例えば、そのPodの1つを再生成しようとすると、Volumeがマウントできなくて失敗します。

このような状況にならないように、PVの`accessModes`を変更する際は、1つのPodのみが該当のPVをマウントしていることを確認することが大切です。
:::

# PVCはどうなる？

PVをeditしたときPVCはどうなるのかが気になって調べてみました。
結果としてはPVCのspecの`accessModes`はそのまま`ReadWriteOnce`で、statusの`accessMode`が`ReadWriteOncePod`となりました。

```yaml
apiVersion: v1
kind: PersistentVolumeClaim
...
spec:
  accessModes:
  - ReadWriteOnce  # ← ここと
...
status:
  accessModes:
  - ReadWriteOncePod  # ← ここ
  phase: Bound
...
```

この状態で、PVは`ReadWriteOncePod`となっているので、他のPodからはマウントできない状態となっており、機能としては意図した状態になっているといえばそうです。

また、`kubectl get pvc`の結果はstatusを表示しているので、一見すると問題はなさそうに見えます。
```
kubectl get pvc
NAME  STATUS   VOLUME       CAPACITY   ACCESS MODES   STORAGECLASS   AGE
pvc   Bound    pvc-xxxxxx   3Gi        RWOP           sc-xxxxxxxx    XdYh
```

しかし、このようなちょっとイレギュラーな状態は、Kubernetesのコーナーケースに当たるため、なにか今後の機能変更時にバグを引き当てたりしそうなので、なるべく避けたいという気持ちもあります。


# PVCも修正したい
PVCのspecの`accessModes`は後から変更することが出来ないので、ここを変更するためには、PVCを一度削除して作り直す必要があります。

しかし、PVの`ReclaimPolicy`が`Delete`になっている場合は、PVCを削除することで対応するPVが一緒に削除されてしまうので作業には注意が必要です。

前節で説明したようにPVをeditして`ReadWriteOncePod`にしておけば、PVCのspecが`ReadWriteOnce`でも、別のPodからはマウントできなくなるので、PVCはそのままにしておく、というのも一つの手かなと思いました。

一方でこの方法は、Kubernetesのマニフェストをリポジトリなどで管理している場合、そのマニフェストを流用して別のシステムを構築する際に、PVが`ReadWriteOnce`で作られてしまうという問題があります。

かといって、PVCのマニフェストだけを修正すると、今度は構成変更を反映するためにそのマニフェストをapplyする際に変更できないエラーとなってしまい、これも不便です。

ということで、以降はPVCを修正する方法を見ていきます。

# StatefulSetを使いPVCを作っている場合

StatefulSetの`volumeClaimTemplates`を使ってPVCを作っている場合は、管理しているマニフェストはStatefulSetのものなので、まず修正すべきはStatefulSetです。

StatefulSetの`volumeClaimTemplates`は後から変更できないので、StatefulSetの作り直しが必要です。

StatefulSetを消してもPVCは残るので、StatefulSetを消して、`accessModes`を`ReadWriteOncePod`に変更したStatefulSetをデプロイすることで、管理しているマニフェストと実環境の矛盾を解消できます。

さて、StatefulSetのみを変更してデプロイし直しができればこれで十分でしょうか？
この際、PVCは以前のStatefulSetの作ったものが引き続き利用されるので、PVCのspecの`accessModes`は`ReadWriteOnce`のままという状況になります。
実体としてのPVはReadWriteOncePodに変更済みなので、実質的な問題はなさそうです。

しかし、やはりPVとPVC、StatefulSetのvolumeClaimTemplatesのaccessModeが異なることは、ややこしいので、後に紹介する方法でPVCも作り直すのが良いと考えます。

```mermaid
graph TD

PVC["PVC
.spec.accessMode = ReadWriteOnce (!)
.status.accessMode = ReadWriteOncePod
"]

PV["PV
.spec.accessMode = ReadWriteOncePod
"]

StatefulSet --> PVC --> PV

```

::: note info
# 謎のPending
この作業をやっている時に、StatefulSetのYAMLに妙なフィールドがあることに気づきました。
.spec.volumeClaimTemplates.statusというフィールドです。

以下のように何故か、specの中なのにstatusというフィールドが有り、ずっとPendingという値になっています。

この件は一応 https://github.com/kubernetes/kubernetes/issues/65706 でバグ報告されていたようですが、特に対応されずCloseされています。

他のリソースの様子をみても問題はないので、Pendingであることを気にする必要はなさそうです。

```yaml
$ kubectl get statefulset -n hedgedoc -o yaml
apiVersion: v1
items:
- apiVersion: apps/v1
  kind: StatefulSet
...
spec:
...
    volumeClaimTemplates:
    - apiVersion: v1
      kind: PersistentVolumeClaim
      metadata:
        creationTimestamp: null
        name: test-ss
      spec:
        accessModes:
        - ReadWriteOnce
        resources:
          requests:
            storage: 3Gi
        storageClassName: test-ss
        volumeMode: Filesystem
      status:
        phase: Pending # <---- これ
...
  status:
```
:::


# PVCを作り直す

PVの内容が消えて良い場合は単純にPVCを消せばOKですが、一般的にPVの内容は保持したいと思うので、その前提で手順を紹介します。

## 該当PVの`persistentVolumeReclaimPolicy`を変更して、PVCと連動して削除されないようにする。

```
kubectl patch pv <PVの名前> -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'
```

この手順では事前にPVCを利用しているPodはすべて削除する必要があります。

## PVCを削除
```
kubectl delete pvc <PVCの名前>
```

## PVがRelease状態になるので、再び利用できるようにする

```
kubectl patch pv <PVの名前> -p '{"spec":{"claimRef": null}}'
```

（これ以降に次のPVC作成の手順を実施するまでの間に、何処か別の仕組みでPVCが作られると、このPVがBindされてしまう事があるので注意）

## `accessModes`を`ReadWriteOncePod`に変更したPVCを作成

StatefulSetでPVCを作る場合は、`volumeClaimTemplates`を変更すると読み替えてください。

この際`.spec.volumeName`で紐づけしたいPVの名前を指定することで、意図通りのPVに紐付けることが出来ます。
（一方で`.spec.volumeName`はあとから編集できないフィールドなので、これをマニフェストファイルに記載して、リポジトリで管理するような場合は、このフィールドの記載がこのクラスタに特化した内容になってしまう点に注意が必要です。）

```
# 一例です
kubectl apply -f pvc.yaml
```

これで、PVもPVCも`ReadWriteOncePod`を設定できました。

## PVの`persistentVolumeReclaimPolicy`をもとに戻す

```
kubectl patch pv <PVの名前> -p '{"spec":{"persistentVolumeReclaimPolicy":"Delete"}}
```

# まとめ
新しく使えるようになった`ReadWriteOncePod`を既存のVolumeに適用する方法を紹介しました。
PV周りは、ちょっと手順を間違えるとデータが消えてしまったりするので、慎重な作業が必要です。

不要なPodからのマウントを防ぐことは、特にKubernetesクラスタを他のチームと共用している場合などに非常に有効な設定です。手順はちょっと面倒ですが、早めに対応するのが良いでしょう。

# 参考

https://kubernetes.io/docs/tasks/administer-cluster/change-pv-access-mode-readwriteoncepod/