K8s 중급 #2 PV / PVC / StorageClass — 영속 데이터 모델

20 분 소요

K8s 중급 시리즈의 두 번째 글입니다. 기초 시리즈 #6까지 우리가 매니페스트에서 분리해 낸 것은 설정비밀이었습니다. 이미지 태그,DB 호스트,API 키 같은 값이 외부 객체로 빠져나가서 워크로드 정의가 환경에 묶이지 않게 됐습니다. 그러나 한 차원이 더 남아 있습니다 — 데이터 자체입니다. 컨테이너 안의 파일시스템은 컨테이너가 죽으면 같이 사라지는 임시 공간이고, DB 데이터,사용자 업로드 파일,Prometheus의 메트릭 시계열 같은 것은 Pod의 생애주기 너머까지 살아남아야 합니다. 이번 글에서는 그 영속 디스크의 모델을 K8s가 어떻게 표현하는지 — PersistentVolume, PersistentVolumeClaim, StorageClass의 삼각관계로 한 사이클 따라가겠습니다.

이번 시리즈는 K8s 중급 7편입니다.

컨테이너 파일시스템의 임시성 — 출발점 #

#1에서 StatefulSet이 풀어 주는 것 중 하나로 “Pod마다 1:1 영속 볼륨"을 짚고, 그 PVC의 자세한 모델은 다음 글로 미뤘습니다. 이번 글이 그 미뤄 둔 내용을 다 풀어내는 글입니다. 출발점부터 정리하겠습니다 — 왜 영속 볼륨이라는 객체가 필요한가입니다.

컨테이너의 기본 파일시스템은 그 컨테이너 안에 갇힌 임시 공간입니다. 컨테이너가 종료되면 그 안에 쌓인 파일도 같이 사라집니다. Pod 안에서 컨테이너가 재시작되면 같은 Pod라도 파일시스템은 새 것으로 깨끗하게 시작합니다. Pod 자체가 다른 노드로 옮겨 가면 그 임시 공간은 더더욱 남지 않습니다. 기초 #4에서 본 Deployment의 모델은 “Pod는 언제든 죽고 다시 떠도 된다"였는데, 그 임시성이 stateless 워크로드에는 자연스럽지만 상태를 들고 있어야 하는 워크로드에는 곤란합니다.

데이터를 살려 두려면 Pod의 파일시스템 안이 아니라 그 밖의 디스크에 써야 합니다. 그 디스크는 Pod가 죽어도 살아 있고, 새 Pod가 떠올랐을 때 그대로 다시 마운트할 수 있어야 합니다. K8s가 이 요구를 표현하는 방식이 PV / PVC / StorageClass 세 객체의 분리입니다.

emptyDir 같은 비영속 볼륨도 있다 #

K8s의 볼륨이 전부 영속인 것은 아닙니다. emptyDir처럼 Pod가 살아 있는 동안만 유지되는 볼륨도 있습니다 — 한 Pod 안의 두 컨테이너가 파일을 주고받는 용도, 큰 임시 파일 작업의 스크래치 공간 같은 곳에 씁니다. emptyDir은 Pod가 사라지면 같이 사라집니다. 이번 글의 주제는 그 반대 — Pod의 생애주기와 분리되어 살아남는 디스크의 모델입니다.

삼각관계 — PV / PVC / StorageClass #

세 객체의 책임을 한 줄씩 갈라 두면 다음과 같습니다.

객체무엇인가스코프누가 만드나
PersistentVolume (PV)디스크 자체의 표현. 클러스터 안의 한 조각 스토리지.클러스터 스코프관리자가 직접 만들거나, StorageClass가 동적으로 만듦
PersistentVolumeClaim (PVC)“이만큼의 디스크를 이런 모드로 줘"라는 요청네임스페이스 스코프앱 개발자가 매니페스트에 적음
StorageClass (SC)PVC가 들어왔을 때 PV를 어떻게 만들지의 청사진클러스터 스코프관리자가 미리 만들어 둠

