[k8s 스터디] #07 PV(Persistent Volume)/PVC(PV Claim)
k8s에서 애플리케이션을 구동하면 컨테이너 환경에서 동작한다.
컨테이너 환경의 가장 큰 단점은 영속적 데이터를 유지하는 것이 힘들다는 점이다.
반복적으로 생성되고 삭제되는 과정에서 데이터가 컨테이너와 함께 사라지기 때문이다.
이걸 도커는 볼륨을 마운트해 해결했다.
k8s도 비슷하게 접근하는데 PV와 앱과 PV간의 중간다리 역할인 PVC이다.
다음 의 질문을 통해 알아보았다.
- 왜 PV와 PVC가 필요한가?
- 각각의 개념과 역할은 무엇인가?
- 실제 YAML 구성은 어떻게 되는가?
- hostPath와 PVC+PV 방식의 차이점은?
또한 실습을 진행하며 어떻게 동작하는지도 확인했다.
왜 필요한가
k8s는 기본적으로 컨테이너의 실행 환경을 관리하며, 컨테이너는 일시적인 특성을 가진다.
즉, 컨테이너가 재시작되거나 사라지면 그 안에 데이터는 사라진다. 하지만 대부분의 애플리케이션은 데이터의 영속성을 요구한다.
- DB
- 로그 파일 등등
따라서 k8s는 이 문제를 해결하기 위해 PV와 PVC 라는 추상화된 리소스를 제공한다.
개념정리
PersistentVolume(PV)
- 클러스터 관리자가 미리 정의하거나 동적으로 생성할 수 있는 스토리지 리소스
- 실제 백엔드 스토리지(NFS, EBS, Ceph, GlusterFS 등등)와 연결됨
- 클러스터 스코프를 가지며, 특정 네임스페이스에 속하지 않음
- 사용자는 직접 PV를 참조하지 않음(PV 내부 로직과 다른 리소스 분리를 위해)
- 용량, 접근 모드, 볼륨 타입 등 메타데이터가 포함됨
PersistentVolumeClaim(PVC)
- 사용자가 특정 용량과 접근 방식(ReadWriteOnce 등)을 요청하는 스토리지 청구 리소스
- 네임스페이스 스코프를 가지며, 파드에서 참조됨
- PVC가 생성되면, 조건에 맞는 PV를 찾아 자동으로 바인딩됨
여기서 클러스터 스코프와 네임스페이스 스코프라는 것은
- 클러스터: 실제 인프라(노드들) + 컨트롤 시스템 전체. 물리적/가상 자원들의 집합
- 네임스페이스: 그 클러스터 안에서 리소스를 논리적으로 분리하는 가상 공간
→ 하나의 컴퓨터가 있고 그 안에 다양한 자바 프로젝트가 있는 것과 같다.
클러스터 스코프는
- 클러스터 전체에 걸쳐 존재하는 리소스
- 격리되지 않고 모든 네임스페이스에서 접근 가능함
네임스페이스 스코프는
- 특정 네임스페이스에 국한된 리소스
- 네임스페이스 간에 격리되어 있음
PV는 저장소니까 모든 네임스페이스가 공유해서 사용하는 것
각 네임스페이스들에서 PVC를 만들고 PV에 자원을 어떻게 쓸지 정하고 그만큼 가져다가 쓰는 것
YAML 파일 분석
이걸 구성하는 과정은
- 먼저 클러스터 관리자 즉 인프라 관리자가 PV를 생성한다.
- 개발자가 파드에서 사용할 PVC를 정의한다.(자신의 코드가 접근하는 방식과 쓸 크기 등등을)
- PVC의 selector.matchLabels 조건과 일치하는 PV가 존재하면 바인딩됨
- 파드에서 해당 PVC를 volumes.persistentVolumeClaim.claimName으로 참조해 마운트
- 실습에서는 ReadWriteMany 모드로 여러 파드에서 동시에 접근할 수 있도록 설정했음
- nodwAffinity 조건을 통해 특정 노드(k8s-master)에서만 이 PV를 사용할 수 있도록 제한했음
### PV
apiVersion: v1
kind: PersistentVolume # PV는 실제 물리/가상 디스크를 정의한 리소스
metadata:
name: api-tester-1231-files # PV의 이름 (PVC에서 selector로 참조됨)
labels:
part-of: k8s-anotherclass
component: backend-server
name: api-tester
instance: api-tester-1231-files
version: 1.0.0
managed-by: dashboard
spec:
capacity:
storage: 2G # 이 PV의 실제 용량 (PVC 요청과 같아야 매칭됨)
volumeMode: Filesystem # 파일 시스템 형태로 마운트됨
accessModes:
- ReadWriteMany # 다중 읽기/쓰기를 지원
local:
path: "/root/k8s-local-volume/1231" # 물리적 경로 (노드 로컬 디스크 경로)
nodeAffinity: # 이 볼륨이 사용할 수 있는 노드를 제한함
required:
nodeSelectorTerms:
- matchExpressions:
- {key: kubernetes.io/hostname, operator: In, values: [k8s-master]} # k8s-master 노드에서만 사용 가능
PV 주요 필드
- capacity.storage: 제공 가능한 스토리지 용량 (예: 2G)
- volumeMode: Filesystem 또는 Block (여기선 Filesystem)
- accessModes: 동일하게 ReadWriteMany로 설정됨
- local.path: 물리적 노드 디렉토리 경로 → 가상머신의 저장할 디렉토리
- nodeAffinity: 사용할 수 있는 노드를 제한 (예: k8s-master)
# PVC
apiVersion: v1
kind: PersistentVolumeClaim # PVC는 파드가 요청하는 저장소 리소스
metadata:
namespace: anotherclass-123 # PVC가 속한 네임스페이스
name: api-tester-1231-files # 이 PVC의 이름 (파드에서 volumes.claimName으로 참조)
labels:
part-of: k8s-anotherclass
component: backend-server
name: api-tester
instance: api-tester-1231-files
version: 1.0.0
managed-by: kubectl
spec:
resources:
requests:
storage: 2G # 요청할 저장 용량 (2GB)
accessModes:
- ReadWriteMany # 여러 파드에서 동시에 읽고 쓰기가 가능함
selector:
matchLabels: # 연결할 PV를 라벨로 선택함
part-of: k8s-anotherclass
component: backend-server
name: api-tester
instance: api-tester-1231-files
PVC 주요 필드
- namespace: PVC가 속한 논리적 공간
- resources.requests.storage: 요청하는 저장 용량 (예: 2G)
- accessModes: 접근 방식 (예: ReadWriteMany)
- ReadWriteOnce: 하나의 파드에서 읽기/쓰기 가능 → 대부분의 기본 디스크 스토리지
- ReadOnlyMany: 여러 파드에서 읽기만 가능 → 공유 설정 파일
- ReadWriteMany: 여러 파드에서 읽기/쓰기 가능 → 로그 공유, 여러 앱 간 데이터 공유 → 동시성 제어랑 일관성 잘 관리해야함
- selector.matchLabels: 연결할 PV를 지정하기 위한 라벨 조건
- 여기에 PV의 label 값을 넣는다.
파드와의 연계
- Deployment에서 정의된 파드는 volumes 항목에서 PVC를 참조한다.
- volumeMounts에서 컨테이너 내부 경로(/usr/src/myapp/files/dev)에 PVC를 연결한다.
- 실제 디스크 경로(/root/k8s-local-volume/1231)가 해당 컨테이너 안에 파일시스템으로 마운트됨.
- 이렇게 하면, 애플리케이션은 로컬 디렉토리를 쓰듯이 외부 볼륨을 사용할 수 있다.
volumes:
- name: files
persistentVolumeClaim:
claimName: api-tester-1231-files
containers:
- name: api-tester-1231
volumeMounts:
- name: files
mountPath: /usr/src/myapp/files/dev
- Deployment가 이렇게 작성되어 있고 이걸 기반으로 새로운 파드가 만들어질텐데 그 때 저 PVC와 연결되고 mountPath 경로에 PV가 마운트되며 우리가 쓰는 디스크처럼 동작한다. 이렇게 컨테이너도 영속적인 데이터를 저장하고 사용할 수 있다.
- 이렇게 항상 같은 PV에 붙어 작업을 계속할 수 있다.
실습
local 동작 확인
// 1번 API - 파일 생성
<http://192.168.56.30:31231/create-file-pod>
<http://192.168.56.30:31231/create-file-pv>
// 2번 - Container 임시 폴더 확인
[root@k8s-master ~]# kubectl exec -n anotherclass-123 -it <pod-name> -- ls /usr/src/myapp/tmp
kubectl exec -n anotherclass-123 -it api-tester-1231-7998bc7d89-dc8f6 -- ls /usr/src/myapp/tmp
// 2번 - Container 영구저장 폴더 확인
[root@k8s-master ~]# kubectl exec -n anotherclass-123 -it <pod-name> -- ls /usr/src/myapp/files/dev
kubectl exec -n anotherclass-123 -it api-tester-1231-7998bc7d89-dc8f6 -- ls /usr/src/myapp/files/dev
// 2번 - master node 폴더 확인
[root@k8s-master ~]# ls /root/k8s-local-volume/1231
// 3번 - Pod 삭제
[root@k8s-master ~]# kubectl delete -n anotherclass-123 pod <pod-name>
kubectl delete -n anotherclass-123 pod api-tester-1231-7998bc7d89-dc8f6
// 4번 API - 파일 조회
<http://192.168.56.30:31231/list-file-pod>
<http://192.168.56.30:31231/list-file-pv>
2번 - Container 확인
3번 - Pod 삭제: 삭제 후 다시 만들어지고 있음
4번 API - 파일 조회
http://192.168.56.30:31231/list-file-pod → 파드가 재생성되어 내부의 데이터는 없어진 모습
http://192.168.56.30:31231/list-file-pv → PV에는 남아있음
hostPath 동작 확인 → 아래처럼 Deployment 수정 후 1~4 동작 확인
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: anotherclass-123
name: api-tester-1231
spec:
template:
spec:
nodeSelector:
kubernetes.io/hostname: k8s-master
containers:
- name: api-tester-1231
volumeMounts:
- name: files
mountPath: /usr/src/myapp/files/dev
- name: secret-datasource
mountPath: /usr/src/myapp/datasource
volumes:
- name: files
persistentVolumeClaim: // 삭제
claimName: api-tester-1231-files // 삭제
// 아래 hostPath 추가
hostPath:
path: /root/k8s-local-volume/1231
- name: secret-datasource
secret:
secretName: api-tester-1231-postgresql
[root@k8s-master ~]# kubectl exec -n anotherclass-123 -it api-tester-1231-7c8d5f68c-qwdjw -- ls /usr/src/myapp/files/dev
gcfjpzyeyj.txt ngqheymgsh.txt zribimpegb.txt
[root@k8s-master ~]# ls /root/k8s-local-volume/1231
gcfjpzyeyj.txt ngqheymgsh.txt zribimpegb.txt
지금 했던 실습 정리
지금 실습환경은 노드가 하나인 즉 마스터 노드가 워크노드인 상황이다.
hostPath가 문제가 되려면 워커 노드가 여러개인 환경에서 발생한다.
지금 실습환경에서는 항상 같은 노드에 만들어지기 때문에 문제가 생기진 않았다.
하지만 만약 워커노드가 여러 개인 환경이면 분명 마운트했던 노드가 아닌 노드에도 생길 수 있고
그때는 기존 데이터를 사용할 수 없을 것이다.
마무리
이번 실습을 통해 Kubernetes에서 컨테이너의 비영속성을 해결하기 위한 방법인 PV(PersistentVolume)와 PVC(PersistentVolumeClaim)의 개념을 배웠다.
이를 통해 어떻게 파드들이 기존의 데이터를 유지하며 수행하는지 알게 됐다.
개요의 질문에 대한 답
- 왜 PV와 PVC가 필요한가?
→ 컨테이너는 휘발성이 강하기 때문에, 데이터를 보존하려면 외부 스토리지가 필요하다. - 각각의 개념과 역할은 무엇인가?
→ PV는 클러스터 관리자가 제공하는 실제 저장소 자원이고, PVC는 사용자가 필요한 저장 공간을 요청하는 방식이다. - 실제 YAML 구성은 어떻게 되는가?
→ PV는 스토리지 속성과 노드 제한 등을 정의하고, PVC는 요청 용량과 접근 방식, 라벨로 PV를 선택해 바인딩한다. - hostPath와 PVC+PV 방식의 차이점은?
→ hostPath는 특정 노드에 종속되어 이식성이 낮고, PVC+PV는 클러스터 자원으로 관리되어 확장성과 유지성이 높다.
이제 다음에는 애플리케이션의 배포와 확장을 다루는 핵심 리소스인 Deployment, Service, HPA에 대해 정리하겠다.