Certified Kubernetes Administrator (CKA) #16 Storage 1: Volume types, PV, PVC 정적 프로비저닝

#15 리소스 관리에서 Pod가 CPU와 메모리를 어떻게 요청하고 제한받는지 다뤘다면, 이번 글부터는 데이터입니다. 컨테이너 안의 파일시스템은 컨테이너가 죽으면 같이 사라지는 임시 공간이라, DB 데이터나 사용자 업로드 파일처럼 Pod의 생애주기 너머까지 살아남아야 하는 데이터는 별도의 저장 모델이 필요합니다. 쿠버네티스는 이것을 Volume, PersistentVolume, PersistentVolumeClaim이라는 세 추상으로 표현합니다.

이번 글은 Storage도메인의 첫 편으로, Volume의 종류와 PV,PVC의 정적 프로비저닝을 정리하겠습니다. 동적 프로비저닝과 StorageClass는 #17에서 이어집니다. Storage는 시험 비중이 10%로 크지는 않지만, PV와 PVC의 바인딩 규칙은 한 글자만 어긋나도 Pending에 걸리므로 손에 익혀 두는 편이 안전합니다.

Volume이 푸는 두 가지 문제 #

Pod 안의 컨테이너가 쓰는 파일시스템은 두 가지 한계를 가집니다. 첫째, 컨테이너가 재시작하면 그동안 쓴 파일이 사라집니다. 둘째, 한 Pod 안의 여러 컨테이너가 같은 파일을 공유할 길이 없습니다. Volume은 이 둘을 함께 해결하는 추상입니다. Volume은 Pod의 spec.volumes에 선언되고, 각 컨테이너의 volumeMounts로 특정 경로에 마운트됩니다.

다만 Volume의 수명은 종류에 따라 다릅니다. 어떤 Volume은 Pod가 사라지면 같이 사라지고, 어떤 Volume은 Pod과 무관하게 살아남습니다. 이 차이가 곧 Volume의 종류를 가릅니다.

Volume의 종류 #

CKA에서 알아야 하는 Volume은 크게 네 갈래입니다.

종류수명용도
emptyDirPod과 동일Pod 안 컨테이너끼리 임시 공유, 캐시,scratch
hostPath노드 디스크에 의존노드의 특정 경로 마운트(단일 노드,테스트)
configMap / secretPod과 동일(소스는 별도 객체)설정,비밀을 파일로 주입
persistentVolumeClaimPVC,PV의 수명진짜 영속 스토리지

emptyDir: Pod 수명의 임시 공간 #

emptyDir은 Pod가 노드에 스케줄될 때 빈 디렉터리로 생기고, Pod가 노드에서 제거되면 같이 사라집니다. 같은 Pod 안의 두 컨테이너가 파일을 주고받을 때 가장 흔히 씁니다.

apiVersion: v1
kind: Pod
metadata:
  name: emptydir-demo
spec:
  containers:
    - name: writer
      image: busybox
      command: ["sh", "-c", "echo hello > /cache/data; sleep 3600"]
      volumeMounts:
        - name: scratch
          mountPath: /cache
    - name: reader
      image: busybox
      command: ["sh", "-c", "sleep 3600"]
      volumeMounts:
        - name: scratch
          mountPath: /shared
  volumes:
    - name: scratch
      emptyDir: {}

scratch라는 Volume을 두 컨테이너가 서로 다른 경로에 마운트합니다. writer가 /cache에 쓴 파일을 reader는 /shared에서 읽습니다. Pod가 죽으면 이 데이터는 사라집니다.

hostPath: 노드 디스크 직접 마운트 #

hostPath는 노드의 특정 경로를 Pod 안으로 마운트합니다. 데이터가 노드 디스크에 남으므로 Pod 재시작에는 견디지만, Pod가 다른 노드로 옮겨 가면 그 데이터에 접근할 수 없습니다. 그래서 운영 클러스터의 일반 워크로드에는 거의 쓰지 않고, 단일 노드 환경이나 노드 자체의 로그,소켓을 읽는 시스템 컴포넌트에 한정됩니다.