이 분리가 K8s 영속 데이터 모델의 핵심입니다. 앱 개발자는 PVC만 적습니다 — “5Gi짜리 RWO 디스크가 필요해"라고 쓰는 것입니다. 어떤 클라우드의 어떤 디스크 타입이 그 요청을 만족시키는지는 SC가 들고 있고, 실제 디스크의 표현은 PV에 매핑됩니다. 이 덕에 같은 매니페스트가 AWS 클러스터에서는 EBS로, GCP에서는 PD로, 온프레미스 클러스터에서는 NFS나 Ceph로 흘러갈 수 있습니다.

머릿속 그림으로는 다음과 같이 그릴 수 있습니다.

PVC가 PV에 바인딩되는 흐름
앱 매니페스트
PVC (5Gi, RWO, storageClassName=fast-ssd)
   ├── (정적) 관리자가 미리 만든 PV 중에서 매칭되는 것을 찾아 Bound
   └── (동적) StorageClass(fast-ssd)의 provisioner가
              새 디스크를 만들고 PV로 등록 → 그 PV에 Bound

Bound라는 상태는 PVC와 PV가 1:1로 짝지어졌다는 뜻입니다. 한 PVC는 한 PV에만 묶이고, 한 PV도 한 PVC에만 묶입니다. Pod는 PVC만 보고 마운트하지 PV를 직접 보지 않습니다. 이 한 단계의 간접화 덕에 디스크 백엔드가 바뀌어도 워크로드 매니페스트는 그대로 둘 수 있습니다.

정적 프로비저닝 — 가장 단순한 모형 #

동적 프로비저닝에 들어가기 전, 가장 단순한 모형부터 따라가겠습니다 — 관리자가 PV를 직접 만들어 두고 PVC가 그것에 바인딩되는 흐름입니다. 이 모형은 운영에서 자주 쓰이지는 않지만, PV와 PVC의 매칭 규칙을 이해하는 데 가장 좋은 출발점입니다.

minikube나 kind 같은 로컬 클러스터에서는 노드의 호스트 파일시스템 한 경로를 PV의 백킹으로 빌려 쓰는 hostPath가 흔합니다. 운영 환경에는 적합하지 않지만 학습용으로는 충분합니다.

pv-static.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-local-1g
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: manual
  hostPath:
    path: /mnt/data/pv-local-1g

spec.capacity.storage가 디스크의 크기, spec.accessModes가 한 번에 몇 명이 어떤 식으로 마운트할 수 있는지(곧 다룹니다), persistentVolumeReclaimPolicy가 PVC가 사라졌을 때 이 PV를 어떻게 처리할지입니다. storageClassName: manual은 “어느 SC에도 속하지 않는, 손으로 만든 PV"라는 표시입니다 — 이 라벨이 PVC와의 매칭에 쓰입니다.

같이 쓸 PVC입니다.

pvc-static.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data
  namespace: default
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  storageClassName: manual

PVC가 PV를 고르는 규칙은 명료합니다. 다음 셋이 모두 맞아야 바인딩됩니다.

  • storageClassName이 같은가 — 위 예시는 양쪽 다 manual입니다. PVC에 storageClassName: ""(빈 문자열)을 적으면 SC가 없는 PV만, 필드 자체를 생략하면 클러스터의 기본 SC를 따라갑니다.
  • accessModes가 PVC의 요구를 PV가 만족하는가 — PVC가 RWO를 요청했는데 PV가 RWO/RWX를 모두 지원하면 OK, 그 반대(PV는 RWO만 가능한데 PVC가 RWX 요구)면 매칭 안 됨.
  • capacity가 PVC의 요구 이상인가 — PVC가 1Gi를 요청했고 PV가 1Gi 이상이면 매칭 가능. 단, 큰 PV가 작은 PVC에 묶이면 그 차액만큼 낭비됩니다.
적용과 상태 확인
kubectl apply -f pv-static.yaml
kubectl apply -f pvc-static.yaml
kubectl get pv,pvc
출력 예시
NAME                          CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM           STORAGECLASS   AGE
persistentvolume/pv-local-1g  1Gi        RWO            Retain           Bound    default/data    manual         10s

