Kyunghoon
Notes

Kubernetes / Sep 23, 2026 / 19 min

Kubernetes Persistent Storage Internals: What Happens After Creating a PVC?

PVC를 생성한 뒤 StorageClass, CSI sidecar, VolumeAttachment, kubelet을 거쳐 실제 디스크가 Pod에 마운트되기까지의 흐름을 추적한다.

KubernetesCSIStorageEBSLinux

1. 문제와 전체 흐름

Kubernetes에서 PersistentVolumeClaim(PVC)은 겉으로 보면 단순하다.

필요한 storage 크기와 access mode, StorageClass를 YAML에 적어 생성하면 된다.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-data
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: gp3
  resources:
    requests:
      storage: 20Gi

이 YAML이 표현하는 요청은 짧다.

gp3 storage 20Gi가 필요하다.

하지만 PVC는 실제 disk가 아니다. PVC object의 저장, EBS volume 생성, 컨테이너 경로로의 mount는 서로 다른 component가 처리하는 별개의 단계다.

그렇다면 PVC object가 API Server에 저장된 뒤에는 무슨 일이 일어날까? 누가 이 요청을 발견하고, 누가 실제 storage를 만들며, 생성된 volume은 어떤 과정을 거쳐 컨테이너의 /var/lib/mysql까지 연결될까?

EKS에서 AWS EBS CSI Driver를 사용하는 경우를 기준으로 전체 흐름을 먼저 단순화하면 다음과 같다.

PVC
 │
 │ StorageClass 선택
 ▼
StorageClass
 │
 │ provisioner = ebs.csi.aws.com
 ▼
external-provisioner
 │
 │ CSI CreateVolume()
 ▼
EBS CSI Controller
 │
 │ EC2 CreateVolume
 ▼
AWS EBS
 │
 ▼
PV 생성
 │
 ▼
PVC ↔ PV Binding
 │
 ▼
Pod Scheduling
 │
 ▼
attach/detach controller
 │ VolumeAttachment 생성
 ▼
VolumeAttachment
 │
 ▼
external-attacher
 │
 │ CSI ControllerPublishVolume()
 ▼
EBS → Worker Node Attach
 │
 ▼
kubelet
 │
 ├─ NodeStageVolume()
 └─ NodePublishVolume()
 │
 ▼
Container mountPath

이 글은 앞의 질문을 단계별로 따라간다. 각 단계에서 왜 다음 component가 필요한지, 어떤 object와 state가 바뀌는지, 그리고 실패했을 때 어디부터 확인해야 하는지를 연결해서 살펴본다.

이 흐름을 이해하면 PVC Pending, AttachVolume.Attach failed, Multi-Attach error, ContainerCreating 같은 storage 문제를 단순히 Kubernetes의 "storage 오류" 하나로 보지 않고, 어느 단계가 실패했는지 나누어 볼 수 있다.

2. Provisioning과 Binding

PVC object가 API Server에 저장됐다고 해서 실제 volume이 저절로 만들어지는 것은 아니다.

그렇다면 누가 이 요청을 발견하고, StorageClass는 어디까지 결정할까?

이 글은 EBS CSI Driver를 사용하는 dynamic provisioning 경로에 집중한다. 여기서 PVC는 application의 storage 요구사항을 담고, StorageClass는 사용할 provisioner와 provisioning policy를 지정한다. PV는 실제 EBS volume 자체가 아니라, 생성된 volume의 identity와 binding state를 Kubernetes API에 표현하는 object다.

관리자가 실제 storage와 PV를 미리 준비하는 static provisioning은 이번 흐름에서 제외한다. 이제 PVC가 API Server에 저장된 뒤 어떤 component가 이를 발견하는지부터 따라가 보자.

CSI는 왜 필요한가

Kubernetes core에 모든 storage vendor 구현을 넣는 방식은 유지하기 어렵다.

AWS EBS, Google Persistent Disk, Ceph, NetApp, Dell, NFS 등 storage backend마다 volume 생성, 삭제, attach, mount 방식이 다르다.

CSI(Container Storage Interface)는 Kubernetes와 storage implementation 사이의 표준 interface를 제공한다.

Kubernetes
     │
     │ CSI
     ▼
Storage Driver
     │
     ├─ AWS EBS
     ├─ Ceph
     ├─ NFS
     └─ ...

이 구조는 CRI, CNI와도 비슷하다.

영역interfaceimplementation 예시
Container RuntimeCRIcontainerd, CRI-O
NetworkingCNICilium, Calico, AWS VPC CNI
StorageCSIEBS, Ceph, other storage drivers

공통점은 Kubernetes가 구체적인 implementation과 직접 강하게 결합되지 않도록 interface를 둔다는 것이다.

다만 CSI에서 중요한 차이가 하나 있다.

Kubernetes control plane component가 storage vendor의 CSI socket을 직접 호출하는 구조가 아니다.

