Skip to content

필사 모드: Synology NAS를 쿠버네티스 동적 PVC 백엔드로 — csi-driver-nfs로 5노드 홈랩에 RWX 스토리지 붙이기

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

들어가며 — 홈랩 클러스터에 스토리지가 없다

앞선 글에서 홈랩 쿠버네티스를 kubeadm 1.34 + Cilium eBPF로 다시 세웠습니다. 그 뒤 워커를 두 대 더 붙여 5노드가 됐습니다.

노드역할IPGPU
cp-1control-plane10.0.0.120
gpu-aworker10.0.0.117RTX 3090
gpu-bworker10.0.0.118RTX 5090
gpu-cworker10.0.0.116RTX 4070 Laptop
gpu-dworker10.0.0.115RTX 4070 Laptop

그런데 kubectl get sc 를 치면 아무것도 없습니다.

No resources found

StorageClass가 없으면 PVC를 만들어도 영원히 Pending에 머뭅니다. 파드가 재시작되면 데이터가 사라지고, 노드를 옮기면 아예 접근할 수 없습니다. 상태를 가진 워크로드를 하나도 올릴 수 없는 상태입니다.

집에 놀고 있는 NAS가 있으니 이걸 붙이기로 했습니다.

1단계 — NAS의 정체 파악하기

NAS 주소는 알지만 어떤 제품인지, 무엇을 지원하는지 모릅니다. 포트를 열어봤습니다.

for p in 111 2049 445 139 3260 5000 5001 8080 443; do
  timeout 2 bash -c "echo >/dev/tcp/10.0.0.109/$p" 2>/dev/null && echo "  $p open"
done
  111 open    ← rpcbind (NFS 필수)
  2049 open   ← NFS
  445 open    ← SMB
  139 open    ← NetBIOS
  3260 open   ← iSCSI
  5001 open   ← ?

5001번이 결정적 단서입니다. Synology DSM은 관리 UI를 5000(HTTP)과 5001(HTTPS)에 올립니다. 80번은 응답이 없었으니 HTTPS 전용으로 운영 중인 Synology입니다.

NFS export 목록을 확인했습니다.

$ showmount -e 10.0.0.109
Export list for 10.0.0.109:
/volume1/k8s     *
/volume1/Public  10.0.0.1/24
/volume1/media  192.168.0.1/24,10.0.0.152

/volume1/ 경로 형태가 Synology임을 다시 확인해 줍니다. 그리고 /volume1/k8s가 이미 *(전체)로 열려 있습니다. 쿠버네티스용으로 준비된 export입니다.

2단계 — 마운트가 되는지 먼저 확인한다

CSI 드라이버를 설치하기 전에 파드에서 NFS 마운트가 실제로 되는지 확인하는 편이 낫습니다. 드라이버를 깔고 나서 실패하면 원인이 드라이버인지 네트워크인지 NAS 설정인지 구분하기 어려워집니다.

첫 시도는 실패했습니다.

mount.nfs: Operation not permitted for 10.0.0.109:/volume1/k8s on /mnt/t
mount: permission denied (are you root?)

privileged: true 를 줬는데도 거부됐습니다. 원인은 hostNetwork: true 가 빠진 것이었습니다. NFS 마운트는 rpcbind와 통신하며 예약 포트(1024 미만)를 소스 포트로 사용하는데, 파드 네트워크 네임스페이스 안에서는 이 동작이 제약을 받습니다.

hostNetwork 를 추가하고 NFS 버전별로 다시 시도했습니다.

apiVersion: v1
kind: Pod
metadata: { name: nfs-probe }
spec:
  restartPolicy: Never
  hostNetwork: true                      # ← 이것이 빠져 있었다
  containers:
    - name: p
      image: alpine:3.20
      securityContext: { privileged: true }
      command: ["sh","-c"]
      args:
        - |
          apk add -q nfs-utils
          for v in 3 4 4.1; do
            mkdir -p /mnt/t$v
            if mount -t nfs -o nfsvers=$v,nolock 10.0.0.109:/volume1/k8s /mnt/t$v; then
              echo "v$v MOUNT_OK"
              touch /mnt/t$v/.wtest && echo "v$v WRITE_OK" && rm -f /mnt/t$v/.wtest
              umount /mnt/t$v
            fi
          done
v3 MOUNT_OK
v3 WRITE_OK
v4 MOUNT_OK
v4 WRITE_OK
v4.1 MOUNT_OK
v4.1 WRITE_OK

