본문으로 건너뛰기

CKAD 개념 노트: persistent volume과 ephemeral volume

  • kubernetes
  • ckad
  • volume
  • persistentvolume
  • storageclass

기준: Kubernetes v1.35 (kind kindest/node:v1.35.8에서 명령어·YAML 확인)

목차

개념

container filesystem만으로 부족한 이유

  • container가 crash하면 kubelet이 container를 깨끗한 상태로 재시작. container 안에서 쓴 파일은 사라짐
  • 같은 Pod 안의 container 여러 개가 파일을 공유할 방법이 없음
  • volume은 이 두 문제를 푸는 수단. 공식 문서 정의: “a directory, possibly with some data in it, which is accessible to the containers in a pod”

ephemeral vs persistent

구분수명예
ephemeral volumePod와 같음. Pod가 사라지면 같이 삭제emptyDir, configMap, secret, downwardAPI, generic ephemeral volume
persistent volumePod와 무관. Pod가 교체돼도 데이터 유지persistentVolumeClaim으로 붙인 PV

둘 다 container 재시작에는 데이터가 남는다. 차이는 Pod가 삭제될 때 드러난다.

volume 선언 구조

  • spec.volumes[]: Pod가 쓸 volume 정의 (종류와 소스)
  • spec.containers[].volumeMounts[]: container별로 어디에 마운트할지. container마다 따로 지정해야 함
  • volumeMounts[].name은 spec.volumes[].name과 일치해야 함
  • volume 안에 다른 volume을 마운트할 수 없음 (비슷한 용도로 subPath)

주요 volume 종류

emptyDir

  • Pod가 노드에 배정될 때 빈 디렉터리로 생성, Pod가 노드에서 제거되면 영구 삭제
  • container crash로는 Pod가 제거되지 않으므로 데이터 유지
  • 용도: scratch 공간, 같은 Pod의 container 간 파일 공유 (sidecar 패턴)
  • medium: Memory → tmpfs. 빠르지만 쓴 파일이 해당 container의 memory limit에 합산
  • sizeLimit로 크기 상한

hostPath

  • 노드의 파일/디렉터리를 Pod에 마운트
  • 공식 문서 경고: 보안 위험이 많으니 피할 수 있으면 피할 것 (대안으로 local PV 제시)
  • 같은 설정의 Pod라도 노드마다 파일 내용이 달라 동작이 달라질 수 있음
  • type: ""(검사 안 함), DirectoryOrCreate, Directory, FileOrCreate, File, Socket, CharDevice, BlockDevice

configMap

  • ConfigMap 데이터를 파일로 마운트. 키 하나가 파일 하나
  • items[].key/items[].path로 특정 키만, 원하는 파일명으로
  • 항상 read-only로 마운트
  • ConfigMap을 먼저 만들어 둬야 함
  • subPath로 마운트하면 ConfigMap이 바뀌어도 container에 갱신이 안 됨

persistentVolumeClaim

  • PVC를 통해 PV를 Pod에 마운트. 아래 “동작 방식” 참고

동작 방식

PV와 PVC

  • PersistentVolume (PV): 클러스터의 스토리지 한 조각. 관리자가 만들거나 StorageClass로 동적 생성. cluster-scoped, Pod와 독립된 수명
  • PersistentVolumeClaim (PVC): 사용자의 스토리지 요청. 크기와 access mode를 요구. namespace 리소스라 사용하는 Pod와 같은 namespace여야 함
  • Pod는 PV를 직접 참조하지 않고 PVC를 참조

provisioning

방식흐름
static관리자가 PV를 미리 생성 → PVC가 조건 맞는 PV에 binding
dynamic맞는 static PV가 없으면 PVC가 요청한 StorageClass의 provisioner가 PV를 생성
  • storageClassName: ""인 PVC는 dynamic provisioning을 스스로 끔
  • dynamic provisioning 기본 동작에는 API server의 DefaultStorageClass admission controller가 필요

binding 규칙

  • control plane의 control loop가 새 PVC를 보고 맞는 PV를 찾아 binding
  • 1:1 매핑. PV와 PVC 양쪽이 claimRef로 서로를 가리킴. 한 번 묶이면 배타적
  • 요청 이상을 받을 수는 있음 (8Gi 요청에 10Gi PV가 묶일 수 있음)
  • 맞는 PV가 없으면 PVC는 무기한 Pending. 조건 맞는 PV가 생기면 그때 binding
  • 매칭 조건: 크기, access mode, storageClassName 일치, selector(있으면), volumeMode
  • volumeName으로 특정 PV를 지정해 미리 binding 가능. 그 PV가 이미 다른 PVC에 묶여 있으면 Pending에서 멈춤

storageClassName 세 가지 경우

PVC 설정동작
storageClassName: fast같은 class의 PV만 binding. 없으면 fast class로 dynamic provisioning
storageClassName: ""class 없는 PV만 binding. dynamic provisioning 안 함
필드 생략default StorageClass가 있으면 그 값이 채워짐. 없으면 unset으로 남았다가 나중에 default가 생기면 갱신됨