대신 Kubernetes CSI sidecar들이 API object를 watch하고 CSI driver RPC를 호출한다.

CSI Driver는 Controller와 Node 두 영역으로 나뉜다

일반적인 CSI 드라이버 배포는 컨트롤러 영역과 노드 영역으로 나뉜다.

공식 구조도: CSI Controller Sidecar 통신

CSI controller sidecar와 CSI Driver의 통신 구조

출처: Kubernetes CSI 공식 배포 문서

Controller component는 보통 Deployment 또는 StatefulSet으로 실행된다. Sidecar는 Kubernetes API를 이해하고, 같은 Pod의 Unix Domain Socket을 통해 CSI Driver를 호출한다.

external-provisioner는 PVC/PV 같은 Kubernetes object를 이해한다.

AWS EBS CSI Driver의 ebs-plugin은 AWS EC2 API를 통해 실제 EBS volume을 어떻게 관리하는지 안다.

이렇게 vendor-independent logic과 vendor-specific logic을 분리한다.

대표 sidecar의 책임은 다음과 같다.

sidecar감시하는 상태주요 CSI RPC
external-provisionerPVC/PVCreateVolume, DeleteVolume
external-attacherVolumeAttachmentControllerPublishVolume, ControllerUnpublishVolume
external-resizerPVC size 변경ControllerExpandVolume
external-snapshotterVolumeSnapshot 계열 objectCreateSnapshot, DeleteSnapshot

Node Component

Node component는 일반적으로 DaemonSet으로 모든 worker node에서 실행되며, node-driver-registrar와 CSI Driver로 구성된다.

공식 구조도: kubelet과 CSI Node Driver 통신

kubelet과 CSI Node Driver의 통신 구조

출처: Kubernetes CSI 공식 배포 문서

node-driver-registrar는 CSI driver를 kubelet에 등록한다.

이후 실제 Node service RPC는 kubelet이 CSI driver에게 직접 호출한다.

대표적으로 다음 RPC가 있다.

  • NodeGetInfo
  • NodeStageVolume
  • NodePublishVolume
  • NodeUnpublishVolume
  • NodeUnstageVolume

즉 controller side와 node side의 책임은 대략 다음과 같다.

Controller Side

  • 볼륨 생성과 삭제
  • 볼륨을 노드에 연결
  • 클라우드 또는 storage 컨트롤 플레인 작업 조정

Node Side

  • 노드에서 디바이스 준비
  • Pod가 사용할 경로에 볼륨 게시
  • 노드와 Linux 마운트 작업 수행

PVC를 생성하면 누가 처음 반응하는가

이제 앞에서 본 mysql-data PVC를 생성하고 실제 lifecycle을 따라가 본다.

kubectl apply -f pvc.yaml

우선 API server에 PVC object가 저장된다.

kubectl
   │
   ▼
kube-apiserver
   │
   ▼
PVC object

그 다음 external-provisioner가 이 PVC를 발견한다.

external-provisioner는 Kubernetes API에서 PVC와 PV를 watch하는 external controller다.

PVC의 StorageClass를 확인하면:

storageClassName: gp3

StorageClass에서 provisioner를 확인할 수 있다.

provisioner: ebs.csi.aws.com

즉 이 claim은 ebs.csi.aws.com driver가 처리할 대상이다.

external-provisioner가 CSI CreateVolume을 호출한다

StorageClass에 provisioner: ebs.csi.aws.com이 적혀 있다면 StorageClass나 external-provisioner가 직접 EBS를 만드는 것일까?

그렇지 않다. StorageClass는 provisioner 이름과 parameter, policy를 담은 object다. external-provisioner는 이를 읽고 선택된 CSI Controller Service에 CreateVolume을 호출한다. external-provisioner 자체가 AWS SDK를 이용해 EBS를 만드는 것이 아니다.

EBS-specific implementation은 EBS CSI Driver에 있다.

external-provisioner
        │
        │ CSI CreateVolume
        ▼
    ebs-plugin
        │
        │ EC2 CreateVolume
        ▼
      AWS API
        │
        ▼
    EBS Volume

이 구조 덕분에 external-provisioner는 EBS가 무엇인지 몰라도 된다.

다른 CSI driver를 사용하면 같은 external-provisioner control pattern 위에서 다른 driver가 실제 backend operation을 구현할 수 있다.

공식 구조도: external-provisioner 수명 주기

참고: external-provisioner 공식 design 문서의 provisioning 및 deletion lifecycle

이 diagram에서 중요한 점은 CSI operation이 단순히 한 번 호출되고 끝나는 imperative script가 아니라는 것이다.

Controller는 desired state와 actual state를 비교하면서 retry한다.

따라서 CSI specification에서 idempotency가 중요하다.

예를 들어 CreateVolume() 호출의 응답이 timeout되었다고 하자.

