infra
Platform

모듈 맵

[Kubernetes] PV와 PVC를 활용한 영구 볼륨 스토리지 바인딩

0 / 29 완료

펼치기
0 / 29 완료0%

Kubernetes · 09 / 29

[Kubernetes] PV와 PVC를 활용한 영구 볼륨 스토리지 바인딩

PV → PVC → Pod 바인딩 흐름, StorageClass 동적 프로비저닝, Access Mode를 실습합니다

🚨INCIDENT ALERT
HIGH

데이터베이스 파드가 재시작된 뒤 주문 데이터가 사라졌다는 알림이 올라왔습니다. 스토리지와 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 vs Delete
  • 6TroubleCase: PVC Pending — StorageClass 미설정 또는 용량 부족
실습 환경 준비

minikube는 기본 StorageClass(standard)가 포함되어 있습니다. EKS/GKE/AKS 등 클라우드 클러스터도 기본 StorageClass가 있습니다. 일반 클러스터는 kubectl get storageclass로 확인하세요.

클러스터 접근 확인
kubectl cluster-info
StorageClass 목록 확인
kubectl get storageclass
실습 네임스페이스 생성
kubectl create namespace storage-lab
PV/PVC 목록 확인 (초기 상태)
kubectl get pv,pvc -n storage-lab
💡개념

컨테이너 스토리지의 임시성과 PV 3단계 구조

새벽에 배포된 데이터베이스 파드가 OOM으로 재시작됐습니다. 팀이 확인해보니 파드 재시작 순간 수천 건의 주문 데이터가 함께 사라졌습니다. 컨테이너 파일시스템은 파드가 종료되면 그대로 삭제되기 때문입니다. 데이터베이스, 파일 서버, 메시지 큐처럼 상태를 유지해야 하는 서비스는 파드 생애 주기와 완전히 분리된 스토리지가 필요합니다. PersistentVolume은 바로 이 요구를 해결하기 위한 Kubernetes의 핵심 스토리지 추상화입니다. PV→PVC→Pod의 3단계 구조는 복잡해 보이지만, 파드가 실제 스토리지의 위치와 종류를 몰라도 되도록 설계된 것입니다.

컨테이너 스토리지의 임시성과 PV 3단계 구조

왜 일반 볼륨만으로는 부족한가

파드 내부의 컨테이너 파일시스템은 파드가 삭제되면 함께 사라집니다. emptyDir을 사용하면 파드 내 컨테이너 간 파일 공유는 가능하지만, 파드 자체가 삭제되면 데이터가 사라집니다. 데이터베이스처럼 상태를 영구적으로 저장해야 하는 워크로드에는 파드 생애 주기와 완전히 독립적인 스토리지가 필요합니다.

PV → PVC → Pod 3단계 추상화

관리자 또는 StorageClass
    │ PV 생성 (물리/클라우드 스토리지 연결)
    ▼
┌──────────────────────────────────┐
│  PersistentVolume (PV)           │
│  - 실제 스토리지 연결 정보        │
│  - 용량, accessMode              │
│  - 클러스터 범위 오브젝트         │
└────────────────┬─────────────────┘
                 │ 바인딩
    ▼
┌──────────────────────────────────┐
│  PersistentVolumeClaim (PVC)     │
│  - 스토리지 요청서                │
│  - "5Gi, ReadWriteOnce 필요"     │
│  - 네임스페이스 범위 오브젝트     │
└────────────────┬─────────────────┘
                 │ 마운트
    ▼
┌──────────────────────────────────┐
│  Pod                             │
│  - PVC 이름만 참조                │
│  - 실제 스토리지 위치 모름        │
└──────────────────────────────────┘

이 구조 덕분에 파드 YAML은 스토리지 종류에 무관하게 동일하게 작성할 수 있습니다. 개발 환경(로컬 hostPath)에서 프로덕션(AWS EBS)으로 전환할 때도 파드 YAML은 변경하지 않고 PV/PVC만 바꾸면 됩니다.

Access Mode 비교

Access Mode약어의미지원 스토리지
ReadWriteOnceRWO단일 노드에서 읽기/쓰기AWS EBS, GCP PD, Azure Disk
ReadOnlyManyROX여러 노드에서 읽기 전용NFS, GCS
ReadWriteManyRWX여러 노드에서 읽기/쓰기NFS, AWS EFS, Azure Files
ReadWriteOncePodRWOP단일 파드에서만 읽기/쓰기 (K8s 1.22+)CSI 드라이버 지원 필요

주의: PVC에 선언한 accessMode가 볼륨의 마운트 방식을 제어합니다. 하지만 스토리지 백엔드 자체가 해당 모드를 지원해야 합니다. EBS는 RWO만 지원하므로 RWX로 선언해도 단일 노드에서만 동작합니다.


💡개념

정적 프로비저닝 — PV와 PVC 수동 생성

클러스터에 처음 데이터베이스를 배포하려는데 StorageClass가 없는 환경이거나, 보안상 스토리지 생성을 인프라 팀이 직접 통제해야 하는 경우가 있습니다. 이럴 때는 관리자가 PV를 직접 생성하고 개발팀이 PVC로 요청하는 정적 프로비저닝 방식을 사용합니다. 온프레미스 클러스터나 레거시 스토리지 시스템과 연동할 때 특히 자주 쓰입니다. 수동 생성이지만 PV와 PVC의 바인딩 조건을 정확히 이해해야 Pending 상태 없이 연결됩니다.

정적 프로비저닝 — PV와 PVC 수동 생성

