Pod가 죽어도 데이터는 남아야 한다 — 볼륨에서 CSI까지의 한 단계

한 팀이 Postgres를 Deployment로 띄웠다. Pod는 로컬 디스크에 데이터를 썼다. 어느 날 노드가 재부팅됐고 Pod는 다른 노드로 재생성됐다 — 그런데 데이터가 없었다. "Pod가 다시 돌아왔는데 왜 데이터가 없지?" 이 질문은 Kubernetes 스토리지 모델을 이해하지 못했을 때 반드시 부딪히는 벽이다.

이 글이 푸는 것은: Kubernetes에서 "데이터를 영속시키려면" Pod의 로컬 파일시스템이 아니라 볼륨 추상화(Volume → PV/PVC → StorageClass → CSI)를 거쳐야 한다는 것, 그리고 각 단계가 왜 존재하는가다.

Pod의 로컬 파일시스템은 Pod와 함께 죽는다

가장 먼저 정리할 사실: 컨테이너의 로컬 파일시스템은 컨테이너(그리고 Pod)가 죽으면 사라진다. Pod가 재생성되면 깨끗한 이미지에서 다시 시작하므로, 이전에 쓴 파일은 없다. 이것이 기본 동작이고, 이것을 바꾸려면 볼륨(volume)을 써야 한다.

# Kubernetes 1.36 — emptyDir: Pod 수명과 같이 사라지는 임시 볼륨
apiVersion: v1
kind: Pod
metadata: {name: temp}
spec:
  volumes:
  - name: scratch
    emptyDir: {}          # Pod가 죽으면 같이 사라짐
  containers:
  - name: app
    image: alpine:3.20
    volumeMounts: [{name: scratch, mountPath: /tmp/data}]

emptyDir은 Pod 수명과 같은 임시 공간. 두 컨테이너가 데이터를 주고받을 때 쓴다. 하지만 Pod가 죽으면 같이 사라지므로 영속 데이터엔 쓸 수 없다.

컨테이너의 이미지 레이어 위에 쓰는 것도 임시다. 그래서 "Pod가 재시작돼도 컨테이너가 쓴 파일이 남을 줄 알았는데"라는 기대는 항상 배신당한다. 영속은 명시적 볼륨으로만.

영구 볼륨의 분리 — PV와 PVC, 그리고 왜 둘인가

영속 데이터를 위해 Kubernetes는 PV(PersistentVolume)PVC(PersistentVolumeClaim)라는 두 객체로 분리했다. 이 분리가 처음엔 헷갈리지만, 의도가 분명하다.

  • PV: 클러스터 관리자가 제공하는 실제 저장소의 추상화. "AWS EBS 50Gi 볼륨이 있다"를 표현.
  • PVC: 사용자의 요구(claim). "나는 10Gi, RWO 볼륨이 필요해"를 표현.

왜 둘로 나눴는가? 역할 분리 때문이다. 개발자는 "어디 저장소인지 모르지만 10Gi 필요하다"(PVC)고만 말하고, 인프라는 그에 맞는 실제 저장소(PV)를 매칭한다. 개발자가 "EBS gp3 50Gi" 같은 인프라 디테일을 매번 알 필요가 없다.

flowchart LR
    U["개발자/Pod"] -->|"요구"| PVC["PVC<br/>'10Gi RWO 원해'"]
    PVC -. 매칭 .-> PV["PV<br/>'EBS 50Gi 실제 볼륨'"]
    PV --> STORAGE["물리 저장소<br/>(EBS/NFS/iSCSI)"]
    PVC --> MOUNT["Pod에 마운트"]

StorageClass — 요구하면 자동으로 만들어 주는 동적 프로비저닝

PVC를 쓸 때마다 관리자가 미리 PV를 만들어두면 비효율적이다 — 용량이 딱 안 맞거나 남거나. StorageClass동적 프로비저닝(dynamic provisioning)이 이 문제를 푼다.

StorageClass는 "이런 종류의 저장소를 자동으로 만들어라"는 레시피다:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata: {name: fast-ebs}
provisioner: ebs.csi.aws.com          # CSI 드라이버
parameters:
  type: gp3
