Computing

Pod 스토리지 (1): Volume, PersistentVolume, PersistentVolumeClaim 개념 본문

Cloud/Kubernetes

Pod 스토리지 (1): Volume, PersistentVolume, PersistentVolumeClaim 개념

jhson989 2023. 5. 4. 22:25

파드 스토리지의 특징 및 외부 스토리지의 필요성

쿠버네티스의 파드(Pod)는 쿠버네티스에서 생성 및 관리되는 가장 최소의 배포 단위이다[1]. 파드는 하나 이상의 리눅스 컨테이너(container)들로 구성된다. 이때 같은 파드 내에서 배포된 컨테이너들은 같은 네트워크 자원을 가지며 스토리지를 공유할 수 있다[1]. 또한 한 파드로 묶여서 배포되는 컨테이너들은 같은 서버에서 동시에 스케쥴링&실행된다.
 
쿠버네티스는 여러 컨테이너들로 구성된 파드를 최소 배포 단위(=애플리케이션)라고 정의한다. 이러한 구성은 하나의 애플리케이션을 여러 개의 컨테이너들로 구성할 수 있기에 유지보수 과정을 쉽게 만들어준다고 한다. 예를 들어, 웹사이트를 만들고자 할 때, 여러 기능을 하나의 컨테이너 이미지로 저장할 수도 있을 것이고, 각 기능마다 독립된 컨테이너 이미지로 저장할 수도 있을 것이다. 모든 기능을 하나의 컨테이너로 만들 경우, 기능 하나가 고장나더라도 전체 기능을 담고 있는 컨테이너 이미지를 수정해야 한다. 그에 비해 각 기능을 독립된 이미지로 저장할 경우, 해당 기능을 담당하는 컨테이너 이미지만을 수정해면 된다. (즉, 마이크로서비스 아키텍처로 애플리케이션을 구현하기에 더 적합한 배포 방식이다)
 
리눅스 컨테이너는 리눅스 커널에 포함된 Namespaces(네임스페이스), cgroups등의 커널 기능을 통해 호스트 컴퓨터로부터 어느정도 독립된 환경에서 실행된다[2]. 이때 네임스페이스 기능은 프로세스별로 컴퓨터 자원을 독립적으로 바라볼 수 있도록 하는 기술이다. 이 기능은 각 컨테이너가 개별 파일 시스템을 가지도록 해준다[3]. 따라서 아무리 한 파드에서 실행되는 컨테이너들이라고 할 지라도 각 컨테이너는 공유되지 않는 개별 파일 시스템을 가진다.
 
또한 개별 컨테이너가 가진 파일 시스템은 개별 컨테이너의 라이프사이클을 따른다. 쉽게 말하자면 컨테이너가 삭제되면 해당 파일 시스템도 삭제된다는 뜻이다[4]. 따라서 컨테이너가 실행되면서 컨테이너 자체 파일 시스템에 저장한 데이터들은 컨테이너가 삭제되면 같이 사라진다.
 
따라서 단순히 파드 하나를 여러 개의 컨테이너들로 구성하여 배포할 경우 다음과 같은 문제가 발생할 수 있다.
1. 컨테이너가 서로 다른 파일 시스템을 가짐으로 컨테이너들간 데이터 공유가 불편하다.
2. 컨테이너가 종료되면 컨테이너가 생성한 데이터가 유지되지 않는다.
 
이러한 문제를 해결하기 위해 파드는 각 컨테이너들 간에 공유될 수 있는 스토리지 컴포넌트를 설정할 수 있으며, 이를 Volume(볼륨)이라고 한다. 볼륨은 외부 스토리지 시스템(nfs, ceph, glusterFS 등)을 컨테이너에 마운트함으로서 컨테이너가 생성한 데이터를 지속(Persistently) 유지할 수 있다. 
 
 
 

Volume과 PersistentVolume 개념

