infra
Platform

모듈 맵

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

0 / 29 완료

펼치기
0 / 29 완료0%

쿠버네티스 & GitOps · 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과 Delete를 비교해 선택할 수 있다
  • 6StorageClass 미설정 또는 용량 부족으로 인한 PVC Pending을 진단할 수 있다
실습 환경 준비

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/PVC 바인딩 — 컨테이너 파일시스템은 재시작 시 사라지므로(임시성) DB·파일 저장엔 영속 스토리지 필요. PV(실제 스토리지)와 PVC(요청서)가 용량·accessMode·storageClassName이 일치할 때 바인딩(Bound)되고, 조건이 안 맞으면 PVC가 Pending에 머묾확대

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

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

PV → PVC → Pod 3단계 추상화

PV/PVC/Pod 스토리지 추상화 — 관리자/StorageClass가 PV(실제 스토리지, 클러스터 범위)를 생성→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로 선언해도 단일 노드에서만 동작합니다.

Kubernetes 볼륨 타입 선택 기준
파드 내 컨테이너끼리 임시 파일을 주고받아야 할 때 (빌드 캐시, 사이드카 공유)emptyDir파드 생애 주기와 동일, 파드 삭제 시 데이터 사라짐, 재시작만으로는 유지됨
데이터베이스, 파일 서버처럼 파드가 재시작하거나 재배치되어도 데이터를 보존해야 할 때PVC (PersistentVolumeClaim)파드와 독립적인 생애 주기, StorageClass로 동적 프로비저닝, 프로덕션 필수
노드의 로컬 디스크 성능을 최대한 활용해야 하는 고성능 스토리지 워크로드hostPath 또는 local PV노드 파일시스템 직접 마운트, 파드가 다른 노드로 이동하면 데이터 접근 불가
여러 파드가 동시에 동일한 볼륨을 읽고 써야 하는 공유 스토리지NFS 기반 PVC 또는 AWS EFSReadWriteMany accessMode 지원, 여러 노드에서 동시 마운트 가능
설정 파일이나 스크립트를 파드 내에 파일로 주입해야 할 때ConfigMap 또는 Secret 볼륨 마운트PV 불필요, 키-값을 파일로 마운트, 변경 시 파드 재시작 없이 일정 시간 후 갱신

💡개념

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

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

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 연결 대기 중
🔍실행 후 확인할 것
  • 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 생성

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가 프로비저닝됩니다.

정적 vs 동적 프로비저닝 — 정적은 관리자가 PV를 수동 생성(kubectl apply -f pv.yaml)한 뒤 PVC가 조건 일치로 바인딩. 동적은 StorageClass(provisioner: aws-ebs 등)를 두면 개발자가 PVC만 만들어도 K8s가 PV를 자동 생성·바인딩(STATUS Bound). volumeBindingMode는 Immediate(즉시) vs WaitForFirstConsumer(파드 스케줄 노드의 가용 영역 고려, 권장)확대

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

💡개념

PVC 한 줄이 실제 디스크에 붙기까지 — 선언부터 mount까지 5단계

kubectl apply -f pvc.yaml 한 줄이면 파드가 볼륨을 쓸 수 있게 됩니다. 그런데 어떤 PVC는 즉시 Bound가 되고, 어떤 PVC는 Pending에 멈추고, 또 어떤 파드는 Bound인데도 ContainerCreating에서 몇 분씩 못 뜹니다. 이 차이는 PVC가 실제 스토리지에 연결되기까지 거치는 단계가 서로 다른 지점에서 막히기 때문입니다. 선언 → 매칭/프로비저닝 → 바인딩 → attach → mount의 흐름을 알면 "Pending인가 ContainerCreating인가"만 보고도 어느 계층 문제인지 좁힐 수 있습니다.

TEXT
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
⑤ mountkubelet이 볼륨을 파일시스템으로 마운트하고 컨테이너에 연결파일시스템·권한 문제 → FailedMount, 역시 ContainerCreating

정리하면 진단의 갈림길은 STATUS가 Pending이냐 ContainerCreating이냐입니다. Pending은 ②③(바인딩 단계) 문제라 kubectl describe pvc의 이벤트(맞는 PV 없음·StorageClass 없음)를 보고, ContainerCreating은 이미 Bound를 지나 ④⑤(attach·mount 단계)에서 막힌 것이라 kubectl describe pod의 이벤트(Multi-Attach·FailedMount)를 봅니다. 이 attach·mount 계층의 자세한 동작과 노드 장애 시나리오는 아래 심화에서 다룹니다.


문제 상황

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

심화 — 바인딩이 끝이 아니다: 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 podMulti-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로 '어디에 아직 붙어 있는지'를 확인합니다.

Kubernetes
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

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"}}}}'
실습 단계
1

실습 네임스페이스 생성 및 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
2

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
3

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
4

파드 재생성 후 데이터 유지 확인

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
5

실습 리소스 정리

kubectl delete namespace storage-lab

예상 출력

namespace "storage-lab" deleted

명령어·단축키 빠른 참조

이 모듈에서 다룬 PV/PVC 상태·프로비저닝·attach 진단 명령을 실전 옵션과 함께 모았습니다. PVC Pending은 바인딩 단계, Multi-Attach는 그 다음 attach 단계 문제입니다.

명령어/단축키용도자주 쓰는 예
kubectl get pv,pvcSTATUS(Available/Bound/Released/Pending) 확인kubectl get pv,pvc -n storage-lab
kubectl describe pvcPending 원인(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-Attachattach 단계 교착 확인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 nodesMulti-Attach 시 이전 노드 NotReady 확인kubectl get nodes

관련 모듈로 더 깊이:

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

지식 확인

퀴즈 — 8문제

Q1

데이터베이스 파드가 재시작됐는데 데이터가 모두 사라졌다. 파드 spec을 보니 emptyDir 볼륨을 사용하고 있었다. 재시작해도 데이터가 유지되게 하려면 어떻게 해야 하는가?

Q2

MySQL StatefulSet을 3개 레플리카로 배포했는데 두 번째 파드가 Pending 상태다. 이벤트에 'volume node affinity conflict'가 떴다. 원인은?

Q3

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

Q4

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

Q5

여러 파드가 동시에 같은 볼륨에 '읽기·쓰기'로 접근해야 한다. 적절한 Access Mode는?

Q6

PV·PVC·Pod의 3단계 추상화에서 PVC의 역할은?

Q7

[심화] ReadWriteOnce(RWO) 볼륨의 '배타 접근'이 적용되는 단위로 옳은 것은?

Q8

[심화] 노드 장애 후 재스케줄된 파드가 ContainerCreating에서 멈추고 describe에 Multi-Attach error for volume이 뜬다. 원인과 1차 대응으로 옳은 것은?

0 / 8 답변

🧪 실습으로 확인하기

Kubernetes PVC 스토리지 — Pending 바인딩·데이터 영속성

중급

PVC를 쓰는 파드가 Pending에서 멈췄다. PVC 바인딩은 PVC(요청)↔StorageClass(프로비저너)↔PV(실제 볼륨)의 협상이다. 왜 바인딩이 안 되는지(StorageClass 없음·용량/접근모드 불일치·프로비저너 미동작)를 진단하고, 데이터가 Pod 재시작/재스케줄에도 살아남는지 영속성을 검증한다.

45📋 3단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요