Step 1: PersistentVolume 생성

YAML
# 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
Kubernetes
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 연결 대기 중
🔍실행 후 확인할 것
  • NAME조회 대상 리소스 이름이 예상한 대상과 일치하는지 확인합니다.
  • STATUS/READYRunning, Ready, Available처럼 정상 상태를 나타내는 필드가 있는지 봅니다.
  • RESTARTS/EVENTS재시작 횟수나 Warning 이벤트가 증가하지 않는지 확인합니다.

Step 2: PersistentVolumeClaim 생성

YAML
# 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 용량 이하여야 바인딩됨)
Kubernetes
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 사용

YAML
# 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     # 컨테이너 내 마운트 경로
Kubernetes
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 확인 및 이해

Kubernetes
# 클러스터의 StorageClass 목록
kubectl get storageclass
# NAME                 PROVISIONER                    RECLAIMPOLICY   VOLUMEBINDINGMODE
# standard (default)   k8s.io/minikube-hostpath        Delete          Immediate
# ↑ (default) = storageClassName 생략 시 자동으로 이 클래스 사용

# StorageClass 상세 확인
kubectl describe storageclass standard
YAML
# 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  # 파드가 스케줄될 노드에 맞춰 프로비저닝

동적 프로비저닝 실습

YAML
# 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
Kubernetes
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

YAML
# 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
Kubernetes
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    ← 파드 재생성 후에도 데이터 유지!

문제 상황

Kubernetes
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 이벤트 확인 — 오류 메시지 읽기

Kubernetes
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가 없는 경우

Kubernetes
kubectl get storageclass
# No resources found   ← StorageClass 없음

# 해결: 기본 StorageClass 설치 또는 PVC에서 storageClassName 제거

PVC 수정 (storageClassName 생략 → default StorageClass 사용):

YAML
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 2Gi
  # storageClassName: fast-ssd  ← 삭제 또는 존재하는 StorageClass로 변경

원인 B: 정적 PV와 요구사항 불일치

Kubernetes
# 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 이하로 조정

YAML
spec:
  resources:
    requests:
      storage: 2Gi   # 3Gi PV에 바인딩 가능한 용량으로 조정

해결 방법 2: 더 큰 용량의 PV 생성

YAML
spec:
  capacity:
    storage: 10Gi    # PVC 요청 용량보다 크게 설정

원인 C: StorageClass 이름 불일치

Kubernetes
# 사용 가능한 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

💼
실무 맥락
현업 패턴

StatefulSet으로 DB 배포 — Deployment 대신 StatefulSet

YAML
# 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 백업 전략

Kubernetes
# 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 재사용

위험 명령어PVC/PV 삭제는 reclaimPolicy와 스토리지 설정에 따라 실제 데이터 손실로 이어질 수 있습니다.

스토리지 리소스 삭제

안전한 실행 조건: 백업과 reclaimPolicy를 확인했고 더 이상 데이터가 필요 없을 때만 실행하세요.

실행 전 반드시 확인

  • 현재 컨텍스트와 Namespace가 의도한 대상인지 확인했는가
  • 운영 트래픽이나 상태 저장 데이터에 미치는 영향을 확인했는가
  • 되돌릴 매니페스트, 백업, 또는 복구 절차가 준비되어 있는가
kubectl delete pvc my-pvc -n storage-lab

위 항목을 모두 확인한 후 복사할 수 있습니다

Kubernetes
# 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 공유 지양)

정리

위험 명령어PVC/PV 삭제는 reclaimPolicy와 스토리지 설정에 따라 실제 데이터 손실로 이어질 수 있습니다.

스토리지 리소스 삭제

안전한 실행 조건: 백업과 reclaimPolicy를 확인했고 더 이상 데이터가 필요 없을 때만 실행하세요.

실행 전 반드시 확인

  • 현재 컨텍스트와 Namespace가 의도한 대상인지 확인했는가
  • 운영 트래픽이나 상태 저장 데이터에 미치는 영향을 확인했는가
  • 되돌릴 매니페스트, 백업, 또는 복구 절차가 준비되어 있는가
kubectl delete pvc <pvc-name> -n <namespace>

위 항목을 모두 확인한 후 복사할 수 있습니다

Kubernetes
# 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"}}}}'

다음 챕터에서는 Namespace로 개발/스테이징/운영 환경을 분리하고 ResourceQuota로 자원을 제한하는 방법을 학습합니다.

지식 확인

퀴즈 — 4문제

Q1

PVC(PersistentVolumeClaim)의 역할로 올바른 것은?

Q2

ReadWriteOnce(RWO) 접근 모드에 대한 설명으로 올바른 것은?

Q3

PVC가 Pending 상태에 머물러 있는 가장 흔한 원인은?

Q4

PVC를 삭제했는데 데이터가 유지되려면 PV의 어떤 설정이 필요한가?

0 / 4 답변

🧪 실습으로 확인하기

K8s 기초 — Pod/Deployment/Service 생성

초급

kubectl로 nginx Pod를 생성하고 Deployment와 Service를 차례로 만들어 클러스터 외부에서 접근 가능한 상태까지 구성한다. K8s 3대 리소스의 역할과 관계를 직접 손으로 익힌다.

40📋 5단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요

kubernetes중급 · 50
[Kubernetes] requests와 limits 적정 값 계산과 CPU 스로틀링 대처
Kubernetes 트랙 계속
docker입문 · 30
[Docker] 백엔드 개발자에게 Docker와 컨테이너 가상화가 필수인 이유
Docker 트랙 시작점