NAME                         STATUS   VOLUME        CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/data   Bound    pv-local-1g   1Gi        RWO            manual         5s

PV와 PVC 모두 STATUS: Bound이고, 서로의 이름을 CLAIM / VOLUME 컬럼에 들고 있는 모양이 정상입니다. 이 PVC를 어떤 Pod가 마운트하면 그 Pod의 컨테이너 안에서는 그냥 보통 디렉터리처럼 보이지만, 그 디렉터리는 사실 노드의 /mnt/data/pv-local-1g에 매핑되어 있습니다.

1:1 직접 바인딩 — claimRef / volumeName #

위의 매칭은 storageClassName + accessModes + capacity로 K8s가 알아서 찾는 방식이지만, 정말 특정 PV를 특정 PVC에만 묶고 싶다면 명시적으로도 적을 수 있습니다.

  • PV의 spec.claimRef에 PVC의 namespace + name을 적기
  • PVC의 spec.volumeName에 PV 이름을 적기

이 둘이 같이 들어가면 K8s는 다른 매칭 후보를 무시하고 그 1:1로 묶어 줍니다. 운영에서 이 패턴을 쓰는 경우는 드물지만, 마이그레이션이나 디스크 복구 시나리오에서 한 번씩 등장합니다.

accessModes — 누가 어떻게 마운트할 수 있나 #

accessModes는 디스크의 동시 마운트 가능성을 표현하는 필드입니다. 네 가지가 있습니다.

모드약어의미
ReadWriteOnceRWO한 노드에서 읽기,쓰기로 마운트. 같은 노드 안의 여러 Pod는 함께 마운트 가능
ReadOnlyManyROX여러 노드에서 동시에 읽기 전용으로 마운트
ReadWriteManyRWX여러 노드에서 동시에 읽기,쓰기로 마운트
ReadWriteOncePodRWOP클러스터 전체에서 단 한 Pod만 읽기,쓰기로 마운트 (1.22+ stable)

운영에서 가장 자주 보는 것은 RWO와 RWX 둘입니다. 그리고 어느 모드가 가능한지는 백엔드 디스크의 종류가 결정합니다.

백엔드지원 모드비고
AWS EBSRWO한 가용영역의 한 노드에만 붙음
GCP Persistent DiskRWO (regional은 ROX 가능)기본은 한 노드
Azure DiskRWO단일 노드
AWS EFS / GCP Filestore / Azure FilesRWXNFS 기반 파일 스토리지
온프레미스 NFSROX, RWX파일 스토리지
Ceph RBD (block)RWO블록
CephFS / GlusterFSRWX파일

한 줄로 정리하면 — 블록 스토리지는 RWO, 파일 스토리지는 RWX까지 가능입니다. RWX가 필요한 워크로드(여러 Pod가 같은 디렉터리를 같이 쓰는 패턴, 예: WordPress의 업로드 디렉터리, 공유 캐시)는 NFS 계열을 백엔드로 골라야 합니다. 블록 디스크에 RWX를 요구하는 PVC는 절대 바인딩되지 않습니다.

ReadWriteOncePod — DB의 split-brain 방지 #

1.22부터 안정화된 ReadWriteOncePod는 RWO보다 더 좁은 제약입니다. RWO는 “한 노드 안에서는 여러 Pod가 같이 마운트 가능"이지만, RWOP는 “전체 클러스터에서 단 한 Pod만 마운트“입니다. 데이터베이스처럼 두 프로세스가 동시에 같은 데이터 파일을 만지면 망가지는 워크로드의 안전장치로 쓰입니다 — 같은 노드 안의 다른 네임스페이스 Pod가 실수로 같은 PVC를 끌어당기는 사고도 막아 줍니다.

StorageClass와 동적 프로비저닝 #

정적 프로비저닝이 자명하게 가지는 부담이 있습니다 — 클러스터 관리자가 PV를 미리 사람 손으로 만들어 둬야 한다는 점입니다. 디스크가 새로 필요해질 때마다 클라우드 콘솔에서 디스크를 만들고, PV 매니페스트를 적고, apply하는 사이클이 사람의 일이 됩니다. 운영 클러스터에서는 이 모양이 곧 병목이 됩니다.