실제 storage backend에서는 volume 생성이 완료됐을 수도 있다.

그 상태에서 controller가 같은 요청을 retry했을 때 driver가 무조건 새로운 volume을 하나 더 만들면 안 된다.

Request 1
CreateVolume("mysql-data")
        ↓
volume A 생성
        ↓
response 유실

Retry
CreateVolume("mysql-data")
        ↓
volume B 생성   ← 잘못된 구현

따라서 CSI RPC는 retry 가능한 control loop 안에서 동작할 수 있도록 idempotent하게 구현되어야 한다.

실제 Volume이 생성된 뒤 PV는 어떻게 만들어지는가

EBS CSI Driver가 CreateVolume을 성공하면 응답에는 생성된 volume identity와 topology 등의 정보가 포함될 수 있다.

EBS 환경에서는 실제 volume이 다음과 같이 만들어졌다고 하자.

AWS EBS

Volume ID: vol-0123456789 Size: 20 GiB Type: gp3 AZ: ap-northeast-2a

external-provisioner는 이를 기반으로 PV object를 생성한다.

개념적으로:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pvc-xxxxxxxx
spec:
  capacity:
    storage: 20Gi
  accessModes:
    - ReadWriteOnce
  storageClassName: gp3
  csi:
    driver: ebs.csi.aws.com
    volumeHandle: vol-0123456789

이제 관계는 다음과 같다.

PVC
mysql-data
    │
    │ Bound
    ▼
PV
pvc-xxxxxxxx
    │
    │ volumeHandle
    ▼
EBS
vol-0123456789

즉 PV는 Kubernetes object와 실제 infrastructure resource 사이의 연결 지점이다.

PVC와 PV Binding은 Provisioning과 다른 단계다

실제 volume과 PV가 생성됐다면 PVC가 요청한 storage는 곧바로 사용할 수 있을까?

아직 아니다. 생성된 PV가 어떤 PVC의 요청을 만족하는지 확정하는 Binding이 남아 있다.

Dynamic provisioning을 설명할 때 흔히 "PVC를 만들면 PV가 생긴다"고 하나의 단계처럼 말한다. 사용자 관점에서는 맞지만 내부적으로는 다음 두 문제를 구분하는 것이 좋다.

Provisioning
=
실제 storage/PV를 준비하는 문제

Binding
=
어떤 PVC와 어떤 PV가 서로 대응하는지 확정하는 문제

Kubernetes의 PV/PVC binding은 1:1 관계다.

Binding이 완료되면 PV에는 해당 claim을 가리키는 claimRef가 기록된다.

PV
 │
 └── claimRef
       namespace: default
       name: mysql-data

이를 확인할 수 있다.

kubectl get pv
kubectl get pvc
kubectl describe pv <pv-name>
kubectl describe pvc mysql-data

Immediate vs WaitForFirstConsumer

StorageClass의 다음 field는 storage scheduling을 이해하는 데 매우 중요하다.

volumeBindingMode: WaitForFirstConsumer

왜 PVC를 생성하자마자 volume을 만들지 않고 기다려야 하는 경우가 있을까?

EBS를 예로 들면 이유가 명확하다.

EBS volume은 Availability Zone에 종속된다.

ap-northeast-2a

Node A EBS Volume A

2a의 EBS volume을 일반적인 방식으로 2c의 EC2 instance에 attach할 수는 없다.

그런데 Immediate provisioning을 사용하면 PVC 생성 시점에는 아직 Pod가 어느 Node에 schedule될지 결정되지 않았을 수 있다.

PVC 생성
   │
   ▼
EBS 생성
AZ = 2a

시간 경과

Pod Scheduling
   │
   ▼
Node B
AZ = 2c

X

이런 topology conflict를 피하기 위해 WaitForFirstConsumer를 사용한다.

PVC
 │
 │ 아직 volume provisioning 보류
 ▼
Pod 생성
 │
 ▼
Scheduler
 │
 │ Node / topology 고려
 ▼
Selected Node
 │
 ▼
external-provisioner
 │
 │ 선택된 topology를 고려
 ▼
Volume Provisioning

즉 storage scheduling이 Pod scheduling과 연결된다.

Scheduler는 단순히 CPU/RAM만 보는 것이 아니다.

Pod의 다음 조건을 함께 고려해야 할 수 있다.

  • resource request
  • node selector
  • affinity / anti-affinity
  • taint / toleration
  • storage topology

EBS처럼 topology constraint가 강한 storage에서 WaitForFirstConsumer가 중요한 이유다.

다만 이 동작은 사용하는 CSI Driver가 WaitForFirstConsumer를 지원할 때 적용된다. 또한 Pod의 nodeName field를 직접 지정하면 scheduler를 우회하므로 PVC가 Pending에 머물 수 있다. 자세한 적용 조건은 Kubernetes StorageClass 공식 문서에서 확인할 수 있다.

