본문 바로가기
Infra & Cloud/DevOps

[Kubernetes] Volume: Pod의 Container 데이터는 어떻게 저장될까? (Volume, PersistentVolume, PersistentVolumeClaim)

by newstellar 2022. 12. 11.
반응형

 

 

1. Volume

 

Kubernetes(쿠버네티스) 환경에서 Container(컨테이너)는 Pod(파드)에 감싸져서 동작합니다. 이때 Container에서 만들어진 데이터는 Docker(도커) 파일 시스템으로서 컨테이너 이미지에서만 제공됩니다. 즉, 컨테이너가 삭제되고 재시작하면 기존에 사용하던 파일 시스템은 새로 만들어진 컨테이너에서는 접근할 수 없습니다. 만약 컨테이너가 지워지더라도 파일 시스템을 유지하고 싶다면 어떻게 하면 좋을까요?

 

위 상황에 대비하여 Kubernetes에는 Volume(볼륨) 이라는 API Object가 존재합니다. Volume은 Pod와 마찬가지로 라이프 사이클을 갖는 디스크 스토리지로, 하나의 Pod 내부에 컨테이너가 여러 개라 있더라도 Pod(.yaml)에서 정의한 Volume을 공유할 수 있습니다.


 - emptyDir
 : 일시적인 데이터를 저장하는 데 사용되는 간단한 빈 디렉터리

 - hostPath
 : 워커 노드의 파일 시스템을 파드의 디렉터리로 마운트하는데 사용

 - nfs
 : NFS 공유 파드에 마운트

 - gcePersistentDisk(GCE Persistent Disk), awsElasticBlockStore(AWS EBS Volume), azureDist(MS Azure Disk Volume)
 : 클라우드 제공자의 전용 스토리지를 마운트

 - cinder, cephfs, iscsi, glusterfs, quobyte, rbd, flexVolume, vsphereVolume, photonPersistentDisk
 : 다른 유형의 네트워크 스토리지를 마운트

 - configMap, secret, downwardAPI
 : 쿠버네티스 리소스나 클러스터 정보를 파드에 노출하는 데 사용되는 특별한 유형의 볼륨

 - persistentVolumeClaim
 : 사전에 혹은 동적으로 프로비저닝된 퍼시스턴트 스토리지를 사용하는 방법


위의 Volume 가운데 대표적으로 사용하는 것들에는 hostPath, CSP(gcp, aws, azure) 스토리지, configMap, secret, persistentVolumeClaim이 있습니다.

(1) emptyDir


  자세히 다루지는 않겠지만 emptyDir는 하나의 Pod에서 실행된 컨테이너끼리 파일을 공유할 때 사용합니다.(아래 .yaml 참조) 아래 luksa/fortune 및 nginx:alpine 이미지 컨테이너 두 개는 하나의 Pod에서 동작하므로 html-generator 컨테이너에서는 /usr/share/nginx/html 에 접근이 가능하며, 반대로 web-server 컨테이너에서도 /var/htdocs 디렉토리에 접근할 수 있습니다. 다만 emptyDir로 Volume을 설정한 컨테이너는 Pod의 삭제-재생성의 무게감이 가벼운 쿠버네티스의 특징을 생각해보면, 한계가 많습니다.

apiVersion: v1
kind: Pod
metadata:
  name: pod-volume
  labels:
    app: pod-volume
spec:
  containers:
    # 첫 번째 컨테이너의 이름은 html-generator, 이미지는 luksa/fortune
    - image: luksa/fortune
      name: html-generator
      # 볼륨(html-volume)을 컨테이너(html-generator)의 /var/htdocs 경로에 마운트
      volumeMounts:
        - name: html-volume
          mountPath: /var/htdocs
    # 두 번째 컨테이너의 이름은 web-server, 이미지는 nginx:alpine
    - image: nginx:alpine
      name: web-server
      # 동일한 볼륨(html-volume)을 /usr/share/nginx/html 경로에 readOnly 마운트
      volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html
          readOnly: true
      ports:
        - containerPort: 80
          protocol: TCP
    # emptyDir 볼륨(html-volume)을 위에서 정의한 두 컨테이너에 마운트
  volumes:
    - name: html
      emptyDir: {}


추가적으로 gitRepo와 같은 유형의 볼륨도 존재했으나 deprecated되어 initContainer의 볼륨을 emptyDir로 지정하는 방식을 사용한다고 합니다.