apiVersion: v1
kind: Pod
metadata:
  name: hostpath-demo
spec:
  containers:
    - name: app
      image: busybox
      command: ["sh", "-c", "sleep 3600"]
      volumeMounts:
        - name: node-data
          mountPath: /data
  volumes:
    - name: node-data
      hostPath:
        path: /var/lib/node-data
        type: DirectoryOrCreate

configMap과 secret: 설정을 파일로 #

configMap과 secret은 #12에서 다룬 객체를 Volume으로 마운트하는 형태입니다. 환경 변수 대신 파일로 주입하고 싶을 때 씁니다. 이때 Volume의 데이터 자체는 ConfigMap,Secret 객체에 들어 있고, Volume은 그것을 컨테이너 경로로 펼쳐 줄 뿐입니다.

PersistentVolume: 클러스터가 가진 스토리지 조각 #

emptyDir과 hostPath는 모두 노드에 묶여 있어 진짜 영속성을 주지 못합니다. Pod가 어느 노드로 가든 같은 데이터를 보장하려면 노드 바깥의 스토리지가 필요하고, 쿠버네티스는 그 스토리지 한 조각을 **PersistentVolume(PV)**이라는 클러스터 수준 객체로 표현합니다. PV는 namespace에 속하지 않으며, 관리자가 미리 만들어 두거나 StorageClass가 동적으로 만들어 냅니다.

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-nfs-5g
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: manual
  nfs:
    server: 10.0.0.10
    path: /exports/data

capacity와 accessModes #

capacity.storage는 이 PV가 제공하는 용량입니다. accessModes는 이 PV를 어떻게 마운트할 수 있는지를 선언하며, 네 가지가 있습니다.

accessMode약자의미
ReadWriteOnceRWO한 노드에서 읽기,쓰기
ReadOnlyManyROX여러 노드에서 읽기 전용
ReadWriteManyRWX여러 노드에서 읽기,쓰기
ReadWriteOncePodRWOP단 하나의 Pod만 읽기,쓰기

여기서 RWO의 “Once"는 Pod 하나가 아니라 노드 하나를 뜻한다는 점이 시험에 자주 나옵니다. 한 노드에 떠 있는 여러 Pod는 RWO PV를 동시에 마운트할 수 있습니다. 진짜로 Pod 하나만 허용하려면 RWOP를 써야 합니다. 그리고 어떤 accessMode를 실제로 쓸 수 있는지는 스토리지 종류가 결정합니다. NFS는 RWX를 지원하지만, 단순 블록 디바이스 기반은 RWO만 지원하는 식입니다.

persistentVolumeReclaimPolicy #

persistentVolumeReclaimPolicy는 PVC가 삭제되어 PV가 풀려났을 때 그 PV를 어떻게 처리할지를 정합니다.

정책동작
RetainPV를 보존하고 Released 상태로 둠. 데이터 보존, 재사용은 관리자가 수동 처리
DeletePV와 backing 스토리지를 함께 삭제
Recycle(폐기됨) 데이터를 지우고 재사용. 현재는 사용 안 함

정적 프로비저닝으로 만든 PV는 데이터를 지키기 위해 보통 Retain을 씁니다. Retain으로 풀려난 PV는 Released 상태가 되며, 이 상태에서는 새 PVC에 자동으로 바인딩되지 않습니다. 다시 쓰려면 관리자가 PV의 claimRef를 비우고 데이터를 정리해 Available로 되돌려야 합니다.

PersistentVolumeClaim: 사용자의 요청 #

PV가 “클러스터에 이만큼의 스토리지가 있다"는 공급이라면, **PersistentVolumeClaim(PVC)**은 “이만큼의 스토리지를 이런 조건으로 달라"는 수요입니다. PVC는 namespace에 속하며, Pod는 PV를 직접 참조하지 않고 항상 PVC를 거칩니다. 이 분리 덕분에 앱 개발자는 스토리지의 실제 종류를 몰라도 용량과 접근 모드만 적어 요청할 수 있습니다.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-data
  namespace: default
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 5Gi
  storageClassName: manual

