본문 바로가기

쿠버네티스

쿠버네티스 PV·PVC란?|Volume과 PersistentVolume 차이 쉽게 이해하기

쿠버네티스에서 Volume이 필요한 이유?

컨테이너의 기본 파일 시스템은 컨테이너의 생명주기와 함께 관리됩니다. 따라서 컨테이너가 제거되고 새롭게 생성되는 경우, 기존 컨테이너의 Writable Layer에 기록되어 있던 데이터는 그대로 승계되지 않습니다.

쿠버네티스 역시 장애 복구나 재배포 과정에서 기존 컨테이너 환경 자체를 복원하는 방식이 아니라, 불변 이미지(Immutable Image)를 기반으로 새로운 컨테이너를 생성하는 방식을 사용합니다. 따라서 별도의 저장 공간을 사용하지 않는다면 새롭게 생성된 컨테이너는 이미지에 정의된 초기 상태에서 다시 시작하게 됩니다.

이러한 특성은 Stateless 애플리케이션에는 적합하지만, 데이터베이스의 데이터 파일이나 애플리케이션 로그처럼 컨테이너가 재생성된 이후에도 유지되어야 하는 상태(State)가 존재하는 애플리케이션에서는 문제가 될 수 있습니다.

이를 해결하기 위해 쿠버네티스는 컨테이너의 파일 시스템과 데이터의 생명주기를 분리할 수 있도록 Volume이라는 스토리지 메커니즘을 제공합니다.

 

쿠버네티스 워크로드를 이해하기 위한 가장 중요한 대전제 중 하나는 "Pod는 언제든 생성되고 소멸할 수 있다"는 것입니다.


그렇다면 한 가지 의문이 생깁니다.


A Pod의 컨테이너 내부에 데이터를 저장하며 애플리케이션을 운영하던 중, 장애나 재배포 등의 이유로 A Pod가 소멸하고 새로운 Pod와 컨테이너가 생성된다면 기존에 저장했던 데이터는 어떻게 될까요?

새로운 컨테이너는 기존 컨테이너의 파일 시스템을 그대로 이어받는 것이 아니라 컨테이너 이미지를 기반으로 새롭게 생성됩니다. 따라서 별도의 스토리지를 사용하지 않았다면 기존 컨테이너 내부에 저장했던 데이터는 새로운 컨테이너로 승계되지 않습니다.

따라서 쿠버네티스는 이러한 환경에서 데이터의 영속성을 확보할 수 있도록 다양한 Volume 기술​을 제공하며, Volume을 사용하면 컨테이너 내부의 특정 경로를 별도의 저장 공간과 연결할 수 있습니다. 이를 통해 컨테이너가 재시작되거나 새로운 컨테이너로 교체되더라도, 데이터의 생명주기를 컨테이너와 분리하여 관리할 수 있습니다.



Docker 컨테이너 환경에서는 일반적으로 Bind Mount 또는 Volume을 이용하여 호스트의 디렉터리나 별도의 저장 공간을 컨테이너 내부 경로에 마운트하여 사용합니다.


쿠버네티스 역시 컨테이너 내부 경로에 외부 저장 공간을 연결한다는 기본 원리는 동일합니다. 다만 Docker처럼 컨테이너와 저장 공간을 직접 연결하는 방식에서 한 단계 더 나아가, Pod의 Volume과 컨테이너의 volumeMount를 통해 스토리지를 선언적으로 정의하고 연결합니다.

즉, 컨테이너 입장에서는 특정 디렉터리에 스토리지가 Mount되어 있다는 점은 동일하지만, 쿠버네티스에서는 스토리지의 종류와 생명주기, 연결 방식을 Volume이라는 추상화된 구조를 통해 관리한다는 차이가 있습니다.



그렇다면 Kubernetes의 Volume에는 어떤 종류가 있을까요?

1. emtpy Volume (임시 볼륨)

 

쿠버네티스에서 가장 기본적으로 사용할 수 있는 emptyDir Volume에 대해 살펴보겠습니다.

화면 왼쪽의 YAML을 보면 Pod 내부에 empty-dir이라는 이름의 Volume을 하나 생성하고 있습니다.

volumes:
  - name: empty-dir
    emptyDir: {}

 

하나의 Pod 안에서 실행되는 두 개의 컨테이너가 동일한 empty-dir Volume을 각각 자신의 경로에 Mount하도록 구성했습니다.


첫 번째 컨테이너는 다음과 같이 /test2 경로에 Mount하고,