Warning:
The gitRepo volume type is deprecated. To provision a container with a git repo, mount an EmptyDir into an InitContainer that clones the repo using git, then mount the EmptyDir into the Pod's container.

참고 : https://kubernetes.io/docs/concepts/storage/volumes/#gitrepo

 

반응형

 

(2) hostPath


hostPath 볼륨은 Pod 단위에서 정의하는 emptyDir의 범위를 넘어, Node 파일시스템의 특정 파일이나 디렉터리로 확장하여 마운트할 수 있게 해줍니다. 우리는 hostPath를 통해 같은 Pod의 컨테이너끼리는 물론, 같은 Node에서 운영하고 있는 Pod 끼리도 데이터를 공유할 수 있습니다. 호스트 OS의 Docker(/var/lib/docker)에 접근하거나 호스트 OS에서 개발한 환경을 Container 내부에 적용해야 하거나, 또는 반대로 컨테이너 환경에서 추가된 파일을 호스트 OS에서 사용해야 할 경우 유용하게 사용할 수 있습니다.

https://bcho.tistory.com/1259


hostPath는 Persistent Storage의 종류로서, Pod가 종료되면 삭제되는 emptyDir 볼륨과 다르게 Pod를 종료해도 볼륨의 데이터를 유지하게 해줍니다. 단, 같은 Node에 삭제된 Pod가 재시작한다는 조건으로 기존 Pod가 저장한 파일 시스템에 접근할 수 있습니다. hostPath의 원리에 따라 만약 Scheduler가 Pod를 다른 Node로 할당했다면 볼륨을 마운트할 수 없다는 말과 동일합니다. 때문에 Node에 Taint가 걸려있어 Pod를 만들 수 없거나 Node 버전 업그레이드를 위한 kubectl cordon [node] 명령으로 Pod가 일시적으로 이사가는 등의 상황에서는 hostPath가 적절한 볼륨은 아닙니다.

추가적으로 kubernetes.io 공식 문서에서는 아래처럼 Warning 문구도 적어두었습니다. (hostPath으로 지정된 경로가 그대로 노출되기 때문에 꼭 필요한 파일/디렉토리만을 명시하고, readOnly를 설정하여 보안 리스크를 줄이라고 하네요.)

Warning:
HostPath volumes present many security risks, and it is a best practice to avoid the use of HostPaths when possible. When a HostPath volume must be used, it should be scoped to only the required file or directory, and mounted as ReadOnly. If restricting HostPath access to specific directories through AdmissionPolicy, volumeMounts MUST be required to use readOnly mounts for the policy to be effective.

참고 : https://kubernetes.io/docs/concepts/storage/volumes/#hostpath

 

apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: registry.k8s.io/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /test-pd
      name: test-volume
  volumes:
  - name: test-volume
    hostPath:
      # directory location on host
      path: /data
      # this field is optional
      type: Directory

 

위 .yaml을 한 번 해석해봅시다. 컨테이너에 마운트할 볼륨의 정보는 spec.volumes에 정의합니다. 따라서 컨테이너가 담길 Pod의 Node에는 /data 라는 디렉토리가 존재해야 하는데, 만약 존재하지 않는 디렉토리 경로를 hostPath.path로 명시한다면 kubelet이 아래와 같이 에러 메시지를 내뱉으며 Pod를 실행시키지 않습니다.

MountVolume Setup failed for volume "test-volume" : hostPath type cheked failed: /data is not a directory

 

(3) CSP(gcp, aws, azure) 스토리지


Pod 내에서만 관리되는 emptyDir의 단점을 극복하여 Node 단위로 확장한 hostPath를 살펴보았습니다. 단점으로도 언급했지만 만약 kube-scheduler에 의해 Taints, Cordon 등 Pod가 다른 Node로 스케줄링되거나 Node가 삭제되는 경우에도 동일한 데이터를 보관해야 한다면 위의 두 유형의 볼륨은 사용할 수 없습니다. Node가 바뀌더라도 클러스터가 변경되더라도 접근 가능한 디스크 볼륨이 필요한 경우 NAS 유형의 볼륨, 즉 GCP, AWS, Azure 등의 CSP(Cloud Service Provider)에서 제공하는 Persistent Storage(퍼시스턴트 스토리지)를 사용해야 합니다.

