6.2 PersistentVolume과 PersistentVolumeClaim
emptyDir까지의 볼륨은 Pod와 운명을 같이한다. 데이터베이스처럼 Pod가 몇 번을 죽고 다시 기동해도 같은 데이터를 이어서 봐야 하는 워크로드에는 Pod 바깥에 존재하는 스토리지가 필요하다. Kubernetes는 이것을 리소스 한 쌍으로 푼다. 스토리지 공급을 표현하는 PersistentVolume(PV)과, 스토리지 사용 요청을 표현하는 PersistentVolumeClaim(PVC)이다.
공급과 소비의 관심사 분리
PV는 관리자의 리소스다. 어느 스토리지 시스템의 어느 자산인지(NFS 서버와 경로, 클라우드 디스크 ID), 용량이 얼마인지, 어떤 접근 모드를 지원하는지를 담는다. namespace에 속하지 않는 클러스터 범위 리소스다.
PVC는 사용자의 리소스다. “10Gi를 ReadWriteOnce로 쓰고 싶다"는 요구만 적는다. 그 스토리지가 어디에서 오는지는 적지 않는다. namespace에 속하며, Pod는 PVC 이름만 참조한다.
이 분리 덕에 애플리케이션 매니페스트는 인프라에서 독립한다. 온프레미스 NFS 위에서 작성한 매니페스트를 클라우드 블록 스토리지 위로 옮겨도, PVC와 Pod 정의는 그대로 두고 PV 쪽(또는 StorageClass)만 바꾸면 된다.
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-manual-10g
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: manual
nfs:
server: nfs.example.com
path: /exports/pg-data
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
storageClassName: manualPod에서는 볼륨 타입으로 persistentVolumeClaim을 지정한다.
volumes:
- name: data
persistentVolumeClaim:
claimName: data-claim바인딩: 무엇이 맞아야 묶이는가
control plane의 바인딩 컨트롤러가 Pending 상태의 PVC에 맞는 PV를 찾아 묶는다. 조건은 네 가지다. storageClassName이 같아야 하고, PV의 accessModes가 PVC가 요청한 모드를 포함해야 하며, PV 용량이 요청 이상이어야 하고, volumeMode(Filesystem 또는 Block)가 일치해야 한다. selector로 label 조건을 더할 수도 있다.
바인딩은 1:1 배타 관계다. 100Gi PV에 10Gi PVC가 묶이면 남은 90Gi는 다른 누구도 쓰지 못한다. 요청보다 큰 PV에 바인딩되는 것은 정상 동작이므로, 수동 프로비저닝에서는 PV 크기를 수요와 비슷하게 맞춰야 낭비가 없다. 맞는 PV가 없으면 PVC는 Pending으로 대기하고, 그 PVC를 쓰는 Pod도 함께 스케줄되지 못한다.
accessModes는 노드 단위 제약이다
| 모드 | 약어 | 마운트 범위 |
|---|---|---|
| ReadWriteOnce | RWO | 노드 하나가 읽기·쓰기 |
| ReadOnlyMany | ROX | 여러 노드가 읽기 전용 |
| ReadWriteMany | RWX | 여러 노드가 읽기·쓰기 |
| ReadWriteOncePod | RWOP | Pod 하나가 읽기·쓰기 |
가장 흔한 오해는 RWO를 “Pod 하나"로 읽는 것이다. RWO의 단위는 노드다. 같은 노드에 스케줄된 Pod 여러 개는 하나의 RWO 볼륨을 동시에 마운트한다. Pod 단위 배타가 필요하면 RWOP를 쓴다. CSI 볼륨 전용이며 Kubernetes 1.29에서 GA가 됐다.
또 하나, accessModes는 스토리지가 지원한다고 표방하는 능력의 목록이고 바인딩과 attach 시점의 검증에 쓰인다. 마운트된 뒤의 파일 수준 쓰기까지 Kubernetes가 차단해 주는 것은 아니다. 실제 지원 범위는 스토리지가 결정한다. 블록 스토리지 대부분은 RWO까지만 지원하고, RWX는 NFS나 CephFS 같은 공유 파일시스템 계열에서 나온다.
replica 여러 개가 볼륨 하나를 같이 쓰는 구성이 잘 안 되는 이유가 여기에 있다. RWO 볼륨을 쓰는 Deployment의 replica를 2로 올리면, 두 Pod가 서로 다른 노드에 배치되는 순간 한쪽은 volume attach 실패로 기동하지 못한다. 데이터베이스처럼 Pod마다 자기 볼륨이 필요한 워크로드는 volumeClaimTemplates로 Pod별 PVC를 만들어 주는 StatefulSet을 쓴다(Part IV. 워크로드 참고).
reclaim policy와 생명주기
PVC가 삭제되면 PV를 어떻게 처리할지는 PV의 persistentVolumeReclaimPolicy가 결정한다. Delete는 PV 오브젝트와 함께 외부 스토리지의 실제 자산까지 삭제한다. 동적 프로비저닝의 기본값이다. Retain은 데이터를 보존한다. PV는 Released 상태로 남고, 실제 디스크의 데이터도 그대로다.
Released는 “재사용 가능"이 아니다. 이전 PVC의 참조(claimRef)가 남아 있어 새 PVC와 자동으로 다시 바인딩되지 않는다. 재사용하려면 관리자가 데이터를 정리한 뒤 PV를 지우고 다시 만들거나, 데이터를 그대로 넘길 의도라면 claimRef를 수동으로 제거한다. 이 수동 개입이 번거로워 보여도, 이전 사용자의 데이터가 담긴 볼륨이 검토 없이 다른 사용자에게 넘어가는 사고를 막는 장치다.
flowchart TD
N["PV 생성"] --> A["Available"]
A -->|"PVC 바인딩"| B["Bound"]
B -->|"PVC 삭제"| R["Released"]
R -->|"Delete 정책"| D["PV·자산 삭제"]
R -->|"Retain 정책"| K["데이터 보존<br/>수동 정리 대기"]
K -->|"정리 후 재생성"| A
kubectl patch pv <이름> -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'로 Retain으로 바꿔 두는 것을 검토한다.정리
- PV는 관리자가 공급하는 클러스터 범위의 스토리지 실체, PVC는 사용자가 namespace 안에서 적는 요구이며, 둘은 storageClassName, accessModes, 용량, volumeMode가 맞을 때 1:1로 바인딩된다.
- accessModes의 단위는 노드다. RWO 볼륨도 같은 노드의 Pod 여러 개는 공유하며, Pod 단위 배타는 RWOP(1.29 GA)가 담당한다.
- Retain 정책의 PV는 PVC 삭제 후 Released로 남아 자동 재바인딩되지 않고, Delete 정책은 외부 스토리지 자산까지 삭제한다.