AWS EBS CSI Driver를 실제 구현으로 보면

AWS EBS CSI Driver 공식 구현은 controller와 node component를 분리한다.

공식 구조도: AWS EBS CSI Driver component

참고: AWS EBS CSI Driver 공식 design 문서의 component 구조

Controller component는 일반적으로 ebs-csi-controller Deployment로 실행된다.

Node component는 ebs-csi-node DaemonSet으로 각 Node에 배포된다.

Controller component에는 csi-provisioner, csi-attacher, csi-resizer와 ebs-plugin이 함께 배치된다. Node component에는 node-driver-registrar와 ebs-plugin이 배치되어 Node OS의 block device와 mount 작업을 처리한다.

같은 ebs-plugin binary/container가 controller mode와 node mode에서 각각 CSI Controller Service와 CSI Node Service 역할을 수행한다.

공식 구조도: 런타임 배포

AWS EBS CSI Driver runtime deployment 구조

출처: AWS EBS CSI Driver 공식 design 문서

이 그림에서 기억해야 할 책임 경계는 Kubernetes object → CSI Sidecar → CSI RPC → EBS Plugin → AWS API 또는 Node OS 순서다.

3. Attach와 Mount

PV와 PVC가 Bound됐다면 Pod는 volume을 바로 사용할 수 있을까?

아직 아니다. 선택된 Node가 volume을 사용할 수 있도록 attach하고, Node 내부에서 Pod가 사용할 경로로 publish해야 한다.

PV가 Bound됐다고 Pod가 바로 사용할 수 있는 것은 아니다

여기까지는 volume이 존재하고 PVC와 PV가 연결된 상태다.

하지만 실제 Pod가 Node에서 그 volume을 읽고 쓰려면 추가 단계가 필요하다.

EBS 같은 block storage를 생각해보자.

EBS Volume

존재함
   │
   X
아직 Worker Node에 연결되지 않았을 수 있음

Pod가 Worker Node에 schedule되면 Kubernetes는 volume을 그 Node에 attach해야 한다.

이 과정에서 등장하는 object가 VolumeAttachment다.

kubectl get volumeattachments.storage.k8s.io

또는:

kubectl get volumeattachment

로 확인할 수 있다.

VolumeAttachment는 무엇을 표현하는가

VolumeAttachment는:

특정 volume을 특정 Node에 attach해야 한다는 desired state

를 Kubernetes API로 표현한다.

개념적으로:

VolumeAttachment

PV:   pvc-xxxx
Node: worker-01
         │
         ▼
"이 volume을 worker-01에 attach해야 한다."

Pod가 Node에 schedule되면 kube-controller-manager 내부의 attach/detach controller가 필요한 attach 상태를 계산하고 VolumeAttachment object를 생성한다. external-attacher는 이 object를 watch하고 CSI Driver의 ControllerPublishVolume을 호출한다.

이 단계는 모든 CSI volume에 항상 존재하지 않는다. CSIDriver.spec.attachRequired가 false인 driver는 별도의 attach 단계를 건너뛸 수 있다. 이 글의 EBS 흐름은 attach가 필요한 경우를 전제로 한다. 자세한 조건은 CSIDriver object 공식 문서에서 확인할 수 있다.

external-attacher와 ControllerPublishVolume

external-attacher는 VolumeAttachment object를 watch한다.

새 attachment가 필요하면 CSI driver Controller Service에 다음 계열 RPC를 호출한다.

ControllerPublishVolume()

CSI 용어에서 ControllerPublishVolume은 일반적인 cloud block storage에서 volume attach에 대응한다.

EBS를 예로 들면:

VolumeAttachment
       │
       ▼
external-attacher
       │
       │ ControllerPublishVolume()
       ▼
EBS CSI Driver
       │
       │ EC2 AttachVolume
       ▼
AWS
       │
       ▼
EBS Volume
       │
       ▼
EC2 Worker Node

여기서 external-attacher가 AWS AttachVolume API를 직접 호출하는 것이 아니다.

다시 책임을 나누면:

external-attacher
=
Kubernetes VolumeAttachment를 이해

EBS CSI Driver
=
CSI RPC를 AWS operation으로 구현

이다.

이러한 책임 구분은 트러블슈팅에서 중요하다.

Attach와 Mount는 같은 작업이 아니다

EBS가 Node에 attach됐다면 mount도 끝난 것일까?

그렇지 않다. Storage 문제를 볼 때 attach와 mount를 하나로 생각하면 원인 구분이 어려워진다.

Attach

Attach는 storage resource를 Node에서 사용할 수 있도록 연결하는 단계다.

EBS라면:

EBS
 │
 │ Attach
 ▼
EC2 Worker Node

이다.

이 작업은 cloud control plane API를 통해 이루어진다.