영구적으로 데이터를 저장하려는 목적으로 MySQL DB를 실행하는 Pod를 생성했다고 가정합니다. AWS의 EKS(Elastic Kubernetes Service)를 바탕으로 Managed 형태의 쿠버네티스 클러스터가 실행중이라면 EBS(Elastic Block Storage) 기반 Persistent Storage를 사용할 수 있습니다. AWS EBS 생성을 하고, MySQL Pod에 해당 볼륨을 연결하면 됩니다.

aws ec2 create-volume --availability-zone=eu-west-1a --size=10 --volume-type=gp2

 

apiVersion: v1
kind: Pod
metadata:
  name: test-ebs
spec:
  containers:
  - image: registry.k8s.io/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /test-ebs
      name: test-volume
  volumes:
  - name: test-volume
    # This AWS EBS volume must already exist.
    awsElasticBlockStore:
      volumeID: "<volume id>"
      fsType: ext4

 


2. PV(PersistentVolume)과 PVC(PersistentVolumeClaim)

 

 

Persistent Volume(PV)은 관리자가 static한 방식으로 provisioning(프로비저닝)하거나 StorageClass를 사용하여 dynamic하게 provisioning할 수 있는 쿠버네티스 클러스터의 Storage '조각'입니다. PV를 이용하면 하나의 Storage를 관심사에 따라 원하는 용량으로 쪼갤 수 있기 때문에 '조각'으로 표현한 것입니다. Node가 클러스터 자원인 것처럼 PV는 볼륨의 자원이라고 볼 수 있지만 PV는 개별 Pod와 묶이지 않는 독립적인 수명 주기를 가지고 있습니다. 앞서 AWS EBS를 이용한 사례처럼 우리는 PV를 사용하여 NFS, iSCSI 또는 CSP의 스토리지 시스템을 Pod와 연결할 수 있습니다.

apiVersion: v1
kind: PersistentVolume
metadata:
  name: foo-pv
spec:
  storageClassName: ""
  claimRef:
    name: foo-pvc
    namespace: foo
  ...

  * 위 PV의 configuration 과정 중 spec.claimRef.name에 PVC의 metadata.name을 명시해주어 PV와 PVC 바인딩을 예약해둘 수 있습니다.

 


PersistentVolumeClaim(PVC)은 사용자가 PV를 사용하겠다며 관리자에게 제출하는 일종의 요청서입니다. Pod는 cpu 및 memory와 같은 Node의 리소스를 할당받고, PVC는 capacity와 accessModes로 PV 리소스를 할당받습니다. AccessModes 옵션으로는 ReadWriteOnce, ReadOnlyMany 또는 ReadWriteMany가 있습니다. storageClassName ""로 설정된 PVC는 항상 클래스가 없는 PV를 요청하는 것으로 해석되어 클래스가 없는 PV(어노테이션이 없거나 ""로 설정)에만 바인딩될 수 있습니다.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: foo-pvc
  namespace: foo
spec:
  storageClassName: "" 
  # 빈 문자열은 명시적으로 설정해야 하며 그렇지 않으면 default storageClass로 설정
  volumeName: foo-pv
  ...


PVC는 바인딩 상태가 아닌 PV 중 capacity가 PVC에서 요구하는 storage size보다 큰 것들을 자동으로 매칭시킵니다. (참고 : https://kubernetes.io/docs/concepts/storage/persistent-volumes/#binding) 만약 원하는 PV를 선택하려면 label selector를 이용할 수 있는데, 아래의 matchLabels 및 matchExpressions 두 종류의 selector와 일치하는 PV에 바인딩될 수 있습니다. (matchExpressions에서 사용가능한 연산자로는 In, NotIn, Exists, DoesNotExist가 존재합니다.)

a) ReadWriteOnce(RWO) : 하나의 노드가 볼륨을 Read/Write 가능하도록 마운트
b) ReadOnlyMany(ROX) : 여러 노드가 Read 전용으로 사용하도록 마운트
c) ReadWriteMany(RWX) : 여러 노드가 Read/Write 가능하도록 마운트

 

1. 클러스터 관리자는 물리적 스토리지와 연결되는 PV를 생성합니다. 만들어진 PV는 Pod와 직접적으로 연결되지 않습니다.