volumeMounts:
  - name: empty-dir
    mountPath: /test2


두 번째 컨테이너는 동일한 Volume을 /test1 경로에 Mount합니다.

volumeMounts:
  - name: empty-dir
    mountPath: /test1


컨테이너마다 보이는 경로는 /test1, /test2로 서로 다르지만, 실제로는 동일한 emptyDir Volume을 바라보고 있습니다.

Container 1에서 /test1 아래에 파일을 생성하면, Container 2에서도 /test2를 통해 동일한 파일을 확인할 수 있습니다.

즉 emptyDir은 하나의 Pod 안에서 여러 컨테이너가 임시 데이터를 공유해야 할 때 유용하게 사용할 수 있는 Volume입니다.



emptyDir은 컨테이너의 생명주기가 아니라 Pod의 생명주기와 함께 동작합니다.

Pod 내부의 특정 컨테이너가 재시작되더라도 동일한 Pod가 유지되고 있다면 emptyDir의 데이터는 그대로 유지됩니다.


반면 Pod 자체가 삭제되거나 새로운 Pod로 재생성되면 기존 emptyDir도 함께 제거됩니다.

emptyDir은 데이터베이스와 같이 영구적으로 보존해야 하는 데이터를 저장하는 용도보다는 캐시, 임시 파일, 연산 중간 데이터 또는 같은 Pod 내부 컨테이너 간 데이터 공유 등의 목적으로 사용하는 것이 적합합니다.


2. hostPath Volume


hostPath는 말 그대로 Kubernetes Node의 실제 파일 시스템 경로를 Pod 내부의 컨테이너에 마운트해서 사용하는 방식입니다.

화면 왼쪽 YAML을 보면 먼저 Pod 수준에서 host-path라는 이름의 Volume을 정의하고 있습니다.

volumes:
  - name: host-path
    hostPath:
      path: /nodetest

 

여기서 /nodetest는 컨테이너 내부 경로가 아니라, Pod가 실행되는 Worker Node의 실제 디렉터리 경로입니다.

그리고 아래 volumeMounts를 보면 해당 Volume을 컨테이너 내부의 /app/test 경로에 연결하고 있습니다.

volumeMounts:
  - name: host-path
    mountPath: /app/test

 

컨테이너 입장에서는 /app/test라는 일반적인 디렉터리처럼 보이지만, 실제로 해당 위치에 기록되는 데이터는 Node의 /nodetest 디렉터리에 저장됩니다.

touch /app/test/sample.txt


실제 Worker Node에서도 동일한 파일을 확인할 수 있습니다.

ls -al /nodetest

 

다만 hostPath를 사용할 때 반드시 고려해야 할 중요한 특징이 있습니다. 바로 스토리지가 특정 Node에 종속된다는 점입니다.

예를 들어 Pod가 Node-01에서 실행되며 /nodetest 경로에 데이터를 저장하고 있다고 가정해보겠습니다. 이후 Node-01에 장애가 발생하여 기존 Pod가 소멸하고 새로운 Pod가 Node-02에 스케줄링되더라도, Node-01의 /nodetest에 저장되어 있던 데이터가 Node-02로 자동으로 이동하거나 복제되지는 않습니다.

즉, Pod가 삭제되더라도 해당 Node가 정상적으로 유지되는 한 hostPath의 데이터는 남아 있을 수 있지만, Pod가 다른 Node로 이동하면 기존 데이터를 그대로 사용할 수 없다는 한계가 있습니다.


이러한 특성 때문에 hostPath는 일반적인 애플리케이션의 영구 스토리지보다는 특정 Node의 파일 시스템에 직접 접근해야 하는 시스템성 워크로드에서 주로 활용됩니다. 대표적으로 Node 로그를 수집하는 에이전트, Node의 설정 파일에 접근해야 하는 모니터링·보안 에이전트, DaemonSet 형태로 배포되는 시스템 에이전트 등이 있습니다.



3. Persistent Volume(PV) / Persistent Calim (PVC)

 

PV/PVC를 처음 접했을 때 가장 먼저 떠오른 것은 전통적인 SI 구축 환경에서의 스토리지 할당 프로세스였습니다.

예를 들어 어떤 업무 시스템을 구축한다고 가정해보겠습니다.

애플리케이션 담당자는 스토리지 장비의 세부 구조까지 알 필요가 없습니다. 보통 다음과 같은 요구사항만 전달합니다.

