Kubernetes 스토리지의 핵심 개념
컨테이너는 본질적으로 임시(ephemeral)다. Pod가 재시작되면 컨테이너 내부 파일시스템은 초기화된다. 데이터베이스, 파일 업로드, 로그 등 영속적인 데이터가 필요한 워크로드에서는 Kubernetes의 Persistent Volume(PV)과 Persistent Volume Claim(PVC)을 통해 스토리지를 관리해야 한다.
PV · PVC · StorageClass 관계
| 개념 | 역할 | 비유 |
|---|---|---|
| PersistentVolume (PV) | 클러스터 레벨 스토리지 리소스 | 실제 디스크 |
| PersistentVolumeClaim (PVC) | Pod가 스토리지를 요청하는 선언 | 디스크 신청서 |
| StorageClass | 동적 프로비저닝 정책 | 디스크 등급 (SSD/HDD) |
PVC가 요청을 보내면, StorageClass에 의해 PV가 동적으로 생성되거나, 관리자가 미리 만들어둔 PV에 정적으로 바인딩된다.
정적 프로비저닝: PV 수동 생성
NFS, 기존 디스크 등 인프라팀이 직접 관리하는 스토리지를 사용할 때:
# pv-nfs.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-nfs-data
labels:
type: nfs
spec:
capacity:
storage: 50Gi
accessModes:
- ReadWriteMany # 여러 Pod에서 동시 읽기/쓰기
persistentVolumeReclaimPolicy: Retain # PVC 삭제 후에도 데이터 보존
nfs:
server: 10.0.0.100
path: /exports/data
---
# pvc-nfs.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-nfs-data
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 50Gi
selector:
matchLabels:
type: nfs # 특정 PV를 라벨로 선택
정적 프로비저닝은 PV와 PVC를 직접 매칭해야 하므로 운영 부담이 크다. 대부분의 클라우드 환경에서는 동적 프로비저닝을 사용한다.
동적 프로비저닝: StorageClass
PVC를 생성하면 StorageClass가 자동으로 PV를 프로비저닝한다:
# storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: ebs.csi.aws.com # AWS EBS CSI 드라이버
parameters:
type: gp3 # SSD 타입
iops: "5000"
throughput: "250"
encrypted: "true"
reclaimPolicy: Delete # PVC 삭제 시 PV도 삭제
volumeBindingMode: WaitForFirstConsumer # Pod 스케줄링 시점에 바인딩
allowVolumeExpansion: true # 볼륨 확장 허용
---
# PVC만 생성하면 PV는 자동 생성
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data
spec:
storageClassName: fast-ssd # StorageClass 지정
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
volumeBindingMode: 스케줄링 타이밍
StorageClass에서 가장 중요한 설정 중 하나:
| 모드 | 동작 | 사용 시점 |
|---|---|---|
| Immediate | PVC 생성 즉시 PV 프로비저닝 | 존 제약 없는 스토리지 |
| WaitForFirstConsumer | Pod가 스케줄링될 때 PV 생성 | 존 바인딩이 필요한 경우 (권장) |
# ❌ Immediate 문제: PV가 us-east-1a에 생성됐는데 Pod가 us-east-1b에 스케줄링
# → Pod가 Pending 상태로 멈춤
# ✅ WaitForFirstConsumer: Pod가 스케줄링된 노드의 존에 PV 생성
# → 항상 같은 AZ에 매칭
K8s Affinity 스케줄링과 함께 사용할 때 WaitForFirstConsumer가 특히 중요하다. Affinity로 특정 존에 Pod를 배치하면 PV도 같은 존에 생성된다.
Access Modes: 접근 모드 이해
| 모드 | 약어 | 설명 | 대표 스토리지 |
|---|---|---|---|
| ReadWriteOnce | RWO | 단일 노드에서 읽기/쓰기 | EBS, GCE PD |
| ReadOnlyMany | ROX | 여러 노드에서 읽기 전용 | NFS, GlusterFS |
| ReadWriteMany | RWX | 여러 노드에서 읽기/쓰기 | EFS, CephFS, NFS |
| ReadWriteOncePod | RWOP | 단일 Pod에서만 읽기/쓰기 | CSI 드라이버 지원 시 |
# DB는 RWO (단일 노드 마운트)
# 업로드 파일 공유는 RWX (여러 Pod 동시 접근)
# 민감 데이터는 RWOP (단일 Pod만 접근, K8s 1.27+)
Reclaim Policy: PVC 삭제 후 동작
# Retain: PVC 삭제 후에도 PV와 데이터 보존 (수동 정리 필요)
# Delete: PVC 삭제 시 PV와 실제 스토리지도 함께 삭제 (동적 프로비저닝 기본값)
# Recycle: 더 이상 사용하지 않음 (deprecated)
# 프로덕션 DB → Retain (데이터 보호)
# 임시 캐시/빌드 → Delete (자동 정리)
실수 방지: 동적 프로비저닝의 기본 reclaimPolicy는 Delete다. 프로덕션 데이터베이스의 PVC를 실수로 삭제하면 디스크 자체가 삭제된다. 중요한 워크로드에는 반드시 Retain을 설정하자.
볼륨 확장 (Volume Expansion)
디스크 용량이 부족할 때 PVC를 수정해 확장할 수 있다:
# StorageClass에 allowVolumeExpansion: true 필요
# PVC 용량 변경
kubectl patch pvc app-data -p '{"spec":{"resources":{"requests":{"storage":"50Gi"}}}}'
# 상태 확인 — FileSystemResizePending이면 Pod 재시작 필요
kubectl get pvc app-data -o jsonpath='{.status.conditions[*].type}'
# 오프라인 확장이 필요한 스토리지는 Pod 삭제 후 재생성
kubectl delete pod app-pod-0
# Pod 재시작 시 파일시스템 확장 자동 수행
주의: 볼륨 축소는 지원되지 않는다. 한번 늘린 용량은 줄일 수 없으므로 신중하게 확장해야 한다.
StatefulSet과 volumeClaimTemplates
StatefulSet에서 Pod마다 독립 PVC를 자동 생성하는 패턴. DB 클러스터에서 핵심적으로 사용된다:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
volumeMounts:
- name: pgdata
mountPath: /var/lib/postgresql/data
resources:
requests:
memory: "1Gi"
cpu: "500m"
volumeClaimTemplates: # Pod마다 별도 PVC 생성
- metadata:
name: pgdata
spec:
storageClassName: fast-ssd
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 100Gi
이 설정으로 pgdata-postgres-0, pgdata-postgres-1, pgdata-postgres-2 세 개의 PVC가 자동 생성된다. StatefulSet을 삭제해도 PVC는 삭제되지 않는다 — 데이터 보호를 위한 의도적 설계다.
스토리지 모니터링과 알림
# PVC 사용량 확인
kubectl get pvc -A -o wide
# 실제 디스크 사용량 확인 (kubelet metrics)
kubectl get --raw /api/v1/nodes/<node>/proxy/stats/summary
| jq '.pods[].volume[]? | select(.pvcRef) | {pvc: .pvcRef.name, used: .usedBytes, capacity: .capacityBytes}'
# Prometheus 알림 규칙 예시
# kubelet_volume_stats_used_bytes / kubelet_volume_stats_capacity_bytes > 0.85
# → 85% 초과 시 알림
K8s ConfigMap·Secret 운영에서 다룬 것처럼, 스토리지도 용량 임계치를 모니터링하고 선제적으로 확장해야 장애를 방지할 수 있다.
스냅샷과 백업 (VolumeSnapshot)
# VolumeSnapshotClass 정의
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: ebs-snapshot-class
driver: ebs.csi.aws.com
deletionPolicy: Retain
---
# 스냅샷 생성
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: pgdata-snapshot-20260305
spec:
volumeSnapshotClassName: ebs-snapshot-class
source:
persistentVolumeClaimName: pgdata-postgres-0
---
# 스냅샷에서 PVC 복원
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pgdata-restored
spec:
storageClassName: fast-ssd
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 100Gi
dataSource:
name: pgdata-snapshot-20260305
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
정리: 스토리지 설계 체크리스트
| 항목 | 권장 설정 |
|---|---|
| 프로비저닝 | 동적 (StorageClass) |
| 바인딩 모드 | WaitForFirstConsumer |
| 프로덕션 DB reclaimPolicy | Retain |
| 볼륨 확장 | allowVolumeExpansion: true |
| 다중 Pod 공유 | RWX (EFS/NFS) 사용 |
| 백업 | VolumeSnapshot + CronJob |
| 모니터링 | 85% 임계치 알림 |
Kubernetes 스토리지는 “PVC만 만들면 끝”이 아니다. volumeBindingMode로 AZ 불일치를 방지하고, reclaimPolicy로 데이터 손실을 막고, VolumeSnapshot으로 백업을 자동화해야 프로덕션 수준의 스토리지 운영이 가능하다.