세 버전 모두 마운트와 쓰기가 됩니다. Synology는 NFSv4가 꺼져 있는 경우도 흔해서 버전별로 확인해 둘 가치가 있습니다. 최신인 4.1을 쓰기로 했습니다.

3단계 — csi-driver-nfs 설치

NFS 동적 프로비저닝에는 선택지가 둘 있습니다.

nfs-subdir-external-provisioner — 오래됐고 단순합니다. PVC마다 하위 디렉터리를 만들어 줍니다. 다만 CSI 표준을 따르지 않아 스냅샷이나 볼륨 확장을 지원하지 않습니다.

csi-driver-nfs — 쿠버네티스 SIG Storage가 관리하는 정식 CSI 드라이버입니다. 스냅샷, 볼륨 확장, 클론을 지원합니다. 지금 시점에서는 이쪽이 맞습니다.

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 \
  --version 4.13.4 \
  --namespace kube-system \
  --set controller.replicas=1 \
  --set externalSnapshotter.enabled=true

컨트롤러 1개와 노드 에이전트(DaemonSet)가 올라옵니다.

deployment "csi-nfs-controller" successfully rolled out
daemon set "csi-nfs-node" successfully rolled out
csi-nfs-controller-78d6b5bf6f-hxw4g   5/5   Running
csi-nfs-node-2w9dp                    3/3   Running
csi-nfs-node-d6t2k                    3/3   Running
csi-nfs-node-k7lmc                    3/3   Running
csi-nfs-node-lckg2                    3/3   Running
csi-nfs-node-whhnz                    3/3   Running

노드 에이전트가 5개 — 모든 노드에 하나씩 떴습니다. 실제로 볼륨을 마운트하는 주체가 이 DaemonSet이므로, 파드가 스케줄될 수 있는 모든 노드에 있어야 합니다.

호스트에 nfs-common 패키지가 필요하다는 점도 미리 확인해 두는 것이 좋습니다. 드라이버 컨테이너가 마운트를 수행하더라도 커널의 NFS 클라이언트는 호스트 쪽 자산입니다.

4단계 — StorageClass 두 벌 만들기

여기가 설계 판단이 들어가는 지점입니다. 용도에 따라 삭제 정책이 달라야 합니다.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-synology
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
provisioner: nfs.csi.k8s.io
parameters:
  server: 10.0.0.109
  share: /volume1/k8s
  subDir: ${pvc.metadata.namespace}-${pvc.metadata.name}-${pv.metadata.name}
  onDelete: archive
reclaimPolicy: Delete
volumeBindingMode: Immediate
allowVolumeExpansion: true
mountOptions:
  - nfsvers=4.1
  - hard
  - nconnect=4
  - noatime

각 항목이 왜 그 값인지 정리하면 이렇습니다.

subDir 패턴 — PVC 하나가 export 아래 디렉터리 하나가 됩니다. 네임스페이스-PVC이름-PV이름 으로 두면 NAS 파일 탐색기에서 열었을 때 어느 워크로드의 데이터인지 바로 알 수 있습니다. PV 이름만 쓰면 pvc-da6bb53d-... 같은 UUID뿐이라 나중에 정체를 알 수 없습니다.

onDelete: archive — PVC를 지워도 디렉터리를 삭제하지 않고 archived- 접두사를 붙여 이름만 바꿉니다. 홈랩에서는 실수로 지우는 일이 흔한데, 이 설정이 안전망이 됩니다. 다만 디스크는 계속 차므로 주기적으로 정리해야 합니다.

hard — NAS가 잠깐 응답하지 않아도 I/O를 포기하지 않고 재시도합니다. soft 로 두면 타임아웃 시 I/O 오류를 반환하는데, 데이터베이스 같은 워크로드에서는 조용한 데이터 손상으로 이어질 수 있습니다.

nconnect=4 — 서버당 TCP 연결을 4개 사용해 처리량을 올립니다. 단일 연결은 하나의 CPU 코어에 묶여 병목이 되기 쉽습니다.

noatime — 파일을 읽을 때마다 접근 시각을 갱신하는 쓰기를 생략합니다. NAS 부하가 줄어듭니다.

두 번째 클래스는 정책만 다릅니다.

metadata:
  name: nfs-synology-retain
parameters:
  onDelete: retain
reclaimPolicy: Retain

모델 체크포인트처럼 절대 잃으면 안 되는 데이터용입니다. PVC를 지워도 PV와 디렉터리가 그대로 남습니다.

NAME                     PROVISIONER      RECLAIMPOLICY   ALLOWVOLUMEEXPANSION
nfs-synology (default)   nfs.csi.k8s.io   Delete          true
nfs-synology-retain      nfs.csi.k8s.io   Retain          true