StorageClass가 그 부담을 줄여 줍니다. SC를 미리 한 번만 만들어 두면, 그 SC를 가리키는 PVC가 들어왔을 때 K8s의 provisioner가 자동으로 디스크를 만들어 PV로 등록하고 PVC와 바인딩합니다. 사람의 손이 PV 단계에서 빠집니다.

storageclass-fast.yaml — AWS EBS 예시
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
  encrypted: "true"
  iops: "3000"
  throughput: "125"
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true

이 SC를 가리키는 PVC는 다음과 같이 가벼워집니다.

pvc-dynamic.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data
  namespace: default
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: fast-ssd

이 PVC가 적용되면 다음 일이 자동으로 일어납니다.

  1. K8s가 PVC의 storageClassName을 보고 fast-ssd SC를 찾습니다.
  2. SC의 provisioner(ebs.csi.aws.com)가 호출되어 AWS EBS에 5Gi gp3 볼륨을 새로 만듭니다.
  3. 그 EBS 볼륨이 PV 객체로 자동 등록됩니다.
  4. 새 PV가 PVC와 Bound 상태가 됩니다.

클러스터별 provisioner의 예를 짧게 표로 정리하면 다음과 같습니다.

환경provisionerSC 기본
AWS EKSebs.csi.aws.com (블록), efs.csi.aws.com (파일)gp3 EBS
GCP GKEpd.csi.storage.gke.io (블록), filestore.csi.storage.gke.io (파일)balanced PD
Azure AKSdisk.csi.azure.com, file.csi.azure.comStandard SSD
minikubek8s.io/minikube-hostpathhostPath
kindrancher.io/local-pathhostPath
온프레미스NFS subdir, Ceph RBD/CSI, Longhorn 등환경마다 다름

운영 클러스터에는 보통 기본 SC(default StorageClass)가 한 개 지정되어 있고, PVC에서 storageClassName을 생략하면 그 기본 SC가 자동으로 적용됩니다. 어느 SC가 기본인지는 SC의 어노테이션 storageclass.kubernetes.io/is-default-class: "true"로 표시됩니다.

기본 SC 확인
kubectl get sc
출력 예시
NAME                 PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
fast-ssd (default)   ebs.csi.aws.com         Retain          WaitForFirstConsumer   true                   10d
slow-hdd             ebs.csi.aws.com         Delete          WaitForFirstConsumer   true                   10d

(default) 표시가 붙은 SC가 기본입니다. 한 클러스터에 두 SC가 모두 default로 표시되어 있으면 새로 만든 PVC의 동작이 모호해지므로, 기본은 한 개만 두는 것이 운영의 표준입니다.

StorageClass의 핵심 필드 셋 #

SC 매니페스트에서 자주 만지게 되는 필드 넷을 짚어 두겠습니다.

provisioner #

어느 CSI 드라이버가 디스크를 만들지 가리키는 필드입니다. 위의 표대로 클러스터 환경마다 값이 정해져 있습니다. CSI(Container Storage Interface)는 K8s가 외부 스토리지 드라이버와 이야기하는 표준 인터페이스로, 1.13에서 stable이 되었고 그 이후로 in-tree 드라이버(K8s 본체에 포함되어 있던 옛 드라이버)는 모두 CSI 외부 드라이버로 옮겨졌습니다. 새 클러스터에서 만지는 provisioner는 거의 다 *.csi.* 형태입니다.

reclaimPolicy #

PVC가 사라졌을 때 그 PVC에 묶여 있던 PV(와 그 뒤의 진짜 디스크)를 어떻게 할지의 정책입니다. 두 값이 실질적인 선택지입니다.

동작
DeletePVC 삭제와 동시에 PV도 삭제, 클라우드 디스크도 같이 삭제. 클라우드 환경의 동적 프로비저닝 기본
RetainPV가 Released 상태로 남고, 클라우드 디스크도 보존. 사람이 의도적으로 정리해야 재사용 가능

