Kubernetes에서 애플리케이션을 실행하다 보면 데이터 저장 문제가 생긴다.
Pod와 컨테이너는 영구적인 존재가 아니다. 장애나 배포로 인해 기존 Pod가 삭제되고 새로운 Pod로 교체될 수 있으며, 새 Pod가 반드시 이전 Pod와 같은 Node에서 실행된다는 보장도 없다.
컨테이너 내부 파일시스템에만 데이터를 저장한다면 컨테이너가 교체될 때 데이터가 함께 사라질 수 있다. 따라서 데이터베이스와 같이 데이터를 지속해서 보관해야 하는 애플리케이션에는 Pod의 생명주기와 분리된 스토리지가 필요하다.
Kubernetes는 이를 위해 Volume, PersistentVolume, PersistentVolumeClaim, StorageClass 등의 스토리지 추상화를 제공한다. Kubernetes Volumes 공식 문서
이번 글에서는 먼저 스토리지의 기본 유형을 살펴보고, Kubernetes에서 영구 스토리지가 제공되는 과정을 정리한다.
스토리지는 데이터를 저장하고 접근하는 방식에 따라 크게 File Storage, Block Storage, Object Storage로 나눌 수 있다.
| 구분 | 데이터 관리 단위 | 접근 방식 | 대표적인 용도 |
|---|---|---|---|
| File Storage | 파일과 디렉터리 | 파일 경로 | 공유 디렉터리, 문서, 정적 파일 |
| Block Storage | 일정 크기의 블록 | 디스크 장치 | 데이터베이스, VM 디스크 |
| Object Storage | 객체 | HTTP API와 객체 키 | 이미지, 동영상, 백업, 로그 |
File Storage는 데이터를 파일과 디렉터리 구조로 관리한다.
/shared
├── documents
│ └── report.pdf
└── images
└── profile.png
사용자는 /shared/images/profile.png와 같은 경로를 이용해 데이터에 접근한다. 여러 서버가 하나의 파일시스템을 공유해야 하는 환경에서 많이 사용된다.
대표적인 예시는 다음과 같다.
장점:
단점:
Block Storage는 데이터를 일정한 크기의 블록으로 나누어 저장한다.
운영체제에서는 일반적으로 하나의 디스크 장치처럼 인식한다.
/dev/sdb
애플리케이션에서 사용하려면 해당 장치에 파일시스템을 생성하고 특정 디렉터리에 마운트해야 한다.
Block Device
↓
Filesystem 생성
↓
/data에 Mount
↓
Application에서 사용
대표적인 예시는 다음과 같다.
장점:
단점:
Object Storage는 데이터를 객체 단위로 저장한다. 각 객체는 일반적으로 다음 요소로 구성된다.
파일시스템 경로 대신 HTTP API와 객체 Key를 이용해 데이터에 접근한다.
Bucket: application-data
Key: images/profile.png
대표적인 예시는 다음과 같다.
장점:
단점:
Kubernetes의 일반적인 Volume과 PV/PVC는 주로 File Storage 또는 Block Storage를 Pod에 연결할 때 사용한다.
Object Storage는 일반적으로 다음처럼 애플리케이션이 API를 통해 직접 접근한다.
Pod → Object Storage API → Bucket
일부 CSI 드라이버나 FUSE 기반 솔루션을 사용하면 Object Storage를 파일시스템처럼 마운트할 수도 있다. 하지만 Object Storage가 본래 Block Storage나 File Storage와 동일한 의미의 파일시스템인 것은 아니다.
컨테이너 내부에 생성된 파일은 기본적으로 컨테이너의 생명주기에 영향을 받는다.
컨테이너가 재시작되는 경우 Kubernetes Volume에 저장된 데이터는 유지될 수 있지만, 컨테이너의 쓰기 가능한 레이어에만 저장된 데이터는 보존되지 않을 수 있다. 또한 Pod 자체가 삭제되면 Pod의 생명주기에 종속된 임시 Volume도 함께 제거될 수 있다.
Kubernetes Volume은 다음 문제를 해결한다.
Pod에서는 .spec.volumes에 사용할 Volume을 선언하고, 컨테이너의 .volumeMounts에서 해당 Volume을 마운트할 경로를 지정한다. Kubernetes Volumes 공식 문서
apiVersion: v1
kind: Pod
metadata:
name: volume-example
spec:
containers:
- name: nginx
image: nginx
volumeMounts:
- name: cache-volume
mountPath: /cache
volumes:
- name: cache-volume
emptyDir: {}
이 예제에서 /cache에 저장된 데이터는 컨테이너가 재시작되어도 같은 Pod가 유지되는 동안에는 남아 있다.
하지만 emptyDir은 Pod의 생명주기에 종속된다. Pod가 삭제되면 해당 데이터도 삭제된다. 따라서 데이터베이스와 같이 Pod가 교체된 후에도 데이터를 유지해야 한다면 PersistentVolume이 필요하다.
Kubernetes는 스토리지를 제공하는 부분과 사용하는 부분을 분리하기 위해 다음 두 리소스를 사용한다.
PersistentVolume, 줄여서 PVPersistentVolumeClaim, 줄여서 PVCPV는 Kubernetes 클러스터에서 사용할 수 있도록 준비된 스토리지 리소스다.
Node가 클러스터의 컴퓨팅 리소스를 나타낸다면, PV는 클러스터의 스토리지 리소스를 나타낸다고 이해할 수 있다.
PV는 다음과 같은 정보를 포함한다.
PV는 특정 Namespace에 속하지 않는 클러스터 범위 리소스다.
PVC는 사용자가 필요한 스토리지를 요청하는 리소스다.
사용자는 실제 스토리지가 Amazon EBS인지 NFS인지 알 필요 없이 다음 조건을 선언한다.
PVC는 특정 Namespace에 속하는 Namespace 범위 리소스다.
Kubernetes API 문서에서도 PVC를 사용자의 영구 볼륨 요청이자 해당 볼륨에 대한 Claim으로 정의한다. PersistentVolumeClaim API
PV와 PVC의 관계는 Node와 Pod의 관계에 비유할 수 있다.
| 컴퓨팅 리소스 | 스토리지 리소스 |
|---|---|
| Node가 CPU와 메모리를 제공 | PV가 스토리지 용량을 제공 |
| Pod가 필요한 자원을 요청 | PVC가 필요한 스토리지를 요청 |
| Scheduler가 Pod와 Node를 연결 | Control Plane이 PVC와 PV를 연결 |
전체 흐름은 다음과 같다.
Storage
↓
PersistentVolume
↑
PersistentVolumeClaim
↑
Pod
조금 더 자연스럽게 표현하면 다음과 같다.
Pod → PVC 요청 → PV와 Binding → 실제 Storage 사용
Pod는 PV를 직접 선택하는 대신 PVC를 참조한다. Kubernetes Control Plane은 PVC의 조건을 만족하는 PV를 찾아 둘을 연결한다.
정적 프로비저닝에서는 관리자가 실제 스토리지와 PV를 미리 생성한다.
관리자
↓
실제 Storage 생성
↓
PV 생성
↓
사용자가 PVC 생성
↓
조건에 맞는 PV와 Binding
예를 들어 다음과 같은 PV를 생성할 수 있다.
apiVersion: v1
kind: PersistentVolume
metadata:
name: example-pv
spec:
capacity:
storage: 10Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: manual
hostPath:
path: /mnt/data
hostPath는 특정 Node의 로컬 경로를 사용하므로 다중 Node 운영 환경의 일반적인 영구 스토리지로는 적합하지 않다. 위 예제는 구조를 이해하기 위한 학습용이다.
사용자는 다음 PVC를 생성한다.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: example-pvc
spec:
storageClassName: manual
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
Control Plane은 다음 조건 등을 확인해 적절한 PV를 찾는다.
조건에 맞는 PV를 찾으면 PV와 PVC의 상태가 Bound로 변경된다.
kubectl get pv
kubectl get pvc
NAME CAPACITY ACCESS MODES STATUS CLAIM
example-pv 10Gi RWO Bound default/example-pvc
PVC가 5Gi를 요청하고 10Gi PV에 연결됐더라도, 남은 5Gi를 다른 PVC에 나누어 할당하는 것은 아니다. 일반적으로 하나의 PV는 하나의 PVC에 바인딩된다.
Pod에서는 persistentVolumeClaim을 이용해 PVC를 참조한다.
apiVersion: v1
kind: Pod
metadata:
name: storage-pod
spec:
containers:
- name: nginx
image: nginx
volumeMounts:
- name: application-data
mountPath: /usr/share/nginx/html
volumes:
- name: application-data
persistentVolumeClaim:
claimName: example-pvc
리소스 간 관계는 다음과 같다.
Pod
└─ volumeMounts: 컨테이너 내부 경로
↑
Volume
↑
PVC
↕ Binding
PV
↓
실제 Storage
여기서 각 리소스의 역할을 구분하는 것이 중요하다.
volumeMounts: 컨테이너 내부에서 Volume이 보이는 경로volumes: Pod가 사용할 Volume 정의PVC: 필요한 스토리지의 조건PV: 실제 스토리지를 나타내는 Kubernetes 리소스PV와 PVC에는 스토리지를 어떤 방식으로 마운트할지 나타내는 Access Mode가 있다.
| Access Mode | 약어 | 의미 |
|---|---|---|
ReadWriteOnce | RWO | 하나의 Node에서 읽기·쓰기로 마운트 |
ReadOnlyMany | ROX | 여러 Node에서 읽기 전용으로 마운트 |
ReadWriteMany | RWX | 여러 Node에서 읽기·쓰기로 마운트 |
ReadWriteOncePod | RWOP | 클러스터 전체에서 하나의 Pod만 읽기·쓰기로 마운트 |
여기서 ReadWriteOnce는 “하나의 Pod만 사용 가능”이라는 뜻이 아니다.
정확히는 하나의 Node에서 읽기·쓰기로 마운트할 수 있다는 의미다. 따라서 같은 Node에서 실행되는 여러 Pod가 해당 볼륨을 사용할 가능성이 있다. 하나의 Pod만 사용하도록 제한하려면 지원되는 CSI 환경에서 ReadWriteOncePod를 사용해야 한다.
또한 모든 스토리지 유형이 모든 Access Mode를 지원하는 것은 아니다. 예를 들어 일반적인 Block Storage는 주로 RWO로 사용하고, NFS 계열의 File Storage는 RWX를 지원하는 경우가 많다.
volumeMode는 Volume을 Pod에 어떤 형태로 제공할지 지정한다.
기본값은 Filesystem이다.
Block Device에 파일시스템을 생성한 뒤 디렉터리 형태로 컨테이너에 마운트한다.
volumeMode: Filesystem
Block Device
↓
Filesystem
↓
Container의 /data에 Mount
Block을 지정하면 파일시스템을 생성하지 않고 Raw Block Device 형태로 컨테이너에 제공한다.
volumeMode: Block
Raw Block Device를 직접 다루는 애플리케이션에서 사용할 수 있지만, 일반적인 애플리케이션에서는 Filesystem 방식이 더 많이 사용된다.
정적 프로비저닝에서는 관리자가 사용자 요청을 예상하고 PV를 미리 만들어야 한다.
사용자가 10Gi 스토리지를 요청했는데 적절한 PV가 없다면 PVC는 Pending 상태로 남는다.
StorageClass는 이런 문제를 해결하기 위해 스토리지의 종류와 생성 방법을 정의한다.
공식 문서에서는 StorageClass를 관리자가 제공하는 스토리지의 클래스를 설명하는 방법으로 정의한다. 클래스는 성능 등급, 백업 정책 또는 관리자가 정한 다른 정책을 나타낼 수 있다. Kubernetes StorageClass 공식 문서
예를 들면 다음과 같이 구분할 수 있다.
| StorageClass | 의미 |
|---|---|
fast-ssd | 고성능 SSD |
standard | 일반적인 범용 스토리지 |
archive | 저렴한 장기 보관용 스토리지 |
StorageClass는 PV와 마찬가지로 특정 Namespace에 속하지 않는 클러스터 범위 리소스다.
StorageClass에는 주로 다음 정보가 포함된다.
provisioner: 스토리지를 생성할 주체parameters: 스토리지 생성 옵션reclaimPolicy: PVC 삭제 후 스토리지 처리 방법volumeBindingMode: 볼륨 생성 및 바인딩 시점allowVolumeExpansion: 볼륨 확장 허용 여부apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-storage
provisioner: example.com/csi-driver
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
provisioner와 parameters는 사용하는 클라우드 환경과 CSI 드라이버에 따라 달라진다.
동적 프로비저닝은 PVC가 생성됐을 때 필요한 스토리지와 PV를 자동으로 생성하는 방식이다.
Kubernetes 공식 문서에서는 동적 프로비저닝을 스토리지 볼륨을 요청 시점에 생성하는 기능으로 설명한다.
사용자가 PVC 생성
↓
StorageClass 확인
↓
Provisioner가 실제 Storage 생성
↓
PV 자동 생성
↓
PVC와 PV Binding
↓
Pod에서 사용
사용자는 PV를 직접 만들지 않고 필요한 StorageClass와 용량만 요청한다.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: application-pvc
spec:
storageClassName: fast-storage
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
이 PVC가 생성되면 fast-storage에 설정된 Provisioner가 실제 볼륨과 PV를 자동으로 생성한다.
| 구분 | 정적 프로비저닝 | 동적 프로비저닝 |
|---|---|---|
| PV 생성 | 관리자가 미리 생성 | PVC 요청에 따라 자동 생성 |
| 실제 Storage 생성 | 관리자가 직접 생성 | Provisioner가 자동 생성 |
| StorageClass | 선택 사항 | 동적 생성 정책으로 사용 |
| 운영 부담 | 비교적 큼 | 비교적 작음 |
| 확장성 | 요청 증가에 수동 대응 | 요청에 따라 자동 대응 |
StorageClass의 volumeBindingMode는 PV를 언제 생성하고 PVC에 연결할지를 결정한다.
PVC가 생성되면 즉시 볼륨을 생성하고 바인딩한다.
volumeBindingMode: Immediate
문제는 Pod가 어느 Node에 배치될지 결정되기 전에 스토리지가 먼저 생성될 수 있다는 것이다.
가용 영역에 종속된 Block Storage라면 다음 문제가 발생할 수 있다.
Volume: Zone A에 생성
Pod: Zone B에 스케줄링 시도
→ Volume 연결 불가
PVC를 사용하는 Pod가 생성되고 스케줄링될 때까지 볼륨 생성을 기다린다.
volumeBindingMode: WaitForFirstConsumer
Scheduler가 Pod의 Node Affinity, 리소스 요청, 가용 영역 등을 고려한 후 적절한 위치에 볼륨을 생성할 수 있다.
Topology 제약이 있는 스토리지에서는 WaitForFirstConsumer가 특히 중요하다.
Reclaim Policy는 PVC가 삭제된 후 PV와 실제 스토리지를 어떻게 처리할지 결정한다.
persistentVolumeReclaimPolicy: Retain
PVC가 삭제돼도 PV와 실제 데이터는 자동으로 삭제되지 않는다.
중요한 데이터를 보호할 수 있지만, 관리자가 직접 정리하고 재사용 절차를 수행해야 한다.
persistentVolumeReclaimPolicy: Delete
PVC가 삭제되면 동적으로 생성된 PV와 외부 스토리지도 함께 삭제될 수 있다.
동적 프로비저닝으로 생성된 PV는 일반적으로 StorageClass의 Reclaim Policy를 상속한다. 공식 문서에 따르면 동적 프로비저닝된 PV의 기본 Reclaim Policy는 Delete다. 중요한 데이터를 다룬다면 StorageClass 설정을 반드시 확인해야 한다. PV Reclaim Policy 공식 문서
PVC를 삭제하기 전에 “Kubernetes 리소스만 삭제되는지, 실제 클라우드 디스크까지 삭제되는지”를 반드시 확인해야 한다.
kubectl get pv
PV는 클러스터 범위 리소스이므로 Namespace를 지정하지 않는다.
kubectl get pvc
kubectl get pvc -n <namespace>
kubectl describe pv <pv-name>
kubectl describe pvc <pvc-name>
PVC가 Pending이라면 다음 항목을 확인한다.
storageClassName이 일치하는가kubectl get storageclass
kubectl describe storageclass <storageclass-name>
kubectl get events --sort-by=.metadata.creationTimestamp
스토리지 문제를 진단할 때는 리소스의 현재 상태뿐 아니라 Events를 함께 확인하는 것이 중요하다.
관리자가 실제 Storage 생성
↓
PV 생성
↑
PVC와 Binding
↑
Pod
Pod
↓
PVC 생성
↓
StorageClass 선택
↓
Provisioner가 Storage 생성
↓
PV 자동 생성
↓
PVC와 PV Binding
↓
Pod에 Mount
각 리소스의 역할을 한 문장으로 정리하면 다음과 같다.
| 리소스 | 역할 |
|---|---|
| Volume | Pod가 사용할 수 있는 스토리지 추상화 |
| PV | 클러스터에 제공된 영구 스토리지 리소스 |
| PVC | 사용자가 필요한 스토리지를 요청하는 리소스 |
| StorageClass | 스토리지의 종류와 동적 생성 정책 |
| CSI Driver | Kubernetes와 실제 스토리지 시스템을 연결 |
Kubernetes Storage를 이해할 때는 YAML 필드를 외우는 것보다 다음 관계를 먼저 이해하는 것이 중요하다.
Pod는 PVC를 요청하고, PVC는 조건에 맞는 PV와 연결되며, PV는 실제 스토리지를 추상화한다.
StorageClass가 없다면 관리자가 실제 스토리지와 PV를 미리 준비해야 한다. StorageClass와 Provisioner를 사용하면 PVC 요청에 따라 실제 스토리지와 PV를 자동으로 생성할 수 있다.
따라서 StorageClass는 단순한 스토리지 이름이 아니다. Kubernetes 환경에서 어떤 스토리지를, 어떤 정책으로, 언제 생성할 것인지 정의하는 프로비저닝 정책에 가깝다.
이번 내용을 다음 세 문장으로 압축할 수 있다.