データベースの Pod を作り直しても、保存したデータは残したい。 そのためには、コンテナとは別にデータの保存先を用意します。 Kubernetes では、欲しい容量などを書いた PersistentVolumeClaim (PVC) でストレージを要求できます。
その要求から、ディスクの用意、ノードへの接続、コンテナから使える状態になるまでに、何が起きるのでしょうか。 この章では実際に PVC を作り、ストレージ用の取り決めである CSI と、各段階の担当を見ていきます。
GOAL この章のゴール
- ストレージの用意、ノードへの接続、マウントの担当を区別できる
- ストレージを要求する設定と、確保された保存先の関係が分かる
- PVC を作り、保存先が割り当てられる様子を観察できる
4 つのリソース
「どんな保存先を、どのくらい欲しいか」という要求と、「実際に何が確保されたか」は、別々のリソースで表します。 ディスクを要求してから使えるようになるまでを、4 つのリソースに分けて見ます。
- StorageClass:管理者が「どの driver (
provisioner) で、どんなパラメータのボリュームを作るか」を書いたカタログ。EBS ならgp3かio2か、暗号化するか、にあたります。 - PersistentVolumeClaim (PVC):利用者が書く「このくらいの容量をこのクラスで欲しい」という要求。kubernetes.io は「Pod がノードの資源を消費するように、PVC は PV の資源を消費する」と説明しています。
- PersistentVolume (PV):実際に確保されたボリュームを表すクラスタ側のリソース。EBS ボリュームそのものに対応し、Pod とは独立した寿命を持ちます。
- VolumeAttachment:「このボリュームをこのノードに attach (または detach) せよ」という意図を表すリソース。EBS を EC2 インスタンスにアタッチする操作に対応します。
PVC と PV の結び付きを kubernetes.io は binding と呼び、1 対 1 で、合う PV が無い間 PVC は unbound のまま待ち続ける、と説明しています。 管理者が PV を先に作っておく静的な方法もありますが、この章で追うのは、StorageClass の指定でボリュームが自動で作られる動的な方法です。 利用者が触るのは PVC だけで、PV は driver 側のプログラムが作り、VolumeAttachment は Kubernetes 本体の controller が作ります。
Controller plugin と Node plugin
CSI は、Kubernetes のようなオーケストレーターに任意のストレージを繋ぐための標準です。 kubernetes-csi のドキュメントは、CSI 以前はストレージベンダーが Kubernetes 本体のコードに手を入れる必要があったのに対し、CSI では本体に触れずに driver を書いて配れる、と説明しています。
CSI の driver は gRPC で 3 つのサービスを提供します。
Identity は自己紹介 (GetPluginInfo や Probe)、Controller はボリュームの作成とノードへの接続 (CreateVolume や ControllerPublishVolume)、Node はノード上でのマウント (NodeStageVolume や NodePublishVolume) です。
CSI の仕様は、Node サービスは「ボリュームが publish されるノード上で動かなければならない」、Controller サービスは「どこで動いてもよい」と定めています。
同じバイナリを 2 箇所で動かし、役割を分けるのはそのためです。
どの呼び出しも、再試行されても同じ結果になること (冪等) が仕様で求められています。
Node plugin の呼び方は CRI と同じで、kubelet が /var/lib/kubelet/plugins/<driver>/csi.sock に繋いで NodeStageVolume と NodePublishVolume を呼びます。
Controller plugin の呼び方は CRI と違います。
kubernetes-csi のドキュメントによると、コントロールプレーンのコンポーネントは driver と直接は話さず、Kubernetes API だけを相手にします。
そこで driver と同じ Pod に sidecar と呼ばれる小さな controller を同居させ、sidecar が apiserver を watch し、PVC の作成のような変化を見つけるたびに、対応する gRPC (たとえば CreateVolume) を driver に送ります。
SIG Storage が保守する sidecar は、次のような分担です。
- external-provisioner:PVC を watch し、StorageClass の
provisionerが driver の名前 (GetPluginInfoの応答) と一致したらCreateVolumeを呼び、PV を作る。 - external-attacher:VolumeAttachment を watch し、
ControllerPublishVolumeを呼んでstatus.attachedを書く。 - external-resizer:PVC の容量変更を watch し、
ControllerExpandVolumeを呼ぶ。 - external-snapshotter:VolumeSnapshot を watch し、
CreateSnapshotを呼ぶ。 - node-driver-registrar:Node plugin の socket を kubelet に登録する (
/var/lib/kubelet/plugins_registry/経由)。 - livenessprobe:driver の死活を確かめ、異常なら Pod の再起動を促す。
この分担のおかげで、driver の作者が実装するのは「ボリュームを作れ」「このノードに繋げ」「このパスにマウントしろ」という gRPC だけで、Kubernetes 側のリソース (VolumeAttachment など) が変わっても、更新するのは共通の sidecar だけで済みます。
PVC から mount まで
provision: PVC → PV
PVC が作られると、StorageClass のprovisionerに一致する driver の external-provisioner が気付き、CreateVolumeを呼びます。成功したら PV オブジェクトを作り、PVC と bind します。この時点で PVC は Bound です。attach: PV → ノード
PVC を使う Pod がスケジュールされてノードが決まると、kube-controller-manager の attach/detach controller が VolumeAttachment を作ります。external-attacher がそれを見てControllerPublishVolumeを呼び、ストレージ側でボリュームをノードに接続します。EBS ならここで新しいブロックデバイスが EC2 に生えます。mount: ノード → Pod
kubelet が Node plugin にNodeStageVolumeとNodePublishVolumeを呼びます。前者はノード上の共通パス (staging path) へのマウントで、CSI の仕様はこれを「ノードごとに 1 つの global mount」と呼びます。後者はそこから Pod ごとのパス (target path) へのマウントです。コンテナはそのパスを volume として受け取ります。
StorageClass の volumeBindingMode が WaitForFirstConsumer のときは、1 と 2 の順番が入れ替わり、Pod のスケジュール先が決まってから provision します。
kubernetes.io は、この遅延は AZ のような場所の制約があるストレージで、Pod が置かれる場所に合わせてボリュームを作るために必要だと説明しています。
AZ をまたいで attach できない EBS が典型です。
既定の Immediate なら、PVC を作った時点で Pod の配置と無関係に PV が作られます。
PV を使い終わった後の扱いは reclaimPolicy が決め、Delete (既定) なら PV と裏のボリュームが消え、Retain なら残って手動の片付けが要ります。
in-tree からの移行
CSI が入る前、EBS や GCE PD のボリュームプラグインは kube-controller-manager と kubelet の中にありました。
CRI の章で見た dockertools と同じ構造です。
移行は 2 段階で進みました。
まず CSIMigration という仕組みで、awsElasticBlockStore: のような既存の PV 定義をそのまま受け取り、処理だけを対応する CSI driver に転送する。
kubernetes.io はこれを「既存の in-tree プラグインに対する操作を、対応する CSI プラグインに向け直す」と説明しています。
利用者は PV や StorageClass を書き換えずに driver を入れるだけで済みます。
その後、in-tree のコードを削除する。
クラウドのディスク以外にも使われる CSI
CSI の接続先は EBS などのクラウドサービスに限りません。 Rook は Kubernetes 上で Ceph を運用する operator で、Pod へのボリューム提供には Ceph-CSI を使います。 Ceph 自体を管理する仕事と、PVC の要求を Ceph のボリュームへ変換する仕事が分かれています。 Ceph が Kubernetes の外で運用されている構成でも、Ceph-CSI で接続できます。
Longhorn も、Kubernetes 上でボリュームとその複製を管理し、CSI plugin からの要求を受けるストレージ実装です。 CSI が揃えているのはボリュームを用意して接続する手順です。 データの複製方法、障害時の復旧、バックアップ、性能まで揃える仕様ではないので、同じ PVC を書けても運用上の性質は接続先によって変わります。
このクラスタの kubevirt-csi-driver
あなたのクラスタ (tenant クラスタ) は、管理クラスタ上の KubeVirt の VM の上で動いています。 kubevirt-csi-driver の README は、この driver を「KubeVirt の VM の上に作った tenant クラスタが、その下の管理クラスタから永続データを得るためのもの」と説明しています。
Controller plugin は管理クラスタの API を呼び、tenant の VM が居る namespace に DataVolume (管理クラスタ側の PVC) を作り、あなたのノードである VM に、稼働中のままディスクとして接続します (README はこのために KubeVirt 側で HotplugVolumes の機能を有効にするよう求めています)。 attach の段が「VM にディスクを挿す」操作に対応するわけです。 Node plugin は VM の中で、そのディスクをフォーマットして Pod にマウントします。 README によると Controller plugin は管理クラスタ側にも tenant クラスタ側にも置けて、前者なら tenant クラスタに管理クラスタの認証情報を渡さずに済みます。 1 つの PVC の裏に、2 つのクラスタの CSI が重なっています。
実機: PVC を作って Bound を見る
まず driver とクラスの登録を確認します。 CSIDriver は driver が自分を名乗るリソース、CSINode は各ノードにどの driver の Node plugin が登録済みかを表すリソースです。
kubectl get csidrivers,csinodes,storageclasses出力例
NAME ATTACHREQUIRED PODINFOONMOUNT ...
csidriver.storage.k8s.io/csi.kubevirt.io true false
NAME DRIVERS AGE
csinode.storage.k8s.io/<node> 1 3h
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ...
kubevirt (default) csi.kubevirt.io Delete WaitForFirstConsumerATTACHREQUIRED: true は、この driver が attach の段 (VolumeAttachment と ControllerPublishVolume) を必要とすることを表します。
ホットプラグがまさにそれです。
VOLUMEBINDINGMODE が WaitForFirstConsumer なら、PVC を作っただけでは Pending のままで、Pod が付いてから provision されます。
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
name: writer
spec:
containers:
- name: app
image: busybox:1.37
command: ["sh", "-c", "date >> /data/log; sleep 3600"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: data
EOF
kubectl get pvc,pv,volumeattachments -w出力例
NAME STATUS VOLUME CAPACITY STORAGECLASS
pvc/data Pending kubevirt
pvc/data Bound pvc-3f1c2a9b-... 1Gi kubevirt
NAME CAPACITY STATUS CLAIM
pv/pvc-3f1c2a9b-... 1Gi Bound default/data
NAME ATTACHER PV NODE ATTACHED
volumeattachment/csi-8a7b... csi.kubevirt.io pvc-3f1c2a9b-... <node> trueCtrl-C で watch を止めてください。
Pending → Bound の間に external-provisioner が CreateVolume を呼び、VolumeAttachment の ATTACHED: true になるまでに external-attacher が ControllerPublishVolume を呼んでいます。
3 段が全部終わると Pod が Running になります。
kubectl describe pvc data | sed -n '/Events/,$p'
kubectl describe pod writer | sed -n '/Events/,$p'出力例
Normal WaitForFirstConsumer waiting for first consumer to be created before binding
Normal Provisioning External provisioner is provisioning volume for claim "default/data"
Normal ProvisioningSucceeded Successfully provisioned volume pvc-3f1c2a9b-...
...
Normal SuccessfulAttachVolume AttachVolume.Attach succeeded for volume "pvc-3f1c2a9b-..."
Normal Started Started container app片付けは Pod → PVC の順です。
PV は StorageClass の RECLAIMPOLICY: Delete に従って自動で消え、裏のボリュームも external-provisioner が DeleteVolume を呼んで消します。
kubectl delete pod writer && kubectl delete pvc dataふりかえり
QPVC を watch して CreateVolume を呼ぶのは誰?
Controller 側の処理は sidecar が担います。external-provisioner が PVC を watch して driver の CreateVolume を呼び、PV を作って bind します。
QVolumeAttachment が表しているのは?
Pod のスケジュール先が決まると attach/detach controller が VolumeAttachment を作り、external-attacher がそれを見て ControllerPublishVolume を呼びます。EBS を EC2 にアタッチする操作に対応します。
QCSI driver の作者が Kubernetes の API を知らなくてよいのはなぜ?
driver が実装するのは CreateVolume や NodePublishVolume などの gRPC だけです。PVC や VolumeAttachment を扱うのは共通の sidecar と kubelet で、Kubernetes 側のリソースが変わっても driver は影響を受けません。
参考
-
Persistent Volumes, Kubernetes Documentation: https://kubernetes.io/docs/concepts/storage/persistent-volumes/
-
Storage Classes, Kubernetes Documentation: https://kubernetes.io/docs/concepts/storage/storage-classes/
-
Volumes (CSI), Kubernetes Documentation: https://kubernetes.io/docs/concepts/storage/volumes/#csi
-
VolumeAttachment, Kubernetes API Reference: https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/volume-attachment-v1/
-
Container Storage Interface (CSI) Specification: https://github.com/container-storage-interface/spec/blob/master/spec.md
-
Introduction, Kubernetes CSI Developer Documentation: https://kubernetes-csi.github.io/docs/introduction.html
-
Sidecar Containers, Kubernetes CSI Developer Documentation: https://kubernetes-csi.github.io/docs/sidecar-containers.html
-
Deploying CSI Driver on Kubernetes, Kubernetes CSI Developer Documentation: https://kubernetes-csi.github.io/docs/deploying.html
-
CSI external-provisioner: https://kubernetes-csi.github.io/docs/external-provisioner.html
-
CSI external-attacher: https://kubernetes-csi.github.io/docs/external-attacher.html
-
CSI node-driver-registrar: https://kubernetes-csi.github.io/docs/node-driver-registrar.html
-
Container Storage Interface (CSI) 設計文書 (attach/detach controller と VolumeAttachment), kubernetes/design-proposals-archive: https://github.com/kubernetes/design-proposals-archive/blob/main/storage/container-storage-interface.md
-
Kubernetes の変更履歴 (CHANGELOG-1.26 / 1.27 / 1.30): https://github.com/kubernetes/kubernetes/tree/master/CHANGELOG
-
kubevirt/csi-driver README: https://github.com/kubevirt/csi-driver/blob/main/README.md
-
Rook「Storage Architecture」: https://www.rook.io/docs/rook/latest/Getting-Started/storage-architecture/
-
Ceph「Ceph Container Storage Interface (CSI)」: https://docs.ceph.com/en/latest/csi/
-
Longhorn「Architecture and Concepts」: https://longhorn.io/docs/1.13.0/concepts/