Recycle이라는 값이 옛 문서에 보일 수 있지만 deprecated 상태이니 새 매니페스트에서는 쓰지 마십시오.

Delete는 편하지만 위험합니다. 운영 클러스터에서 누군가가 PVC를 실수로 지우면 디스크가 같이 사라져 복구 불가 상태가 됩니다. 그래서 운영에서는 SC의 reclaimPolicyRetain으로 굳혀 두는 패턴이 흔합니다. PVC가 지워져도 PV는 Released 상태로 남아 있고, 그 안의 데이터도 보존됩니다. 정리가 필요하다고 판단되면 사람이 PV를 명시적으로 지우거나, 다른 PVC와 다시 묶어 데이터를 복구할 수 있습니다.

Retain — PVC 삭제 후 PV 상태
NAME           CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS     CLAIM             AGE
pv-...         5Gi        RWO            Retain           Released   default/data      30m

Released는 PVC와의 결연이 풀렸지만 디스크는 살아 있다는 뜻입니다. 이 PV를 다시 쓸 일이 없다면 kubectl delete pv ...로 명시 정리합니다 — 단, 이때 클라우드 디스크는 자동으로 사라지지 않으므로 클라우드 콘솔에서 따로 정리해야 비용이 멈춥니다.

volumeBindingMode #

PVC가 만들어졌을 때 PV(와 디스크)를 언제 만들지의 정책입니다.

동작
ImmediatePVC가 만들어지자마자 디스크 생성
WaitForFirstConsumerPVC를 마운트하는 Pod가 어느 노드에 스케줄될지 결정된 뒤에 디스크 생성

Immediate는 단순하지만 멀티 가용영역(AZ) 환경에서 사고를 부릅니다. 예를 들어 AWS EBS는 한 AZ에 묶인 디스크입니다. Immediate로 디스크가 ap-northeast-2a에 만들어졌는데, 그 PVC를 마운트하는 Pod가 스케줄러에 의해 ap-northeast-2c 노드에 떨어지면 — 그 Pod는 해당 노드에서 그 디스크를 영원히 마운트할 수 없습니다. 매니페스트로는 멀쩡해 보이지만 Pod가 Pending 상태에서 멈춰 있는 사고가 됩니다.

WaitForFirstConsumer는 그 사고를 원천 차단합니다. PVC가 만들어져도 디스크를 만들지 않고 기다립니다 — 그 PVC를 마운트하는 Pod가 등장하면, 스케줄러가 그 Pod를 어느 노드(어느 AZ)에 둘지 결정한 뒤, 그 AZ에 디스크를 만듭니다. 운영의 안전한 기본값이고, 위 SC 매니페스트도 이 값으로 두었습니다. 단일 AZ 클러스터라면 Immediate로도 큰 문제는 없지만, 멀티 AZ 환경에서는 거의 의무에 가깝게 WaitForFirstConsumer를 적어야 합니다.

allowVolumeExpansion #

true로 두면 나중에 PVC의 spec.resources.requests.storage를 키워 디스크를 같이 키울 수 있습니다. 기본값은 false이고, 한 번 true로 만든 SC를 다시 false로 되돌리는 식으로 운영하지 않습니다 — 처음 SC 만들 때 true로 두는 편이 거의 항상 좋습니다. 디스크는 운영 중에 줄이는 것이 까다롭지만 키우는 일은 자주 있습니다. 자세한 동작은 뒤의 절에서 다시 봅니다.

Pod에 PVC 마운트하기 #

PVC가 Bound 상태가 되면 Pod가 그것을 마운트해서 쓸 차례입니다. Pod 매니페스트의 모양은 어느 워크로드(Pod / Deployment / StatefulSet)에서나 같습니다 — spec.volumes에 PVC를 참조 + spec.containers[].volumeMounts로 컨테이너 안 경로에 붙입니다.

deployment-with-pvc.yaml — 발췌
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 1
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: nginx
          image: nginx:1.27
          volumeMounts:
            - name: html
              mountPath: /usr/share/nginx/html
      volumes:
        - name: html
          persistentVolumeClaim:
            claimName: data

