안녕하세요? 정리하는 개발자 워니즈 입니다. 이번시간에는 쿠버네티스의 볼륨에 대해서 정리를 해보고자 합니다.
지난 글들은 아래를 참고 해주시면 됩니다.
- 쿠버네티스 1편 : 설치 가이드
- 쿠버네티스 2편 : pod
- 쿠버네티스 3편 : service
- 쿠버네티스 4편 : deployment
- 쿠버네티스 5편 : pod 설정
- 쿠버네티스 6편 : 배포 전략
- 쿠버네티스 7편 : volume
- 쿠버네티스 8편 : daemonset
- 쿠버네티스 9편 : 테라폼을 통한 클러스터 구성
- 쿠버네티스 10편 : eks에서 volume 사용하기
- 쿠버네티스 11편 : helm
- 쿠버네티스 12편 : helm chart template
- 쿠버네티스 13편 : helm deploy
- 쿠버네티스 14편 : fluentd를 통한 log수집
- 쿠버네티스 15편 : chartmuseum
- 쿠버네티스 16편 : 배포툴(ArgoCD 설치방법/사용법)
- 쿠버네티스 17편 : 배포툴(ArgoCD 구성/알람)
- 쿠버네티스 18편 : 쿠버네티스 Autoscailing
- 쿠버네티스 19편 : 쿠버네티스 로깅 아키텍처
도커 컨테이너를 올렸다가 삭제 하게 되면, 내부에서 사용하던 내용물도 모두 사라지게 됩니다.
컨테이너는 기본적으로 상태가 없는 상태에서 구동됩니다. 즉, 컨테이너가 실행되서 상태를 가져도 컨테이너가 종료되면 컨테이너에서 생성되었던 모든 데이터는 사라진다는 뜻입니다. 이것은 Kubernetes상에서 자유롭게 Pod를 복제하고 배포하는데 있어 커다란 장점입니다.
하지만, 로그 및 데이터베이스와 같은 애플리케이션은 종료되더라도 데이터가 유지되어야만 하는 특징이 있습니다. 이런 데이터의 상태를 유지할 수 있도록 사용하는 것이 Volume입니다.
Kubernetes에서는 Docker와는 다르게 Pod 단위로 Volume을 관리하며, Life Cycle과 제공되는 디스크 Type 따라 다양한 옵션과 종류가 존재합니다. 이 장에서는 Kubernetes의 영구 Volume체계인 PersistentVolume(PV)와 PersistentVolumeClaim(PVC)에 대해 살펴봅니다.