2. 클러스터 사용자는 개발한 애플리케이션에 적합한 PV에 바인딩되는 PVC를 생성합니다.

3. 2번에서 생성한 PVC를 사용하는 Pod를 생성합니다.


운영 환경에서는 hostPath를 사용하여 PV를 Pod에 바인딩하지는 않습니다. GCE(Google Compute Engine) 영구 디스크, NFS 공유 또는 Amazone Elastic Block Store 볼륨과 같은 네트워크 자원을 프로비저닝하거나 스토리지클래스(StorageClasses)를 사용하여 동적 프로비저닝을 설정하는 것이 일반적입니다. CKA 시험에서 hostPath를 이용한 PV 및 PVC 생성 후 Pod와 연결하라는 문제가 단골로 출제되기 때문에 예시로 다뤄보겠습니다.

우선 작업 Node에 /mnt/data 디렉토리를 만들고, 그 내부에 index.html 파일을 만듭니다.

# This again assumes that your Node uses "sudo" to run commands as the superuser

sudo mkdir /mnt/data
sudo sh -c "echo 'Hello from Kubernetes storage' > /mnt/data/index.html"

 

# task-pv-volume.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: task-pv-volume
  labels:
    type: local
spec:
  storageClassName: volume-class
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/mnt/data"


.yaml 파일에 클러스터 Node의 /mnt/data 경로에 persistent volume을 위치시키고, capacity에서 볼륨 크기를 10 Gibibyte로, accessModes를 단일 노드가 읽기-쓰기 모드로 볼륨을 마운트하도록 지정합니다. 여기서는 dynamic provisioning을 위해 storageClassName의 이름은 volume-class로 정의하며, PVC의 요청을 바인딩하는 데 사용합니다.

PVC의 configuration file에도 storageClassName을 volume-class로 동일하게 써줘야 합니다.

 

persistentVolumeReclaimPolicy 설정을 통해, 후술할 PVC가 삭제되었을 때 PV 역시 아예 삭제되느냐(Delete), 아니면 사용할 수 없는 Released한 상태로 남느냐(Retain)를 결정할 수도 있습니다. (default: Retain) Recycle 옵션도 존재했으나 현재는 deprecated되어 동적 프로비저닝을 권장하고 있습니다.

kubectl apply -f task-pv-volume.yaml
kubectl get pv task-pv-volume

NAME             CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS      CLAIM     STORAGECLASS   REASON    AGE
task-pv-volume   10Gi       RWO           Retain          Available             volume-class             4s


이제 PersistentVolumeClaim을 만들어봅시다. 위에서 PersistentVolume(PV)를 만들었다 하더라도 Pod와 직접 연결하는 API Object는 PVC이므로, PV의 accessModes와 동일하게, 단 storage 크기는 PV의 capacity보다 작게 지정해줍니다. PV와 PVC는 1:1 관계이기 때문에 PV에 할당된 storage 크기보다 작은 PVC를 Pod에 연결했다 하더라도, 다른 Pod에 사용될 다른 PVC를 동일한 PV와 연결할 수는 없습니다. 만약 PVC는 있지만 PV가 모두 사용 중인 경우, PVC는 PV가 바인딩될 때까지 pending 상태로 남게 됩니다.

# task-pv-claim.yaml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: task-pv-claim
spec:
  storageClassName: volume-class
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 3Gi

 

kubectl apply -f task-pv-claim.yaml

// PVC가 연결된 PV의 STATUS와 CLAIM colume 값이 바뀜
kubectl get pv task-pv-volume

NAME             CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS    CLAIM                   STORAGECLASS   REASON    AGE
task-pv-volume   10Gi       RWO           Retain          Bound     default/task-pv-claim   volume-class             2m


kubectl get pvc task-pv-claim

NAME            STATUS    VOLUME           CAPACITY   ACCESSMODES   STORAGECLASS   AGE
task-pv-claim   Bound     task-pv-volume   10Gi       RWO           volume-class   30s

 

이제 마지막으로 앞서 만들어놓은 PV에 바인딩된 PVC를 연결하는 Pod를 만들어줍니다. Pod의 관점에서는 PVC가 곧 volume이라는 점을 명심합니다. spec.volumes에 PVC를 정의하되, spec.volumes[*].name은 configuration file(.yaml) 안에서만 spec.containers.volumeMounts[*].name과 같도록 지정해주면 됩니다. (여기서는 임의로 task-pv-storage라는 이름을 사용) spec.volumes[*].persistentVolumeClaim.claimName만 PVC의 metadata.name으로 적어주면 끝입니다.