PVC가 PV에 바인딩되는 규칙 #

PVC를 만들면 컨트롤러가 조건에 맞는 Available 상태의 PV를 찾아 1대1로 바인딩합니다. 바인딩이 성립하려면 다음 세 가지가 모두 맞아야 합니다.

  1. 용량. PV의 capacity.storage가 PVC의 requests.storage이상이어야 합니다. PVC가 5Gi를 요청했는데 PV가 3Gi면 바인딩되지 않습니다. 반대로 PV가 더 크면 바인딩은 되지만 남는 용량은 버려집니다(정적 PV는 분할되지 않습니다).
  2. accessModes. PVC가 요청한 모든 accessMode를 PV가 지원해야 합니다.
  3. storageClassName. PVC와 PV의 storageClassName이 같아야 합니다. 정적 프로비저닝에서는 양쪽 모두 manual 같은 동일한 이름을 적거나, 양쪽 모두 비워 둡니다.

이 중 하나라도 어긋나면 PVC는 Pending에 머뭅니다. 시험에서 PVC가 Bound로 넘어가지 않으면 위 세 항목을 PV와 나란히 비교하는 것이 가장 빠른 진단입니다. selector(spec.selector)로 특정 라벨의 PV만 받도록 좁힐 수도 있습니다.

# PVC와 PV의 상태를 나란히 확인
k get pvc,pv

# 바인딩이 안 될 때 사유 확인
k describe pvc pvc-data

Pod에서 PVC 마운트 #

PVC가 PV에 Bound되면, Pod는 그 PVC를 Volume으로 참조해 마운트합니다. Pod 입장에서는 뒤에 NFS가 있든 클라우드 디스크가 있든 신경 쓸 필요 없이, PVC 이름만 적으면 됩니다.

apiVersion: v1
kind: Pod
metadata:
  name: app-with-pvc
spec:
  containers:
    - name: app
      image: nginx
      volumeMounts:
        - name: data
          mountPath: /usr/share/nginx/html
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: pvc-data

volumespersistentVolumeClaim.claimName이 앞서 만든 PVC를 가리킵니다. 이 PVC가 아직 Pending이면 Pod도 스케줄되지 못하고 ContainerCreating이나 Pending에 걸립니다. 즉 Pod의 기동은 PVC의 바인딩에, PVC의 바인딩은 조건에 맞는 PV의 존재에 차례로 의존합니다.

정적 프로비저닝: 관리자가 PV를 미리 만든다 #

지금까지의 흐름이 곧 정적 프로비저닝입니다. 관리자가 스토리지를 미리 준비해 PV 객체로 등록해 두고, 사용자는 PVC로 그중 하나를 요청해 가져다 씁니다. PV가 먼저 존재하고, PVC가 그것을 골라잡는 순서입니다.

관리자: PV 생성  →  PV(Available)
                       ↑ 바인딩(용량,accessModes,SC 일치)
사용자: PVC 생성 →  PVC  →  Pod가 PVC 마운트

정적 프로비저닝은 NFS 서버처럼 미리 정해진 스토리지가 있는 환경, 또는 어떤 스토리지를 누가 쓸지 관리자가 통제하고 싶은 환경에 맞습니다. 한계도 분명합니다. 새 요청이 올 때마다 관리자가 PV를 손으로 만들어 줘야 하고, 용량이 정확히 맞아떨어지지 않으면 공간이 낭비됩니다. 이 수작업을 없애는 것이 #17에서 다룰 동적 프로비저닝입니다.

전체 흐름을 한 번에 #

PV, PVC, Pod를 묶어 정적 프로비저닝을 한 사이클 돌려 보겠습니다.

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-manual-1g
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: manual
  hostPath:
    path: /mnt/data
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-manual
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 500Mi
  storageClassName: manual
