15분 읽기

쿠버네티스에서 동적 스토리지 만들기

PVC를 만들었는데 누가 실제 디스크를 만들고, Pod가 시작될 때 누가 그 디스크를 Node에 붙여 줄까? 동적 프로비저닝(dynamic provisioning)은 이 질문의 답을 자동화한다. 개발자가 용량과 StorageClass를 담은 PVC를 요청하면, Kubernetes와 CSI 드라이버가 실제 스토리지를 만들고 Pod에 연결한다.

운영자는 CSI 드라이버와 StorageClass를 한 번 표준화해 두고, 팀마다 디스크를 수동으로 만들어 주는 반복 작업을 줄인다.

개발자는 백엔드 API나 스토리지 자격 증명 없이, PVC 요청만으로 워크로드에 필요한 영속 볼륨을 얻는다.

Kubernetes 공식 Dynamic Volume Provisioning 문서는 StorageClass를 통해 사용자의 PVC에 맞춰 볼륨을 자동으로 만들 수 있다고 설명한다.

동적 스토리지는 어떻게 만들어질까?

동적 프로비저닝은 크게 두 구간으로 나뉜다.

  1. 볼륨 생성 단계: PVC 요청을 보고 실제 볼륨과 PV를 만든 뒤 PVC와 PV를 연결한다.
  2. Pod 연결 단계: Pod가 배치되면 해당 Node의 kubelet과 CSI node plugin이 볼륨을 연결하고 마운트한다.
flowchart LR
  subgraph NS["team-a namespace · 개발자"]
    PVC["PVC\n20Gi · fast-ssd"]
    POD["Pod\n/var/lib/app에 마운트"]
  end

  subgraph CP["클러스터 제어 영역 · 운영자가 준비"]
    SC["StorageClass\nprovisioner와 정책"]
    PROV["external-provisioner\nCSI CreateVolume 호출"]
    PV["PV\nPVC와 바인딩"]
  end

  subgraph NODE["Pod가 배치된 Node"]
    KUBELET["kubelet"]
    NODEPLUGIN["CSI node plugin\nDaemonSet"]
  end

  BACKEND[("스토리지 백엔드\n디스크 · 파일시스템 · SAN")]

  PVC --> SC
  SC --> PROV
  PROV --> BACKEND
  PROV --> PV
  PV --> PVC
  POD --> PVC
  POD --> KUBELET
  KUBELET --> NODEPLUGIN
  NODEPLUGIN --> BACKEND

각 구성 요소는 맡은 일이 다르다.

구성 요소주된 역할누가 준비·관리하는가
PVC필요한 용량·접근 모드·StorageClass를 요청개발자
StorageClass어떤 provisioner와 정책으로 볼륨을 만들지 선언운영자
CSI controller plugin실제 스토리지에 볼륨을 생성·삭제하고, 필요하면 Node 연결을 제어운영자/스토리지 업체
CSI sidecarKubernetes API 이벤트를 보고 CSI 드라이버 호출을 중계운영자/스토리지 업체
CSI node pluginPod가 있는 Node에서 마운트·마운트 해제를 수행운영자/스토리지 업체
kubeletNode의 CSI plugin을 호출해 Pod가 쓸 볼륨을 준비Kubernetes

CSI(Container Storage Interface)는 Kubernetes와 여러 스토리지 제품을 같은 방식으로 연결하기 위한 표준이다. CSI 드라이버는 보통 두 부분으로 나뉜다. 컨트롤러 부분(controller component)은 볼륨 생성과 삭제를 맡고, Node 부분(node component)은 각 Node에서 볼륨을 마운트한다. Kubernetes CSI 배포 문서는 컨트롤러 부분을 Deployment 또는 StatefulSet으로, Node 부분을 DaemonSet으로 설명한다.

운영자는 무엇을 준비할까?

개발자가 PVC를 만들기 전에, 운영자는 스토리지 백엔드를 쓸 수 있는 CSI 드라이버와 StorageClass를 배포한다. 아래는 의미를 보여 주기 위한 예시다. provisionerparameters의 정확한 값은 사용하는 CSI 드라이버 문서를 따라야 한다.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: csi.example.com
parameters:
  tier: ssd
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true

provisioner는 이 StorageClass의 PVC를 어느 CSI 드라이버가 처리할지 알려 준다. parameters는 스토리지 등급이나 암호화처럼 벤더별 생성 옵션을 전달한다. reclaimPolicy, allowVolumeExpansion, volumeBindingMode는 모든 팀이 같은 방식으로 스토리지를 쓰도록 만드는 운영 정책이다.

CSI 컨트롤러 부분에는 드라이버 본체와 이를 돕는 sidecar 컨테이너가 함께 들어가는 경우가 많다. 그중 external-provisioner는 PVC가 생기는지 지켜보다가 드라이버에 볼륨 생성이나 삭제를 요청한다. sidecar는 Kubernetes API와 통신하는 공통 작업을 맡고, 드라이버는 스토리지 제품에 맞는 동작을 처리한다. 이렇게 역할을 나누면 스토리지 업체가 Kubernetes와 통신하는 코드를 매번 새로 만들지 않아도 된다.

