Kubernetes: ReadWriteOnceとReadWriteOncePodの動作検証

# はじめに

Kubernetes v1.22にてPVC/PVの新しいAccessModeとして登場した`ReadWriteOncePod (RWOP)`について、これまでのAccessModeの`ReadWriteOnce (RWO)`との違いを動作検証を交えながら紹介します。

# AccessModeとは

PVC/PVにて使用するオプションで、これによりマウントする際のアクセス権が指定されます。
Kubernetes v1.21までは`ReadWriteOnce (RWO)`, `ReadOnlyMany (ROX)`, `ReadWriteMany (RWX)`の3つがありました。

![accessmode.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229880/a5c0a5aa-de1f-e97c-135f-a7c822611b20.png)

このAccessModeに、Kubernetes v1.22 ではアルファ機能として`ReadWriteOncePod (RWOP)`が新たに追加されました。

## ReadWriteOnceとReadWriteOncePodの違い

`ReadWriteOnce`と`ReadWriteOncePod`は、一見似ているのですが、対象がNodeかPodなのかが異なります。

- ReadWriteOnce
  - 1つの**Node**からRead/Writeでマウントできる
- ReadWriteOncePod
  - 1つの**Pod**からRead/Writeでマウントできる

言い換えると、`ReadWriteOnce`は、同じNode上のPodであればRead/Writeでマウントできます。
以下に図で示します。

![rwo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229880/0191a17e-33f3-c7c9-7998-74896df7f65a.png)

`ReadWriteOnce`はNode2のようなことが可能となります。
それに対し、新たに追加された`ReadWriteOncePod`は、1PodからのみRead/Writeできるように制限されます。

`ReadWriteOnce`又は`ReadWriteOncePod`を利用するケースの多くは、ブロックストレージを利用する場合ではないでしょうか。
ブロックストレージは、ロックのメカニズムを持っていないため、1台のホスト(Node)からのみWriteされるように構成を組む必要があります。

![blockstorage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229880/a6d6cd5e-ea86-a05d-8049-545e7ad3790f.png)

そのため、ストレージ視点だと場合、Writeを発行するNodeは1台と保証するAccessModeである`ReadWriteOnce`が必要となります。
しかし、上記のように同じNode上のPodからは同時にWriteできてしまうため、注意が必要でした。
(StatefulSetのVolumeClaimTemplateを利用している場合は1Pod/PVC/PVとなるため、本構成にはなりません)

この`ReadWriteOnce`は、ストレージ視点では正しいかと思うのですが、Pod視点で見た場合には、少々不自然な印象でした。
Kubernetesでは、インフラを抽象化したモデルで操作するのを特徴としているにも関わらず、ストレージのマウント構成が滲みでてしまっています。
ユーザは、同じ`ReadWriteOnce`のPVC/PVを同じNodeでマウントしないように注意しなければならず、インフラを意識せざるを得ませんでした。
そこで、登場したのが`ReadWriteOncePod`です。`ReadWriteOncePod`は、Pod視点で1Pod/PVC/PVとなるように設計されたAccess Modeになります。

# 動作検証
本検証では、上記で説明した`ReadWriteOnce`と`ReadWriteOncePod`の違いを検証します。

## 検証環境の構築

minikube v1.22.0 を使い、2node構成のKubernetes v1.22.0をセットアップします。
また、`ReadWriteOncePod`はアルファ機能のため、feature-gatesで有効化します。

```
$ minikube start --driver=virtualbox --kubernetes-version=1.22.0 --nodes 2 --feature-gates='ReadWriteOncePod=true'
```

上記コマンドで、構築したKubernetesの構成は以下となっています。

```
$ kubectl get node,sc
NAME                STATUS   ROLES                  AGE   VERSION
node/minikube       Ready    control-plane,master   45m   v1.22.0
node/minikube-m02   Ready    <none>                 43m   v1.22.0

NAME                                             PROVISIONER                RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
storageclass.storage.k8s.io/standard (default)   k8s.io/minikube-hostpath   Delete          Immediate           false                  45m
```