1. Volume 종류
Volume은 제공되는 디스크 Type에 따라 여러가지 종류의 Volume Type을 지원합니다.
| Temp | Local | Network |
|---|---|---|
| emptyDir | hostPath | NFS, AWS EBS.. |
- emptyDir은 동일한 Pod내에서 사용가능한 임시 Volume으로 Pod가 삭제되면 Volume의 데이터도 같이 삭제됩니다.
- hostPath는 특정 노드에 mount된 파일시스템을 Volume으로 사용합니다. 본 교육에서는 로컬 VM환경이므로 HostPath Type으로 실습하는 예제입니다.
- Network : 네트워크 또는 클라우드 상에 존재하는 storage를 Volume으로 사용합니다.
2. Volume Provisioning 에 따른 분류
Kubernetes에서는 자동으로 Volume을 생성할 수 있느냐 없느냐에 따라 Static Volume Provisioning과 DynamicVolume Provisoning으로 구분할 수 있는데,
- Static Volume Provisioning : 관리자가 수동으로 Kubernetes에서 Persistent Volume을 생성
- DynamicVolume Provisoning : 개발자의 요청에 의해 자동으로 Kubernetes에서 Persistent Volume을 생성
즉, DynamicVolume Provisoning은 개발자나 클라우드 사용자가 손쉽게 Volum사용요청(PVC) 만으로 Volume을 사용할 수 있습니다.
3. Persistent Volume(PV) & Persistent Volume Claim(PVC)
쿠버네티스에서 볼륨을 사용하는 구조는 스토리지를 매핑시킨 PersistentVolume(PV)과 그 PV를 Pod에서 사용할때 PV로 Volume내역을 요청하는 PersistentVolumeClaim(PVC) 2단계로 분리되어 있습니다.
PV는 Storage와 mount되어 있으므로 쉽게말해 볼륨 또는 Storage 자체를 의미합니다.
PVC는 개발자 또는 클라우드 사용자가 PV에게 Volume을 할당해달라는 요청입니다. 사용하고 싶은 용량과 각종 Volume에 관한 설정값을 정해서 요청하게 됩니다.
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv0003
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle
storageClassName: slow
mountOptions:
- hard
- nfsvers=4.1
nfs:
path: /tmp
server: 172.17.0.2
Phase
Volume은 아래와 같은 상태를 가집니다. 해당 상태는 PV이 배포되고, kubectl get pv 명령을 통해 조회할 수 있습니다.
- Available – PV가 생성되고 PVC가 바인딩되기 전 상태
- Bound – PVC가 PV에 바인된 상태
- Released – PVC가 삭제된 상태
- Failed – 실패상태
Access Modes
- ReadWriteOnce – 단일 노드에 의한 읽기-쓰기로 볼륨이 마운트될 수 있습니다.
- ReadOnlyMany – 여러 노드에 의한 읽기 전용으로 볼륨이 마운트될 수 있습니다.
- ReadWriteMany – 여러 노드에 의한 읽기-쓰기로 볼륨이 마운트될 수 있습니다.
CLI에서는 아래와 같이 표시됩니다.
- RWO – ReadWriteOnce
- ROX – ReadOnlyMany
- RWX – ReadWriteMany
Reclaim Policy
PV와 바인딩된 PVC를 삭제했을때 PV는 Released 상태로 변경됩니다. 이때 PV와 연결된 디스크의 파일들을 어떻게 처리할 것인지에 대한 설정으로 아래와 같이 설정할 수 있습니다.
- Retain – PVC가 삭제되어도 PV 및 데이터 유지(다시 쓰기 위해서는 PV를 삭제하고 재생성해야함)
- Recycle – PV는 유지하고 파일만 삭제 (
rm -rf /thevolume/*) - Delete – 볼륨 및 데이터 삭제
4. PersistentVolumeClaims
Each PVC contains a spec and status, which is the specification and status of the claim.
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: myclaim
spec:
accessModes:
- ReadWriteOnce
volumeMode: Filesystem
resources:
requests:
storage: 8Gi
storageClassName: slow
selector:
matchLabels:
release: "stable"
matchExpressions:
- {key: environment, operator: In, values: [dev]}
Claims As Volumes
Pod는 아래와 같이 PVC로 PV에 접근할 수 있습니다.
kind: Pod
apiVersion: v1
metadata:
name: mypod
spec:
containers:
- name: myfrontend
image: nginx
volumeMounts:
- mountPath: "/var/www/html"
name: mypd
volumes:
- name: mypd
persistentVolumeClaim:
claimName: myclaim
5. 실습 예제
Create a Secret for MySQL Password
실습에서 사용할 mysql 패스워드를 secret으로 생성해두겠습니다.
# kubectl create secret generic mysql-pass --from-literal=password=pwd
Volume
Create PersistentVolumes
PersistentVolume yaml파일 생성
apiVersion: v1
kind: PersistentVolume
metadata:
name: wp-pv-volume
labels:
type: local
spec:
capacity:
storage: 3Gi
volumeMode: Filesystem
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Delete
storageClassName: local-storage
local:
path: "/data/mysql"
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- node
Create PersistentVolumeClaims
PersistentVolumeClaims yaml 파일 생성
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: mysql-pv-claim
spec:
storageClassName: local-storage
accessModes:
- ReadWriteMany
resources:
requests:
storage: 2Gi
selector:
matchLabels:
type: local
Backend
Creating a Mysql Pod
Mysql Pod yaml파일 생성
apiVersion: apps/v1
kind: Deployment
metadata:
name: wordpress-mysql
labels:
app: wordpress
spec:
selector:
matchLabels:
app: wordpress
tier: mysql
strategy:
type: Recreate
template:
metadata:
labels:
app: wordpress
tier: mysql
spec:
containers:
- image: mysql:5.6
imagePullPolicy: Always
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-pass
key: password
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: mysql-persistent-storage
mountPath: /var/lib/mysql
volumes:
- name: mysql-persistent-storage
persistentVolumeClaim:
claimName: mysql-pv-claim
Creating a Mysql Service
Mysql Service yaml 파일 생성
apiVersion: v1
kind: Service
metadata:
name: wordpress-mysql
labels:
app: wordpress
spec:
ports:
- port: 3306
selector:
app: wordpress
tier: mysql
clusterIP: None
FrontEnd
Creating a WordPress Pod
apiVersion: apps/v1
kind: Deployment
metadata:
name: wordpress
labels:
app: wordpress
spec:
selector:
matchLabels:
app: wordpress
tier: frontend
strategy:
type: Recreate
replicas: 1
template:
metadata:
labels:
app: wordpress
tier: frontend
spec:
containers:
- image: wordpress:4.8-apache
imagePullPolicy: Always
name: wordpress
env:
- name: WORDPRESS_DB_HOST
value: wordpress-mysql
- name: WORDPRESS_DB_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-pass
key: password
ports:
- containerPort: 80
name: wordpress
Creating a WordPress Service
Service yaml파일 생성
apiVersion: v1
kind: Service
metadata:
name: wordpress
labels:
app: wordpress
spec:
type: NodePort
ports:
- port: 8080
targetPort: 80
protocol: TCP
selector:
app: wordpress
tier: frontend
일괄로 pv, pvc, pod, svc를 생성합니다.
kubectl apply -f /{예제폴더}/
결과확인
root@master:/lab/dashboard# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 172.168.1.1 443/TCP 4h56m
wordpress NodePort 172.168.1.177 8080:32382/TCP 10m
wordpress-mysql ClusterIP None 3306/TCP 10m
브라우저에서 http://localhost:32382 로 접속해봅니다.
node VM에서 mysql데이터가 생성되었는지 확인해봅니다.
root@node:/data/mysql# ll
total 110616
drwxr-xr-x 5 vboxadd vboxsf 4096 12월 5 15:39 ./
drwxr-xr-x 3 root root 4096 12월 5 15:12 ../
-rw-rw---- 1 vboxadd vboxsf 56 12월 5 15:38 auto.cnf
-rw-rw---- 1 vboxadd vboxsf 12582912 12월 5 15:38 ibdata1
-rw-rw---- 1 vboxadd vboxsf 50331648 12월 5 15:38 ib_logfile0
-rw-rw---- 1 vboxadd vboxsf 50331648 12월 5 15:38 ib_logfile1
drwx------ 2 vboxadd vboxsf 4096 12월 5 15:38 mysql/
drwx------ 2 vboxadd vboxsf 4096 12월 5 15:38 performance_schema/
drwx------ 2 vboxadd vboxsf 4096 12월 5 15:39 wordpress/
6. 볼륨 종류별로 다시 정리 (EmptyDir, HostPath, PV/PVC)
이 글을 처음 쓴 것이 2019년인데요, 그 뒤로 쿠버네티스 버전이 여러 번 올라가면서 볼륨 쪽도 달라진 부분이 꽤 있습니다. 위에서는 실습 위주로 흘러가다 보니 종류별로 언제 쓰고 언제 피해야 하는지를 한자리에 모아두지 못했는데, 이번에 쿠버네티스 공식 문서(v1.37 기준)를 다시 읽으면서 종류별로 정리를 해보았습니다.
| 종류 | 데이터가 사라지는 시점 | 주로 쓰는 곳 | 피해야 하는 곳 |
|---|---|---|---|
| emptyDir | Pod 가 노드에서 제거될 때 | 컨테이너 간 파일 공유, 임시 작업 공간 | 남겨야 하는 데이터 |
| hostPath | 노드 디스크를 지우기 전까지 남지만 그 노드에만 있음 | 노드 로그 수집, 노드 에이전트 | 일반 애플리케이션 데이터 |
| PV/PVC | Reclaim Policy 에 따라 다름(Retain, Delete) | DB, 업로드 파일처럼 Pod 보다 오래 살아야 하는 데이터 | 잠깐 쓰고 버리는 캐시 |
emptyDir 는 언제 쓰고, 컨테이너가 죽으면 데이터도 사라지나요?
emptyDir 는 Pod 가 노드에 배치되는 순간 빈 디렉터리로 만들어지고, Pod 가 어떤 이유로든 노드에서 제거되면 그 안의 데이터도 영구히 삭제됩니다. 다만 컨테이너가 크래시로 재시작되는 것은 Pod 가 제거되는 것이 아니라서, 컨테이너가 죽었다 살아나도 emptyDir 의 데이터는 그대로 남아 있습니다. 필자도 처음에는 컨테이너가 재시작되면 같이 날아가는 줄 알았는데, 공식 문서에 컨테이너 크래시에는 안전하다고 따로 적혀 있습니다.
그래서 쓰임새도 여기에 맞춰져 있는데요, 같은 Pod 안의 컨테이너끼리 파일을 주고받거나(예를 들어 한 컨테이너가 받아온 파일을 웹서버 컨테이너가 서빙하는 구조), 긴 계산의 중간 결과를 체크포인트로 남겨두는 용도로 많이 씁니다. 예전에 있던 gitRepo 볼륨도 공식 문서에서는 init 컨테이너가 emptyDir 에 git clone 을 받는 방식으로 대체하라고 안내하고 있습니다.
volumes:
- name: cache-volume
emptyDir:
sizeLimit: 500Mi
medium: Memory
여기서 한 가지 주의할 점이 있는데요, medium: Memory 를 주면 디스크 대신 tmpfs(메모리 기반 파일시스템)로 마운트되어 속도는 빠르지만, 거기에 쓴 파일이 그 파일을 쓴 컨테이너의 메모리 한도에 포함됩니다. 캐시를 메모리로 올렸다가 컨테이너가 OOM 으로 죽는 경우가 이 때문입니다. sizeLimit 을 주지 않으면 메모리 기반 볼륨은 노드의 할당 가능 메모리만큼 잡히기 때문에, 메모리로 쓸 때는 sizeLimit 을 같이 적어두는 편이 안전한 것 같습니다.
hostPath 는 왜 운영 환경에서 피하라고 하나요?
위 실습에서는 로컬 VM 환경이라 hostPath 로 진행했는데요, 공식 문서는 hostPath 에 대해 보안 위험이 많으니 피할 수 있으면 피하라고 경고하고 있습니다. 노드의 파일시스템을 Pod 안으로 그대로 가져오는 구조라서, kubelet 인증 정보나 컨테이너 런타임 소켓 같은 경로가 노출되면 컨테이너 밖으로 빠져나가는 통로가 될 수 있기 때문입니다.
보안 말고도 운영에서 자주 부딪히는 문제가 하나 더 있습니다. hostPath 의 데이터는 그 노드 한 대에만 있어서, Pod 가 다른 노드로 다시 스케줄되면 같은 경로라도 전혀 다른 내용을 보게 됩니다. 같은 Deployment 로 만든 Pod 라도 노드마다 파일이 달라지는 것인데요, 로그나 업로드 파일을 hostPath 에 두었다가 Pod 가 재배치된 뒤 “파일이 없어졌다”고 느끼는 것이 대부분 이 경우입니다.
hostPath 에는 type 필드로 경로가 없을 때의 동작을 정할 수 있습니다.
| type | 동작 |
|---|---|
| DirectoryOrCreate | 경로가 없으면 빈 디렉터리를 만든다(권한 0755) |
| Directory | 디렉터리가 이미 있어야 한다 |
| FileOrCreate | 파일이 없으면 빈 파일을 만든다(권한 0644). 상위 디렉터리는 만들지 않는다 |
| File | 파일이 이미 있어야 한다 |
FileOrCreate 가 상위 디렉터리까지 만들어 주지 않는다는 점은 한 번쯤 걸리는 부분이라 적어둡니다. 공식 문서가 권하는 대안은 노드 디스크를 꼭 써야 한다면 hostPath 대신 local PersistentVolume 을 정의해서 쓰는 방식이고, 읽기만 필요하다면 readOnly: true 로 마운트하는 것이 최소한의 안전장치입니다.
PV/PVC 는 2019년과 비교해 무엇이 달라졌나요?
PV 와 PVC 의 기본 구조는 위 3장, 4장에서 정리한 그대로인데요, 몇 가지는 지금 기준으로 고쳐 읽어야 합니다.
- Access Mode 가 하나 늘었습니다. ReadWriteOnce 는 노드 한 대 단위라서, 같은 노드에 있는 Pod 라면 여러 개가 동시에 붙을 수 있습니다. 클러스터 전체에서 딱 한 Pod 만 쓰게 하려면 ReadWriteOncePod 를 쓰는데, v1.29 에서 stable 이 되었고 CSI 볼륨에서만 지원됩니다.
- Recycle 정책은 deprecated 입니다. 공식 문서는 Recycle 대신 동적 프로비저닝을 쓰라고 안내하고 있어서, 지금은 Retain 과 Delete 둘 중에서 고른다고 보시면 됩니다.
- 클라우드 디스크는 CSI 드라이버로 넘어갔습니다. 위 1장 표에 적었던 AWS EBS 같은 클라우드 디스크는 예전에는 쿠버네티스 안에 내장된 플러그인이었는데, 지금은 각 클라우드의 CSI 드라이버로 옮겨졌습니다. 예를 들어 gcePersistentDisk 내장 드라이버는 v1.17 에서 deprecated 되고 v1.28 에서 완전히 제거되었습니다.
그리고 동적 프로비저닝에서는 StorageClass 의 volumeBindingMode: WaitForFirstConsumer 를 많이 쓰는데요, 이 모드에서는 PVC 를 쓰는 Pod 가 만들어질 때까지 PV 바인딩과 생성을 미룹니다. Pod 가 뜰 노드의 가용 영역에 맞춰 디스크를 만들기 위해서인데, 이걸 모르면 PVC 만 만들어 놓고 Pending 이 풀리지 않는다고 오해하기 쉽습니다.
PVC 가 Pending 에서 안 넘어갈 때는 무엇부터 봐야 하나요?
볼륨 쪽에서 가장 자주 만나는 증상이 PVC Pending 인데요, 필자가 확인하는 순서를 정리하면 아래와 같습니다.
kubectl describe pvc {이름}의 Events 를 먼저 봅니다. 원인이 대부분 여기에 찍혀 있습니다.- StorageClass 가 WaitForFirstConsumer 인지 봅니다. 그렇다면 Pod 를 만들기 전까지 Pending 인 것이 정상입니다.
- 정적 PV 를 쓰는 경우라면 PVC 가 요청한 용량, accessModes, storageClassName 이 PV 와 맞는지 봅니다. 공식 문서 표현대로 맞는 볼륨이 없으면 PVC 는 계속 바인딩되지 않은 채로 남습니다.
- 기본 StorageClass 가 있는지 봅니다(
kubectl get storageclass에서 default 표시). storageClassName 을 비워두면 기본 StorageClass 가 붙는데, 정적 PV 에 붙이려면 빈 문자열""을 명시해야 합니다.
반대로 PVC 를 지웠는데 Terminating 에서 멈춰 있는 경우도 있는데요, 이건 고장이 아니라 보호 장치입니다. Pod 가 사용 중인 PVC 는 바로 지워지지 않고, 그 PVC 를 쓰는 Pod 가 사라진 뒤에 삭제됩니다. 데이터 유실을 막기 위한 동작이라 Pod 부터 정리하면 자연스럽게 풀립니다.
종류를 고를 때는 어떤 순서로 판단하면 되나요?
정리하면 질문 두 개로 대부분 갈리는 것 같습니다. 먼저 “Pod 가 사라져도 이 데이터가 남아야 하는가”를 묻고, 아니라면 emptyDir 로 충분합니다. 남아야 한다면 PV/PVC 로 가고, 클라우드라면 CSI 드라이버와 StorageClass 로 동적 프로비저닝을 쓰는 것이 지금은 기본입니다. hostPath 는 노드 로그를 수집하는 에이전트처럼 노드 자체를 들여다봐야 하는 경우에만 제한적으로 쓰고, 애플리케이션 데이터를 담는 용도로는 쓰지 않는 것이 좋습니다. 위 실습의 hostPath 예제도 로컬 VM 에서 개념을 익히는 용도로만 봐주시면 되겠습니다.
볼륨 마운트는 어떻게 쓰나요? (volumeMounts, subPath, readOnly)
볼륨은 두 군데에 적습니다. 파드의 spec.volumes 에 볼륨을 선언하고, 컨테이너의 volumeMounts 에 그 볼륨 이름과 컨테이너 안 경로를 적습니다(쿠버네티스 문서 Volumes).
spec:
containers:
- name: app
image: nginx
volumeMounts:
- name: config-vol
mountPath: /etc/config
readOnly: true
- name: config-vol
mountPath: /etc/nginx/conf.d/default.conf
subPath: default.conf
volumes:
- name: config-vol
configMap:
name: app-config
name은volumes쪽 이름과 같아야 합니다. 이름이 어긋나면 파드 생성 요청이 검증에서 거절됩니다.subPath는 볼륨 전체가 아니라 그 안의 파일 하나나 하위 디렉터리 하나만 그 경로에 붙입니다. 위 예처럼 디렉터리의 다른 파일은 그대로 두고 설정 파일 하나만 넣을 때 씁니다.- 다만 ConfigMap 을 subPath 로 붙이면 ConfigMap 을 바꿔도 컨테이너 안 파일은 갱신되지 않습니다(공식 문서). 바뀐 값을 쓰려면 파드를 다시 띄워야 합니다.
readOnly: true는 컨테이너가 그 경로에 쓰지 못하게 합니다. ConfigMap 볼륨은 원래 읽기 전용으로 붙습니다.
7. 마치며..
이번시간에는 쿠버네티스 볼륨에 대해서 알아보는 시간을 갖었습니다. 보통 어플리케이션 로그의 종류를 외부로 빼서 가져가는것 같습니다. 필자가 속한 프로젝트에서도 쿠버네티스 도입을 시도하고있는데, 웹/와스 구조중에 웹쪽을 컨테이너로 넣어서 관리를 할예정입니다.
웹서버에 필요한 로그파일들을 외부의 PV로 빼서 관리를 하게되면, 확인하기도 훨씬 간편하고 POD의 생명주이게 상관없이 로그 보관도 용이할 것 같습니다.
다음 이시간에는 HELM에 대해서 알아보도록 하겠습니다.
새 글이 올라오면 Threads @wonizz.ai 에도 올립니다. 팔로우해 두면 피드에서 바로 볼 수 있습니다.