Mount / Publish

그 다음 Node 내부에서 block device를 filesystem으로 준비하고 Pod path에 mount해야 한다.

EBS
 │
 ▼
Block Device on Node
 │
 ▼
Filesystem
 │
 ▼
Kubelet volume path
 │
 ▼
Pod mountPath

이쪽은 kubelet과 CSI Node plugin이 담당한다.

따라서 에러도 다르게 나타날 수 있다.

Attach 단계 실패
→ AttachVolume.Attach failed

Node mount 단계 실패
→ MountVolume.SetUp failed
→ NodeStageVolume / NodePublishVolume 관련 오류

kubelet은 Node에서 CSI Driver와 직접 통신한다

Controller side에서는 CSI sidecar가 driver RPC를 호출했다.

Node side에서는 구조가 다르다.

kubelet이 CSI Node Service를 직접 호출한다.

통신은 Unix Domain Socket을 사용한다.

kubelet
   │
   │ Unix Domain Socket
   ▼
CSI Node Driver

이를 위해 node-driver-registrar가 driver socket을 kubelet에 등록한다.

보통 개념적으로 다음과 같은 host path들이 사용된다.

/var/lib/kubelet/plugins/<driver-name>/ /var/lib/kubelet/plugins_registry/

구현 세부 경로는 환경과 driver 배포 방식에 따라 달라질 수 있다.

NodeStageVolume과 NodePublishVolume

두 RPC의 차이는 CSI를 이해할 때 자주 헷갈리는 부분이다.

NodeStageVolume

driver가 STAGE_UNSTAGE_VOLUME capability를 제공하는 경우, kubelet은 먼저 NodeStageVolume을 호출할 수 있다.

개념적으로는:

이 Node에서 volume을 사용할 수 있는 공용 staging 상태로 준비해라.

에 가깝다.

Block storage의 filesystem volume이라면 구현에 따라:

Block Device
   │
   ├─ filesystem 확인/생성
   ├─ fsck 등 준비
   │
   ▼
Node staging path

같은 작업이 포함될 수 있다.

NodePublishVolume

그 다음 NodePublishVolume은:

이 volume을 특정 workload가 사용하는 target path에 publish해라.

라는 의미다.

Node staging path
        │
        │ mount / bind mount
        ▼
Pod-specific volume path
        │
        ▼
Container mountPath

따라서 개념적으로:

NodeStageVolume
=
Node 단위 준비

NodePublishVolume
=
Pod 또는 워크로드가 사용할 대상 경로에 노출

라고 구분할 수 있다.

모든 드라이버가 완전히 동일한 Linux 작업을 수행하는 것은 아니다. CSI capability와 storage 유형에 따라 stage 단계가 생략될 수도 있다.

Container의 /data까지 어떻게 연결되는가

Pod manifest를 보자.

apiVersion: v1
kind: Pod
metadata:
  name: mysql
spec:
  containers:
    - name: mysql
      image: mysql:8
      volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: mysql-data

애플리케이션 입장에서는 단순하다.

MySQL
 │
 ▼
/var/lib/mysql

하지만 밑에는 다음 chain이 존재한다.

Container
/var/lib/mysql
      │
      ▼
Pod volume mount
      │
      ▼
kubelet-managed path
      │
      ▼
CSI NodePublishVolume
      │
      ▼
staging / filesystem
      │
      ▼
Node block device
      │
      ▼
EBS attachment
      │
      ▼
AWS EBS

즉 container는 AWS EBS를 직접 아는 것이 아니다.

애플리케이션이 보는 것은 filesystem path일 뿐이다.

4. EKS + EBS 전체 흐름

지금까지 나누어 본 단계를 하나의 요청 흐름으로 연결하면 어떻게 보일까?

EKS와 EBS CSI Driver를 기준으로 PVC가 저장된 순간부터 컨테이너가 mountPath를 사용하기까지의 state 변화와 담당 component를 정리한다.

단계state 또는 object 변화주요 component
요청PVC가 API Server에 저장됨kubectl, kube-apiserver
StorageClass 선택provisioner: ebs.csi.aws.com 확인external-provisioner
ProvisioningCSI CreateVolume 호출, EBS volume 생성external-provisioner, EBS CSI controller, AWS API
BindingPV 생성 후 PVC와 1:1로 연결external-provisioner, persistent volume controller
SchedulingPod가 storage topology를 만족하는 Node에 배치됨kube-scheduler
AttachmentVolumeAttachment 생성 후 EBS를 Node에 attachattach/detach controller, external-attacher, EBS CSI controller
Node stagingblock device와 filesystem을 Node에서 준비kubelet, CSI Node Driver
Pod publishworkload target path에 volume을 노출kubelet, CSI Node Driver
사용container가 /var/lib/mysql 같은 mountPath로 접근container runtime, Linux mount