5단계 — 진짜로 동작하는지 검증

StorageClass가 생겼다고 끝이 아닙니다. PVC를 만들어 봤습니다.

apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: test-pvc }
spec:
  accessModes: [ReadWriteMany]
  resources: { requests: { storage: 2Gi } }

storageClassName 을 적지 않았습니다. 기본 클래스로 지정해 뒀으니 자동으로 선택됩니다.

NAME       STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS
test-pvc   Bound    pvc-da6bb53d-766c-43ef-9fbd-feb3e6d6fba5   2Gi        RWX            nfs-synology

Bound. PV가 자동 생성됐습니다.

RWX가 진짜인지 — 서로 다른 노드에서 확인

NFS의 진가는 ReadWriteMany 입니다. 블록 스토리지는 보통 한 노드만 붙일 수 있지만, NFS는 여러 노드가 동시에 읽고 쓸 수 있습니다. 말로만 하면 안 되니 확인했습니다.

gpu-a에 파드를 띄워 파일을 씁니다.

spec:
  nodeSelector: { kubernetes.io/hostname: gpu-a }
  containers:
    - name: w
      command: ["sh","-c","echo \"gpu-a이 쓴 데이터 $(date -Is)\" > /data/hello.txt && sleep 300"]

gpu-c에 다른 파드를 띄워 같은 PVC를 붙입니다.

spec:
  nodeSelector: { kubernetes.io/hostname: gpu-c }

결과입니다.

=== gpu-a이 쓴 내용 ===
gpu-a이 쓴 데이터 2026-08-19T10:23:21+00:00

=== gpu-c에서 읽은 내용 (RWX 검증) ===
gpu-a이 쓴 데이터 2026-08-19T10:23:21+00:00

=== gpu-c에서 쓰기 ===
gpu-a이 쓴 데이터 2026-08-19T10:23:21+00:00
gpu-c도-씁니다

=== 파드 배치 ===
reader Running gpu-c
writer Running gpu-a

서로 다른 물리 장비에 있는 두 파드가 같은 볼륨을 공유합니다. 학습 데이터셋을 여러 노드에서 동시에 읽거나, 여러 워커가 결과를 한 곳에 모으는 용도로 쓸 수 있습니다.

NAS에는 어떻게 저장됐나

NFS export를 직접 마운트해서 확인했습니다.

drwxr-xr-x  2 root root 4096 Aug 19 10:23 pvc-test-test-pvc-pvc-da6bb53d-766c-43ef-9fbd-feb3e6d6fba5

네임스페이스-PVC이름-PV이름 규칙대로 만들어졌습니다. 안에는 파드가 쓴 hello.txt 가 그대로 있습니다.

예상 못 한 것 — 이전 클러스터의 유산

NFS export 루트를 열어보니 낯선 디렉터리가 잔뜩 있었습니다.

11.7G  archived-legacy-monitoring-opensearch-cluster-master-...-2-pvc-e480bde4-...
10.1G  archived-legacy-monitoring-opensearch-cluster-master-...-1-pvc-ded61eb8-...
 4.2G  archived-legacy-monitoring-opensearch-cluster-master-...-2-pvc-be60de69-...
 3.2G  legacy-monitoring-prometheus-...-prometheus-2-pvc-7a69f80c-...
 3.2G  legacy-monitoring-prometheus-...-prometheus-1-pvc-d18dca32-...
 3.1G  legacy-monitoring-prometheus-...-prometheus-0-pvc-5f1f07ed-...

디렉터리 32개, 전부 예전 클러스터의 모니터링 스택(Prometheus, Grafana, OpenSearch, Alertmanager) 데이터입니다. archived- 접두사가 붙은 것들은 그때도 onDelete: archive 를 쓰고 있었다는 뜻입니다.

Filesystem   Size   Used  Avail  Use%
             19.9T  9.4T  10.5T   47%

19.9T 중 9.4T를 쓰고 있어 당장 급하진 않습니다. 하지만 이것이 archive 정책의 대가입니다. 안전한 대신 아무도 지우지 않으면 계속 쌓입니다. 클러스터를 재구축해도 NAS의 데이터는 남아 있으므로, 주기적으로 archived- 디렉터리를 점검하는 습관이 필요합니다.

이 구성의 한계

정직하게 짚어둘 부분이 있습니다.

