볼륨: 컨테이너에 디스크 스토리지 연결
- 6.1 Volume 소개
- 6.2 컨테이너 간 데이터 공유 — emptyDir, gitRepo
- 6.3 노드 파일시스템 접근 — hostPath
- 6.4 영구 저장소 쓰기 — 클라우드 디스크, NFS 등
- 6.5 PersistentVolume과 PersistentVolumeClaim
- 6.6 동적 프로비저닝과 StorageClass
6장은 한 마디로 "Pod에 디스크를 어떻게 붙이나"에 대한 장이야. 컨테이너의 파일시스템은 이미지에서 만들어지고 컨테이너가 죽으면 사라져. 근데 DB 같은 건 데이터가 컨테이너 재시작을 넘어서 영속돼야 하고, 같은 Pod 안의 여러 컨테이너가 파일을 공유해야 하는 경우도 있거든. 쿠버네티스는 이걸 Volume으로 해결해.
먼저 Volume의 위치부터 짚어두자. Volume은 Pod의 일부야. 독립적인 최상위 리소스가 아니라 Pod spec 안에 정의돼. Pod와 수명을 같이하고(기본적으로), Pod 안의 컨테이너들이 각자 원하는 경로에 마운트해서 써. 왜 컨테이너가 아니라 Pod 수준에 두냐면, 컨테이너는 재시작될 수 있는데 재시작되면 새 컨테이너는 이전 컨테이너가 파일시스템에 쓴 걸 못 보거든. Pod 수준에 볼륨을 두고 컨테이너들이 거기를 마운트하면, 컨테이너가 재시작돼도 데이터가 유지되고 같은 Pod의 다른 컨테이너와 공유할 수 있어. 볼륨 타입은 많아. 크게 임시·공유용(emptyDir, gitRepo), 노드 파일시스템 접근(hostPath), 네트워크 스토리지(nfs, iscsi, cephfs, 클라우드별 디스크 등), 쿠버네티스 메타데이터 노출용 특수 볼륨(configMap, secret, downwardAPI — 7장과 8장에서 다룸), 그리고 추상화 레이어인 persistentVolumeClaim으로 나눌 수 있어. 마지막 게 6.5의 핵심이야.
emptyDir은 가장 단순한 볼륨이야. Pod가 노드에 스케줄되는 순간 빈 디렉터리가 생성되고, Pod가 삭제되면 그 내용도 사라져. 같은 Pod의 여러 컨테이너가 이 볼륨을 마운트하면 파일을 공유할 수 있어. 책의 fortune 예제가 감 잡기 좋은데, Pod 하나에 컨테이너 두 개를 둬. html-generator는 주기적으로 fortune 명령 결과를 /var/htdocs/index.html에 쓰고, web-server는 Nginx가 /usr/share/nginx/html에서 파일을 서빙해. 두 컨테이너가 같은 emptyDir 볼륨을 각자 다른 경로에 마운트하면, 한쪽이 쓴 파일을 다른 쪽이 바로 읽을 수 있어. 이게 사이드카 패턴의 전형이야. emptyDir의 저장 매체는 기본적으로 노드의 실제 디스크지만, **medium: Memory**를 주면 tmpfs(메모리)에 생성돼. 빠른 임시 저장이 필요할 때 유용해.
gitRepo 볼륨은 사실상 emptyDir + Git clone이야. Pod 시작 시점에 특정 Git 저장소를 checkout해. 정적 웹사이트를 배포할 때 유용할 수 있어. 다만 볼륨이 이후 저장소와 동기화되지 않아 — 업데이트하려면 Pod를 지웠다 다시 만들어야 해. 그래서 실전에선 git-sync 사이드카를 따로 쓰는 게 정석이야. Nginx 컨테이너 옆에 git-sync 컨테이너를 붙이고 둘이 같은 emptyDir을 공유하면, git-sync가 주기적으로 pull하고 Nginx는 항상 최신 파일을 서빙해. 그리고 gitRepo 볼륨은 private repo를 지원하지 않아서 어차피 실전에선 사이드카 방식을 쓰게 돼.
hostPath는 워커 노드의 파일시스템 특정 경로를 Pod에 마운트해. 이건 대부분의 앱에선 쓰면 안 돼. 이유는 명확해 — Pod가 다른 노드로 재스케줄되면 그 경로의 데이터를 못 봐. 한 노드에 묶이게 되는 셈이야. 그럼 언제 쓰냐면, 시스템 레벨 파드, 주로 DaemonSet에서 써. 노드의 로그 디렉터리(/var/log)를 읽는 로그 수집기(Fluentd 같은 거), 노드의 컨테이너 로그 디렉터리를 파싱하는 도구, 노드의 kubeconfig나 CA 인증서 접근 같은 거. 실제로 kube-system 네임스페이스의 fluentd 같은 시스템 Pod들이 hostPath를 두 개씩 마운트한 걸 볼 수 있어. 노드 데이터를 읽기 위한 창구지, Pod 데이터를 영속화하는 수단이 아니야.
앱 데이터가 Pod 재스케줄링을 넘어 살아남으려면 네트워크 스토리지가 필요해. 어느 노드에서든 접근 가능해야 하니까. 책은 MongoDB를 예로 들어 GCE Persistent Disk를 직접 연결하는 방식을 보여줘. Pod spec에 gcePersistentDisk 볼륨을 적고 디스크 이름과 fs 타입을 지정하는 식이야. 이러면 Pod가 어느 노드로 가든 같은 GCE 디스크를 붙이기 때문에 Pod 삭제 후 재생성해도 이전에 쓴 MongoDB 데이터가 그대로 남아. AWS EBS는 awsElasticBlockStore, NFS는 nfs(서버 IP와 export 경로 지정), Ceph RBD나 iSCSI 같은 것도 다 비슷한 구조로 적어.
문제는 이 방식이 Pod 정의에 인프라 정보를 직접 박는다는 거야. 개발자가 "NFS 서버 IP가 뭐지, 디스크 이름이 뭐지"를 알아야 하고, 그 Pod 정의는 특정 클러스터에 종속돼. 다른 클러스터에선 못 써. 이건 쿠버네티스가 추구하는 "앱과 인프라의 분리"에 정면으로 어긋나거든. 그래서 다음의 추상화가 나와.
**PersistentVolume(PV)과 PersistentVolumeClaim(PVC)**은 스토리지를 두 역할로 분리하는 추상화야. 클러스터 관리자는 물리 스토리지(NFS, 클라우드 디스크 등)를 준비하고 그걸 PV 리소스로 등록해 — "1Gi짜리 NFS 볼륨이 여기 있고, ReadWriteOnce 모드를 지원한다" 같은 식. 앱 개발자는 Pod가 필요할 때 PVC로 "1Gi짜리 ReadWriteOnce 필요해"라고 요청해. 뒤에 NFS인지 Ceph인지 GCE PD인지는 몰라. 쿠버네티스가 PVC와 맞는 PV를 찾아서 바인딩해주면, Pod는 PVC를 볼륨으로 참조해서 써. 개발자는 인프라를 몰라도 되고, 관리자는 앱을 몰라도 돼.
PV의 주요 필드를 짚어보면, **capacity**는 크기, **accessModes**는 접근 모드, **persistentVolumeReclaimPolicy**는 PVC가 삭제됐을 때 이 PV를 어떻게 할지야. accessModes가 헷갈리기 쉬운데, "노드 수" 기준이지 Pod 수 기준이 아니야. **RWO(ReadWriteOnce)**는 한 번에 한 노드만 R/W, **ROX(ReadOnlyMany)**는 여러 노드에서 읽기, **RWX(ReadWriteMany)**는 여러 노드에서 R/W (NFS 같은 게 지원). reclaimPolicy는 Retain(그대로 둠 — 관리자가 수동으로 처리), Delete(밑의 실제 스토리지까지 삭제), Recycle(데이터 지우고 다시 쓸 수 있게 — deprecated)의 세 가지가 있어. 그리고 한 가지 중요한 차이 — PV는 네임스페이스가 없는 클러스터 레벨 리소스고, PVC는 네임스페이스 안에 존재해. 그래서 같은 네임스페이스의 Pod만 그 PVC를 쓸 수 있어.
PVC를 만들면 쿠버네티스가 조건을 만족하는 PV를 찾아 바인딩해. capacity가 충분하고 요청한 accessMode를 지원해야 해. 바인딩 후에는 다른 PVC가 같은 PV를 가져갈 수 없어 — 1:1 관계야. Pod에서 쓸 때는 PV가 아니라 PVC를 참조해. volumes 아래에 persistentVolumeClaim: { claimName: mongodb-pvc } 식으로. 이제 Pod 정의에는 "어떤 PVC가 필요해"만 있고 실제 스토리지 기술은 어디에도 안 나타나. 이 Pod는 다른 클러스터에 가져가도 이름이 같은 PVC만 있으면 돌아.
수동 PV도 귀찮아. 사용자마다 PVC를 만들 때마다 관리자가 PV를 새로 만들어줄 수는 없거든. 그래서 동적 프로비저닝이 있어. **StorageClass(SC)**는 "이런 종류의 스토리지를 어떻게 만들지"를 정의한 템플릿이야. 프로비저너(GCE PD, AWS EBS, Ceph RBD 같은 플러그인)와 파라미터(디스크 타입, 복제 수 같은 거)를 지정해. PVC에서 storageClassName을 지정하면, 매칭되는 PV가 없을 때 쿠버네티스가 해당 StorageClass로 PV를 즉석에서 만들어줘. 관리자가 매번 개입할 필요가 없어.
클러스터에 default StorageClass가 지정돼 있으면, PVC에 storageClassName을 아예 안 써도 그걸로 만들어져. 반대로 storageClassName: ""(빈 문자열)로 두면 "동적 프로비저닝 하지 마라, 수동 PV하고만 매칭해"라는 뜻이 돼. 미묘한 차이지만 중요해. 정리하면 스토리지 접근에는 네 단계가 있는 거야. 첫째 Pod에 직접 볼륨 — 인프라 정보가 Pod에 박힘. 비추천. 둘째 관리자가 PV 수동 생성 + 사용자가 PVC — 인프라와 앱이 분리되지만 관리자가 일이 많아. 셋째 StorageClass로 동적 프로비저닝 + PVC — 사용자가 PVC만 쓰면 자동 생성. 정석. 넷째 default SC + 아무것도 지정 안 한 PVC — 가장 단순. 클라우드 환경에서 많이 써.
정리
6장 읽고 기억할 거 세 가지:
- Volume은 Pod 수준에 정의되고 컨테이너들이 마운트해서 쓴다. 컨테이너 재시작을 넘어서 데이터를 유지하고, 같은 Pod 안의 사이드카 컨테이너끼리 파일을 공유하는 수단. emptyDir은 Pod 수명 동안만, hostPath는 노드 파일시스템 접근 전용(DaemonSet 시스템 Pod용), 진짜 영속 데이터는 네트워크 스토리지를 써야 한다.
- Pod 정의에 인프라 정보(NFS 서버, 디스크 이름 등)를 직접 박으면 이식성이 박살난다. PersistentVolume/PersistentVolumeClaim 추상화가 이걸 해결한다. 관리자는 PV로 스토리지를 제공하고 개발자는 PVC로 "얼마큼, 어떤 모드로 필요해"만 요청하면 된다. Pod는 PVC를 참조할 뿐 실제 스토리지 기술은 모른다.
- StorageClass는 PV를 자동으로 찍어내는 템플릿이다. 사용자가 PVC만 만들면 쿠버네티스가 해당 SC로 PV를 동적 생성해 바인딩한다. accessMode(RWO/ROX/RWX)는 Pod 수가 아니라 노드 수 기준이라는 점, 그리고 PV는 클러스터 레벨이고 PVC는 네임스페이스 스코프라는 점은 자주 헷갈리니 기억해둘 것.