볼륨은 파드와 같은 쿠버네티스 리소스가 아닌, 파드의 컴포넌트이다. 다음 yaml 파일과 같이 파드 yaml의 spec에 작성된다. 볼륨은 파드 내에 정의되며 사용하고자 하는 스토리지를 가리키는 포인터, 인터페이스와 같은 역할을 한다. 파드 내 정의된 컨테이너가 해당 볼륨을 참조(volumeMounts에 명시)하면, 볼륨이 가리키는 스토리지가 실제로 해당 컨테이너에 마운트된다. 즉 이제부터 컨테이너는 볼륨이 가리키는 스토리지를 사용할 수 있다.
 

kind: Pod
apiVersion: v1
metadata:
  name: test-pod
spec:
  volumes:
    - name: nfs-pvc
      persistentVolumeClaim:
        claimName: test-nfs-pvc
  containers:
  - name: test-pod
    image: busybox:stable
    volumeMounts:
      - name: nfs-pvc
        mountPath: "/mnt"

 
위 yaml 코드를 보면 Pod-spec-volumes 밑에 [nfs-pvc]라는 볼륨이 정의된 것을 확인할 수 있다. 이처럼 볼륨은 파드에 종속적이다. 그렇기에 해당 파드가 종료되면 그 밑에 정의된 볼륨들 또한 모두 제거된다. 다만 주의할 점은 볼륨 = 스토리지(데이터 저장소)를 의미하는 것이 아니고 스토리지를 가리키는 인터페이스 역할이기에, 볼륨이 제거된다고 해도 꼭 데이터가 제거된다는 것은 아니다. (Fig 1.과 참고. Pod A가 삭제되면 vol1, vol2는 삭제되지만, NFS Server, AWS Block 등 외부 스토리지는 삭제되지 않는다. 자세한 설명은 밑에서 계속)
 

Fig 1. 외부 저장소를 가리키는 Volume 컴포넌트. 파드가 사라지면 Volume도 사라지지만 외부 저장소는 남음

 
Pod-spec-containers 밑을 보면 [test-pod]라는 컨테이너가 정의된 것을 확인할 수 있다. 파드 내에 정의된 볼륨은 파드 내에 정의된 컨테이너의 특정 위치(예제에서는 "/mnt")에 마운드하여 사용할 수 있다. Fig 2.와 같이 만약 파드가 두 개의 컨테이너들을 실행하고, 두 컨테이너들 모두 같은 볼륨을 마운트한다면 마운트된 위치에 데이터를 저장함으로서 쉽게 데이터를 공유할 수 있다. 앞서 말했던 1번 문제, 데이터 공유의 힘듦을 해결할 수 있다.
 

Fig 2. 볼륨을 공유하는 컨테이너 A, B

 
다만 2번 문제, 데이터의 지속(영구) 보존의 문제는 해결되지 않았다. 이는 볼륨이 외부 저장소를 가리키도록 하면 해결될 수 있다(Fig 1.의 상황). 볼륨이 가리킬 수 있는 스토리지는 여러개가 존재한다. emptyDir, hostPath, nfs, cephfs, awsElasticBlockStore, rbd 등등이 있다. 이 중 emptyDir을 제외한 나머지 스토리지 종류들은 파드 외부 저장소를 가리킨다. 즉 파드가 종료되더라도 외부 저장소가 살아있는 한 데이터는 유지된다. hostPath는 파드가 실행되고 있는 노드에, nfs는 nfs 서버에, cephfs는 ceph 서버에, awsElasticBlockStore은 AWS 서버에 데이터가 저장된다. 따라서 데이터의 지속 보존 문제를 해결할 수 있다.
 
