Kubernetes 解体新書Part 4 インターフェース

CSI: Pod にディスクが届くまで

保存先を要求してから、Pod で使えるようになるまで。ストレージの用意、接続、マウントの担当を順に追います。

  • L1 Web ターミナル
  • 更新 2026年10月6日

データベースの 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 箇所で動かし、役割を分けるのはそのためです。 どの呼び出しも、再試行されても同じ結果になること (冪等) が仕様で求められています。

CSI の Controller plugin と Node plugin、そして sidecarController plugin は Deployment として動き、external-provisioner と external-attacher の sidecar が apiserver を watch して driver の gRPC を呼ぶ。Node plugin は DaemonSet として各ノードで動き、node-driver-registrar が kubelet に登録し、kubelet が NodeStageVolume と NodePublishVolume を呼ぶ。kube-apiserverController plugin (Deployment、どこか 1 箇所)provisionersidecarattachersidecarCSI driverCreateVolume / ControllerPublishsidecar が PVC / VolumeAttachment を watchNode plugin (DaemonSet、各ノード)kubeletregistrarsidecarCSI driverNodeStage / NodePublishcsi.sock登録kubelet が Pod を watchストレージの API (EBS / Ceph / KubeVirt)作成、ノードへの attachPodappmountdriver は 2 箇所で同じバイナリ。Kubernetes を知るのは sidecar と kubelet だけ
図 1Controller plugin は Deployment でクラスタに 1 箇所、Node plugin は DaemonSet で各ノードに。driver を Kubernetes に繋ぐのは sidecar と kubelet。

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 まで

PVC から Pod のマウントまでの流れ1. PVC を作ると external-provisioner が CreateVolume を呼び PV を作って bind する。2. Pod がスケジュールされると attach/detach controller が VolumeAttachment を作り、external-attacher が ControllerPublishVolume を呼ぶ。3. kubelet が NodeStageVolume と NodePublishVolume を呼び、Pod にマウントされる。1. provision (PVC → PV)PVCPendingwatchexternal-provisionerCreateVolumeCSI driverPV を作って bindPV ⇄ PVC Bound2. attach (ノードに繋ぐ)PodnodeName 決定attach/detach controller が作るVolumeAttachmentstorage.k8s.iowatchattachersidecardriverControllerPublish3. mount (kubelet)kubeletNodeStageNode plugin/var/lib/kubelet/plugins/…NodePublishPod の mount point/var/lib/kubelet/pods/<uid>/volumes/…
図 2provision、attach、mount の 3 段。それぞれ別の controller と gRPC が担当する。
  1. provision: PVC → PV

    PVC が作られると、StorageClass の provisioner に一致する driver の external-provisioner が気付き、CreateVolume を呼びます。成功したら PV オブジェクトを作り、PVC と bind します。この時点で PVC は Bound です。
  2. attach: PV → ノード

    PVC を使う Pod がスケジュールされてノードが決まると、kube-controller-manager の attach/detach controller が VolumeAttachment を作ります。external-attacher がそれを見て ControllerPublishVolume を呼び、ストレージ側でボリュームをノードに接続します。EBS ならここで新しいブロックデバイスが EC2 に生えます。
  3. 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 と同じ構造です。

in-tree ボリュームプラグインから CSI driver への移行左: kube-controller-manager と kubelet の中に AWS EBS や GCE PD のコードがあった。中: CSIMigration が in-tree の API (awsElasticBlockStore など) を CSI driver に転送する。右: in-tree コードは削除され、CSI driver だけが残る。〜 2019 (in-tree)kube-controller-manager / kubeletaws-ebsgce-pdcinderazure-diskベンダーの修正が K8s のリリース待ち2019 〜 2023 (CSIMigration)in-tree の API はそのままawsElasticBlockStore: …転送CSI driver が処理ebs.csi.aws.com既存の PV / PVC を書き換えずに裏側だけ差し替える2026 (out-of-tree)kube-controller-manager / kubeletストレージ固有コード無しCSI (gRPC)CSI driver (別リポジトリ)aws-ebs-csi-driver など1.26 Cinder、GlusterFS / 1.27 EBS、Azure Disk /1.30 vSphere、Azure File / 1.31 RBD、CephFS
図 3in-tree の API (awsElasticBlockStore など) を残したまま、裏側を CSI driver に転送する CSIMigration を経て、in-tree コードが削除された。

移行は 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 クラスタが、その下の管理クラスタから永続データを得るためのもの」と説明しています。

このクラスタの kubevirt-csi-driver: tenant クラスタの PVC が管理クラスタのボリュームになるtenant クラスタで PVC を作ると、kubevirt-csi の Controller plugin が管理クラスタに DataVolume を作り、VM にホットプラグする。tenant 側のノード (VM) にはディスクとして見え、Node plugin がそれを Pod にマウントする。あなたの tenant クラスタ (VM の中)PVCcsi.kubevirt.ioPVBoundkubevirt-csi Controller pluginprovisioner / attacher + driver (管理クラスタ側にも置ける)Node pluginDaemonSetPodappmountVM に見える新しいディスク (/dev/vdX)管理クラスタ (KubeVirt が動く側)DataVolume / PVC管理クラスタの StorageClass でボリュームを作るVirtualMachineInstanceあなたのノード (VM)hotplug volume管理クラスタ側の CSI driverinfraStorageClassName で指定した StorageClass管理クラスタの API を呼ぶVM にディスクが生える
図 4tenant クラスタの PVC に対応するボリュームが管理クラスタ側に作られ、動いている 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 が登録済みかを表すリソースです。

driver の登録と StorageClass を見る
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          WaitForFirstConsumer

ATTACHREQUIRED: true は、この driver が attach の段 (VolumeAttachment と ControllerPublishVolume) を必要とすることを表します。 ホットプラグがまさにそれです。 VOLUMEBINDINGMODE が WaitForFirstConsumer なら、PVC を作っただけでは Pending のままで、Pod が付いてから provision されます。

PVC と、それを使う Pod を作る
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>   true

Ctrl-C で watch を止めてください。 Pending → Bound の間に external-provisioner が CreateVolume を呼び、VolumeAttachment の ATTACHED: true になるまでに external-attacher が ControllerPublishVolume を呼んでいます。 3 段が全部終わると Pod が Running になります。

イベントで 3 段を確かめる
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 は影響を受けません。

参考