spec.volumes[].persistentVolumeClaim.claimName이 위에서 만든 PVC data를 가리키고, 그 볼륨이 컨테이너 안 /usr/share/nginx/html에 마운트됩니다. 컨테이너 코드 입장에서는 그 경로가 그냥 보통 디렉터리이고, 그 안에 쓴 파일은 Pod가 죽고 다시 떠도 살아남습니다.

한 PVC를 여러 Pod가 마운트하면 #

위 Deployment의 replicas를 2로 키우면 어떻게 될까요. PVC data가 RWO이고 EBS 같은 블록 백엔드라면 두 Pod가 같은 노드에 떨어졌을 때만 동시에 마운트 가능합니다. 다른 노드에 떨어진 두 번째 Pod는 그 디스크가 이미 다른 노드에 attach 되어 있어서 마운트가 실패하고 Pending이나 ContainerCreating 상태에서 멈춥니다.

이 사고를 피하는 길은 두 가지입니다.

  • 워크로드를 RWX 백엔드(NFS / EFS 등)로 이전accessModes: ReadWriteMany인 PVC를 만들어 여러 Pod가 같이 쓰게 함
  • 워크로드를 StatefulSet으로 옮겨 Pod마다 자기 PVC를 쓰게 — 각 Pod가 자기 디스크를 가짐, 곧 다음 절의 volumeClaimTemplates

stateless 웹 서버 같이 디스크에 데이터를 안 두는 워크로드는 애초에 PVC 자체가 없으니 이 사고가 안 납니다. PVC 마운트가 필요한 시점은 곧 이 두 길 중 하나를 골라야 하는 시점입니다.

StatefulSet의 volumeClaimTemplates 재방문 #

#1에서 짧게 본 StatefulSet 매니페스트를 다시 펼쳐 보겠습니다.

web-statefulset.yaml — 발췌
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: web
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: nginx
          image: nginx:1.27
          volumeMounts:
            - name: data
              mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: fast-ssd
        resources:
          requests:
            storage: 1Gi

volumeClaimTemplates는 정확히 표현하면 Pod마다 PVC를 자동으로 만들어 내는 템플릿입니다. 위 매니페스트가 replicas: 3으로 적용되면 K8s가 다음 일을 합니다.

  1. Pod web-0을 만들 때, PVC data-web-0을 자동으로 만듭니다.
  2. Pod web-1을 만들 때, PVC data-web-1을 자동으로 만듭니다.
  3. Pod web-2를 만들 때, PVC data-web-2를 자동으로 만듭니다.

PVC 이름의 규칙은 <volumeClaimTemplates.metadata.name>-<statefulset.metadata.name>-<ordinal>입니다. 위 예시에서는 템플릿 이름이 data, StatefulSet 이름이 web이므로 data-web-0, data-web-1, data-web-2가 됩니다.

각 PVC는 storageClassName: fast-ssd로 지정된 SC의 동적 프로비저닝을 거쳐 PV로 매핑됩니다. AWS 환경이라면 EBS 볼륨 세 개가 새로 만들어지고, 각각 한 Pod에 1:1로 묶입니다. Pod가 죽고 다시 떠도 같은 인덱스(예: web-0)는 같은 PVC(data-web-0)를 다시 마운트하므로 데이터가 그대로 보입니다.

volumeClaimTemplates + WaitForFirstConsumer SC 조합은 #1에서 본 멀티 AZ 환경에서도 잘 굴러갑니다 — 각 Pod가 어느 AZ에 스케줄되는지 보고 그 AZ에 EBS 볼륨이 만들어지므로, AZ 미스매치 사고가 자동으로 차단됩니다.

스케일다운 시 PVC가 남는 것은 reclaimPolicy와 별개 #