여기서 PV Bound는 provisioning과 binding이 끝났다는 뜻이지 attach와 mount까지 완료됐다는 뜻은 아니다. 따라서 Pod가 ContainerCreating에 머문다면 VolumeAttachment 이후의 상태를 이어서 확인해야 한다.

5. 조건과 예외

앞에서 본 흐름은 모든 cluster와 CSI Driver에서 같은 결과로 이어질까?

그렇지는 않다. 큰 흐름은 비슷해 보여도 cluster 버전, CSI Driver capability, StorageClass 설정에 따라 실제로 나타나는 단계와 결과가 달라질 수 있다.

Volume Expansion은 어떻게 동작하는가

StorageClass가 다음을 허용한다고 하자.

allowVolumeExpansion: true

PVC의 size를:

resources:
  requests:
    storage: 20Gi

에서:

resources:
  requests:
    storage: 50Gi

로 확장한다.

단순히 PV object의 숫자만 바꾸는 것이 아니다.

CSI 기반 expansion은 개념적으로:

PVC size 변경
      │
      ▼
external-resizer
      │
      │ ControllerExpandVolume()
      ▼
CSI Driver
      │
      ▼
Actual Storage Resize
      │
      ▼
Node / Filesystem Resize

과정을 거칠 수 있다.

블록 디바이스 용량이 늘어나는 것과 파일시스템 용량이 늘어나는 것은 별개의 문제라는 점도 중요하다.

Block Device
20Gi → 50Gi

!=

Filesystem
20Gi → 50Gi

driver와 filesystem이 online expansion 등을 지원하는지에 따라 세부 lifecycle이 달라질 수 있다. Kubernetes의 volume expansion은 용량을 늘리는 동작만 지원하며 축소는 지원하지 않는다. CSI volume에서는 driver 지원 여부와 StorageClass의 allowVolumeExpansion field를 함께 확인해야 한다. Kubernetes StorageClass 공식 문서

PVC를 삭제하면 EBS도 삭제되는가

항상 그런 것은 아니다.

여기서 persistentVolumeReclaimPolicy가 중요하다.

Delete

동적으로 provision된 storage에서 흔히 사용하는 정책이다.

PVC 삭제
   │
   ▼
PV release/delete lifecycle
   │
   ▼
CSI DeleteVolume()
   │
   ▼
Actual Storage 삭제

StorageClass에 reclaim policy가 지정되어 있으면 dynamic provisioning으로 생성된 PV가 그 정책을 사용한다.

Retain

Retain이면 claim이 사라져도 underlying storage를 자동 삭제하지 않고 관리자가 데이터를 복구하거나 수동으로 정리할 수 있도록 남길 수 있다.

PVC 삭제
   │
   ▼
PV Released
   │
   ▼
Actual Storage 유지

DB처럼 데이터가 중요한 workload에서는 reclaim policy를 deployment 단계에서 명시적으로 확인하는 것이 좋다.

ReadWriteOnce는 "Pod 하나"라는 뜻이 아니다

Access Mode도 흔히 오해하는 부분이다.

ReadWriteOnce (RWO)

한 Node에서 read-write로 mount될 수 있다는 의미다.

Node A

Pod 1 ─┐
       ├─ RWO Volume
Pod 2 ─┘

조건에 따라 같은 Node의 여러 Pod가 접근할 수 있으므로 RWO = Pod 하나라고 이해하면 정확하지 않다.

ReadWriteOncePod (RWOP)

하나의 Pod만 read-write로 사용하도록 보장하려면 ReadWriteOncePod가 별도로 존재한다.

ReadWriteOncePod는 CSI volume에서만 사용할 수 있다. Kubernetes v1.29부터 stable이며, 실제 사용 가능 여부는 cluster와 CSI sidecar 버전도 함께 확인해야 한다. Kubernetes Persistent Volumes 공식 문서

ReadWriteMany (RWX)

여러 Node에서 read-write mount가 가능한 mode다.

NFS/EFS/CephFS 같은 shared filesystem 계열에서 흔히 볼 수 있다.

Storage backend가 어떤 access mode를 실제로 지원하는지가 중요하다.

Default StorageClass에서 생기는 미묘한 차이

PVC에서 storageClassName을 아예 생략하는 것과 빈 문자열로 지정하는 것은 의미가 다르다.

Field를 생략

spec:
  resources:
    requests:
      storage: 10Gi

Cluster에 default StorageClass가 있다면 default class가 적용될 수 있다.

Empty String

spec:
  storageClassName: ""

이는 class를 사용하지 않는 claim을 명시하는 의미가 있다.

Static provisioning 실습에서 이 차이를 모르고 default StorageClass가 활성화되어 있으면 의도와 달리 dynamic provisioning이 발생할 수 있다.

6. 장애 단계별 확인