reclaimPolicy: Delete                  # PVC 삭제 시 PV도 삭제
volumeBindingMode: WaitForFirstConsumer

이제 PVC가 이 StorageClass를 가리키면:

apiVersion: v1
kind: PersistentVolumeClaim
metadata: {name: data}
spec:
  storageClassName: fast-ebs
  accessModes: ["ReadWriteOnce"]
  resources: {requests: {storage: 10Gi}}

CSI 드라이버가 자동으로 실제 볼륨(EBS)을 만들고, 그것을 가리키는 PV를 만들고, PVC와 묶는다. 개발자는 PVC 하나만 쓰면 끝.

volumeBindingMode: WaitForFirstConsumer가 중요하다 — Pod가 실제로 어느 노드(어느 가용영역)에 스케줄될지 알 때까지 볼륨 바인딩을 미룬다. 그래야 "Pod는 zone-A인데 볼륨은 zone-B" 비극이 안 생긴다. 특히 클라우드 EBS는 영역 종속적이라 필수.

accessModes — 볼륨을 어떻게 공유할 것인가

PVC/PV의 핵심 속성 accessModes:

  • ReadWriteOnce(RWO): 한 노드에서만 읽기/쓰기. EBS 등 블록 스토리지. 가장 흔함.
  • ReadOnlyMany(ROX): 여러 노드에서 읽기 전용.
  • ReadWriteMany(RWX): 여러 노드에서 동시 읽기/쓰기. NFS, CephFS, S3 등만 지원.
  • ReadWriteOncePod(1.27+ GA): 한 Pod에서만. RWO보다 더 엄격.

이게 왜 중요한가? RWX를 지원 안 하는 저장소(EBS)에 여러 노드의 Pod가 마운트하려 하면 실패한다. "이 데이터를 여러 Pod가 동시에 읽어야 한다"면 NFS/CephFS 같은 RWX 저장소가 필수다. StatefulSet(06장)의 DB는 보통 RWO — 각 Pod가 자기 전용 볼륨을 단독 쓰므로.

CSI — Container Storage Interface, 스토리지도 탈부착 가능하게

스케줄링이 CRI/CNI/CSI로 플러그인화됐듯(02장), 스토리지도 CSI(Container Storage Interface)로 표준화됐다. CSI 드라이버는 "볼륨 만들기/삭제/연결/해제"를 표준 gRPC로 노출하고, Kubernetes는 그것을 호출한다.

과거엔 각 클라우드 저장소(EBS, GCE PD, Azure Disk) 코드가 Kubernetes 코어 안에 있었다(in-tree). 유지 부담이 컸고, 버전업이 Kubernetes 릴리스에 종속됐다. CSI로 빼면서 — 이제 저장소 벤더가 자체 드라이버를 독립적으로 배포한다. Kubernetes 코어는 점점 in-tree 드라이버를 제거하고 CSI로 옮기는 추세.

KubeEdge 같은 엣지 환경에서도 CSI는 작동하지만, 엣지의 제약(오프라인, 느린 링크)에 맞춘 저장소 전략이 따로 필요하다. KubeEdge 영역(04)에서 재회.

reclaimPolicy — PVC를 지우면 데이터는 어떻게 되나

PVC를 삭제할 때 볼륨(데이터)을 어떻게 처리할지가 reclaimPolicy:

  • Delete(StorageClass 기본): PVC 삭제 시 볼륨도 삭제. 데이터 사라짐.
  • Retain: PVC 삭제 후에도 볼륨(PV)은 남김. Released 상태로. 수동 정리까지 보존.

운영 함정: 개발 환경에서 StorageClass 기본(Delete)인 줄 모르고 PVC를 지웠다가 데이터를 잃는 사례가 흔하다. 중요 데이터는 Retain 또는 백업이 필수다.

직접 확인하기

# Kubernetes 1.36 — kind는 기본 StorageClass와 동적 프로비저닝 제공
kubectl get storageclass
NAME                 PROVISIONER             RECLAIMPOLICY   ...
standard (default)   rancher.io/local-path   Delete          ...
# PVC 만들기 — 자동으로 PV 생성 (동적 프로비저닝)
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata: {name: demo}
spec:
  accessModes: ["ReadWriteOnce"]
  resources: {requests: {storage: 1Gi}}
