쿠버네티스에서 NFS 를 동적 프로비저닝에 쓰는 방법은 오랫동안 두 갈래였다. 하나는 nfs-subdir-external-provisioner 로, 파드 하나가 NFS 를 마운트한 뒤 PVC 마다 하위 디렉터리를 만들어 주는 외부 프로비저너다. 다른 하나는 csi-driver-nfs 로, kubernetes-csi 조직이 관리하는 정식 CSI 드라이버다.
지금 새로 구축한다면 csi-driver-nfs 를 쓴다. 현행 버전은 v4.13.4 다.[1] 구조상의 차이가 분명하다.
| 항목 | nfs-subdir-external-provisioner | csi-driver-nfs |
|---|---|---|
| 구조 | 디플로이먼트 파드 1개가 마운트를 대신한다 | 컨트롤러 + 노드 데몬셋. 각 노드가 직접 마운트한다 |
| 프로비저너 이름 | k8s-sigs.io/nfs-subdir-external-provisioner |
nfs.csi.k8s.io |
| 단일 장애점 | 프로비저너 파드 | 컨트롤러가 죽어도 기존 볼륨의 I/O 는 계속된다 |
| 마운트 옵션 | 프로비저너 파드의 마운트를 따라간다 | StorageClass 의 mountOptions 가 노드 마운트에 직접 반영된다 |
| 스냅샷 · 리사이즈 | 없음 | 사이드카로 지원 |
이미 nfs-subdir-external-provisioner 로 만든 PVC 가 있는 환경에서는 한 번에 갈아타지 않는다.
csi-driver-nfs 를 설치하고 새 StorageClass 를 만든다.PV 를 CSI 로 자동 변환하는 경로는 없다. 두 프로비저너가 같은 NFS 공유를 바라보게 해 두면 데이터 자체는 한 곳에 있으므로, 이관은 사실상 디렉터리 옮기기나 복사다.
데이터 복사는 두 PVC 를 동시에 마운트한 임시 파드에서 한다.
apiVersion: v1
kind: Pod
metadata:
name: pvc-migrate
spec:
restartPolicy: Never
containers:
- name: migrate
image: registry.example.com/library/alpine:3.20
command: ["sh", "-c", "apk add --no-cache rsync && rsync -aAX --numeric-ids /old/ /new/ && ls -al /new"]
volumeMounts:
- { name: oldpvc, mountPath: /old }
- { name: newpvc, mountPath: /new }
volumes:
- name: oldpvc
persistentVolumeClaim:
claimName: old-pvc
- name: newpvc
persistentVolumeClaim:
claimName: new-pvc-csi
복사 중에 원본을 쓰는 워크로드는 미리 내린다. 복사가 끝나면 워크로드의 claimName 을 새 PVC 로 바꿔 배포한다.
쿠버네티스를 업그레이드한다고 해서 기존 PV · PVC · StorageClass 가 사라지지는 않는다. 옛 프로비저너를 당장 걷어내야 할 이유는 없고, 다만 새 버전에서 동작을 검증받지 못한 구성 요소를 계속 안고 가는 것이 위험 요소일 뿐이다.
helm repo add csi-driver-nfs https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/charts
helm repo update
helm install csi-driver-nfs csi-driver-nfs/csi-driver-nfs \
--namespace kube-system --version v4.13.4
kubectl -n kube-system get pod -l app.kubernetes.io/name=csi-driver-nfs
kubectl get csidrivers nfs.csi.k8s.io
컨트롤러 파드 하나와 노드마다 하나씩 도는 데몬셋 파드가 보여야 한다. 노드에는 nfs-utils(또는 nfs-common) 가 설치돼 있어야 한다. 실제 마운트는 노드의 커널이 하기 때문이다.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-csi
provisioner: nfs.csi.k8s.io
parameters:
server: 10.10.10.20
share: /srv/nfs/data
reclaimPolicy: Delete
volumeBindingMode: Immediate
allowVolumeExpansion: true
mountOptions:
- nfsvers=4.1
- hard
- timeo=600
- retrans=2
- noresvport
드라이버는 share 아래에 PV 이름으로 하위 디렉터리를 만들고 그것을 볼륨으로 내준다. 디렉터리 이름 규칙은 subDir 파라미터로 바꿀 수 있다.
reclaimPolicy 를 Delete 로 두면 PVC 를 지울 때 하위 디렉터리도 지워진다. 운영 데이터라면 Retain 으로 두고 정리를 사람이 판단하게 하는 편이 안전하다. onDelete: retain 파라미터로 드라이버 수준에서 보존하게 할 수도 있다.
마운트 옵션에 nolock 을 넣지 않는다. 메시지는 줄지만 파일 잠금이 없어져 데이터가 깨질 수 있다.
시험.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-test-pvc
spec:
accessModes: [ReadWriteMany]
storageClassName: nfs-csi
resources:
requests:
storage: 1Gi
kubectl get pvc nfs-test-pvc
kubectl get pv
Bound 가 되면 NFS 서버에서 하위 디렉터리가 생겼는지 직접 확인한다.
필요한 이미지는 차트 버전마다 다르다. 짐작하지 말고 차트에서 뽑는다.
helm pull csi-driver-nfs/csi-driver-nfs --version v4.13.4
helm template csi-driver-nfs ./csi-driver-nfs-v4.13.4.tgz \
| grep -E '^\s+image:' | awk '{print $2}' | tr -d '"' | sort -u
대체로 아래 네다섯 개다.
registry.k8s.io/sig-storage/csi-driver-nfs
registry.k8s.io/sig-storage/csi-provisioner
registry.k8s.io/sig-storage/csi-node-driver-registrar
registry.k8s.io/sig-storage/csi-resizer
registry.k8s.io/sig-storage/livenessprobe
외부망에서 받아 tar 로 만든다.
for img in $(cat images.txt); do
docker pull "$img"
docker save -o "$(echo "$img" | tr '/:' '__').tar" "$img"
done
docker save 는 받은 아키텍처 하나만 담는다. 노드가 여러 아키텍처면 skopeo copy --all 이나 crane 으로 매니페스트 리스트째 옮긴다.
docker load -i registry.k8s.io_sig-storage_csi-driver-nfs_v4.13.4.tar
docker tag registry.k8s.io/sig-storage/csi-driver-nfs:v4.13.4 \
harbor.example.com/sig-storage/csi-driver-nfs:v4.13.4
docker push harbor.example.com/sig-storage/csi-driver-nfs:v4.13.4
차트는 이미지 레지스트리를 한 곳에서 바꿀 수 있게 돼 있다. 매니페스트를 손으로 고치는 것보다 낫다.
image:
baseRepo: harbor.example.com/sig-storage
nfs:
repository: harbor.example.com/sig-storage/csi-driver-nfs
tag: v4.13.4
pullPolicy: IfNotPresent
csiProvisioner:
repository: harbor.example.com/sig-storage/csi-provisioner
tag: v5.3.0
nodeDriverRegistrar:
repository: harbor.example.com/sig-storage/csi-node-driver-registrar
tag: v2.14.0
livenessProbe:
repository: harbor.example.com/sig-storage/livenessprobe
tag: v2.17.0
externalResizer:
repository: harbor.example.com/sig-storage/csi-resizer
tag: v1.14.0
imagePullSecrets:
- name: regcred
확인 필요 — 사이드카 태그는 차트 버전마다 다르다. 위 값을 그대로 쓰지 말고 helm show values 로 그 차트의 기본값을 읽어 맞춘다.
helm show values ./csi-driver-nfs-v4.13.4.tgz > values-default.yaml
helm install csi-driver-nfs ./csi-driver-nfs-v4.13.4.tgz \
--namespace kube-system -f values.yaml
레지스트리 인증이 필요하면 시크릿을 먼저 만든다.
kubectl -n kube-system create secret docker-registry regcred \
--docker-server=harbor.example.com \
--docker-username="${HARBOR_USER}" \
--docker-password="${HARBOR_PASSWORD}"
--docker-server 에는 스킴 없이 호스트만 적는다. https:// 를 붙이면 매칭이 어긋나 인증이 되지 않는다.
| 증상 | 확인 |
|---|---|
ImagePullBackOff |
이미지 경로, imagePullSecrets, 시크릿이 드라이버와 같은 네임스페이스에 있는지 |
PVC 가 Pending |
kubectl describe pvc 의 이벤트. 컨트롤러 파드와 csi-provisioner 사이드카 로그 |
파드가 ContainerCreating 에서 멈춤 |
kubectl describe pod 의 마운트 오류. 노드에 nfs-utils 가 있는지, 노드 IP 가 export 목록에 있는지 |
permission denied |
NFS 서버의 export 옵션과 디렉터리 권한. 컨테이너가 쓰는 UID 와 서버 파일 소유자 |
| 마운트 옵션이 반영되지 않음 | StorageClass 의 mountOptions 는 새 PV 에만 적용된다. 기존 PV 는 kubectl get pv <name> -o jsonpath='{.spec.mountOptions}' 로 확인 |
kubectl -n kube-system logs -l app=csi-nfs-controller -c nfs --tail=100
kubectl -n kube-system logs -l app=csi-nfs-node -c nfs --tail=100
csi-driver-nfs 최신 버전 v4.13.4 (2026-07-01) — 2026-09-20 확인. https://github.com/kubernetes-csi/csi-driver-nfs/releases/latest ↩︎