다만 이러한 방식은 쿠버네티스 인프라를 애플리케이션(파드) 개발자에게 공개한다는 문제가 있다. 즉 애플리케이션 개발자가 파드를 생성하고자 할 때, 어떤 외부 스토리지가 있는 지를 스스로 알고 있어야 한다. nfs의 경우 nfs 서버의 IP와 경로까지도 알고 있어야 한다. 쿠버네티스 인프라의 규모가 작다면 대응 가능한 상황이지만, 인프라의 규모가 크다면 하나하나 기억하기 어렵다. 사실 인프라 자체는 인프라 담당자만이 관리하도록 하고, 애플리케이션 개발자는 몰라도 사용할 수 있도록 하는 것이 중요하다. 이를 해결하기 위해서 쿠버네티스는 PersistentVolume이라는 리소스를 제공한다.
 
PersistentVolume(퍼시스턴트 볼륨, 지속되는 볼륨)은 외부 저장소를 추상화한 인터페이스라고 생각하면 좋다. 인프라 담당자는 미리 쿠버네티스에 인프라 환경에 맞는 퍼시스턴트 볼륨을 만들어둔다. 이때 외부 저장소가 hostPath든, NFS이든, Ceph든, AWS든 상관없이 추상화되어 퍼시스턴트 볼륨이라는 통일된 리소스로 제공된다. 애플리케이션 개발자는 외부 저장소가 필요할 때 미리 만들어진 퍼시스턴트 볼륨들 중에서 선택(정확히는 용량 및 접근 모드를 명시)하여 요청하면 된다. 이때 요청하기 위해 애플리케이션 개발자가 만들어야할 리소스(요청서)를 PersistenrVolumeClaim(퍼시스턴트 볼륨 클레임)이라고 한다.
 
 
 

PersistentVolume, Static Provisioning

정리하자면 인프라 담당자는 퍼시스턴트 볼륨을 미리 만들어두는 역할, 애플리케이션 개발자는 원하는 스펙(용량, 접근모드)을 가진 퍼시스턴트 볼륨을 요청하는 퍼시스턴트 볼륨 클레임을 작성하는 역할로 구분된다. 이를 통해 개발자는 인프라에 어떠한 외부 저장소가 있는 지 어떻게 구성되었는 지 알 필요 없이, 자신이 필요로 한 용량만큼의 외부저장소를 요청해서 자신의 Pod에 볼륨으로 정의해놓기만 하면 된다.
 
다만 이러한 퍼시스턴트 볼륨 + 퍼시스턴트 볼륨 클레임 방식의 외부 저장소 요청은 단점도 있다. 바로 인프라 담당자가 미리 퍼시스턴트 볼륨을 만들어두어야 한다는 것이다. 이렇게 컴퓨터 자원을 준비해두는 작업을 Provisioning(프로비저닝)이라고 한다. 이때 얼마나 필요한 지 요청이 들어오기 전에 미리 선점해서 준비해두는 것을 Static Provisioning(정적 프로비저닝)이라고 한다. 반대로 요청이 들어오면 그때 자원을 준비해두는 것을 Dynamic Provisioning(동적 프로비저닝)이라고 한다.

 

이렇게 인프라 담당자가 미리 퍼시스턴트 볼륨을 설정해두는 것은 정적 프로비저닝 방식이다. 사용자가 얼마나 요청할 지 모르는 상태에서 미리 퍼시스턴트 볼륨을 만들어 놓는다면 저장공간의 낭비가 될 수도 있다. 혹은 사용자들이 만들어 놓은 퍼시스턴트 볼륨 개수보다 더 많은 개수를 요청할 경우 요청이 실패할 수도 있다. 이러한 문제를 해결하기 위해서 쿠버네티스는 동적 프로비저닝을 지원한다. 
(관련 내용은 다음 블로그에서 정리할 예정이다.)
 
 
 

Reference

[1] https://kubernetes.io/docs/concepts/workloads/pods/
[2] https://www.nginx.com/blog/what-are-namespaces-cgroups-how-do-they-work/
[3] https://stackoverflow.com/questions/62228843/does-every-container-in-same-pod-has-independent-file-system
[4] https://docs.docker.com/storage/storagedriver/#container-and-layers