## ReadWriteOnceの検証
まずは、`ReadWriteOnce`を検証します。
PVCのManifest(`RWO-PVC1.yaml`, `RWO-PVC2.yaml`)を用意します。
`RWO-PVC1.yaml`と`RWO-PVC2.yaml`はNameのみ異なっています。

- RWO-PVC1.yaml

```YAML
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc1
spec:
  storageClassName: standard
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
```

- RWO-PVC2.yaml

```YAML
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc2
spec:
  storageClassName: standard
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
```

上記PVCのManifestをデプロイします。

```
$ kubectl apply -f RWO-PVC1.yaml -f RWO-PVC2.yaml 
persistentvolumeclaim/pvc1 created
persistentvolumeclaim/pvc2 created
```

つづいて、pvc1をマウントするPod(`pod1`)とpvc2を同じNodeでマウントするPod(`pod2`, `pod3`)を立ち上げます。
以下にManifestを示します。
NodeSelectorを使いデプロイするNodeを指定しています。

- pod1.yaml

```
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: pod1
  name: pod1
spec: 
  nodeSelector:
    kubernetes.io/hostname: minikube
  containers:
  - image: ubuntu:18.04
    name: pod1
    command:
    - sleep
    - infinity
    volumeMounts:
    - mountPath: /mnt/data
      name: myvolume
  volumes:
  - name: myvolume
    persistentVolumeClaim:
      claimName: pvc1
```

- pod2.yaml

```
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: pod2
  name: pod2
spec: 
  nodeSelector:
    kubernetes.io/hostname: minikube-m02
  containers:
  - image: ubuntu:18.04
    name: pod2
    command:
    - sleep
    - infinity
    volumeMounts:
    - mountPath: /mnt/data
      name: myvolume
  volumes:
  - name: myvolume
    persistentVolumeClaim:
      claimName: pvc2
```

- pod3.yaml

```
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: pod3
  name: pod3
spec: 
  nodeSelector:
    kubernetes.io/hostname: minikube-m02
  containers:
  - image: ubuntu:18.04
    name: pod3
    command:
    - sleep
    - infinity
    volumeMounts:
    - mountPath: /mnt/data
      name: myvolume
  volumes:
  - name: myvolume
    persistentVolumeClaim:
      claimName: pvc2
```

pod2.yamlとpod3.yamlは同一Node上で同じPVCをマウントするため、Pod名やコンテナ名しか違いはありません。

```diff
 diff -u pod2.yaml pod3.yaml 
--- pod2.yaml   2021-08-31 15:36:55.000000000 +0900
+++ pod3.yaml   2021-08-31 15:37:18.000000000 +0900
@@ -2,14 +2,14 @@
 kind: Pod
 metadata:
   labels:
-    run: pod2
-  name: pod2
+    run: pod3
+  name: pod3
 spec: 
   nodeSelector:
     kubernetes.io/hostname: minikube-m02
   containers:
   - image: ubuntu:18.04
-    name: pod2
+    name: pod3
     command:
     - sleep
     - infinity
```

PodのManifestをデプロイします。

```
$ kubectl apply -f pod1.yaml -f pod2.yaml -f pod3.yaml 
pod/pod1 created
pod/pod2 created
pod/pod3 created
```

Pod,PVC,PVを確認します。

```
$ kubectl get pod,pvc,pv -o wide
NAME       READY   STATUS    RESTARTS   AGE   IP           NODE           NOMINATED NODE   READINESS GATES
pod/pod1   1/1     Running   0          15s   10.244.0.4   minikube       <none>           <none>
pod/pod2   1/1     Running   0          15s   10.244.1.5   minikube-m02   <none>           <none>
pod/pod3   1/1     Running   0          15s   10.244.1.6   minikube-m02   <none>           <none>

NAME                         STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE   VOLUMEMODE
persistentvolumeclaim/pvc1   Bound    pvc-ffd4f1aa-eb86-4ac4-8355-56e57a74a43a   1Gi        RWO            standard       22m   Filesystem
persistentvolumeclaim/pvc2   Bound    pvc-a3bc28e8-752c-4457-b788-6a4f057f9fbe   1Gi        RWO            standard       22m   Filesystem

NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM          STORAGECLASS   REASON   AGE   VOLUMEMODE
persistentvolume/pvc-a3bc28e8-752c-4457-b788-6a4f057f9fbe   1Gi        RWO            Delete           Bound    default/pvc2   standard                22m   Filesystem
persistentvolume/pvc-ffd4f1aa-eb86-4ac4-8355-56e57a74a43a   1Gi        RWO            Delete           Bound    default/pvc1   standard                22m   Filesystem
```