# task-pv-pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: task-pv-pod
spec:
  volumes:
    - name: task-pv-storage
      persistentVolumeClaim:
        claimName: task-pv-claim
  containers:
    - name: task-pv-container
      image: nginx
      ports:
        - containerPort: 80
          name: "http-server"
      volumeMounts:
        - mountPath: "/usr/share/nginx/html"
          name: task-pv-storage

 

kubectl apply -f task-pv-pod.yaml
kubectl get pod task-pv-pod


지금까지 PV -> PVC -> Pod 순서로 API Object를 생성했습니다. 글을 끝맺기 전에 우리가 직전에 만든 Pod(task-pv-pod)의 nginx 이미지 컨테이너로 직접 들어가 shell을 통해 PV 내부에 정의했던 index.html 파일이 80번 포트를 통해 제대로 serve되고 있는지 확인해봅시다.

master$ kubectl exec -it task-pv-pod -- /bin/bash
root@task-pv-container:/#

 

# Be sure to run these 3 commands inside the root shell that comes from
# running "kubectl exec" in the previous step

root@task-pv-container:/# apt update
root@task-pv-container:/# apt install curl
root@task-pv-container:/# curl http://localhost/


아래처럼 결과가 나온다면 성공입니다.

Hello from Kubernetes storage

 

정리하자면, PVC는 볼륨을 사용하고자 하는 Pod와 동일한 네임스페이스에 있어야 합니다. 클러스터는 해당 네임스페이스에서 PVC를 찾고 이를 사용하여 PVC과 바인딩된 PV를 찾습니다. 이후, PV가 Pod에 마운트되는 플로우로 이해하시면 됩니다.

 

 

[Kubernetes] Authentication/Authentication(인증/인가) 프로세스 및 User/Role 생성(User, CSR, RBAC, RoleBinding)

1. 쿠버네티스에서의 User 개념 쿠버네티스에서는 User(human)와 ServiceAccount(machine) 두 종류의 사용자가 존재합니다. User에는 클러스터 관리자 및 사용자(일반 개발자)가 있으며, ServiceAccount는 Prometheus

newstellar.tistory.com

 

[Kubernetes] NetworkPolicy를 통해 Pod끼리의 트래픽을 통제해보자. (Ingress/Egress, from/to, namespaceSelector, pod

[ NetworkPolicy ] 1. Ingress vs Egress 네트워크 트래픽은 외부로부터 유입되는 Ingress(inbound)와 내부로부터 외부로 나가는 Egress(outbound)로 구분됩니다. 우리가 공부하고 있는 쿠버네티스는 기본적으로 non-

newstellar.tistory.com

 

[Kubernetes] 쿠버네티스 스케쥴러(Scheduler)를 직접 만들어보자. (kube-scheduler 개념/작동방식)

들어가며 쿠버네티스 스케줄링(Kubernetes Scheduling)이란 적절한 node의 kubelet이 Pod를 실행하도록 할당하는 것을 뜻합니다. Kubernetes에서는 master node(control plane)의 kube-scheduler가 위 역할을 담당합니다.

newstellar.tistory.com

 

[Kubernetes] Master Node(Control Plane)의 Pod를 삭제하면 어떻게 될까? (컨테이너런타임/Static Pod)

시작하며 일반 개발자가 아닌, 관리자의 입장에서는 Kubernetes 클러스터의 노드에서 Pod가 실행될 때 발생하는 에러들에 대해서 알아야 합니다. (만약 CKAD 수준의 쿠버네티스 이해 및 사용 능력이

newstellar.tistory.com

 

[Kubernetes] CKA(Certified Kubernetes Administration) 자격증 접수 방법/예약/출제 범위/할인바우처 (+CKAD, CKS

CNCF(Cloud Native Computing Foundation) 재단에서 주관하는 CKA(관리), CKAD(개발), CKS(보안) 세 자격증 가운데, 가장 대표적인 CKA 자격증에 대해 알아봅니다. 접수 및 시험 예약 방법, 추천하는 강좌 그리고

newstellar.tistory.com

 

반응형

댓글