はじめに
Kubernetes上でボリュームを調達してPodにマウントして使う場合に、CSI(Container Storage Interface)と呼ばれるインターフェースを使うことが多いです。例えばAmazon EBSを使いたい場合はAmazon EBS CSI Driver、オンプレミスのKubernetesでOpenEBSを使いたい時はOpenEBS CSI Driver等が利用可能です。CSIを使うと、Kubernetes上でStorageClass、PVC、PV等のリソースにより、様々なストレージを共通の方法で利用できるようになります。
今回はCSIがどんな動きをしているのかを、NFSのCSIドライバー(NFS CSI Driver)を使って調べてみました。利用するCSIドライバーは、Kubernetes CSIのGitHubで公開されている以下のcsi-driver-nfsです。
必要条件
以下が準備できている必要があります。本記事では、それぞれの詳細については触れません。
- Kubernetesクラスターを構築済であること
- Kubernetesのノードからアクセス可能なNFSサーバがあること
- kubectlを実行するマシンに、HelmとHelmfileがインストールされていること
csi-driver-nfs のHelmチャートのインストール
以下のHelmfileを使ってインストールします。
repositories:
- name: csi-driver-nfs
url: https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/charts
releases:
- name: csi-driver-nfs
namespace: kube-system
chart: csi-driver-nfs/csi-driver-nfs
version: v4.9.0
values:
- storageClass:
create: true
name: nfs-csi
parameters:
server: 10.0.0.5 # 構築済みのNFSサーバのホスト名を記載
share: /srv/nfs_share # NFSサーバで /etc/exports で共有しているディレクトリを記載
mountOptions:
- nfsvers=4.1
インストールコマンド
helmfile apply -f csi-driver-nfs.yaml
作成されたリソースの確認
ワークロードリソースおよびサービスリソースは、以下の DaemonSet、Deployment が作成されています。
$ kubectl get deploy,ds -n kube-system -l helm.sh/chart=csi-driver-nfs-v4.9.0
NAMESPACE NAME READY UP-TO-DATE AVAILABLE AGE
kube-system deployment.apps/csi-nfs-controller 1/1 1 1 2m9s
NAMESPACE NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
kube-system daemonset.apps/csi-nfs-node 2 2 2 2 2 kubernetes.io/os=linux 2m9s
またストレージに関するリソースとして、以下の nfs-csi
というStorageClass が作成されます。
kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
nfs-csi nfs.csi.k8s.io Delete Immediate false 4m43s
csi-nfs-controller と csi-nfs-node に関して、各Podにどんなコンテナが作られているかを確認すると、以下のように複数立ち上がっているのが分かります。
csi-nfs-controller(Deployment) の Pod で使われているコンテナ
$ kubectl get deploy -n kube-system csi-nfs-controller -o jsonpath='{range .spec.template.spec.containers[*]}- Name: {.name}{"\n"} Image: {.image}{"\n"}{end}'
- Name: csi-provisioner
Image: registry.k8s.io/sig-storage/csi-provisioner:v5.0.2
- Name: csi-snapshotter
Image: registry.k8s.io/sig-storage/csi-snapshotter:v8.0.1
- Name: liveness-probe
Image: registry.k8s.io/sig-storage/livenessprobe:v2.13.1
- Name: nfs
Image: registry.k8s.io/sig-storage/nfsplugin:v4.9.0
csi-nfs-node(DaemonSet) の Pod で使われているコンテナ
$ kubectl get ds -n kube-system csi-nfs-node -o jsonpath='{range .spec.template.spec.containers[*]}- Name: {.name}{"\n"} Image: {.image}{"\n"}{end}'
- Name: liveness-probe
Image: registry.k8s.io/sig-storage/livenessprobe:v2.13.1
- Name: node-driver-registrar
Image: registry.k8s.io/sig-storage/csi-node-driver-registrar:v2.11.1
- Name: nfs
Image: registry.k8s.io/sig-storage/nfsplugin:v4.9.0
この状態を図にしてみましょう。
色分けしたそれぞれのコンテナは、以下の機能を持ちます。
Kubernetes CSI Sidecar Container(オレンジ色)
NFSドライバに限らず、他のCSIドライバでも共通に利用するサイドカーコンテナです。Kubernetes APIと通信して、その内容を後述のCSI driver containerに伝えます。正式な情報はこちらを参照ください。
Third-party CSI Driver Container(水色)
本記事の例では「NFS用のCSIプラグインとしてNFSストレージ固有に使われるコンテナ」ですが、一般的には CSI Driver と呼ばれています。サイドカーコンテナから届いたリクエストに応じて、実際のストレージにボリュームを作ったりマウントしたりします。
こちらはサイドカーコンテナとは異なり、利用するストレージによって各ベンダーから提供されるもの(例えばAmazon EBS用とかCephFS用とか)を使います。
CSIドライバーはこのように、「ストレージ固有のCSI Driver」と「Kubernetes APIと通信するサイドカーコンテナ」が連携して動作する仕組みになっています。各コンテナで役割を分けることで、CSI driverを開発するベンダーは Kubernetes API の仕様や通信を気にせずに、ストレージ固有のロジックに集中して実装できるメリットがあります。
PVCを作ってみる
それでは、さきほどインストールした NFS CSI driver を使ってPVCを作ってみましょう。
以下のマニフェストを用意します。storageClassName
を nfs-csi
に指定するのがポイントです。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-pvc
namespace: default
spec:
storageClassName: nfs-csi
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
実行コマンド
$ kubectl apply -f nfs-pvc.yaml
persistentvolumeclaim/nfs-pvc created
PVCだけでなく、動的プロビジョニングによりPVも作られました。
$ kubectl get pvc,pv -n default
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
persistentvolumeclaim/nfs-pvc Bound pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a 1Gi RWX nfs-csi <unset> 6h9m
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE
persistentvolume/pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a 1Gi RWX Delete Bound default/nfs-pvc nfs-csi <unset> 6h9m
NFSサーバが起動しているホスト上の端末でも、次のようにPVに対応するディレクトリが作られたのを確認できます。
[host-of-nfs-server]$ ls -l /srv/nfs_share/
total 4
drwxr-xr-x 2 nobody nogroup 4096 Nov 8 22:29 pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a
どのようにしてボリュームが作られたのか?
PVCが作られた際に、CSIドライバーを使ってボリュームがどのように作られるかを図にしてみました。ボリューム作成で関係するのは csi-nfs-controller
のDeploymentリソースのみです。
PVCが作られると、csi-provisioner
コンテナ がPVCが作られたというイベントを検知します。その内容をCSI Driverの nfs
コンテナ に通知して、NFSボリュームが作られるという流れです。
上記の流れを、実際にログで確認してみましょう。
ログで確認: (1)PVC作成を検知
csi-provisioner
のログを確認すると、
kubectl logs -n kube-system deploy/csi-nfs-controller -c csi-provisioner
以下ログの1行目で、PVC作成のイベントを検知しているのがわかります。
I1108 22:29:25.621276 1 event.go:389] "Event occurred" object="default/nfs-pvc" fieldPath="" kind="PersistentVolumeClaim" apiVersion="v1" type="Normal" reason="Provisioning" message="External provisioner is provisioning volume for claim \"default/nfs-pvc\""
I1108 22:29:26.169109 1 controller.go:955] successfully created PV pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a for PVC nfs-pvc and csi volume name 10.0.0.5#srv/nfs_share#pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a##
I1108 22:29:26.179005 1 event.go:389] "Event occurred" object="default/nfs-pvc" fieldPath="" kind="PersistentVolumeClaim" apiVersion="v1" type="Normal" reason="ProvisioningSucceeded" message="Successfully provisioned volume pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a"
ログで確認: (2)ボリューム作成リクエスト ~ (3)ボリューム作成 ~ (4)PV作成
nfs
のログを確認すると、
kubectl logs -n kube-system deploy/csi-nfs-controller -c nfs
以下のログが出力されます。
I1108 22:29:25.622630 1 utils.go:110] GRPC call: /csi.v1.Controller/CreateVolume
I1108 22:29:25.622698 1 utils.go:111] GRPC request: {"capacity_range":{"required_bytes":1073741824},"name":"pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a","parameters":{"csi.storage.k8s.io/pv/name":"pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a","csi.storage.k8s.io/pvc/name":"nfs-pvc","csi.storage.k8s.io/pvc/namespace":"default","server":"10.0.0.5","share":"/srv/nfs_share"},"volume_capabilities":[{"AccessType":{"Mount":{"mount_flags":["nfsvers=4.1"]}},"access_mode":{"mode":5}}]}
I1108 22:29:25.623128 1 controllerserver.go:496] internally mounting 10.0.0.5:/srv/nfs_share at /tmp/pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a
I1108 22:29:25.623316 1 nodeserver.go:132] NodePublishVolume: volumeID(10.0.0.5#srv/nfs_share#pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a##) source(10.0.0.5:/srv/nfs_share) targetPath(/tmp/pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a) mountflags([nfsvers=4.1])
I1108 22:29:25.625159 1 mount_linux.go:243] Detected OS without systemd
I1108 22:29:25.625191 1 mount_linux.go:218] Mounting cmd (mount) with arguments (-t nfs -o nfsvers=4.1 10.0.0.5:/srv/nfs_share /tmp/pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a)
I1108 22:29:26.136750 1 nodeserver.go:149] skip chmod on targetPath(/tmp/pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a) since mountPermissions is set as 0
I1108 22:29:26.136795 1 nodeserver.go:151] volume(10.0.0.5#srv/nfs_share#pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a##) mount 10.0.0.5:/srv/nfs_share on /tmp/pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a succeeded
I1108 22:29:26.140767 1 controllerserver.go:511] internally unmounting /tmp/pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a
I1108 22:29:26.140808 1 nodeserver.go:172] NodeUnpublishVolume: unmounting volume 10.0.0.5#srv/nfs_share#pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a## on /tmp/pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a
I1108 22:29:26.140819 1 nodeserver.go:177] force unmount 10.0.0.5#srv/nfs_share#pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a## on /tmp/pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a
I1108 22:29:26.141001 1 mount_helper_common.go:56] unmounting "/tmp/pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a" (corruptedMount: false, mounterCanSkipMountPointChecks: true)
I1108 22:29:26.141020 1 mount_linux.go:789] Unmounting /tmp/pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a
I1108 22:29:26.164058 1 mount_helper_common.go:150] Deleting path "/tmp/pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a"
I1108 22:29:26.166352 1 nodeserver.go:185] NodeUnpublishVolume: unmount volume 10.0.0.5#srv/nfs_share#pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a## on /tmp/pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a successfully
I1108 22:29:26.166567 1 utils.go:117] GRPC response: {"volume":{"volume_context":{"csi.storage.k8s.io/pv/name":"pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a","csi.storage.k8s.io/pvc/name":"nfs-pvc","csi.storage.k8s.io/pvc/namespace":"default","server":"10.0.0.5","share":"/srv/nfs_share","subdir":"pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a"},"volume_id":"10.0.0.5#srv/nfs_share#pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a##"}}
ログの各箇所で、以下が実行されたことを確認できます。
- (2) ボリューム作成リクエスト : 1行目
-
csi-provisioner
からgRPCで、CreateVolumeというリクエストが来ている
-
- (3) ボリューム作成 : 3~15行目
-
/tmp
ディレクトリに一時的にNFSをマウントして、pvc-7d13...
という名前のボリュームを作成している
-
- (4) PV作成 : 2,16行目
- gRPCでPV作成のリクエストをしてレスポンスが返っている
PodにNFSボリュームをマウントしてみる
続いて、PVCを経由してPodにNFSボリュームをマウントしてみます。
以下のマニフェストを使います。
apiVersion: v1
kind: Pod
metadata:
name: mnt-nfs-pod
namespace: default
spec:
containers:
- name: mnt-nfs-pod
image: busybox
command:
- tail
- -f
- /dev/null
volumeMounts:
- name: nfs-volume
mountPath: /data
volumes:
- name: nfs-volume
persistentVolumeClaim:
claimName: nfs-pvc
Podを作成します。
kubectl apply -f mnt-nfs-pod.yaml
Podが作成されて、Pod内の /data
ディレクトリにNFSボリュームがマウントされたことが確認できます。
$ kubectl get pod -n default
NAME READY STATUS RESTARTS AGE
mnt-nfs-pod 1/1 Running 0 41s
$ kubectl exec -n default mnt-nfs-pod -- df -h | grep /data
14.4G 8.8G 5.5G 62% /data
どのようにしてボリュームがPodにマウントされたのか?
こちらもCSI Driverを使ってボリュームがPodにマウントされる様子を図示してみます。Podの配置が決まったノードで起動しているkubeletと、csi-nfs-node
のPodが今回は登場します。
Podがデプロイされるノードが決まると、そのノードで稼働しているkubeletが csi-nfs-node
のnfs
コンテナ経由で提供されるマウントポイントを呼び出します。それを受けて、nfs
コンテナが mnt-nfs-pod
のPodの /data
ディレクトリに対してマウントを実行します。
ログで確認: (1)マウントポイント呼出
起動したPodのNode上で kubelet のログを確認します。
journalctl -xeu kubelet
以下ログ(各行の先頭部分は非表示)の1,4行目に、NFSボリュームのマウントに関連しそうなメッセージが出ています。
I1109 04:44:35.529999 765 reconciler_common.go:245] "operationExecutor.VerifyControllerAttachedVolume started for volume \"pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a\" (UniqueName: \"kubernetes.io/csi/nfs.csi.k8s.io^10.0.0.5#srv/nfs_share#pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a##\") pod \"mnt-nfs-pod\" (UID: \"ea906b1b-7336-406f-8bb4-d8e13ef81d04\") " pod="default/mnt-nfs-pod"
I1109 04:44:35.530088 765 reconciler_common.go:245] "operationExecutor.VerifyControllerAttachedVolume started for volume \"kube-api-access-2jjmj\" (UniqueName: \"kubernetes.io/projected/ea906b1b-7336-406f-8bb4-d8e13ef81d04-kube-api-access-2jjmj\") pod \"mnt-nfs-pod\" (UID: \"ea906b1b-7336-406f-8bb4-d8e13ef81d04\") " pod="default/mnt-nfs-pod"
I1109 04:44:35.639210 765 csi_attacher.go:380] kubernetes.io/csi: attacher.MountDevice STAGE_UNSTAGE_VOLUME capability not set. Skipping MountDevice...
I1109 04:44:35.639278 765 operation_generator.go:580] "MountVolume.MountDevice succeeded for volume \"pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a\" (UniqueName: \"kubernetes.io/csi/nfs.csi.k8s.io^10.0.0.5#srv/nfs_share#pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a##\") pod \"mnt-nfs-pod\" (UID: \"ea906b1b-7336-406f-8bb4-d8e13ef81d04\") device mount path \"/var/lib/kubelet/plugins/kubernetes.io/csi/nfs.csi.k8s.io/5263efb4ed712e05427adcaa418778b03e13c046edc3ea4a9ab867ea11f8cd8d/globalmount\"" pod="default/mnt-nfs-pod"
I1109 04:44:38.587628 765 pod_startup_latency_tracker.go:104] "Observed pod startup duration" pod="default/mnt-nfs-pod" podStartSLOduration=2.312397567 podStartE2EDuration="3.587609387s" podCreationTimestamp="2024-11-09 04:44:35 +0000 UTC" firstStartedPulling="2024-11-09 04:44:36.81953173 +0000 UTC m=+479.945761594" lastFinishedPulling="2024-11-09 04:44:38.09474355 +0000 UTC m=+481.220973414" observedRunningTime="2024-11-09 04:44:38.586733896 +0000 UTC m=+481.712963780" watchObservedRunningTime="2024-11-09 04:44:38.587609387 +0000 UTC m=+481.713839271"
ログで確認: (2)マウント実行
csi-nfs-nodeのログを見てみます。
kubectl logs -n kube-system ds/csi-nfs-node -c nfs
NodePublishVolume というgRPCのリクエストをkubeletから受けて、-t nfs
のオプションでマウントを実行しているのがわかります。
I1109 04:44:35.655828 1 utils.go:110] GRPC call: /csi.v1.Node/NodePublishVolume
I1109 04:44:35.656029 1 utils.go:111] GRPC request: {"target_path":"/var/lib/kubelet/pods/ea906b1b-7336-406f-8bb4-d8e13ef81d04/volumes/kubernetes.io~csi/pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a/mount","volume_capability":{"AccessType":{"Mount":{"mount_flags":["nfsvers=4.1"]}},"access_mode":{"mode":5}},"volume_context":{"csi.storage.k8s.io/pv/name":"pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a","csi.storage.k8s.io/pvc/name":"nfs-pvc","csi.storage.k8s.io/pvc/namespace":"default","server":"10.0.0.5","share":"/srv/nfs_share","storage.kubernetes.io/csiProvisionerIdentity":"1731104072010-4747-nfs.csi.k8s.io","subdir":"pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a"},"volume_id":"10.0.0.5#srv/nfs_share#pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a##"}
I1109 04:44:35.658545 1 nodeserver.go:132] NodePublishVolume: volumeID(10.0.0.5#srv/nfs_share#pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a##) source(10.0.0.5:/srv/nfs_share/pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a) targetPath(/var/lib/kubelet/pods/ea906b1b-7336-406f-8bb4-d8e13ef81d04/volumes/kubernetes.io~csi/pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a/mount) mountflags([nfsvers=4.1])
I1109 04:44:35.659827 1 mount_linux.go:243] Detected OS without systemd
I1109 04:44:35.660546 1 mount_linux.go:218] Mounting cmd (mount) with arguments (-t nfs -o nfsvers=4.1 10.0.0.5:/srv/nfs_share/pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a /var/lib/kubelet/pods/ea906b1b-7336-406f-8bb4-d8e13ef81d04/volumes/kubernetes.io~csi/pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a/mount)
I1109 04:44:36.190344 1 nodeserver.go:149] skip chmod on targetPath(/var/lib/kubelet/pods/ea906b1b-7336-406f-8bb4-d8e13ef81d04/volumes/kubernetes.io~csi/pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a/mount) since mountPermissions is set as 0
I1109 04:44:36.190476 1 nodeserver.go:151] volume(10.0.0.5#srv/nfs_share#pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a##) mount 10.0.0.5:/srv/nfs_share/pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a on /var/lib/kubelet/pods/ea906b1b-7336-406f-8bb4-d8e13ef81d04/volumes/kubernetes.io~csi/pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a/mount succeeded
I1109 04:44:36.190503 1 utils.go:117] GRPC response: {}
ちなみにマウント先のパスが、
/var/lib/kubelet/pods/ea906b1b-7336-406f-8bb4-d8e13ef81d04/volumes/kubernetes.io~csi/pvc-7d13ebab-8f44-4d9e-bc62-9d77eae1ec2a/mount/
という非常に長い名前になっていますが、パスの途中に出てくる ea906b1b...
は mnt-nfs-pod
のPodのUIDと一致します。
$ kubectl get po -n default mnt-nfs-pod -o jsonpath='{.metadata.uid}'
ea906b1b-7336-406f-8bb4-d8e13ef81d04
したがって、CSI経由で動的プロビジョニングによりPodにマウントされたパスは、ノード上だと
/var/lib/kubelet/pods/[pod-uid]/volumes/kubernetes.io~csi/[pv-name]/mount/
に格納されるようです。
おわりに
本記事では、CSIドライバー経由でPVCを作成してPodにマウントした場合の動きを実際にKubernetesリソースを作成して追ってみました。ただ今回の内容で、CSIドライバーで必須の全ての動作を説明しているわけではない(例えば csi-nfs-node
の node-driver-registrar
コンテナの説明はスキップしている)ことにご注意ください。