데이터베이스 파드가 재시작된 뒤 주문 데이터가 사라졌다는 알림이 올라왔습니다. 스토리지와 Pod 생명주기를 분리하지 않으면 컨테이너 재생성이 곧 데이터 손실로 이어질 수 있습니다. PV와 PVC는 상태 있는 워크로드를 Kubernetes에서 안전하게 운영하기 위한 핵심입니다.
PersistentVolume — 파드가 재시작해도 살아남는 스토리지
데이터베이스를 파드로 배포하고 데이터를 넣었는데 파드가 재시작하자 모든 데이터가 사라졌습니다. 파드의 컨테이너 레이어는 임시적이어서 파드 종료와 함께 사라집니다. Kubernetes에서 상태가 있는 서비스(데이터베이스, 파일 서버, 메시지 큐)를 운영하려면 파드 생애 주기와 독립적인 영구 스토리지가 필요합니다. PersistentVolume이 이 역할을 합니다. PV→PVC→Pod로 이어지는 3단계 추상화는 처음에는 복잡해 보이지만, 파드가 실제 스토리지의 종류(NFS인지 EBS인지 로컬 디스크인지)를 몰라도 되게 해주는 중요한 설계입니다. StorageClass의 동적 프로비저닝까지 이해하면, 클라우드 스토리지를 선언만으로 자동 생성하는 패턴으로 자연스럽게 연결됩니다.
파드 재시작에도 데이터가 유지되는 영구 스토리지의 구조와 동작 원리를 이해하고, 실제 데이터베이스 배포에 적용합니다.
- 1컨테이너 스토리지의 임시성과 PersistentVolume이 필요한 이유를 설명할 수 있다
- 2PV, PVC, Pod의 3단계 바인딩 흐름을 설명할 수 있다
- 3Access Mode(RWO, ROX, RWX, RWOP)를 비교해 선택할 수 있다
- 4StorageClass로 동적 프로비저닝을 구성할 수 있다
- 5reclaimPolicy의 Retain과 Delete를 비교해 선택할 수 있다
- 6StorageClass 미설정 또는 용량 부족으로 인한 PVC Pending을 진단할 수 있다
minikube는 기본 StorageClass(standard)가 포함되어 있습니다. EKS/GKE/AKS 등 클라우드 클러스터도 기본 StorageClass가 있습니다. 일반 클러스터는 kubectl get storageclass로 확인하세요.
kubectl cluster-infokubectl get storageclasskubectl create namespace storage-labkubectl get pv,pvc -n storage-lab컨테이너 스토리지의 임시성과 PV 3단계 구조
새벽에 배포된 데이터베이스 파드가 OOM으로 재시작됐습니다. 팀이 확인해보니 파드 재시작 순간 수천 건의 주문 데이터가 함께 사라졌습니다. 컨테이너 파일시스템은 파드가 종료되면 그대로 삭제되기 때문입니다. 데이터베이스, 파일 서버, 메시지 큐처럼 상태를 유지해야 하는 서비스는 파드 생애 주기와 완전히 분리된 스토리지가 필요합니다. PersistentVolume은 바로 이 요구를 해결하기 위한 Kubernetes의 핵심 스토리지 추상화입니다. PV→PVC→Pod의 3단계 구조는 복잡해 보이지만, 파드가 실제 스토리지의 위치와 종류를 몰라도 되도록 설계된 것입니다.
확대
왜 일반 볼륨만으로는 부족한가
파드 내부의 컨테이너 파일시스템은 파드가 삭제되면 함께 사라집니다. emptyDir을 사용하면 파드 내 컨테이너 간 파일 공유는 가능하지만, 파드 자체가 삭제되면 데이터가 사라집니다. 데이터베이스처럼 상태를 영구적으로 저장해야 하는 워크로드에는 파드 생애 주기와 완전히 독립적인 스토리지가 필요합니다.
PV → PVC → Pod 3단계 추상화
확대
이 구조 덕분에 파드 YAML은 스토리지 종류에 무관하게 동일하게 작성할 수 있습니다. 개발 환경(로컬 hostPath)에서 프로덕션(AWS EBS)으로 전환할 때도 파드 YAML은 변경하지 않고 PV/PVC만 바꾸면 됩니다.
Access Mode 비교
| Access Mode | 약어 | 의미 | 지원 스토리지 |
|---|---|---|---|
| ReadWriteOnce | RWO | 단일 노드에서 읽기/쓰기 | AWS EBS, GCP PD, Azure Disk |
| ReadOnlyMany | ROX | 여러 노드에서 읽기 전용 | NFS, GCS |
| ReadWriteMany | RWX | 여러 노드에서 읽기/쓰기 | NFS, AWS EFS, Azure Files |
| ReadWriteOncePod | RWOP | 단일 파드에서만 읽기/쓰기 (K8s 1.22+) | CSI 드라이버 지원 필요 |
주의: PVC에 선언한 accessMode가 볼륨의 마운트 방식을 제어합니다. 하지만 스토리지 백엔드 자체가 해당 모드를 지원해야 합니다. EBS는 RWO만 지원하므로 RWX로 선언해도 단일 노드에서만 동작합니다.
정적 프로비저닝 — PV와 PVC 수동 생성
클러스터에 처음 데이터베이스를 배포하려는데 StorageClass가 없는 환경이거나, 보안상 스토리지 생성을 인프라 팀이 직접 통제해야 하는 경우가 있습니다. 이럴 때는 관리자가 PV를 직접 생성하고 개발팀이 PVC로 요청하는 정적 프로비저닝 방식을 사용합니다. 온프레미스 클러스터나 레거시 스토리지 시스템과 연동할 때 특히 자주 쓰입니다. 수동 생성이지만 PV와 PVC의 바인딩 조건을 정확히 이해해야 Pending 상태 없이 연결됩니다.
Step 1: PersistentVolume 생성
# pv-example.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: my-pv-001
spec:
capacity:
storage: 5Gi # 이 PV가 제공하는 용량
accessModes:
- ReadWriteOnce # 단일 노드 읽기/쓰기
persistentVolumeReclaimPolicy: Retain # PVC 삭제 후에도 데이터 보존
storageClassName: manual # PVC와 매칭할 StorageClass 이름
hostPath: # 테스트용 — 프로덕션에서는 사용하지 않음
path: /data/pv-001
type: DirectoryOrCreate
kubectl apply -f pv-example.yaml
kubectl get pv my-pv-001
# NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS STORAGECLASS
# my-pv-001 5Gi RWO Retain Available manual
# ↑ PVC 연결 대기 중
- kubectl get pv에서 STATUS 열 먼저 확인 — Available이면 PVC 연결 대기 중, Bound이면 PVC에 연결됨, Released이면 이전 PVC가 삭제됐지만 데이터가 남아 있는 상태(Retain 정책)
- PVC STATUS 기준: Pending이면 조건을 충족하는 PV가 없는 것. kubectl describe pvc <name>의 Events에서 "no persistent volumes available" 또는 "storageclass not found" 메시지로 정확한 원인 확인
- PV STATUS=Released이고 PVC를 새로 만들어도 Pending이면 → Released PV는 자동으로 재사용되지 않음. kubectl edit pv <name>에서 claimRef 섹션을 삭제해야 Available 상태로 전환됨
Step 2: PersistentVolumeClaim 생성
# pvc-example.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc
namespace: storage-lab
spec:
accessModes:
- ReadWriteOnce
storageClassName: manual # PV의 storageClassName과 일치
resources:
requests:
storage: 3Gi # 요청 용량 (PV 용량 이하여야 바인딩됨)
kubectl apply -f pvc-example.yaml
kubectl get pvc my-pvc -n storage-lab
# NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS
# my-pvc Bound my-pv-001 5Gi RWO manual
# ↑ Bound = PV와 연결됨
kubectl get pv my-pv-001
# STATUS: Bound ← PVC가 연결되면 PV도 Bound로 변경
Step 3: Pod에서 PVC 사용
# pod-with-pvc.yaml
apiVersion: v1
kind: Pod
metadata:
name: pvc-test-pod
namespace: storage-lab
spec:
volumes:
- name: my-storage
persistentVolumeClaim:
claimName: my-pvc # PVC 이름 참조
containers:
- name: app
image: busybox
command: ["sh", "-c", "echo 'hello pv' > /data/test.txt && sleep 3600"]
volumeMounts:
- name: my-storage
mountPath: /data # 컨테이너 내 마운트 경로
kubectl apply -f pod-with-pvc.yaml
# 파드에서 파일 쓰기
kubectl exec pvc-test-pod -n storage-lab -- cat /data/test.txt
# hello pv
# 파드 삭제 후 재생성해도 데이터 유지 확인
kubectl delete pod pvc-test-pod -n storage-lab
kubectl apply -f pod-with-pvc.yaml
kubectl exec pvc-test-pod -n storage-lab -- cat /data/test.txt
# hello pv ← 파드 재생성 후에도 데이터 유지!
StorageClass와 동적 프로비저닝
정적 프로비저닝은 PV를 관리자가 미리 만들어야 해서 번거롭습니다. StorageClass를 사용하면 PVC를 생성할 때 자동으로 PV가 프로비저닝됩니다.
확대
StorageClass 확인 및 이해
# 클러스터의 StorageClass 목록
kubectl get storageclass
# NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE
# standard (default) k8s.io/minikube-hostpath Delete Immediate
# ↑ (default) = storageClassName 생략 시 자동으로 이 클래스 사용
# StorageClass 상세 확인
kubectl describe storageclass standard
# storageclass-example.yaml (참고용 — minikube에서는 이미 있음)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
annotations:
storageclass.kubernetes.io/is-default-class: "true" # 기본 StorageClass 지정
provisioner: kubernetes.io/aws-ebs # AWS EBS 프로비저너
parameters:
type: gp3 # EBS 볼륨 타입
fsType: ext4
reclaimPolicy: Delete # PVC 삭제 시 PV도 삭제
volumeBindingMode: WaitForFirstConsumer # 파드가 스케줄될 노드에 맞춰 프로비저닝
동적 프로비저닝 실습
# pvc-dynamic.yaml — StorageClass 지정 시 PV 자동 생성
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: dynamic-pvc
namespace: storage-lab
spec:
accessModes:
- ReadWriteOnce
storageClassName: standard # 이 StorageClass가 자동으로 PV 생성
resources:
requests:
storage: 2Gi
kubectl apply -f pvc-dynamic.yaml
# PVC 생성 직후 — 자동으로 PV가 만들어지고 바인딩됨
kubectl get pvc dynamic-pvc -n storage-lab
# NAME STATUS VOLUME CAPACITY
# dynamic-pvc Bound pvc-a1b2c3d4-xxxx-xxxx-xxxx-xxxxxxxxxxxx 2Gi
# ↑ 자동 생성된 PV 이름
kubectl get pv
# NAME CAPACITY STATUS CLAIM
# pvc-a1b2c3d4-xxxx-xxxx-xxxx-xxxxxxxxxxxx 2Gi Bound storage-lab/dynamic-pvc
실제 데이터베이스 배포 예시 — PostgreSQL
# postgres-with-pvc.yaml
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data-pvc
namespace: storage-lab
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
# storageClassName 생략 시 default StorageClass 사용
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
namespace: storage-lab
spec:
replicas: 1 # DB는 보통 StatefulSet 사용하지만 학습용으로 Deployment
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
volumes:
- name: postgres-data
persistentVolumeClaim:
claimName: postgres-data-pvc
containers:
- name: postgres
image: postgres:15-alpine
env:
- name: POSTGRES_PASSWORD
value: "changeme"
- name: PGDATA
value: "/var/lib/postgresql/data/pgdata"
ports:
- containerPort: 5432
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
kubectl apply -f postgres-with-pvc.yaml
# PVC 바인딩 확인
kubectl get pvc postgres-data-pvc -n storage-lab
# 파드 실행 확인
kubectl get pods -n storage-lab -l app=postgres
# DB에 접속하여 데이터 입력
kubectl exec -it $(kubectl get pod -n storage-lab -l app=postgres -o name) \
-n storage-lab -- psql -U postgres -c "CREATE TABLE test (id INT, name TEXT);"
kubectl exec -it $(kubectl get pod -n storage-lab -l app=postgres -o name) \
-n storage-lab -- psql -U postgres -c "INSERT INTO test VALUES (1, 'hello');"
# 파드 강제 삭제 후 재생성
kubectl delete pod -n storage-lab -l app=postgres
# Deployment가 자동으로 새 파드 생성
# 새 파드에서 데이터 확인
kubectl exec -it $(kubectl get pod -n storage-lab -l app=postgres -o name) \
-n storage-lab -- psql -U postgres -c "SELECT * FROM test;"
# id | name
# ----+-------
# 1 | hello ← 파드 재생성 후에도 데이터 유지!
PVC 한 줄이 실제 디스크에 붙기까지 — 선언부터 mount까지 5단계
kubectl apply -f pvc.yaml 한 줄이면 파드가 볼륨을 쓸 수 있게 됩니다. 그런데 어떤 PVC는 즉시 Bound가 되고, 어떤 PVC는 Pending에 멈추고, 또 어떤 파드는 Bound인데도 ContainerCreating에서 몇 분씩 못 뜹니다. 이 차이는 PVC가 실제 스토리지에 연결되기까지 거치는 단계가 서로 다른 지점에서 막히기 때문입니다. 선언 → 매칭/프로비저닝 → 바인딩 → attach → mount의 흐름을 알면 "Pending인가 ContainerCreating인가"만 보고도 어느 계층 문제인지 좁힐 수 있습니다.
kubectl apply -f pvc.yaml (storage: 5Gi, accessModes: RWO)
│
① PVC 선언 (용량 · accessMode · storageClassName)
│
② 매칭 PV 탐색 or 동적 프로비저닝 (조건 맞는 기존 PV / StorageClass가 새 PV 생성)
│
③ 바인딩 (PVC ↔ PV 1:1) (STATUS: Pending → Bound)
│
④ 파드 스케줄된 노드에 attach (attach-detach 컨트롤러가 클라우드 API 호출)
│
⑤ 그 노드에서 mount (kubelet이 파일시스템 마운트 → 컨테이너에 bind)
▼
컨테이너가 mountPath에서 읽기·쓰기 (파드가 죽어도 ②의 볼륨은 남아 데이터 유지)
각 단계에서 무슨 일이 일어나고, 막히면 어떤 증상인가:
| 단계 | 하는 일 | 여기서 막히면 |
|---|---|---|
| ① 선언 | 파드가 아니라 PVC가 "얼마짜리·어떤 접근모드가 필요하다"를 신청. 파드는 PVC 이름만 참조 | (거의 안 막힘) 오타는 아래 단계에서 증상으로 드러남 |
| ② 매칭·프로비저닝 | 정적: 조건 맞는 기존 PV를 탐색 / 동적: StorageClass의 프로비저너가 실제 볼륨 생성 | 맞는 PV 없음·StorageClass 없음·용량 부족 → 바인딩으로 못 넘어감 |
| ③ 바인딩 | 조건이 맞으면 PVC와 PV를 1:1로 묶고 둘 다 Bound로 전환 | 용량·accessMode·storageClassName 불일치 → PVC가 Pending에 머묾 |
| ④ attach | 컨트롤러가 파드가 스케줄된 노드에 볼륨을 붙임(VolumeAttachment 생성) | RWO 볼륨이 이전 노드에 붙은 채 → Multi-Attach error, 파드 ContainerCreating |
| ⑤ mount | kubelet이 볼륨을 파일시스템으로 마운트하고 컨테이너에 연결 | 파일시스템·권한 문제 → FailedMount, 역시 ContainerCreating |
정리하면 진단의 갈림길은 STATUS가 Pending이냐 ContainerCreating이냐입니다. Pending은 ②③(바인딩 단계) 문제라 kubectl describe pvc의 이벤트(맞는 PV 없음·StorageClass 없음)를 보고, ContainerCreating은 이미 Bound를 지나 ④⑤(attach·mount 단계)에서 막힌 것이라 kubectl describe pod의 이벤트(Multi-Attach·FailedMount)를 봅니다. 이 attach·mount 계층의 자세한 동작과 노드 장애 시나리오는 아래 심화에서 다룹니다.
문제 상황
kubectl apply -f pvc-dynamic.yaml
kubectl get pvc my-pvc -n storage-lab
# NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS
# my-pvc Pending <empty> <empty> <empty> fast-ssd
# ↑ Pending 상태 — PV가 바인딩되지 않음
진단 1: PVC 이벤트 확인 — 오류 메시지 읽기
kubectl describe pvc my-pvc -n storage-lab
# Events:
# Warning ProvisioningFailed no persistent volumes available for this
# claim and no storage class is set
#
# 또는:
# Warning ProvisioningFailed storageclass.storage.k8s.io "fast-ssd" not found
원인 A: StorageClass가 없는 경우
kubectl get storageclass
# No resources found ← StorageClass 없음
# 해결: 기본 StorageClass 설치 또는 PVC에서 storageClassName 제거
PVC 수정 (storageClassName 생략 → default StorageClass 사용):
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
# storageClassName: fast-ssd ← 삭제 또는 존재하는 StorageClass로 변경
원인 B: 정적 PV와 요구사항 불일치
# PV 목록과 상태 확인
kubectl get pv
# NAME CAPACITY ACCESS MODES STATUS STORAGECLASS
# my-pv-001 3Gi RWO Available manual
# PVC가 요청하는 것 확인
kubectl describe pvc my-pvc -n storage-lab | grep -E "Capacity|Access|StorageClass"
# Capacity: 5Gi ← 5Gi 요청
# Access Modes: RWO
# StorageClass: manual
# 문제: PV는 3Gi인데 PVC는 5Gi 요청 → 바인딩 불가
해결 방법 1: PVC 용량을 PV 이하로 조정
spec:
resources:
requests:
storage: 2Gi # 3Gi PV에 바인딩 가능한 용량으로 조정
해결 방법 2: 더 큰 용량의 PV 생성
spec:
capacity:
storage: 10Gi # PVC 요청 용량보다 크게 설정
원인 C: StorageClass 이름 불일치
# 사용 가능한 StorageClass 확인
kubectl get storageclass
# NAME PROVISIONER AGE
# standard k8s.io/minikube-hostpath 10d
# PVC에서 잘못된 StorageClass 지정
# storageClassName: fast-ssd ← 존재하지 않는 이름
# storageClassName: standard ← 올바른 이름으로 수정
빠른 진단 스크립트
# PVC 상태 전체 확인
echo "=== PVC 상태 ==="
kubectl get pvc -n storage-lab
echo "=== PV 상태 ==="
kubectl get pv
echo "=== StorageClass 목록 ==="
kubectl get storageclass
echo "=== PVC 이벤트 (오류 확인) ==="
kubectl describe pvc -n storage-lab | grep -A5 Events
심화 — 바인딩이 끝이 아니다: attach·detach의 세계
심화: 볼륨의 attach·detach 생애 — RWO는 '한 파드'가 아니라 '한 노드'다
PVC가 Bound가 되면 "이제 파드가 바로 쓸 수 있다"고 여기기 쉽지만, 블록 스토리지는 Bound 이후에도 노드에 실제로 붙는 단계를 더 거칩니다. 이 단계를 모르면 롤아웃이나 노드 장애 때 파드가 몇 분씩 멈추는 이유를 설명하지 못합니다.
블록 볼륨(EBS·PD·Azure Disk)은 세 단계를 거쳐 컨테이너에 도달합니다.
- provision → Bound: StorageClass가 실제 볼륨을 만들고 PV/PVC가 연결됩니다(여기까지가 앞 내용).
- attach: 컨트롤러 매니저의 attach-detach 컨트롤러가 클라우드 API로 그 볼륨을 파드가 스케줄된 노드에 붙입니다. 이때
VolumeAttachment오브젝트가 생깁니다. - mount: 그 노드의 kubelet이 볼륨을 파일시스템으로 마운트하고 컨테이너에 bind합니다.
여기서 RWO(ReadWriteOnce)의 진짜 의미가 드러납니다. RWO는 "한 번에 한 노드에만 attach"라는 뜻이지 "한 파드"가 아닙니다. 그래서 같은 노드 위의 여러 파드는 RWO 볼륨을 함께 마운트할 수 있고, 정말로 파드 하나만 허용하려면 별도 모드인 RWOP(ReadWriteOncePod)가 필요합니다.
문제는 노드를 옮길 때입니다. 정상 롤링 업데이트라면 이전 파드 종료 → 이전 노드에서 detach → 새 노드에 attach → 새 파드 mount 순서라 매끄럽습니다. 그런데 노드가 갑자기 NotReady(크래시)가 되면 그 노드의 kubelet이 죽어 정상 detach 신호를 보내지 못합니다. attach-detach 컨트롤러는 두 노드가 동시에 쓰면 파일시스템이 깨지므로 함부로 강제 detach하지 않고 타임아웃(기본 약 6분)을 기다립니다. 그 사이 새 노드로 옮겨간 파드는 attach를 못 해 ContainerCreating에서 멈춥니다. "Bound인데 왜 파드가 안 뜨지?"의 답이 바로 이 attach 계층에 있습니다.
상황: 노드 하나가 죽거나 재부팅된 뒤 그 위에 있던 상태ful 파드가 다른 노드로 재스케줄됐습니다. 그런데 새 파드가 ContainerCreating에서 진행이 안 되고, kubectl describe pod에 Multi-Attach error for volume ... Volume is already exclusively attached to one node가 보입니다. 앞 TroubleCase의 Pending(바인딩 실패)과 달리, PVC는 이미 Bound인데 attach 단계에서 막힌 것입니다.
원인: RWO 볼륨이 아직 이전(NotReady) 노드에 attach된 채 남아 있습니다. 그 노드의 kubelet이 죽어 정상 detach가 일어나지 못했고, attach-detach 컨트롤러는 데이터 손상을 막으려 강제 detach를 약 6분 유예합니다. RWO는 노드 단위 배타 접근이라 두 노드에 동시에 붙을 수 없으므로, 이전 노드에서 detach가 끝나야 새 노드가 attach할 수 있습니다.
진단: 에러 메시지와 VolumeAttachment로 '어디에 아직 붙어 있는지'를 확인합니다.
kubectl describe pod <new-pod> -n <ns> | grep -A3 "Multi-Attach"
# 볼륨이 어느 노드에 attach돼 있는지, attached 상태인지 확인
kubectl get volumeattachment | grep <pv-name>
# 이전 노드가 정말 NotReady인지 확인
kubectl get nodes
VolumeAttachment가 이전 노드를 가리키며 ATTACHED=true이고 그 노드가 NotReady이면 원인이 확정됩니다.
해결: 대응은 이전 노드의 운명에 따라 갈립니다. (1) 노드가 일시 장애면 기다립니다 — 타임아웃(약 6분) 뒤 컨트롤러가 force-detach하고 새 노드에 attach되어 파드가 뜹니다. 자동 복구를 신뢰하는 게 기본입니다. (2) 노드가 영구 소실(하드웨어 폐기)됐다면, 이전 노드가 실제로 볼륨을 더는 쓰지 않음을 확인한 뒤 stale VolumeAttachment를 제거해 강제 detach합니다 — 단, 두 노드가 동시에 쓰면 파일시스템이 손상되므로 '살아 있는 노드가 아님'을 반드시 먼저 확인합니다. (3) 예방: PDB(PodDisruptionBudget) 설정으로 가용성 지키며 드레인하기와 안정적 스케줄링으로 불필요한 재배치를 줄이고, 빠른 페일오버가 핵심이면 RWX(EFS 등)나 앱 레벨 복제(운영 DB는 스토리지 attach가 아니라 복제로 가용성 확보)를 검토합니다.
StatefulSet으로 DB 배포 — Deployment 대신 StatefulSet
# postgres-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: storage-lab
spec:
serviceName: postgres
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15-alpine
env:
- name: POSTGRES_PASSWORD
value: "changeme"
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates: # StatefulSet은 파드마다 PVC를 자동 생성
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
StatefulSet의 volumeClaimTemplates는 파드마다 개별 PVC를 자동으로 만들어줍니다. 레플리카가 3개면 PVC도 3개(data-postgres-0, data-postgres-1, data-postgres-2)가 생성됩니다.
PV 백업 전략
# PVC를 사용하는 파드 찾기
kubectl get pods -n storage-lab -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{range .spec.volumes[*]}{.persistentVolumeClaim.claimName}{"\n"}{end}{end}'
# Velero (백업 도구)로 PVC 포함 전체 네임스페이스 백업
velero backup create daily-backup \
--include-namespaces storage-lab \
--storage-location default
reclaimPolicy Retain 사용 시 PV 재사용
스토리지 리소스 삭제
안전한 실행 조건: 백업과 reclaimPolicy를 확인했고 더 이상 데이터가 필요 없을 때만 실행하세요.
실행 전 반드시 확인
- 현재 컨텍스트와 Namespace가 의도한 대상인지 확인했는가
- 운영 트래픽이나 상태 저장 데이터에 미치는 영향을 확인했는가
- 되돌릴 매니페스트, 백업, 또는 복구 절차가 준비되어 있는가
kubectl delete pvc my-pvc -n storage-lab위 항목을 모두 확인한 후 복사할 수 있습니다
# PVC 삭제 후 Retain 정책의 PV 상태
kubectl delete pvc my-pvc -n storage-lab
kubectl get pv my-pv-001
# STATUS: Released ← PVC는 없어졌지만 데이터는 보존됨
# Released PV를 새 PVC에 연결하려면 claimRef 제거 필요
kubectl patch pv my-pv-001 -p '{"spec":{"claimRef": null}}'
# 이후 STATUS: Available → 새 PVC와 바인딩 가능
실무 체크리스트
- 프로덕션 DB PV는
reclaimPolicy: Retain필수 volumeBindingMode: WaitForFirstConsumer로 파드 스케줄링 노드와 같은 가용 영역에 볼륨 생성- PVC 용량은 나중에 늘릴 수 있지만(StorageClass allowVolumeExpansion: true), 줄일 수는 없음
- 다중 복제본 StatefulSet은 각 파드마다 별도 PVC 사용 (RWX 공유 지양)
정리
스토리지 리소스 삭제
안전한 실행 조건: 백업과 reclaimPolicy를 확인했고 더 이상 데이터가 필요 없을 때만 실행하세요.
실행 전 반드시 확인
- 현재 컨텍스트와 Namespace가 의도한 대상인지 확인했는가
- 운영 트래픽이나 상태 저장 데이터에 미치는 영향을 확인했는가
- 되돌릴 매니페스트, 백업, 또는 복구 절차가 준비되어 있는가
kubectl delete pvc <pvc-name> -n <namespace>위 항목을 모두 확인한 후 복사할 수 있습니다
# PV/PVC 관련 주요 명령어
# PV/PVC 목록 확인
kubectl get pv
kubectl get pvc -n <namespace>
# PVC 상세 정보 (바인딩 상태, 용량, 이벤트)
kubectl describe pvc <pvc-name> -n <namespace>
# StorageClass 목록
kubectl get storageclass
# 파드의 볼륨 마운트 확인
kubectl describe pod <pod-name> -n <namespace> | grep -A10 "Volumes:"
# PVC 삭제 (reclaimPolicy에 따라 PV도 삭제될 수 있음)
kubectl delete pvc <pvc-name> -n <namespace>
# PV 삭제
kubectl delete pv <pv-name>
# PVC 용량 확장 (StorageClass가 allowVolumeExpansion: true인 경우)
kubectl patch pvc <pvc-name> -n <namespace> \
-p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'
실습 네임스페이스 생성 및 StorageClass 확인
kubectl create namespace storage-lab
kubectl get storageclass예상 출력
namespace/storage-lab created NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE standard (default) k8s.io/minikube-hostpath Delete Immediate
PersistentVolumeClaim 생성 및 바인딩 확인
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc
namespace: storage-lab
spec:
accessModes:
- ReadWriteOnce
storageClassName: standard
resources:
requests:
storage: 1Gi
EOF
kubectl get pvc my-pvc -n storage-lab예상 출력
NAME STATUS VOLUME CAPACITY ACCESS MODES my-pvc Bound pvc-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 1Gi RWO
PVC를 사용하는 파드 생성 및 데이터 쓰기
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: pvc-test-pod
namespace: storage-lab
spec:
volumes:
- name: my-storage
persistentVolumeClaim:
claimName: my-pvc
containers:
- name: app
image: busybox
command: ["sh", "-c", "echo 'hello pv' > /data/test.txt && sleep 3600"]
volumeMounts:
- name: my-storage
mountPath: /data
EOF
kubectl wait pod pvc-test-pod -n storage-lab --for=condition=Ready --timeout=60s
kubectl exec pvc-test-pod -n storage-lab -- cat /data/test.txt예상 출력
hello pv
파드 재생성 후 데이터 유지 확인
kubectl delete pod pvc-test-pod -n storage-lab
kubectl get pvc my-pvc -n storage-lab예상 출력
NAME STATUS VOLUME CAPACITY ACCESS MODES my-pvc Bound pvc-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 1Gi RWO
실습 리소스 정리
kubectl delete namespace storage-lab예상 출력
namespace "storage-lab" deleted
명령어·단축키 빠른 참조
이 모듈에서 다룬 PV/PVC 상태·프로비저닝·attach 진단 명령을 실전 옵션과 함께 모았습니다. PVC Pending은 바인딩 단계, Multi-Attach는 그 다음 attach 단계 문제입니다.
| 명령어/단축키 | 용도 | 자주 쓰는 예 |
|---|---|---|
kubectl get pv,pvc | STATUS(Available/Bound/Released/Pending) 확인 | kubectl get pv,pvc -n storage-lab |
kubectl describe pvc | Pending 원인(Events) 확인 | kubectl describe pvc my-pvc -n storage-lab(no PV / storageclass not found) |
kubectl get storageclass | 기본 StorageClass·프로비저너 확인 | kubectl get storageclass((default) 표시) |
kubectl describe pod | grep Volumes | 파드의 볼륨 마운트 확인 | kubectl describe pod <pod> -n <ns> | grep -A10 Volumes |
kubectl get volumeattachment | 볼륨이 어느 노드에 attach됐는지 확인 | kubectl get volumeattachment | grep <pv-name> |
kubectl describe pod | grep Multi-Attach | attach 단계 교착 확인 | kubectl describe pod <new-pod> -n <ns> | grep -A3 Multi-Attach |
kubectl patch pv(claimRef=null) | Released PV를 Available로 재사용 | kubectl patch pv my-pv-001 -p '{"spec":{"claimRef":null}}' |
kubectl patch pvc(storage) | PVC 용량 확장(allowVolumeExpansion 필요) | kubectl patch pvc <name> -p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}' |
kubectl delete pvc/pv | 스토리지 삭제(reclaimPolicy 주의) | kubectl delete pvc my-pvc -n storage-lab |
kubectl wait --for=condition=Ready | 파드 Ready 대기 후 검증 | kubectl wait pod pvc-test-pod -n storage-lab --for=condition=Ready --timeout=60s |
kubectl exec -- cat | 재생성 후 데이터 보존 확인 | kubectl exec pvc-test-pod -n storage-lab -- cat /data/test.txt |
kubectl get nodes | Multi-Attach 시 이전 노드 NotReady 확인 | kubectl get nodes |
관련 모듈로 더 깊이:
- DaemonSet과 상태 저장형 앱 배포를 위한 StatefulSet 완벽 분석 — StatefulSet이 PVC 템플릿으로 파드마다 영속 볼륨을 보장하는 방식
- ConfigMap과 Secret을 이용한 코드와 환경 설정 분리 — 데이터가 아닌 설정 파일을 볼륨으로 마운트하는 또 다른 패턴
- requests와 limits 적정 값 계산과 CPU 스로틀링 대처 — 스토리지 용량 요청과 함께 CPU/메모리 자원을 제어하는 법
- 컨테이너가 삭제되어도 안전하게 데이터를 보관하는 Volume & Bind Mount — 단일 호스트에서 컨테이너 데이터를 보존하는 Docker Volume·Bind Mount, PV/PVC의 원형 (Docker 트랙)
다음 챕터에서는 Namespace로 개발/스테이징/운영 환경을 분리하고 ResourceQuota로 자원을 제한하는 방법을 학습합니다.