---
apiVersion: v1
kind: Pod
metadata:
  name: pv-consumer
spec:
  containers:
    - name: app
      image: nginx
      volumeMounts:
        - name: store
          mountPath: /data
  volumes:
    - name: store
      persistentVolumeClaim:
        claimName: pvc-manual

PVC는 500Mi를 요청했지만 1Gi PV에 바인딩됩니다(용량은 요청 이상이면 성립). accessModes와 storageClass(manual)가 양쪽 모두 일치하므로 바인딩이 성사됩니다. 적용 후 상태를 확인하겠습니다.

k apply -f static.yaml
k get pv pv-manual-1g       # STATUS가 Bound, CLAIM에 default/pvc-manual
k get pvc pvc-manual        # STATUS가 Bound, VOLUME에 pv-manual-1g
k get pod pv-consumer       # STATUS가 Running

시험 포인트 #

CKA의 Storage 작업에서 점수를 가르는 지점을 정리하겠습니다.

  • RWO는 노드 단위. “Once"가 Pod가 아니라 노드를 뜻한다는 점을 헷갈리지 않아야 합니다. Pod 하나만 허용하려면 RWOP입니다.
  • 바인딩 3조건. PVC가 Pending이면 용량(PV ≥ PVC), accessModes, storageClassName 세 가지를 PV와 나란히 대조합니다. 셋 중 하나만 어긋나도 바인딩되지 않습니다.
  • storageClassName 일치. 정적 프로비저닝에서는 PV와 PVC의 storageClassName을 같게 적거나 둘 다 비웁니다. 한쪽만 비우면 기대와 다르게 동작합니다.
  • reclaimPolicy Retain. Retain으로 풀려난 PV는 Released 상태로 남고 자동 재바인딩되지 않습니다. 데이터 보존이 목적이면 Retain입니다.
  • Pod는 PVC만 참조. Pod는 PV를 직접 마운트하지 않습니다. 항상 PVC를 거칩니다.
  • 빠른 진단 명령. k get pvc,pv로 상태를 나란히 보고, k describe pvc로 Pending 사유를 확인하는 흐름을 손에 익혀 둡니다.

같은 주제를 앱 개발자 관점에서 더 풀어 둔 글로 K8s 중급 #2 PV / PVC / StorageClass가 있습니다. 모델이 잘 안 잡힌다면 함께 읽으면 도움이 됩니다.

정리 #

이번 글에서 잡은 것:

  • Volume의 종류. emptyDir(Pod 수명 임시 공간), hostPath(노드 디스크), configMap,secret(설정 주입), persistentVolumeClaim(영속 스토리지)
  • PV. capacity, accessModes(RWO/ROX/RWX/RWOP), persistentVolumeReclaimPolicy(Retain/Delete). 클러스터 수준 객체로 namespace에 속하지 않음
  • PVC. 용량과 접근 모드를 요청하는 namespace 객체. Pod는 PVC만 참조
  • 바인딩 규칙. 용량(PV ≥ PVC), accessModes, storageClassName 세 조건이 모두 맞아야 Bound
  • 정적 프로비저닝. 관리자가 PV를 미리 만들고 사용자가 PVC로 골라 씀. 수작업과 용량 낭비가 한계

다음: Storage 2 #

정적 프로비저닝은 관리자가 PV를 일일이 만들어 줘야 하는 부담이 있었습니다. #17 Storage 2에서는 이 수작업을 없애는 StorageClass와 동적 프로비저닝을 다루겠습니다. PVC를 만들면 PV가 자동으로 생기는 흐름, reclaim policy가 동적 환경에서 어떻게 작동하는지, allowVolumeExpansion으로 PVC 용량을 늘리는 expansion, 그리고 volumeBindingMode의 WaitForFirstConsumer가 스케줄링과 스토리지 위치를 어떻게 맞추는지까지 이어 가겠습니다.

X