flowchart TB
  subgraph CONTROLLER["CSI controller component · Deployment/StatefulSet"]
    EP["external-provisioner\nPVC 감시"]
    EA["external-attacher\n필요한 경우 attach 중계"]
    DRIVER_C["CSI controller plugin\nCreateVolume · ControllerPublishVolume"]
    EP -->|공유 Unix socket| DRIVER_C
    EA -->|공유 Unix socket| DRIVER_C
  end

  API["Kubernetes API"] --> EP
  API --> EA
  DRIVER_C --> BACKEND[("스토리지 백엔드 API")]

  subgraph EVERY_NODE["모든 Node · DaemonSet"]
    REG["node-driver-registrar"]
    DRIVER_N["CSI node plugin\nNodeStage/NodePublish"]
    REG --> DRIVER_N
  end

  KUBELET["각 Node의 kubelet"] -->|host의 Unix socket| DRIVER_N

모든 CSI 드라이버가 같은 sidecar를 쓰거나 별도의 Node 연결(attach) 단계를 요구하는 것은 아니다. 예를 들어 네트워크 파일시스템은 별도 연결 작업이 필요 없을 수 있다. 운영자는 선택한 드라이버가 지원하는 접근 모드, 위치 제약, 용량 확장, 스냅샷, Node 연결 방식을 StorageClass와 운영 문서에 반영해야 한다.

개발자가 PVC를 만들면 무슨 일이 생길까?

개발자의 선언은 간단하다.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: app-data
  namespace: team-a
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: fast-ssd
  resources:
    requests:
      storage: 20Gi

조건이 맞는 기존 PV가 없다면 StorageClass의 provisioner가 동적 프로비저닝을 시작한다. Immediate는 PVC가 만들어지면 곧바로 볼륨을 만드는 방식이며, 순서는 다음과 같다.

sequenceDiagram
  participant D as 개발자
  participant API as API server / PV controller
  participant P as external-provisioner
  participant C as CSI controller plugin
  participant S as 스토리지 백엔드

  D->>API: PVC(20Gi, fast-ssd) 생성
  API-->>P: provision 대상 PVC 감지
  P->>C: CreateVolume(용량, StorageClass 파라미터)
  C->>S: 실제 볼륨 생성
  S-->>C: volume ID 반환
  C-->>P: 생성 완료
  P->>API: volume ID를 담은 PV 생성
  API->>API: PV와 PVC 바인딩

이 시점에는 아직 볼륨이 컨테이너에 마운트되지 않았다. PV가 만들어지고 PVC가 Bound가 된 것은 “어떤 스토리지를 쓸지 정해졌다”는 뜻이다. Pod가 실제로 어느 Node에서 실행되는지는 다음 단계에서 결정된다.

Pod가 실행될 위치에 맞춰 볼륨 만들기

클라우드 블록 디스크처럼 가용 영역이나 Node 토폴로지에 제약이 있는 스토리지는 볼륨을 너무 일찍 만들면 문제가 생길 수 있다. 예를 들어 PVC가 먼저 A 영역에 볼륨을 만들었는데, Pod가 B 영역에만 배치 가능하다면 Pod는 실행되지 못한다.

volumeBindingMode: WaitForFirstConsumer는 Pod가 실행될 위치와 볼륨의 위치를 맞추는 설정이다. PVC가 생겨도 실제 볼륨을 바로 만들지 않고, 이 PVC를 쓰는 Pod의 위치가 정해질 때까지 기다린다. 스케줄러가 Pod에 맞는 Node와 가용 영역을 고르면 provisioner가 그 위치에 볼륨을 만든다. StorageClass 공식 문서는 이를 위치를 고려한 볼륨 프로비저닝(topology-aware volume provisioning)이라고 설명한다.

sequenceDiagram
  participant D as 개발자
  participant API as API server
  participant SCH as scheduler
  participant P as external-provisioner
  participant S as 스토리지 백엔드
  participant K as kubelet

  D->>API: PVC + Pod 생성
  API-->>SCH: Pod와 아직 Bound가 아닌 PVC
  SCH->>SCH: Node·topology를 고려해 Node 선택
  SCH-->>P: 선택된 Node 정보가 담긴 provision 요청
  P->>S: 해당 topology에 볼륨 생성
  P->>API: PV 생성 · PVC Bound
  API-->>K: 선택된 Node에서 Pod 시작
  K->>S: attach / mount 준비

따라서 이 모드의 PVC가 Pending이고 아직 Pod도 없다면 정상일 수 있다. 반대로 Pod가 있는데도 오래 Pending이면 스케줄러 이벤트와 PVC 이벤트를 함께 봐야 한다. 둘 중 하나만 보면 어떤 단계가 멈췄는지 놓치기 쉽다.