PVC가 Pending이거나 Pod가 ContainerCreating에 멈췄다면 어디부터 확인해야 할까?

Storage 장애는 하나의 오류가 아니라 여러 control loop와 Node 작업 중 한 단계가 멈춘 결과다. 보이는 증상에서 lifecycle을 거꾸로 따라가며 범위를 좁힌다.

PVC Pending을 어떻게 볼 것인가

이제 troubleshooting 관점으로 돌아가 보자.

PVC가:

STATUS = Pending

이면 처음부터 "디스크가 고장났다"고 생각하기보다 provisioning/binding 단계부터 확인한다.

Step 1 — PVC 자체 확인

kubectl get pvc
kubectl describe pvc <pvc-name>

특히 Event가 중요하다.

확인할 것은 다음과 같다.

  • StorageClass가 존재하는가?
  • provisioner 이름이 올바른가?
  • 요청한 access mode를 지원하는가?
  • capacity 요구 조건을 만족할 수 있는가?
  • topology 관련 메시지가 있는가?
  • ProvisioningFailed event가 있는가?

Step 2 — StorageClass 확인

kubectl get storageclass
kubectl describe storageclass <class-name>

또는:

kubectl get sc

다음 field를 확인한다.

  • provisioner
  • volumeBindingMode
  • reclaimPolicy
  • allowVolumeExpansion
  • parameters

WaitForFirstConsumer라면 Pod가 아직 없어서 PVC가 Pending인 것이 정상일 수도 있다.

이 상태를 provisioning failure와 혼동하면 안 된다.

Step 3 — CSI Controller 확인

예를 들어 EBS CSI:

kubectl get pods -n kube-system \
  -l app=ebs-csi-controller

환경에 따라 label은 달라질 수 있으므로 실제 deployment 이름을 확인한다.

kubectl get deploy -n kube-system
kubectl get pods -n kube-system

controller logs에서는 provisioner와 driver를 분리해서 보는 것이 좋다.

external-provisioner log vs ebs-plugin controller log

왜냐하면:

PVC를 감지하지 못하는 문제 != CSI RPC는 갔지만 AWS API가 실패하는 문제

이기 때문이다.

Pod가 ContainerCreating에 멈추면

PVC가 이미 Bound라면 다음 단계로 내려간다.

PVC Bound
   │
   ▼
Pod ContainerCreating

우선:

kubectl describe pod <pod-name>

에서 Events를 본다.

대표적으로 attach/mount 오류가 나타날 수 있다.

AttachVolume.Attach failed MountVolume.SetUp failed Multi-Attach error

이제 이 오류가 어느 계층인지 분류한다.

AttachVolume
      │
      ▼
VolumeAttachment / external-attacher /
ControllerPublishVolume / cloud API

MountVolume
      │
      ▼
kubelet / CSI Node plugin /
NodeStageVolume / NodePublishVolume

VolumeAttachment를 직접 확인하기

CSI attach가 필요한 volume이라면 다음이 매우 유용하다.

kubectl get volumeattachment

상세:

kubectl describe volumeattachment <name>

확인해야 할 것은 다음과 같다.

  • 어떤 PV인가?
  • 어떤 Node인가?
  • attached=true인가?
  • attach error가 있는가?

Pod만 보면:

ContainerCreating

한 줄로 보이던 문제가 VolumeAttachment까지 내려가면:

PV → Node attach가 아직 완료되지 않음

으로 좁혀질 수 있다.

Node 쪽에서는 무엇을 확인할까

Attach가 완료되었는데 mount가 실패한다면 Node 관점으로 이동한다.

CSI node Pod:

kubectl get daemonset -n kube-system
kubectl get pods -n kube-system -o wide

CSI object:

kubectl get csidriver
kubectl get csinode

Node의 kubelet log도 중요한 source다.

systemd 기반 Node라면 예를 들어:

journalctl -u kubelet

Node에서 실제 block device와 mount를 확인할 수도 있다.

lsblk
findmnt
mount

클라우드 공급자와 드라이버에 따라 실제 디바이스 이름과 경로가 달라질 수 있으므로 특정 /dev/... 이름을 고정적으로 가정해서는 안 된다.

Troubleshooting을 Lifecycle로 나누기

앞에서 본 전체 흐름을 장애 단계에 대응시키면 원인 범위를 빠르게 좁힐 수 있다. 같은 Pending이나 ContainerCreating 상태라도 멈춘 lifecycle에 따라 확인할 component와 상태가 달라진다.

증상우선 확인할 계층
PVC PendingStorageClass, provisioner, topology, binding
PVC Bound + Pod Pendingscheduling, topology
AttachVolume.Attach failedVolumeAttachment, external-attacher, CSI controller, cloud API
Multi-Attach error기존 attachment, access mode, 노드 전환
MountVolume.SetUp failedkubelet, CSI node plugin, filesystem, mount
Pod Running + I/O errorfilesystem, storage backend, application

