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 + 백업.
생각해 볼 문제
- Postgres를 Deployment로 띄우고 emptyDir에 데이터를 썼다. 노드 재부팅 후 데이터가 사라진 이유를 설명하라. 어떻게 고쳐야 하나?
WaitForFirstConsumer가 없는 StorageClass에서 다중 영역 클러스터에 Pod를 띄우면 어떤 문제가 생기나?- RWO 볼륨에 두 노드의 Pod가 동시 마운트를 시도하면? 실제 동작은?
- 동적 프로비저닝이 활성화된 클러스터에서 PVC를 만들었는데 영원히 Pending이다. 가능한 원인 3가지.
- reclaimPolicy Delete에서 PVC를 실수로 지웠다. 볼륨은 이미 삭제됐다. 복구 가능한가? 백업 정책의 역할은?
- RWX 볼륨이 필요한 워크로드와 RWO면 충분한 워크로드를 각각 예를 들어 분류하라.
참고
- Kubernetes 공식 문서 - Volumes - 접근 2026-07-12 (emptyDir 등)
- Kubernetes 공식 문서 - Persistent Volumes - 접근 2026-07-12 (PV/PVC, accessModes, reclaimPolicy)
- Kubernetes 공식 문서 - Storage Classes - 접근 2026-07-12 (동적 프로비저닝, WaitForFirstConsumer)
- Kubernetes 공식 문서 - Container Storage Interface (CSI) - 접근 2026-07-12
- Kubernetes CSI 개발 문서 - kubernetes-csi - 접근 2026-07-12
- Kubernetes 1.36 API - storage - 접근 2026-07-12
'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 |