"DB용으로 500GB 정도 필요합니다."
"여러 서버에서 동시에 접근해야 합니다."
"운영 데이터이기 때문에 삭제되면 안 됩니다."


그러면 인프라 또는 스토리지 담당자는 이러한 요구사항을 바탕으로 실제 스토리지 장비에서 공간을 할당합니다.

👉  Application 담당자
👉  500GB Storage 필요
👉  Storage 담당자
👉  SAN / NAS / Storage Pool
👉  Volume 또는 LUN 할당

여기서 중요한 점은 Application 개발자는 실제 Storage 장비가 무엇인지 몰라도 된다는 것입니다.

어떤 제조사의 Storage인지, 어떤 Disk Pool을 사용하는지, 실제 물리적인 저장 위치가 어디인지는 인프라 운영 영역의 책임입니다.

사용자는 단지 자신에게 필요한 Storage의 조건만 요청하면 됩니다.

쿠버네티스의 PV/PVC 역시 이와 매우 유사한 구조를 가지고 있습니다.


👉 Application 담당자
👉 PVC "이 정도 Storage가 필요합니다."
👉 Kubernetes
👉 PV "이 조건을 만족하는 Storage가 있습니다."
👉 Physical Storage NFS / SAN / Ceph / Cloud Disk


즉 PVC(PersistentVolumeClaim)는 사용자의 스토리지 요청서에 가깝고, PV(PersistentVolume)는 Kubernetes가 사용할 수 있도록 등록된 실제 스토리지 자원에 가깝습니다.


PV는 쿠버네티스 클러스터에서 사용할 수 있도록 등록된 실제 스토리지 자원을 의미합니다.

예제에서는 다음과 같이 1Gi 크기의 스토리지를 정의하고 있습니다.

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-app-logs
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteMany
  storageClassName: fast-storage
  hostPath:
    path: /srv/k8s/data/app-logs


여기서 /srv/k8s/data/app-logs는 실제 데이터를 저장하는 스토리지 경로입니다.


즉 PV는 쉽게 말하면, “쿠버네티스에서 사용할 수 있는 1Gi 크기의 스토리지가 준비되어 있다.”


라고 등록해 놓은 자원이라고 이해하면 됩니다.

그 다음 가운데에 있는 것이 PVC입니다.

PVC는 PersistentVolumeClaim이라는 이름 그대로 사용자가 필요한 스토리지를 요청하는 리소스입니다.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-app-logs
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: fast-storage
  resources:
    requests:
      storage: 1Gi


여기서 중요한 점은 PVC에는 실제 스토리지 경로가 존재하지 않는다는 것입니다.

사용자는 단순히 다음과 같은 요구사항만 정의합니다.

1Gi의 저장 공간이 필요하다.
ReadWriteMany 방식이 필요하다.
fast-storage StorageClass를 사용하겠다.

 

PVC가 생성되면 Kubernetes는 PVC의 요청 조건과 맞는 PV를 찾아 연결합니다.

이 과정을 Binding이라고 합니다.

마지막으로 화면 오른쪽의 Pod를 보겠습니다. Pod는 실제 PV를 직접 참조하지 않습니다.

Pod에서는 PVC의 이름만 지정하여 Volume을 사용합니다.

volumeMounts:
  - name: app-logs
    mountPath: /usr/local/tomcat/logs

volumes:
  - name: app-logs
    persistentVolumeClaim:
      claimName: pvc-app-logs

여기서 claimName: pvc-app-logs를 통해 앞에서 생성한 PVC를 참조하고 있습니다.

그리고 해당 Volume을 Tomcat Container 내부의 경로에 Mount합니다.


결과적으로 Tomcat이 /usr/local/tomcat/logs에 로그를 기록하면, 데이터는 컨테이너 내부의 임시 파일 시스템이 아니라 PVC와 PV를 통해 연결된 실제 스토리지에 저장됩니다.

/usr/local/tomcat/logs



이것이 바로 PV/PVC를 사용하는 가장 큰 이유 중 하나인 스토리지 추상화입니다.


전통적인 인프라 환경에서는 애플리케이션 담당자가 스토리지 요청을 하고, 인프라 담당자가 실제 스토리지를 할당한 뒤 서버에 Mount하는 과정을 거쳤다면, Kubernetes에서는 이러한 과정을 PV와 PVC라는 리소스를 통해 표현합니다.

이 구조를 이해하면 이후에 등장하는 StorageClass와 Dynamic Provisioning도 자연스럽게 이해할 수 있습니다.