NAS가 단일 실패 지점입니다. NAS가 꺼지면 모든 PVC가 멈춥니다. hard 마운트라 파드는 오류를 내지 않고 I/O에서 무한정 대기하는데, 이는 데이터 안전 측면에서는 옳지만 장애 시 원인 파악을 어렵게 만듭니다. 프로덕션이라면 Longhorn이나 Ceph 같은 분산 스토리지를 고려해야 합니다.

성능이 로컬 디스크에 못 미칩니다. 기가비트 이더넷이라면 대역폭이 약 125MB/s로 제한됩니다. 데이터베이스나 etcd처럼 지연에 민감한 워크로드는 로컬 NVMe에 두는 편이 낫습니다. NFS는 데이터셋, 모델 체크포인트, 로그, 미디어처럼 크고 순차적인 접근에 적합합니다.

용량 제한이 강제되지 않습니다. PVC에 2Gi를 요청했지만 NFS에는 이를 강제할 수단이 없습니다. 파드가 100GB를 써도 막히지 않습니다. Synology의 공유 폴더 할당량으로 별도 설정해야 합니다.

정리

항목
NASSynology, /volume1/k8s export
프로토콜NFS v4.1 (v3·v4도 동작 확인)
드라이버csi-driver-nfs 4.13.4
노드 에이전트5/5 Running
StorageClassnfs-synology(기본, archive) / nfs-synology-retain
접근 모드RWX 검증 완료 (gpu-a ↔ gpu-c)
볼륨 확장지원

과정을 되짚으면 순서가 중요했습니다. 먼저 마운트가 되는지 확인하고, 그다음 드라이버를 설치하고, 마지막에 서로 다른 노드에서 읽고 써 봤습니다. 첫 단계를 건너뛰었다면 hostNetwork 누락이라는 원인을 찾는 데 훨씬 오래 걸렸을 것입니다.

onDelete: archiveretain 클래스를 나눈 것도 되돌아보면 잘한 선택입니다. 홈랩에서는 실험이 잦고 그만큼 실수도 잦은데, 안전망이 있으면 과감하게 지울 수 있습니다.

🧠 이해도 체크 퀴즈

1. privileged 파드인데도 NFS 마운트가 "Operation not permitted"로 실패한 이유는 무엇인가요?

hostNetwork: true 가 없었기 때문입니다. NFS 마운트는 rpcbind와 통신하며 1024 미만의 예약 포트를 소스 포트로 사용하는데, 파드 네트워크 네임스페이스 안에서는 이 동작이 제약을 받습니다. privileged는 capability를 주지만 네트워크 네임스페이스 문제는 해결하지 못합니다.

2. subDir 에 PV 이름만 쓰지 않고 네임스페이스와 PVC 이름을 함께 넣는 이유는 무엇인가요?

PV 이름은 pvc-da6bb53d-766c-... 같은 UUID라, NAS 파일 탐색기에서 디렉터리를 열었을 때 어느 워크로드의 데이터인지 알 수 없습니다. 네임스페이스와 PVC 이름을 앞에 붙이면 클러스터 밖에서도 정체를 즉시 파악할 수 있고, 정리할 때 무엇을 지워도 되는지 판단할 수 있습니다.

3. 마운트 옵션에서 soft 대신 hard 를 쓰는 이유는 무엇인가요?

soft 는 타임아웃 시 I/O 오류를 애플리케이션에 반환합니다. 데이터베이스처럼 쓰기 도중 오류를 받으면 조용한 데이터 손상으로 이어질 수 있습니다. hard 는 서버가 돌아올 때까지 무한정 재시도하므로 데이터는 안전합니다. 대신 NAS 장애 시 파드가 오류 없이 멈춘 것처럼 보여 원인 파악이 어려워지는 대가가 있습니다.

4. PVC에 2Gi를 요청했는데 파드가 100GB를 써도 막히지 않는 이유는 무엇인가요?

NFS에는 클라이언트 측에서 용량을 강제할 수단이 없습니다. PVC의 용량 요청은 스케줄링과 회계 목적의 메타데이터일 뿐이고, 실제 제한은 스토리지 백엔드가 해야 합니다. 블록 스토리지는 볼륨 크기가 물리적 한계가 되지만 NFS 하위 디렉터리는 그렇지 않습니다. Synology의 공유 폴더 할당량으로 별도 설정해야 합니다.

참고 자료

현재 단락 (1/195)

[앞선 글](/blog/kubernetes/kubeadm-1-34-cilium-ebpf-homelab)에서 홈랩 쿠버네티스를 kubeadm 1.34 + Cilium eBPF로 다시...

작성 글자: 0원문 글자: 8,283작성 단락: 0/195