k8s Persistent Volumes の ReadWriteOnce は単一 Pod からのアクセスに絞るための設定じゃない

kubernates 1.17。

$ kubectl version
Client Version: version.Info{Major:"1", Minor:"17", GitVersion:"v1.17.2", GitCommit:"59603c6e503c87169aea6106f57b9f242f64df89", GitTreeState:"clean", BuildDate:"2020-01-18T23:30:10Z", GoVersion:"go1.13.5", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"17", GitVersion:"v1.17.3", GitCommit:"06ad960bfd03b39c8310aaf92d1e7c12ce618213", GitTreeState:"clean", BuildDate:"2020-02-11T18:07:13Z", GoVersion:"go1.13.6", Compiler:"gc", Platform:"linux/amd64"}

Persistent Volumes の accessModes

Persistent Volumes(以下 PV) には accessModes という設定項目がある。
参考 Persistent Volumes

ReadWriteOnce – the volume can be mounted as read-write by a single node
ReadOnlyMany – the volume can be mounted read-only by many nodes
ReadWriteMany – the volume can be mounted as read-write by many nodes

そしてこれは「Pod から見て PV がどう見えるか?」ではなくて「PV をどの単位で公開するか?」という視点で書かれている。つまり ReadWriteOnce を使ったからといって、単一 Pod だけのアクセスを許すようになるわけではなく、公開範囲の複数の Pod が PV を利用できる。

k8s の Issue でも次のようなコメントがあった。
参考 Multiple pods able to write to PV with accessMode of ReadWriteOnce

AccessModes as defined today, only describe node attach (not pod mount) semantics, and doesn't enforce anything

accessModes はノードにアタッチするときの話で、Pod のマウントの範囲とは関係ないよ、ということが書いてある。前述の accessModes の設定をよく見ると、「…by a single node/many nodes」と書いてあって Pod については触れられていないことに気づいたでしょうか。私は気づきませんでした。つまり、これらは PV レベルでの障害を発生させないための制約であって、Pod からのアクセス制御をする仕組みではない

こういった違いなら今までもあった。たとえばブロックストレージは、ファイルシステムがブロック内にスーパーブロック作ったりデータブロック作ったりという管理をしながら利用している。これがもしも他のノードからもマウントされてフォーマットされると、あるファイルシステムが書いたブロックを他のファイルシステムが壊しゃったよトホホ…という状態になる。なので、そういった障害が起きないように、複数ノードから共有ストレージを使うときは NFS を立てて I/O 要求を一元管理/排他制御し、ローカルのファイルシステムに書き込んだりしていた。こういう、低レイヤの話が accessModes の制限事項だと思われる。

検証する

複数の Pod から書き込みができることを確認する。

Pod 内の複数コンテナから書き込む

まず accessModes が ReadWriteOnly な pv/pvc を用意する。

$ kubectl get pv
NAME         CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                STORAGECLASS   REASON   AGE
sample-vol   1Gi        RWO            Recycle          Bound    default/sample-pvc                           57s

$ kubectl get pvc
NAME         STATUS   VOLUME       CAPACITY   ACCESS MODES   STORAGECLASS   AGE
sample-pvc   Bound    sample-vol   1Gi        RWO                           14s

つぎに Pod 内の複数コンテナからアクセスできることを確認する。

apiVersion: v1
kind: Pod
metadata:
  name: sample-pod
spec:
  containers:
  - name: busy1
    image: busybox:musl
    tty: true
    volumeMounts:
    - mountPath: /data1
      name: vol
  - name: busy2
    image: busybox:musl
    tty: true
    volumeMounts:
    - mountPath: /data2
      name: vol
  volumes:
  - name: vol
    persistentVolumeClaim:
      claimName: sample-pvc

起動できるし、書き込みもできることがわかる。

$ kubectl create -f pod.yaml
pod/sample-pod created

$ kubectl get pods
NAME         READY   STATUS    RESTARTS   AGE
sample-pod   2/2     Running   0          3s

$ kubectl exec -it sample-pod -c busy1 -- touch /data1/busy1
$ kubectl exec -it sample-pod -c busy2 -- touch /data2/busy2
$ kubectl exec -it sample-pod -c busy1 -- ls /data1
busy1  busy2

ReplicaSet の複数 Pod から書き込む

Deployment で replicas で 2 以上を設定した場合、複数のコンテナ が pv/pvc を共有する。このときも同様にアクセスできることを確認する。

deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: busybox-deployment
  labels:
    app: busy
