Certified Kubernetes Application Developer (CKAD) #17 Volumes: emptyDir, PVC, projected, ephemeral
컨테이너의 파일시스템은 컨테이너가 죽는 순간 함께 사라집니다. 크래시로 재시작되든, 롤링 업데이트로 새 이미지가 올라오든, 그 안에 쓴 파일은 남지 않습니다. 그렇다면 로그를 잠깐 모아 두거나, 데이터베이스 파일을 영구히 보관하거나, 컨테이너 두 개가 같은 디렉터리를 공유해야 할 때는 데이터를 어디에 두어야 할까요. 이번 글은 그 질문에 답하는 쿠버네티스 volume의 종류를 실기 관점에서 정리하겠습니다.
K8s 실무 트랙의 스토리지 편에서 PV,PVC의 개념을 한 번 다뤘다면, 여기서는 CKAD 시험에서 실제로 작성하게 되는 매니페스트 형태에 집중하겠습니다. 외워야 하는 것은 필드 이름이 아니라, 어떤 상황에 어떤 volume을 고르는지의 판단입니다.
volume이 푸는 문제 #
쿠버네티스의 volume은 Pod의 컨테이너에 디렉터리를 붙여 주는 추상화입니다. volume의 종류에 따라 그 디렉터리의 실체가 노드의 임시 디스크일 수도, 네트워크 스토리지일 수도, 메모리일 수도 있습니다. CKAD에서 자주 등장하는 종류는 네 가지로 좁혀집니다. 컨테이너 간 임시 공유용 emptyDir, 영구 데이터용 PersistentVolumeClaim, 여러 설정 소스를 묶는 projected, 그리고 PVC를 Pod 안에서 즉석으로 정의하는 generic ephemeral입니다.
volume은 spec.volumes에서 정의하고, 각 컨테이너의 volumeMounts에서 마운트 경로를 지정합니다. 정의와 마운트가 분리되어 있다는 점이 핵심입니다. 하나의 volume을 여러 컨테이너가 각자의 경로에 마운트할 수 있고, 이것이 컨테이너 간 공유의 기반입니다.
emptyDir: Pod 수명 동안의 임시 공간 #
emptyDir는 Pod가 노드에 스케줄될 때 빈 디렉터리로 생성되고, Pod가 노드에서 제거될 때 함께 삭제되는 volume입니다. 컨테이너가 크래시로 재시작되어도 emptyDir의 내용은 유지됩니다. Pod 자체가 사라질 때만 사라지므로, 컨테이너 한 번의 수명보다는 길고 Pod 한 번의 수명과는 같습니다.
가장 흔한 용도는 같은 Pod 안의 두 컨테이너가 파일을 주고받는 통로입니다. 예를 들어 생성기 컨테이너가 파일을 쓰고 서버 컨테이너가 그 파일을 읽는 sidecar 구성에서, emptyDir를 양쪽에 마운트하면 됩니다.
medium: Memory 옵션을 주면 디스크가 아니라 tmpfs(메모리)에 디렉터리를 만듭니다. 속도가 빠르고 디스크에 흔적을 남기지 않지만, 메모리를 소비하며 Pod의 메모리 사용량에 포함됩니다. 캐시나 민감한 임시 파일에 쓰입니다.
apiVersion: v1
kind: Pod
metadata:
name: shared-emptydir
spec:
containers:
- name: writer
image: busybox
command: ["sh", "-c", "while true; do date >> /data/out.log; sleep 5; done"]
volumeMounts:
- name: scratch
mountPath: /data
- name: reader
image: busybox
command: ["sh", "-c", "tail -f /data/out.log"]
volumeMounts:
- name: scratch
mountPath: /data
volumes:
- name: scratch
emptyDir: {}writer와 reader가 같은 scratch volume을 각자 /data에 마운트하므로, writer가 쓴 로그를 reader가 그대로 읽습니다. 메모리 기반으로 바꾸려면 emptyDir: {}를 emptyDir: { medium: Memory }로 교체합니다.
hostPath: 노드 디스크 직접 마운트 (시험에서는 드묾) #
hostPath는 노드의 파일시스템 경로를 Pod에 직접 붙입니다. 노드의 특정 디렉터리나 디바이스에 접근해야 하는 시스템 수준 작업에 쓰이지만, 그 Pod가 그 노드에 떠 있을 때만 데이터가 의미를 가집니다. Pod가 다른 노드로 옮겨 가면 그곳의 hostPath는 비어 있습니다. 노드 종속성과 보안 위험 때문에 앱 데이터 저장에는 권장되지 않으며, CKAD 실기에서도 직접 작성할 일은 드뭅니다. 개념과 위험만 알아 두면 충분합니다.
PV와 PVC: 영구 데이터의 표준 #
데이터베이스 파일처럼 Pod가 사라져도 살아남아야 하는 데이터는 PersistentVolume(PV)과 PersistentVolumeClaim(PVC)으로 다룹니다. PV는 클러스터가 가진 실제 스토리지 조각이고, PVC는 “이만큼의 용량을 이런 접근 방식으로 달라"는 사용자의 요청입니다. Pod는 PV를 직접 가리키지 않고 PVC를 통해 스토리지를 소비합니다. 이 간접 계층 덕분에 앱 매니페스트는 백엔드 스토리지의 구현을 몰라도 됩니다.
accessModes #
PVC가 요청하는 접근 방식은 세 가지가 핵심입니다.
| 모드 | 약어 | 의미 |
|---|---|---|
| ReadWriteOnce | RWO | 단일 노드에서 읽기,쓰기 마운트 |
| ReadOnlyMany | ROX | 여러 노드에서 읽기 전용 마운트 |
| ReadWriteMany | RWX | 여러 노드에서 읽기,쓰기 마운트 |
RWO는 노드 단위의 제한이므로 같은 노드의 여러 Pod는 동시에 마운트할 수 있습니다. RWX는 NFS 같은 공유 파일시스템이 뒷받침해야 가능하며, 모든 스토리지가 지원하지는 않습니다.
StorageClass와 동적 프로비저닝 #
과거에는 관리자가 PV를 미리 만들어 두고 PVC가 그 PV에 바인딩되기를 기다렸습니다. 지금은 StorageClass를 지정하면 PVC를 생성하는 순간 클러스터가 PV를 자동으로 만들어 붙입니다. 이것이 동적 프로비저닝입니다. PVC의 storageClassName에 클래스 이름을 적으면 되고, 비워 두면 클러스터의 기본 StorageClass가 쓰입니다.
PVC와 PV의 바인딩은 일대일입니다. PVC가 요청한 용량,accessModes,StorageClass 조건을 만족하는 PV가 선택되어 한 번 묶이면, 그 PV는 다른 PVC가 쓸 수 없습니다. PVC가 Pending 상태에 머문다면 조건에 맞는 PV가 없거나 동적 프로비저닝이 동작하지 않는 것이므로, kubectl describe pvc로 이벤트를 먼저 확인합니다.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: standard
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
name: db
spec:
containers:
- name: db
image: busybox
command: ["sh", "-c", "echo hello > /var/lib/data/seed; sleep 3600"]
volumeMounts:
- name: store
mountPath: /var/lib/data
volumes:
- name: store
persistentVolumeClaim:
claimName: data-pvcPod의 volume에서 persistentVolumeClaim.claimName으로 PVC를 가리키는 것이 마운트의 전부입니다. 이름이 PVC 이름과 정확히 일치해야 하며, 다르면 Pod가 스토리지를 찾지 못해 기동하지 못합니다.
projected volume: 여러 설정을 한 디렉터리로 #
projected volume은 secret, configMap, serviceAccountToken, downwardAPI 같은 서로 다른 소스를 하나의 디렉터리로 결합합니다. 컨테이너 입장에서는 출처가 무엇이든 한 경로 아래에 파일들이 모여 있어, 설정을 한곳에서 읽을 수 있습니다. 인증서와 설정값과 토큰을 같은 마운트 지점에 모아야 할 때 유용합니다.
각 소스는 sources 리스트의 항목으로 들어가고, 항목마다 어떤 키를 어떤 파일 이름으로 노출할지 지정합니다.
downwardAPI: Pod 자신의 정보를 파일,env로 #
downwardAPI는 Pod가 자기 자신의 메타데이터를 읽게 해 주는 통로입니다. 컨테이너는 자신이 어느 namespace에 있는지, Pod 이름이 무엇인지, 어떤 label이 붙어 있는지를 이미지에 하드코딩하지 않고도 알 수 있습니다. 노출 방식은 두 가지로, env 변수로 주입하거나 파일로 마운트합니다.
env로 쓸 때는 fieldRef로 metadata.name, metadata.namespace, status.podIP 같은 필드를 가리킵니다. 컨테이너의 리소스 요청,제한 값은 resourceFieldRef로 노출합니다. 파일로 쓸 때는 projected나 downwardAPI volume의 items에 필드 경로와 파일 경로를 적습니다. label과 annotation처럼 키가 여러 개인 정보는 env보다 파일 방식이 자연스럽습니다.
apiVersion: v1
kind: Pod
metadata:
name: projected-demo
labels:
app: demo
tier: web
spec:
containers:
- name: app
image: busybox
command: ["sh", "-c", "ls -l /etc/podinfo; cat /etc/podinfo/labels; sleep 3600"]
env:
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
volumeMounts:
- name: bundle
mountPath: /etc/podinfo
readOnly: true
volumes:
- name: bundle
projected:
sources:
- configMap:
name: app-config
- secret:
name: app-secret
- downwardAPI:
items:
- path: labels
fieldRef:
fieldPath: metadata.labels
- path: name
fieldRef:
fieldPath: metadata.name이 Pod는 configMap과 secret의 키들, 그리고 자신의 label과 이름을 모두 /etc/podinfo 한 디렉터리 아래 파일로 봅니다. 동시에 POD_NAMESPACE env로 namespace도 받습니다. projected와 downwardAPI를 한 번에 보여 주는 형태이므로, 두 개념의 결합을 이 예제로 익혀 두면 좋습니다.
generic ephemeral volume: 한 줄 요약 #
generic ephemeral volume은 Pod 정의 안에 PVC 템플릿을 직접 적어, Pod와 같은 수명을 갖는 영구 볼륨을 즉석에서 프로비저닝하는 방식입니다. Pod가 삭제되면 함께 정리되므로, StorageClass의 동적 프로비저닝을 쓰면서도 별도 PVC 객체를 미리 만들 필요가 없을 때 쓰입니다. CKAD에서 비중은 낮으니 이런 선택지가 있다는 정도만 기억하면 됩니다.
시험 포인트 #
- emptyDir는 Pod 수명과 같다. 컨테이너 재시작은 견디지만 Pod 삭제 시 사라집니다. 컨테이너 간 공유가 필요하면 같은 volume을 양쪽
volumeMounts에 붙입니다. medium: Memory는 tmpfs. 빠르지만 메모리를 쓰고 Pod 메모리에 잡힙니다.- PVC 마운트는
persistentVolumeClaim.claimName. 이름이 PVC와 정확히 일치해야 합니다. PVC가Pending이면describe로 이벤트부터 봅니다. - accessModes 약어를 외운다. RWO는 단일 노드, ROX는 다중 노드 읽기, RWX는 다중 노드 읽기,쓰기입니다.
- projected는
sources리스트. 한 디렉터리에 configMap,secret,downwardAPI,serviceAccountToken을 모읍니다. - downwardAPI는 fieldRef와 resourceFieldRef. 메타데이터는 fieldRef, 리소스 값은 resourceFieldRef로 노출합니다.
- dry-run으로 PVC와 ConfigMap 뼈대를 뽑되, projected와 downwardAPI는 generator가 없으므로 문서나
kubectl explain pod.spec.volumes.projected로 필드 경로를 확인합니다.
정리 #
이번 글에서 잡은 것:
- 컨테이너 파일시스템은 휘발한다는 전제에서, 데이터를 둘 곳을 volume 종류별로 구분해 살펴봤습니다.
- emptyDir. Pod 수명 동안의 임시 공간이자 컨테이너 간 공유 통로.
medium: Memory로 tmpfs 전환 - hostPath. 노드 디스크 직접 마운트. 노드 종속과 보안 위험으로 앱 데이터에는 부적합
- PV/PVC. PVC로 스토리지를 요청하고 StorageClass로 동적 프로비저닝. accessModes(RWO,ROX,RWX)와 일대일 바인딩
- projected. 여러 설정 소스를 한 디렉터리로 결합
- downwardAPI. Pod 자신의 메타데이터,리소스를 파일이나 env로 노출
- generic ephemeral. Pod 수명에 묶인 즉석 PVC 프로비저닝
다음: Services #
지금까지는 Pod 안쪽, 즉 컨테이너가 데이터를 어떻게 다루는지를 봤습니다. 이제 Pod 바깥, 트래픽이 Pod로 어떻게 들어오는지로 넘어갑니다.
#18 Services: ClusterIP, NodePort, LoadBalancer, ExternalName에서는 Pod의 IP가 수시로 바뀌는 환경에서 안정적인 접근점을 만드는 Service의 네 가지 타입, selector와 label로 Pod를 묶는 방식, kubectl expose로 Service를 빠르게 뽑는 법, 그리고 시험에서 자주 나오는 “이 Service가 왜 Pod로 트래픽을 못 보내는가” 유형까지 직접 만들어 보며 정리하겠습니다.