만든 볼륨을 Pod에 어떻게 연결할까?

PVC가 Bound이고 스케줄러가 Pod를 Node에 배치하면, kubelet이 Node 쪽의 CSI 드라이버를 호출한다. 별도 연결이 필요한 드라이버라면 Kubernetes의 attach/detach controller가 VolumeAttachment 객체를 만들고, external-attacher가 CSI controller plugin의 ControllerPublishVolume을 호출한다. 그 후 kubelet이 CSI node plugin의 stage·publish 기능을 호출해 Node와 Pod 경로에 볼륨을 준비한다.

CSI 메서드 이름을 모두 외울 필요는 없다. 아래 순서만 이해하면 전체 흐름을 따라갈 수 있다.

flowchart LR
  A["Pod가 Node에 스케줄됨"] --> B{"별도 attach가\n필요한 드라이버인가?"}
  B -->|예| C["VolumeAttachment 생성\nexternal-attacher → ControllerPublishVolume"]
  B -->|아니오| D["kubelet이 node plugin 호출"]
  C --> D
  D --> E["NodeStageVolume\nNode에 장치·파일시스템 준비"]
  E --> F["NodePublishVolume\nPod volume 경로에 mount"]
  F --> G["컨테이너 volumeMount에서 사용"]

NodeStageVolumeNodePublishVolume은 드라이버가 지원하는 기능에 따라 사용 방식이 다를 수 있다. 중요한 점은 애플리케이션 컨테이너가 스토리지 시스템에 직접 접속하지 않는다는 것이다. kubelet과 CSI node plugin이 Node에서 준비한 마운트를 컨테이너가 사용한다. CSI node plugin이 DaemonSet으로 모든 Node에 있어야 하는 이유도 여기에 있다. Kubernetes CSI 배포 문서는 kubelet이 CSI Node 서비스를 호출해 볼륨을 마운트하거나 해제한다고 설명한다.

오류가 나면 어디부터 확인할까?

동적 프로비저닝 오류는 “스토리지가 안 된다”로만 보지 말고, 어느 단계에서 멈췄는지 나눠서 살펴야 빠르게 해결할 수 있다.

보이는 상태먼저 확인할 것주로 확인하는 사람
PVC가 Pending, Pod 없음StorageClass 이름·기본 StorageClass·WaitForFirstConsumer 여부개발자, 운영자
PVC가 Pending, Pod도 PendingPod scheduler 이벤트, Node·토폴로지 제약, PVC 이벤트개발자, 운영자
PVC provisioning 이벤트가 실패CSI controller와 external-provisioner 로그, 백엔드 권한·용량·파라미터운영자
PVC는 Bound, Pod가 ContainerCreatingPod 이벤트, VolumeAttachment, CSI node plugin·kubelet 로그운영자 중심
Pod는 실행되지만 쓰기 오류접근 모드, 권한·파일시스템, 애플리케이션의 마운트 경로개발자, 운영자

개발자는 namespace 권한만으로도 아래 신호를 확인할 수 있다.

kubectl get pvc -n team-a
kubectl describe pvc app-data -n team-a
kubectl get pod -n team-a
kubectl describe pod <pod-name> -n team-a
kubectl get events -n team-a --sort-by=.lastTimestamp

운영자는 여기에 CSI controller·node plugin의 상태와 로그, CSIDriver, 필요할 때 VolumeAttachment까지 확인한다. PV와 VolumeAttachment는 클러스터 범위 리소스다. 여러 팀이 한 클러스터를 함께 쓰는 환경에서는 이 정보를 모두 공개하기보다, 개발자에게 PVC·Pod 이벤트와 명확한 지원 절차를 제공하는 편이 안전하다.

핵심 정리

동적 프로비저닝은 “PVC가 생기면 디스크가 생긴다”보다 조금 더 긴 흐름이다. StorageClass가 정책을 고르고, external-provisioner와 CSI controller plugin이 실제 볼륨과 PV를 만든다. 스케줄러와 kubelet, CSI node plugin은 그 볼륨을 Pod가 쓸 경로에 마운트한다.

  • 운영자는 CSI 드라이버와 StorageClass를 한 번 운영 가능한 표준으로 만들면, 팀마다 생성·연결 작업을 수동으로 반복하지 않고 정책과 관측에 집중할 수 있다.
  • 개발자는 스토리지 제품의 API, 자격 증명, Node 마운트 절차를 몰라도 PVC에 필요한 조건만 선언해 영속 스토리지를 사용할 수 있다.

이 편리함을 안전하게 누리려면 역할을 분명히 나눠야 한다. 운영자는 스토리지 백엔드와 CSI의 안정성, 권한, 위치 정책을 책임진다. 개발자는 필요한 용량과 접근 모드, 데이터 보관 기간에 맞는 PVC를 요청한다. 이 경계가 분명하면 동적 프로비저닝을 안전한 셀프서비스로 운영할 수 있다.

참고 자료