Node(`minikube-m02`)上に1つのPVC(`pvc2`)をマウントする2つのPod(`pod2`, `pod3`)が出来ています。
次に、`pod2`からマウントしたVolume上のファイルに書き込み(Write)をしてみます。

```
$ kubectl exec -ti pod2 -- sh -c 'echo "Hello" >/mnt/data/hoge'
```

続いて、`pod3`から同じファイル(`/mnt/data/hoge`)に対して追加で書き込みを行います。

```
$ kubectl exec -ti pod3 -- sh -c 'echo "Hoge" >>/mnt/data/hoge'
```

次に、`pod2`から、文字列を書き込んだファイル(`/mnt/data/hoge`)の中身を確認してみます。

```
$ kubectl exec -ti pod2 -- cat /mnt/data/hoge
Hello
Hoge
```

同じVolumeへ書き込みを行なっているため、`pod2`と`pod3`から書き込んだ文字列が格納されています。
ストレージからすれば、Node上で動作するドライバ(SCSIなど)からWriteされた順でデータを格納するため、障害にはなりませんが、Pod上で動作するアプリからみたら、同一ファイルをロックなしにデータを書き込むため、データ破損のリスクがある危険な構成です。

Pod,PVC,PVを削除します。

```
$ kubectl delete pod --all
pod "pod1" deleted
pod "pod2" deleted
pod "pod3" deleted

$ kubectl delete pvc --all
persistentvolumeclaim "pvc1" deleted
persistentvolumeclaim "pvc2" deleted
```

## ReadWriteOncePodの検証

続いて、Kubernetes v1.22で新しくアルファ機能として登場した`ReadWriteOncePod`を試します。
まず、`ReadWriteOncePod`のAccessModeを指定したPVC(`pvc3`)を作成します。
Manifestは以下です。

- RWOP-PVC3.yaml

```YAML
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc3
spec:
  storageClassName: standard
  accessModes:
    - ReadWriteOncePod
  resources:
    requests:
      storage: 1Gi
```

Manifestをデプロイします。

```
$ kubectl apply -f RWOP-PVC3.yaml 
persistentvolumeclaim/pvc3 created
```

PVC/PVを確認します。

```
$ kubectl get pvc,pv
NAME                         STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/pvc3   Bound    pvc-611e7784-e5d7-4b20-b994-445fa1a22083   1Gi        RWOP           standard       7s

NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM          STORAGECLASS   REASON   AGE
persistentvolume/pvc-611e7784-e5d7-4b20-b994-445fa1a22083   1Gi        RWOP           Delete           Bound    default/pvc3   standard                7s
```

`ReadWriteOncePod (RWOP)`でPVC/PVが作成されていることが確認できます。
次に、作成したPVC/PVをマウントするPod(`pod4`)を作成します。
Manifestは以下です。

- pod4.yaml

```YAML
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: pod4
  name: pod4
spec: 
  nodeSelector:
    kubernetes.io/hostname: minikube
  containers:
  - image: ubuntu:18.04
    name: pod4
    command:
    - sleep
    - infinity
    volumeMounts:
    - mountPath: /mnt/data
      name: myvolume
  volumes:
  - name: myvolume
    persistentVolumeClaim:
      claimName: pvc3
```

Manifestをデプロイし、Pod,PVC,PVを確認します。