spec:
  replicas: 3
  selector:
    matchLabels:
      app: busy
  template:
    metadata:
      labels:
        app: busy
    spec:
      containers:
      - name: busy
        image: busybox:musl
    tty: true
        volumeMounts:
        - mountPath: /data2
          name: vol
      volumes:
      - name: vol
        persistentVolumeClaim:
          claimName: sample-pvc

起動できるし、書き込みできるのがわかる。

$ kubectl apply -f pod.yaml
pod/sample-pod created

$ kubectl get pods -o wide
NAME                                READY   STATUS    RESTARTS   AGE     IP                NODE              NOMINATED NODE   READINESS GATES
busybox-deployment-78887649-2g46b   1/1     Running   0          2m20s   192.168.130.1     ip-172-30-0-112   <none>           <none>
busybox-deployment-78887649-cv7zv   1/1     Running   0          2m20s   192.168.130.63    ip-172-30-0-112   <none>           <none>
busybox-deployment-78887649-s2r4c   1/1     Running   0          2m20s   192.168.165.140   ip-172-30-0-246   <none>           <none>

$ kubectl exec -it sample-pod busybox-deployment-78887649-2g46b -- touch /data2/2g46b
$ kubectl exec -it busybox-deployment-78887649-cv7zv -- touch /data2/cv7zv
$ kubectl exec -it busybox-deployment-78887649-s2r4c --  ls /data2
2g46b  cv7zv

※ なぜか Pod とPV がノードをまたいでますが、これは accessModes が効くのは Pod のスケジューリングのタイミングだから、だと思います。まだマージされてませんが、fix: adding csi link and correct some words used to clarify access mode.というドキュメントの修正PR があり、そこに書いてました。

今回の NFS は ReadWriteMany に対応してるので全ノードから同じデータにアクセスできましたが、 hostPath などの ReadWriteMany に対応していない PV は、データがノードまたいで見えることはありませんでした。さすがに。

replicas = 1 なら…?

じゃあ replicas=1 なら問題ないかというと、それもダメ。なぜなら、オートヒーリングやローリングアップデート時に瞬間的に同じ Pod が立ち上がる可能性があるから。

今回は docker stop で無理やり Pod を止めてみたところ、次のように複数 Pod が起動された。

$ kubectl get pods -o wide  -w
NAME                                READY   STATUS    RESTARTS   AGE     IP               NODE              NOMINATED NODE   READINESS GATES
busybox-deployment-78887649-9lhpr   1/1     Running   0          3m11s   192.168.130.15   ip-172-30-0-112   <none>           <none>
busybox-deployment-78887649-9lhpr   0/1     Error     0          3m23s   192.168.130.15   ip-172-30-0-112   <none>           <none>
busybox-deployment-78887649-9lhpr   1/1     Running   1          3m24s   192.168.130.15   ip-172-30-0-112   <none>           <none>

Deployment での ReadWriteOnly の利用

若干わき道にそれるが、Deployment で ReadWriteOnly を使う場合、ホスト障害が発生するとダウンタイムが発生する。前の Pod がノード単位の PV をつかんでいるので、別の ノードに Pod をスケジュールできないため。というかそもそもの話として、 Deployment はステートレスなコンテナを扱うために設計されてるらしい。

Deployment はステートレス アプリケーション用に設計されているため、同じ永続ボリューム要求が Deployment のすべてのレプリカで共有されます。作成されたレプリカポッドはそれぞれ同一なので、この設定では ReadOnlyMany モードと ReadWriteMany モードのボリュームだけが動作します。
引用元: Google Cloud 永続ディスクを使用した永続ボリューム

検証のまとめ

いろいろやったが、 accessModes は Pod からの書き込みを絞る仕組みではないだろう、という確からしさが高まった。ただし「このパラメータって Pod に対する設定っぽいよね」という Issue もあるようなので、今後変わるかもしれない。自分も紛らわしいと思う。
参考 Issue with k8s.io/docs/concepts/storage/persistent-volumes/

アプリケーションレベルでの排他

PV 側で accessModes が適切に扱われていれば、PV レベルでの整合性は担保できる。しかしそれはあくまで PV の話。上述したように複数の Pod から書き込みができる以上は、アプリケーションレベルでの整合性が保てるわけではない。PV が同じ設定で同じ場所にマウントされていて Pod で同じアプリケーションが動いているので、意図せずデータを上書きする可能性がある。これは accessModes が ReadWriteMany の場合でも同じことだと思う。

ということでアプリケーションレベルでの排他を考えると、 Pod 単位で PV を占有したいことがある。こんなときに使えるのが StatefulSet らしい。
Google Cloud 永続ディスクを使用した永続ボリューム
Why StatefulSets? Can't a stateless Pod use persistent volumes?

StatefulSet については今後勉強します。