EOF
kubectl get pvc,pv

확인할 것: PVC와 그에 매칭된 PV가 자동으로 생긴다. kind는 local-path 프로비저너로 노드 로컬 경로를 볼륨으로 만든다.

# Pod에서 PVC 마운트 사용
apiVersion: v1
kind: Pod
metadata: {name: writer}
spec:
  volumes: [{name: data, persistentVolumeClaim: {claimName: demo}}]
  containers:
  - name: app
    image: alpine:3.20
    command: ["sh","-c","echo hello > /data/x; sleep 3600"]
    volumeMounts: [{name: data, mountPath: /data}]
kubectl delete pod writer          # Pod 죽임
kubectl apply -f writer.yaml       # 같은 PVC로 재생성
kubectl exec writer -- cat /data/x

확인할 것: Pod가 재생성돼도 /data/x에 "hello"가 남아 있다 — 볼륨이 영속했기 때문.

volumeMode — Filesystem vs Block

PVC의 volumeMode가 볼륨을 파일시스템으로 마운트할지 블록 장치로 직접 쓸지 결정:

  • Filesystem(기본): PV를 파일시스템(ext4/xfs)으로 마운트. 일반적.
  • Block: raw 블록 장치로 마운트(파일시스템 없음). 앱이 직접 블록 I/O.
spec:
  volumeMode: Block           # 또는 Filesystem(기본)
  accessModes: ["ReadWriteOnce"]

Block 모드가 필요한 경우: 데이터베이스(자체 파일시스템 관리, 성능), 고성능 I/O 앱. 파일시스템 오버헤드를 피해 직접 블록 접근. 단 파일시스템 보호(journaling 등)를 앱이 알아서. 일반 앱은 Filesystem이 기본.

volumeBindingMode — 언제 PV를 PVC에 묶을까

09장 본문에서 WaitForFirstConsumer가 Pod가 스케줄될 때까지 바인딩을 미룬다고 했다. 정확한 값:

  • Immediate(기본): PVC 생성 즉시 PV 매칭. 빠르지만 — Pod 스케줄 전에 볼륨이 멀리 영역에 생길 수 있음.
  • WaitForFirstConsumer: Pod가 실제로 스케줄될 때까지 대기. Pod가 가용영역 A에 스케줄되면 그 영역의 PV에 바인딩. "Pod/볼륨 영역 불일치" 방지.

이것이 특히 토폴로지 제약(가용영역)에서 중요. 클라우드 EBS는 영역 종속적 — A 영역 Pod가 B 영역 EBS를 못 쓴다. Immediate 바인딩이면 B 영역 PV가 묶여 Pod가 A에 못 스케줄되는 비극. WaitForFirstConsumer가 이것을 근본적으로 방지.

임시 볼륨(emptyDir)의 진짜 한계 — 언제 쓰고 언제 못 쓰나

emptyDir은 Pod 수명의 임시 공간. 그런데 "임시"의 의미를 정확히:

  • Pod가 삭제되면 emptyDir도 삭제.
  • 하지만 Pod가 재시작(같은 노드에서 컨테이너 크래시 복구)되면 emptyDir은 유지(같은 노드, 같은 Pod 인스턴스).

이 미묘함: "컨테이너 재시작 시 데이터 유지"는 emptyDir이 해주지만, "Pod 재스케줄(다른 노드) 시 데이터 유지"는 안 됨. 그래서 emptyDir은 캐시, 빌드 산출물, scratch에 적합하지만, 영속 데이터에는 절대 쓰면 안 됨.

emptyDir의 매체도 선택 가능:

  • 기본(노드 디스크): 보통.
  • medium: Memory: tmpfs(RAM). 매우 빠르지만 노드 메모리 소비. 대용량은 노드 메모리 부담.
  • sizeLimit: 한도 설정(11장 eviction 대상).

노드 로컬 볼륨(hostPath)의 위험 — 언제 (조심해서) 쓰나