```
$ kubectl get pod,pvc,pv -o wide
NAME       READY   STATUS    RESTARTS   AGE   IP           NODE       NOMINATED NODE   READINESS GATES
pod/pod4   1/1     Running   0          21s   10.244.0.6   minikube   <none>           <none>

NAME                         STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE    VOLUMEMODE
persistentvolumeclaim/pvc3   Bound    pvc-611e7784-e5d7-4b20-b994-445fa1a22083   1Gi        RWOP           standard       103s   Filesystem

NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM          STORAGECLASS   REASON   AGE    VOLUMEMODE
persistentvolume/pvc-611e7784-e5d7-4b20-b994-445fa1a22083   1Gi        RWOP           Delete           Bound    default/pvc3   standard                103s   Filesystem
```

`ReadWriteOncePod (RWOP)`のAccessModeのPVCをマウントしたPod(`pod4`)が立ち上がりました。
続いて、同じNode(`minikube`)上に同じPVC(`pvc3`)をマウントするPod(`pod5`)を作成してみます。
Manifestは以下です。

- pod5.yaml

```YAML
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: pod5
  name: pod5
spec: 
  nodeSelector:
    kubernetes.io/hostname: minikube
  containers:
  - image: ubuntu:18.04
    name: pod5
    command:
    - sleep
    - infinity
    volumeMounts:
    - mountPath: /mnt/data
      name: myvolume
  volumes:
  - name: myvolume
    persistentVolumeClaim:
      claimName: pvc3
```

なお、先にデプロイしたpod4.yamlとpod5.yamlの差分は名前のみです。

```diff
$ diff -u pod4.yaml pod5.yaml 
--- pod4.yaml   2021-08-31 16:08:51.000000000 +0900
+++ pod5.yaml   2021-08-31 16:13:22.000000000 +0900
@@ -2,14 +2,14 @@
 kind: Pod
 metadata:
   labels:
-    run: pod4
-  name: pod4
+    run: pod5
+  name: pod5
 spec: 
   nodeSelector:
     kubernetes.io/hostname: minikube
   containers:
   - image: ubuntu:18.04
-    name: pod4
+    name: pod5
     command:
     - sleep
     - infinity
```

pod5.yamlのManifestをデプロイします。

```
$ kubectl apply -f pod5.yaml 
pod/pod5 created
```

Podの状態を確認します。

```
$ kubectl get pod -o wide
NAME   READY   STATUS    RESTARTS   AGE     IP           NODE       NOMINATED NODE   READINESS GATES
pod4   1/1     Running   0          5m52s   10.244.0.6   minikube   <none>           <none>
pod5   0/1     Pending   0          30s     <none>       <none>     <none>           <none>
```

pod5は`Pending`となっています。
詳細を確認します。

```
$ kubectl describe pod pod5
...
Events:
  Type     Reason            Age               From               Message
  ----     ------            ----              ----               -------
  Warning  FailedScheduling  3s (x2 over 87s)  default-scheduler  0/2 nodes are available: 2 node has pod using PersistentVolumeClaim with the same name and ReadWriteOncePod access mode.
```

同じPVCの名前を持つPodが既に存在するため、マウントできるNodeがなく、スケジューリングできずに`Pending`となっていることが確認できます。
`ReadWriteOncePod`が正しく動作していることが確認できました。

# 感想

本レポートでは、Kubernetes v1.22で新たに登場したAccessMode(`ReadWiteOncePod`)を試しました。
これまで、`ReadWriteOnce`がNode単位の制御であったため、データ破損のリスクのある構成を取ることが出来てしまいました。
多くのユーザにとっては、望んでいない構成かと思います。
今回追加された`ReadWiteOncePod`により、より安全に1Pod/PVC/PVを保証するAccessModeが出来たことで、ブロックストレージを利用する多くのPVC/PVは、今後`ReadWiteOnce`の代わりに`ReadWiteOncePod`を利用するケースが増えてくるかと推察します。
`ReadWriteOnce`と`ReadWriteOncePod`の違いを抑え、安全にPVC/PVを利用すると良いでしょう。