실제로 관찰해 보기

다음은 Dynamic provisioning을 사용하는 격리된 Kubernetes 실험 환경에서 수행할 수 있는 실험 제안이다.

이 글에서는 아래 명령을 직접 실행하지 않았다. 따라서 아래 설명은 실제 관측 결과가 아니라 예상 결과이며, cluster 설정과 CSI Driver에 따라 달라질 수 있다.

StorageClass

kubectl get sc
kubectl describe sc <storage-class>

PVC

kubectl get pvc -w

다른 terminal에서:

kubectl apply -f pvc.yaml

예상 결과: PVC가 Pending에서 Bound로 바뀌거나, WaitForFirstConsumer라면 이를 사용하는 Pod가 생성될 때까지 Pending으로 유지될 수 있다.

PV

kubectl get pv -w

예상 결과: Dynamic provisioning이 성공하면 사용자가 직접 만들지 않은 PV가 생성되고 PVC와 Bound 상태가 된다.

Pod

kubectl get pod -w

VolumeAttachment

kubectl get volumeattachment -w

예상 결과: EBS처럼 attach가 필요한 CSI storage라면 Pod scheduling 이후 VolumeAttachment object가 생성되고 attached 상태가 갱신된다.

CSI resources

kubectl get csidriver
kubectl get csinode

Events

kubectl get events --sort-by=.lastTimestamp

예상 결과: 정상 흐름에서는 provisioning, binding, scheduling, attach, mount 순서의 상태 변화를 확인할 수 있다. 실패한다면 Event를 통해 어느 단계에서 중단됐는지 좁힐 수 있다.

핵심 정리

  • PVC는 필요한 storage 조건을 표현하고, PV는 실제 storage를 Kubernetes에서 표현한다.
  • StorageClass는 storage 자체가 아니라 dynamic provisioning 방법과 정책을 정의한다.
  • external-provisioner는 Kubernetes object를 watch하고 CSI CreateVolume을 호출한다.
  • vendor CSI Driver는 CSI RPC를 실제 storage backend operation으로 구현한다.
  • EBS처럼 attach가 필요한 storage에서는 VolumeAttachment와 external-attacher가 동작한다.
  • attach와 mount는 서로 다른 단계이며 실패 지점과 확인할 component도 다르다.
  • Node에서는 kubelet이 CSI Node Service를 호출해 stage와 publish를 수행한다.
  • WaitForFirstConsumer, attachRequired, access mode, reclaimPolicy는 driver와 cluster 조건을 함께 확인해야 한다.
  • 장애는 Provisioning → Binding → Scheduling → Attachment → Stage → Publish → I/O 순서로 나누어 확인한다.

이해 확인 질문

  1. StorageClass와 external-provisioner의 책임은 어떻게 다른가?
  2. Provisioning과 PVC/PV Binding을 구분해야 하는 이유는 무엇인가?
  3. WaitForFirstConsumer가 topology-aware storage에서 필요한 이유는 무엇인가?
  4. VolumeAttachment object는 누가 만들고 누가 처리하는가?
  5. EBS attach와 Node의 filesystem mount는 어떻게 다른가?
  6. NodeStageVolume과 NodePublishVolume의 책임은 어떻게 다른가?
  7. PVC가 Bound인데 Pod가 ContainerCreating이라면 어느 단계부터 확인해야 하는가?
  8. ReadWriteOnce와 ReadWriteOncePod는 어떤 차이가 있는가?

References

Kubernetes Documentation

Persistent Volumes — Kubernetes Documentation https://kubernetes.io/docs/concepts/storage/persistent-volumes/

Storage Classes — Kubernetes Documentation https://kubernetes.io/docs/concepts/storage/storage-classes/

Dynamic Volume Provisioning — Kubernetes Documentation https://kubernetes.io/docs/concepts/storage/dynamic-provisioning/

Kubernetes CSI

Kubernetes CSI Documentation — Introduction https://kubernetes-csi.github.io/docs/introduction.html

Deploying a CSI Driver on Kubernetes https://kubernetes-csi.github.io/docs/deploying.html

CSI External Provisioner https://github.com/kubernetes-csi/external-provisioner

External Provisioner Design https://github.com/kubernetes-csi/external-provisioner/blob/master/doc/design.md

CSI External Attacher https://github.com/kubernetes-csi/external-attacher

CSI Node Driver Registrar https://github.com/kubernetes-csi/node-driver-registrar

CSIDriver Object https://kubernetes-csi.github.io/docs/csi-driver-object.html

AWS EBS CSI Driver

AWS EBS CSI Driver https://github.com/kubernetes-sigs/aws-ebs-csi-driver

AWS EBS CSI Driver Design https://github.com/kubernetes-sigs/aws-ebs-csi-driver/blob/master/docs/design.md