""과 생략은 다르다. static PV에 묶고 싶은데 클러스터에 default StorageClass가 있으면 ""를 명시해야 한다.

access mode

모드약어의미
ReadWriteOnceRWO노드 하나에서 read-write 마운트. 같은 노드의 Pod 여러 개는 접근 가능
ReadOnlyManyROX여러 노드에서 read-only
ReadWriteManyRWX여러 노드에서 read-write
ReadWriteOncePodRWOPPod 하나에서만 read-write (v1.29 stable, CSI volume만)
  • volume은 여러 모드를 지원해도 한 번에 하나의 모드로만 마운트
  • RWO/ROX/RWX는 volume에 실제 제약을 걸지 않음. ROX로 만들어도 read-only가 보장되지는 않음. RWOP만 단일 Pod로 제약
  • 지원 모드는 volume plugin마다 다름 (예: HostPath는 RWO만)

volumeMode

  • Filesystem(기본): 디렉터리로 마운트
  • Block: 파일시스템 없는 raw block device로 노출

reclaim policy

PVC가 삭제된 뒤 PV를 어떻게 할지. PV의 persistentVolumeReclaimPolicy 필드.

정책동작
RetainPV가 남고 Released 상태. 이전 데이터가 남아 있어 다른 PVC에 바로 binding 안 됨. 관리자가 수동 정리
DeletePV 객체와 외부 스토리지 자산을 함께 삭제
Recycledeprecated. rm -rf /thevolume/* 후 재사용. 공식 문서는 dynamic provisioning 권장
  • 수동으로 만든 PV의 기본값은 Retain
  • dynamic provisioning으로 만든 PV는 StorageClass의 reclaimPolicy를 물려받고, 그 기본값은 Delete

PV phase

phase의미
Available아직 어느 PVC에도 묶이지 않음
BoundPVC에 묶임
ReleasedPVC는 삭제됐지만 아직 회수 전
Failed자동 회수 실패

Storage Object in Use Protection

  • Pod가 사용 중인 PVC, PVC에 묶인 PV는 삭제 요청을 해도 바로 지워지지 않음 (finalizer로 보류)
  • PVC를 쓰는 Pod가 사라진 뒤에 실제 삭제

StorageClass

  • apiVersion: storage.k8s.io/v1, cluster-scoped
  • 필드: provisioner(필수), parameters, reclaimPolicy(기본 Delete), allowVolumeExpansion, volumeBindingMode(기본 Immediate), mountOptions
  • 이름이 곧 PVC가 요청하는 class 이름. 관리자가 처음 만들 때 이름과 파라미터를 정함
  • default 지정: annotation storageclass.kubernetes.io/is-default-class: "true". 여러 개면 가장 최근 것이 사용됨
volumeBindingMode동작
ImmediatePVC 생성 즉시 binding·provisioning
WaitForFirstConsumer그 PVC를 쓰는 Pod가 생길 때까지 지연. scheduler가 Pod 배치를 고려해 volume 위치 결정

WaitForFirstConsumer에서 Pod에 nodeName을 쓰면 scheduler를 우회해 PVC가 Pending에 머문다. 공식 문서는 kubernetes.io/hostname nodeSelector를 대안으로 제시.

PVC 확장

  • StorageClass에 allowVolumeExpansion: true일 때 PVC의 spec.resources.requests.storage를 늘리면 확장
  • 축소는 지원 안 함

generic ephemeral volume

  • Pod spec의 volumes[].ephemeral.volumeClaimTemplate에 PVC 스펙을 인라인으로 작성 (v1.23 stable)
  • Pod 생성 시 ephemeral volume controller가 같은 namespace에 PVC를 만들고, Pod가 그 PVC의 owner
  • PVC 이름: <Pod 이름>-<volume 이름>. 예: Pod my-app + volume scratch-volume → my-app-scratch-volume
  • Pod가 삭제되면 garbage collector가 PVC 삭제. StorageClass reclaim policy가 기본 Delete라 보통 volume도 같이 삭제
  • emptyDir과 비슷한 용도지만 네트워크 스토리지, 고정 크기, snapshot/clone/resize 같은 스토리지 드라이버 기능을 쓸 수 있음
  • 이름 충돌 주의: Pod pod-a + volume scratch와 Pod pod + volume a-scratch는 둘 다 pod-a-scratch. 같은 namespace의 수동 PVC와도 충돌 가능. 충돌하면 기존 PVC는 건드리지 않지만 Pod가 시작되지 못함
  • 공식 문서 권장 binding mode: WaitForFirstConsumer

예시

emptyDir: container 간 공유

apiVersion: v1
kind: Pod
metadata:
  name: shared-scratch
spec:
  containers:
    - name: writer
      image: busybox:1.36
      command:
        ["sh", "-c", "while true; do date >> /data/log.txt; sleep 5; done"]
      volumeMounts:
        - name: scratch
          mountPath: /data
    - name: reader
      image: busybox:1.36
      command: ["sh", "-c", "tail -F /data/log.txt"]
      volumeMounts:
        - name: scratch
          mountPath: /data
  volumes:
    - name: scratch
      emptyDir:
        sizeLimit: 500Mi

configMap volume: 키 하나만 파일로

apiVersion: v1
kind: Pod
metadata:
  name: configmap-pod
spec:
  containers:
    - name: app
      image: busybox:1.36
      command: ["sh", "-c", "cat /etc/config/log_level.conf && sleep 3600"]
      volumeMounts:
        - name: config-vol
          mountPath: /etc/config
  volumes:
    - name: config-vol
      configMap:
        name: log-config
        items:
          - key: log_level
            path: log_level.conf

log-config의 log_level 키가 /etc/config/log_level.conf로 들어간다.

static PV + PVC + Pod

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-data
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: ""
  hostPath:
    path: /mnt/data
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-data
  namespace: default
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 500Mi
  storageClassName: ""
---
apiVersion: v1
kind: Pod
metadata:
  name: pvc-pod
  namespace: default
spec:
  containers:
    - name: web
      image: nginx:1.27
      volumeMounts:
        - name: data
          mountPath: /usr/share/nginx/html
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: pvc-data
  • PV·PVC 모두 storageClassName: ""라 default StorageClass가 있어도 dynamic provisioning 없이 이 PV에 묶임
  • 500Mi 요청에 1Gi PV가 묶임 (요청 이상 허용)
  • hostPath PV는 공식 문서상 단일 노드 테스트용

StorageClass + dynamic provisioning

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast
provisioner: csi-driver.example-vendor.example
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-fast
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 2Gi
  storageClassName: fast

provisioner는 클러스터에 설치된 CSI 드라이버 이름으로 바꿔야 한다. WaitForFirstConsumer라 이 PVC를 쓰는 Pod가 생기기 전까지 PVC는 Pending이 정상.

generic ephemeral volume

apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  containers:
    - name: app
      image: busybox:1.36
      command: ["sleep", "1000000"]
      volumeMounts:
        - name: scratch-volume
          mountPath: /scratch
  volumes:
    - name: scratch-volume
      ephemeral:
        volumeClaimTemplate:
          spec:
            accessModes: ["ReadWriteOnce"]
            storageClassName: fast
            resources:
              requests:
                storage: 1Gi

위 fast class의 provisioner는 예시용 이름이라 이대로 띄우면 PVC와 Pod가 Pending에 머문다. kind에서 직접 돌려 볼 때는 storageClassName을 기본 class인 standard로 바꾸면 my-app-scratch-volume PVC가 Bound되고, Pod를 지우면 PVC도 같이 사라지는 걸 볼 수 있다.

확인용 kubectl

kubectl get pv
kubectl get pvc -A
kubectl describe pvc pvc-data          # Events에서 Pending 원인 확인
kubectl get storageclass               # (default) 표시로 default class 확인
kubectl get pvc my-app-scratch-volume  # generic ephemeral volume이 만든 PVC
kubectl patch pv pv-data -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'

헷갈리는 것 비교

항목emptyDirhostPathgeneric ephemeralPVC (persistent)
수명Pod노드 (Pod와 무관)PodPV 기준, Pod와 무관
데이터 위치노드 로컬 디스크 또는 RAM노드의 특정 경로스토리지 드라이버스토리지 드라이버
다른 노드에서 재배치 시데이터 없음그 노드의 경로 내용새 PVC같은 데이터 (access mode 허용 시)
PVC 객체없음없음자동 생성·자동 삭제사용자가 생성·삭제
헷갈리는 쌍차이
RWO vs RWOPRWO는 노드 단위, 같은 노드의 Pod 여럿 가능. RWOP는 클러스터 전체에서 Pod 하나
Retain vs DeletePVC 삭제 후 PV가 Released로 남음 vs PV와 실제 스토리지까지 삭제
storageClassName: "" vs 생략class 없는 PV만 vs default StorageClass 적용
Immediate vs WaitForFirstConsumerPVC 생성 즉시 binding vs Pod가 생길 때까지 Pending
PV vs PVC 범위PV는 cluster-scoped, PVC는 namespaced
configMap volume vs subPath 마운트전체 마운트는 ConfigMap 변경이 반영, subPath는 반영 안 됨

시험 포인트

빨리 푸는 방법

  • PV·PVC·StorageClass는 kubectl create 하위 명령이 없음 → 공식 문서 YAML을 복사해 수정
  • Pod는 kubectl run <name> --image=<img> --dry-run=client -o yaml > pod.yaml로 뼈대를 만든 뒤 volumes/volumeMounts 추가

kubectl explain

kubectl explain pod.spec.volumes                                # volume type 목록
kubectl explain pod.spec.volumes.persistentVolumeClaim          # claimName, readOnly
kubectl explain pod.spec.volumes.emptyDir                       # medium, sizeLimit
kubectl explain pod.spec.volumes.ephemeral.volumeClaimTemplate  # generic ephemeral volume
kubectl explain pv.spec --recursive | less                      # PV 필드 트리
kubectl explain pvc.spec                                        # accessModes, resources, storageClassName
kubectl explain storageclass                                    # provisioner, volumeBindingMode

참고 문서