#1에서 짚은 “StatefulSet을 스케일다운하면 PVC는 남는다"는 동작이 PV의 reclaimPolicy와 헷갈리기 쉬운 부분입니다. 두 동작은 서로 다른 층의 정책입니다.

  • StatefulSet 스케일다운 시 PVC 보존 — StatefulSet 컨트롤러의 동작입니다. replicas를 줄여도 PVC 객체는 그대로 남습니다. 1.27부터의 spec.persistentVolumeClaimRetentionPolicy로 이 동작을 바꿀 수 있습니다.
  • PVC 삭제 시 PV(와 디스크) 처리 — SC의 reclaimPolicy입니다. PVC가 진짜로 사라졌을 때, 그 PVC가 묶고 있던 PV와 그 뒤의 디스크를 어떻게 할지의 정책입니다.

스케일다운한다고 PVC가 자동으로 사라지지 않으므로 reclaimPolicy가 발동될 일이 없고, 사람이 명시적으로 PVC를 지웠을 때 비로소 SC의 reclaimPolicy가 동작합니다. 이 두 층이 같이 안전망이 되어 운영 사고에서 데이터가 살아남는 모양을 만듭니다.

PVC 확장 — allowVolumeExpansion #

운영 중인 PVC의 디스크가 부족해지는 일이 자주 있습니다. allowVolumeExpansion: true로 만든 SC라면 PVC의 spec.resources.requests.storage를 키워 디스크를 같이 키울 수 있습니다.

PVC 확장 — kubectl edit
kubectl edit pvc data
수정 부분
spec:
  resources:
    requests:
      storage: 10Gi   # 5Gi -> 10Gi

K8s가 다음 단계를 자동으로 진행합니다.

  1. CSI 드라이버에 클라우드 디스크 확장(예: EBS volume modification)을 요청.
  2. 클라우드 디스크가 새 크기로 늘어남.
  3. 그 디스크를 마운트하고 있는 컨테이너 안의 파일시스템을 확장(xfs_growfs / resize2fs).

대부분의 CSI 드라이버는 위 셋을 Pod 재시작 없이 진행합니다(online expansion). 다만 일부 환경,파일시스템 조합에서는 Pod의 재시작이 필요한 경우도 있고, 이 경우 PVC 상태에 FileSystemResizePending 같은 컨디션이 보입니다. Pod를 재시작하면(예: Deployment의 롤링 업데이트, StatefulSet의 한 Pod 삭제) 그 단계가 마무리됩니다.

디스크 축소(shrink)는 K8s가 지원하지 않습니다. PVC의 storage를 줄여 적어도 거부됩니다. 디스크를 줄이고 싶다면 더 작은 새 PVC를 만들어 데이터를 옮기고 옛 PVC를 정리하는 길밖에 없습니다.

백업과 스냅샷의 위치 #

PVC와 PV의 모델 위에 한 층 더 얹히는 것이 **VolumeSnapshot**과 VolumeSnapshotClass 객체입니다. 클라우드 디스크의 스냅샷 기능을 K8s 매니페스트로 표현하는 방식이고, CSI 드라이버가 스냅샷을 지원해야 합니다(AWS EBS,GCP PD,Azure Disk의 CSI 드라이버는 모두 지원).

volumesnapshot-data.yaml — 짧은 예시
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
  name: data-snap-2026-05-09
spec:
  volumeSnapshotClassName: ebs-snap
  source:
    persistentVolumeClaimName: data

이 객체가 만들어지면 CSI 드라이버가 클라우드 디스크의 스냅샷을 떠서 그 핸들을 K8s 안의 VolumeSnapshotContent로 등록합니다. 나중에 그 스냅샷에서 새 PVC를 복원하는 것도 매니페스트로 됩니다(PVC의 spec.dataSource로 스냅샷을 가리키기). 깊이 들어가는 건 K8s 실전 트랙으로 미루지만, 영속 데이터 모델 위에 백업,복구가 어떻게 얹히는지의 모양만 짚어 두는 정도로 충분합니다.

운영에서 데이터 백업은 보통 두 길 중 하나를 갑니다.

  • K8s VolumeSnapshot 위의 도구 — Velero, Kasten K10 같은 K8s 네이티브 백업 도구가 VolumeSnapshot을 묶음으로 관리해 줍니다.
  • 앱 차원 덤프 — DB의 경우 pg_dump / mysqldump / Redis RDB 같은 앱 차원 덤프를 별도 스토리지(S3 등)로 보내는 방식. 디스크 스냅샷보다 정합성이 더 보장됩니다.