hostPath가 노드의 파일시스템 경로를 직접 Pod에. 강력하지만 위험:

  • Pod가 노드의 민감 파일(/etc, /var/lib/kubelet)에 접근 가능 → 보안 위험.
  • Pod가 다른 노드로 재스케줄되면 다른 노드의 hostPath → 데이터 불일치.

그래서 hostPath는 시스템 Pod(DaemonSet, CNI 에이전트)에만 제한적으로. 일반 앱에는 절대. 05장(Pod Security Admission)의 restricted 프로파일이 hostPath를 금지하는 이유.

흔히 묻는 것, 흔히 틀리는 것

오해 정정
"컨테이너에 쓴 파일은 Pod 재시작해도 남는다" 남지 않는다. 영속은 명시적 볼륨(PV/PVC)으로만
"PVC가 실제 저장소다" PVC는 요구(claim). 실제 저장소는 PV(또는 동적 프로비저닝으로 만들어진)
"RWO 볼륨을 여러 노드 Pod가 마운트할 수 있다" 안 된다. 한 노드만. 여러 노드엔 RWX 저장소(NFS/CephFS) 필요
"동적 프로비저닝이면 관리자가 PV를 미리 만들어야" 반대. 사용자 PVC 요청 시 자동으로 PV가 생김
"CSI는 Kubernetes 코어 기능이다" 플러그인 인터페이스다. 드라이버는 별도 배포. 코어 in-tree는 제거 추세
"reclaimPolicy Delete면 안전 삭제" 반대. PVC 지우면 볼륨(데이터)도 삭제. 백업 없으면 영구 상실

요약 — 이 글의 결론

  • Pod의 로컬 파일시스템은 Pod와 함께 죽는다. 영속 데이터는 명시적 볼륨으로만. emptyDir은 Pod 수명의 임시 공간.
  • PV/PVC 분리가 역할 분리다. PVC(개발자 요구)와 PV(인프라 제공 저장소)를 매칭시켜, 개발자가 저장소 디테일을 몰라도 되게.
  • StorageClass + 동적 프로비저닝이 PVC 요청만으로 실제 볼륨을 자동 생성. 관리자가 미리 만들 필요 없다.
  • WaitForFirstConsumer가 Pod의 가용영역 결정 후 볼륨 바인딩을 미뤄 "Pod/볼륨 영역 불일치" 비극을 막는다.
  • accessModes(RWO/ROX/RWX/RWO-Pod)가 "이 볼륨을 누가 어떻게 쓰나"를 결정. 다중 노드 동시 쓰기엔 RWX 저장소가 필수.
  • CSI가 스토리지도 탈부착 가능하게. 코어 in-tree 드라이버는 제거 추세, 벤더가 자체 CSI 드라이버 배포.
  • reclaimPolicy가 데이터 생사를 결정. Delete는 PVC 삭제=데이터 삭제. 중요 데이터는 Retain + 백업.

생각해 볼 문제

  1. Postgres를 Deployment로 띄우고 emptyDir에 데이터를 썼다. 노드 재부팅 후 데이터가 사라진 이유를 설명하라. 어떻게 고쳐야 하나?
  2. WaitForFirstConsumer가 없는 StorageClass에서 다중 영역 클러스터에 Pod를 띄우면 어떤 문제가 생기나?
  3. RWO 볼륨에 두 노드의 Pod가 동시 마운트를 시도하면? 실제 동작은?
  4. 동적 프로비저닝이 활성화된 클러스터에서 PVC를 만들었는데 영원히 Pending이다. 가능한 원인 3가지.
  5. reclaimPolicy Delete에서 PVC를 실수로 지웠다. 볼륨은 이미 삭제됐다. 복구 가능한가? 백업 정책의 역할은?
  6. RWX 볼륨이 필요한 워크로드와 RWO면 충분한 워크로드를 각각 예를 들어 분류하라.

참고

'Tech Artifacts > Kubernetes' 카테고리의 다른 글

Kubernetes - 11. resource qos  (0) 2026.07.13
Kubernetes - 10. config secret  (0) 2026.07.13
Kubernetes - 08. scheduling  (0) 2026.07.13
Kubernetes - 07. daemonset job cronjob  (0) 2026.07.13
Kubernetes - 06. statefulset  (0) 2026.07.13