정리 #

이번 글에서 잡은 흐름을 정리하겠습니다.

  • 컨테이너 파일시스템은 임시 — Pod의 생애주기와 함께 사라짐. DB,업로드,메트릭 같은 데이터는 그 밖의 디스크로 분리해야 함.
  • 세 객체의 분리PV(디스크 자체, 클러스터 스코프), PVC(요청, 네임스페이스 스코프), StorageClass(어떻게 PV를 만들지의 청사진, 클러스터 스코프). 앱 개발자는 PVC만 적고, 나머지는 SC와 provisioner가 자동으로 채움.
  • 정적 vs 동적 프로비저닝 — 정적은 사람이 PV를 미리 만들고 PVC가 매칭되는 모형, 동적은 SC가 PVC 등장 시 PV를 자동으로 만드는 모형. 운영의 표준은 동적.
  • accessModes — 블록(EBS / PD / Azure Disk)은 RWO, 파일(EFS / Filestore / NFS)은 RWX까지. ReadWriteOncePod(1.22+)는 클러스터 전체에서 단 한 Pod만.
  • SC의 핵심 필드provisioner(어느 CSI), reclaimPolicy(Delete / Retain, 운영은 Retain이 안전), volumeBindingMode(WaitForFirstConsumer가 멀티 AZ 안전 기본), allowVolumeExpansion(처음부터 true 권장).
  • Pod 마운트spec.volumes에 PVC 참조 + spec.containers[].volumeMounts로 경로에 붙임. RWO PVC를 여러 Pod가 동시에 쓰면 노드 미스매치 사고 — 해결은 RWX 또는 StatefulSet.
  • StatefulSet의 volumeClaimTemplates — Pod마다 PVC를 자동으로 만드는 템플릿. PVC 이름은 <template>-<sts>-<ordinal>. 스케일다운 시 PVC 보존(StatefulSet 정책)과 PVC 삭제 시 디스크 처리(SC reclaimPolicy)는 별개의 두 층.
  • 확장과 백업allowVolumeExpansion: true SC에서 PVC storage를 키워 디스크 확장, 축소는 미지원. 백업,스냅샷은 VolumeSnapshot + Velero 같은 도구의 위치.

이 모델까지 손에 들어오면, 회사 클러스터의 매니페스트 디렉터리에서 PV,PVC,SC 객체를 만났을 때 누가 무엇을 만들고 어떻게 매칭되는지 한 줄로 읽을 수 있습니다.

다음 — Ingress와 Ingress Controller #

이번 글까지 다룬 것은 Pod 안의 데이터가 클러스터 안에서 어떻게 살아남는가의 모델이었습니다. 다음 편의 주제는 시점을 클러스터 바깥으로 돌립니다 — 외부 트래픽이 어떻게 클러스터 안의 Service로 들어오는가입니다.

기초 #5에서 Service의 세 타입(ClusterIP / NodePort / LoadBalancer) 중 LoadBalancer가 외부 진입의 표준이라고 짚었지만, 한 클러스터에 외부 노출이 필요한 Service가 수십 개라면 LoadBalancer를 그만큼 띄우는 것은 비용,관리 양쪽에서 부담이 됩니다. 도메인이나 경로별로 라우팅을 갈라야 하는 요구도 LoadBalancer 한 단으로는 풀리지 않습니다.

#3 Ingress와 Ingress Controller — 외부 진입점에서는 그 부담을 한 곳에 모으는 객체 Ingress와, 그 매니페스트를 실제 트래픽 라우팅으로 풀어 주는 Ingress Controller(nginx / Traefik / GKE Ingress 등)의 모델을 한 사이클로 따라가겠습니다. HTTP / HTTPS 라우팅, TLS 종단, 가상 호스트, 경로 기반 라우팅까지 매니페스트 한 장의 모양으로 정리하겠습니다.

X