Kubernetes in Action : Chapter6. 볼륨

    들어가기 전

    이 글은 쿠버네티스 인 액션 6장을 공부하며 작성한 글입니다.


    6. 볼륨 : 컨테이너에 디스크 스토리지 연결

    파드는 내부적으로 CPU / 메모리 / 네트워크 인터페이스를 따로 배정 받아서 동작한다. 그렇지만 파드가 사용하는 파일 시스템은 컨테이너 이미지로부터 만들어진다. 따라서 기본적으로 파드에 저장한 데이터들은 파드가 재시작하면 사라진다. 파드에 저장한 데이터들을 보존하려면 '볼륨'을 사용해야한다. 볼륨은 파드와 동일한 라이프 사이클을 가진다.

    이 장에서는 볼륨을 공부해보고자 한다. 


    6.1 볼륨 소개

    • 볼륨은 독립적인 쿠버네티스 오브젝트가 아니므로 자체적으로 생성 / 삭제될 수 없다. 
    • 볼륨은 파드의 모든 컨테이너에서 사용 가능하지만, 접근하려는 컨테이너에서 각각 마운트 돼야한다. 

    6.1.2 사용 가능한 볼륨 유형 소개

    쿠버네티스에서는 다양한 형태의 볼륨을 제공해준다.

    • emptyDir : 일시적인 데이터를 저장하는데 사용되는 간단한 빈 디렉터리.
    • hostPath : 워커 노드의 파일시스템을 파드의 디렉토리로 마운트하는데 사용한다. 
    • nfs : NFS 공유를 파드에 마운트한다. 
    • gcePersistentDisk, awsElasticBlockStore, azureDisk : 클라우드 제공자의 전용 스토리지를 마운트 하는데 사용함. 
    • cinder, cephfs, iscsi, glusterfs ... : 다른 유형의 네트워크 스토리지를 마운트하는데 사용한다. 
    • configMap, secret, downwardAPI : 쿠버네티스 리소스나 클러스터 정보를 파드에 노출하는데 사용되는 특별한 유형의 볼륨. 
    • PersistentVolumeClaim : 사전에 생성 / 동적으로 프로비저닝된 Persistent Volume을 사용하는 방법 
    • projectedVolume : 여러 볼륨을 하나의 볼륨으로 모을 수 있음.

    6.2.1 emptyDir 볼륨 사용

    emptyDir은 다음과 같은 특성을 가진다. 

    • emptyDir는 빈 디렉토리로 시작하는 볼륨. 
    • emptyDir는 파드가 만들어진 노드 디스크에 생성됨. 따라서 emptyDir의 성능은 노드 디스크의 종류에 따라 바뀜 (SSD, HDD)
      • 이를 방지하기 위해 medium: memory를 이용해 메모리 타입을 지원함. 
    • emptyDir 볼륨은 파드가 생성될 때 만들어지고, 파드가 삭제될 때 삭제됨.
    • emptyDir 볼륨은 동일 파드에서 실행 중인 컨테이너 간 파일을 공유할 때 유용함. 

    사용 할 때는 다음과 같이 사용한다.

    #EmptryDIr
    apiVersion: v1
    kind: Pod
    metadata:
      name: c6-1
    spec:
      containers:
        - name: html-generator
          image: luksa/fortune
          volumeMounts:
            - mountPath: /var/htdocs 
              name: html
        - name: web-server
          image: nginx:alpine
          volumeMounts:
            - mountPath: /usr/share/nginx/html 
              name: html
              readOnly: true
      volumes:
        - name: html
          # emptyDir
          emptyDir: {}

    6.3 워커 노드 파일시스템의 파일 접근 (HostPath)

    일반적인 어플리케이션이 동작하는 파드라면 호스트 노드를 인식하지 못해야 한다. 호스트 노드를 인식해서 동작해야 한다는 것은 호스트 노드의 의존성을 가지는 작업인데, 일반적인 상황이라면 바람직하지 못하기 때문이다. 

    그렇지만 시스템 레벨의 파드(DaemonSet)으로 동작하는 파드라면, 노드의 파일을 읽거나 해야 할 필요가 있다. DaemonSet으로 동작하는 파드라면 HostPath를 사용해도 괜찮을 것 같다. 

     


    6.3.1 HostPath 볼륨 소개

    HostPath 볼륨은 다음 특징을 가진다. 

    • hostPath 볼륨은 노드 파일 시스템의 특정 파일 / 디렉터리를 volumeMount로 사용할 수 있다. 
    • 파드가 종료되어도 hostPath의 볼륨의 값은 삭제되지 않음. 따라서 재기동 된 파드가 동일한 노드에서 생성되면, 이전 파드가 남긴 모든 항목을 볼 수 있음.
    • HostPath를 볼륨으로 사용한다면, 어떤 노드에 스케쥴링 되는지가 중요함. 따라서 DaemonSet 같은 것들만 사용하는 것이 좋음. 
    • HostPath 볼륨을 사용하는 것은 노드의 데이터 중, 파드에서 읽기 전용으로 사용하는 녀석이 좋음.
      • 예시 : kubeconfig, CA 인증서

    그림으로 살펴보면 다음과 같다.

     


    6.3.2 HostPath 볼륨을 사용하는 시스템 파드 검사하기

    앞서 이야기 했던 것처럼 daemonSet처럼 시스템 레벨의 파드는 HostPath를 이용해 노드의 파일 시스템 정보를 사용하기도 한다. 대표적으로 kube-system 네임 스페이스에 있는 파드들 중에 DaemonSet이 많기 때문에 hostPath 볼륨을 사용하는지 확인할 수 있다. 

    $ kubectl get pod -n kube-system -o yaml | grep hostPath -A 5
    >>>
      ...
      - hostPath:
          path: /run/xtables.lock
          type: FileOrCreate
        name: xtables-lock
      - hostPath:
          path: /lib/modules
          type: ""
        name: lib-modules
      - name: kube-api-access-qxds9
      ...

    쿠버네티스 컴포넌트의 DaemonSet 파드들에서 HostPath 볼륨을 사용하는 공통적인 방법은 다음과 같다. 

    • 로그 파일
    • kubeconfig
    • CA 인증서

    등에 접근하기 위해 HostPath 볼륨을 사용한다. 즉, HostPath 볼륨을 이용해 데이터를 저장하지 않는다. 필요한 값을 읽어오는 '읽기전용'의 느낌으로 HostPath 볼륨을 사용한다. 

     


    6.4 퍼시스턴스 스토리지 사용

    다음 요구 조건이 있는 경우라면 앞에서 공부했던 hostPath, emptyDir은 사용할 수 없다. 이럴 때는 Persistent Storage를 사용해야 한다.

    • 파드에서 실행 중인 어플리케이션이 데이터를 디스크에 저장해야 함. 
    • 파드가 다른 노드에서 재기동되어도 기존 데이터를 이용할 수 있어야 함. 

     

    6.5 기반 스토리지 기술과 파드 분리

    네트워크 스토리지(iscsi, NFS 등)를 이용해서 볼륨을 생성하려면 개발자가 NFS 익스포트가 위치하는 실제 서버를 알아야한다. 그런데 이것은 어플리케이션과 개발자를 실제 인프라스트럭쳐와 분리하려는 쿠버네티스의 기본 아이디어에 반대된다.

    쿠버네티스가 원하는 형태는 개발자가 파드를 생성할 때 CPU, 메모리를 요구하는 것처럼 Persistent Storage를 요청하면 쿠버네티스가 이에 응하는 방식이어야 한다. 즉, 개발자와 인프라 스트럭쳐를 분리해야한다. 

     

    6.5.1 PersistenVolume과 PersistentVolumeClaim

    개발자가 볼륨을 요청할 때, 개발자와 인프라 스트럭쳐를 분리하기 위해 PV(Persistent Volume) / PVC(Persistent Volume Claim)이 도입되었다. PV / PVC는 다음 형태로 사용된다. 중요한 아이디어는 개발자는 파드를 배포하는 것처럼 PVC를 요청해서 Volume으로 사용한다는 것이다. 

    과정을 정리하면 다음과 같다.

    1. 클러스터 관리자는 네트워크 스토리지 유형을 설정함.
    2. 관리자는 PV 매니페스트 파일을 API 서버에 전달해, PV를 생성함. 
    3. 사용자는 PVC를 생성함. (여기서 인프라 스트럭쳐의 추상화) 
    4. 쿠버네티스는 PVC에 PV를 바인딩 해 줌. 
    5. 사용자는 PVC를 볼륨으로 마운트하는 파드 생성함. 

    앞서서 설명했던 여러 Volume들은 노드와 결합하거나, 네트워크 스토리지(인프라 스트럭쳐)를 자세히 알아야 한다는 단점이 있었다. 이 부분은 클러스터 관리자가 처리해주고, 사용자는 파드를 선언하는 것처럼 PVC를 선언해서 사용하면 된다. 


    6.5.2 PV 생성

    아래와 같이 PV 매니페스트 파일을 작성해서 배포 요청을 하면 PV가 생성된다. 자세히 보면 인프라 스트럭쳐 내용이 들어가는 것을 알 수 있다.

    apiVersion: v1
    kind: PersistentVolume
    metadata:
      name: c6-9
    spec:
      capacity:
        storage: 1Gi
      accessModes:
        - ReadWriteOnce
        - ReadWriteMany
      persistentVolumeReclaimPolicy: Retain
      gcePersistentDisk:
        pdName: mongodb
        fsType: ext4

    PV를 생성 요청한 후에는 다음과 같이 생성된 것을 알 수 있다. PV의 상태는 Available, Bound가 존재하는데 Available은 PVC와 바인딩 되기 전 상태를 의미한다. PV가 PVC와 바인딩 되면 Bound 상태로 바뀌게 되고, 이 PV는 해당 PVC가 사용하는 상태라고 생각할 수 있다. 

    $ kubectl get pv
    >>>
    NAME        CAPACITY     RECLAIMPOLICY    ACCESSMODES  STATUS     CLAIM
    mongodb-pv  1Gi          Retain           RWO,ROX      Available

     

    PV는 클러스터 수준 리소스. 

    PVC는 네임스페이스 수준 리소스이며, PV는 노드처럼 클러스터 수준의 리소스다. 클러스터 수준의 리소스라는 것은 어떤 네임 스페이스에서도 접근할 수 있는 수준의 리소스라는 의미이다. 

     


    6.5.3 PVC 생성을 통한 PV 요청

    PVC와 관련된 매니패스트는 다음과 같이 작성할 수 있다. 

    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: c6-11
    spec:
      resources:
        requests:
          storage: 1Gi
      accessModes:
        - ReadWriteOnce
      storageClassName: "" # 동적 프로비저닝과 관련있음.

    PVC를 배포하고 나면, 쿠버네티스는 PVC의 조건을 만족하는 PV를 찾아서 바인딩 시킨다. 이 때, 쿠버네티스는 다음 조건을 만족하는 PV를 찾아서 PVC와 바인딩 시킨다.

    • PVC가 요청한 Storage보다 더 많은 Storage를 가진 PV 
    • PVC가 요청한 Access 모드를 포함하는 PV

    이 때 각각 PV, PVC를 살펴보면 다음과 같다. 

    $ kubectl get pvc 
    NAME        STATUS  VOLUME      CAPACITY  ACCESSMODES  AGE
    mongodb-pvc BOUND   mongodb-pv  1Gi       RWO,ROX      3s
    
    $ kubectl get pv 
    >>>
    NAME        CAPACITY     RECLAIMPOLICY    ACCESSMODES  STATUS     CLAIM  
    mongodb-pv  1Gi          Retain           RWO,ROX      BOUND      default/mongodb-pvc

    주의해야 할 점은 여러 노드에서 사용할 수 있는 PV라고 할 지라도 Namespace가 다른 파드는 같은 PV를 사용할 수 없다는 것이다. 

     

    Access MODE

    RWO(Read Write Once) : 단일 노드만이 읽기/쓰기로 볼륨 마운트 가능

    ROX(Read Only Many) : 여러 노드에서 읽기용으로 볼륨 마운트 가능

    RWX(Read Write Many) : 여러 노드에서 읽기/쓰기로 볼륨 마운트 가능 

    이 때 조심해서 이해해야할 것은 Access Mode의 단위는 파드가 아닌 '노드' 단위다. 


    6.5.4 파드에서 PVC 사용

    PVC의 생성이 완료되었으면, 파드의 Volumes 부분에 PVC를 볼륨으로 사용하겠다는 것을 선언한 후에 볼륨 마운트를 하면 된다. 아래 방법처럼 사용할 수 있다. 

    apiVersion: v1
    kind: Pod
    metadata:
      name: c6-12
    spec:
      containers:
        - name: mongodb
          image: mongo
          volumeMounts:
            - mountPath: /data/db
              name: mongodb-data
          ports:
            - containerPort: 27017
              protocol: TCP
      volumes:
        - name: mongodb-data
          persistentVolumeClaim:
            claimName: mongodb-pvc

    6.5.5 PV / PVC 사용의 장점 이해하기

    볼륨을 사용할 때는 네트워크 스토리지를 직접 사용하는 방법과 PV + PVC를 사용하는 방법이 존재한다. 네트워크 스토리지 대신 PV / PVC를 사용할 때의 장점은 무엇일까? 아래 그림에서 볼 수 있듯이 '개발자와 네트워크 인프라 스트럭쳐의 분리'가 가장 큰 장점이다. 

    네트워크 스토리지 사용 vs PV + PVC 사용

    개발자는 인프라 스트럭쳐를 추상화 시킨 'PVC'를 이용해서 볼륨을 요청하면 되고, 쿠버네티스는 클러스터 관리자가 미리 만들어둔 PV와 개발자가 요청한 PVC를 바인딩 시켜준다. 그리고 개발자는 PVC를 통해 PV를 사용하기만 하면 된다.


    6.5.6 PV 재사용

    PV는 PersistentVolumeReclaimPolicy 설정을 통해서 PV를 재사용할 수도 있다. Reclaim 정책은 다음이 존재한다.

    • Retain : PV 재사용 + 데이터도 재사용 가능
    • Recycle : 데이터 재사용. 현재는 Deprecated
    • Delete : 데이터 + PV는 바로 삭제됨.

    Retain은 PVC가 삭제되어도 PV는 남아있게 된다. 아래에서 볼 수 있듯이 Release 상태로 남아있고, Release 상태인 PV는 자동으로 PVC와 바인딩 되지는 않는다. 

    $ kubectl get pv
    NAME       CAPACITY   ACCESSMODES    STATUS    CLAIM       
    mongodb-pv 1Gi        RWO,ROX        Released  default/mongodb-pvc

    Release 상태에서는 두 가지 방법을 사용할 수 있다.

    1. PV와 관련된 볼륨(네트워크 스토리지)의 데이터를 모두 삭제한 후, kubectl edit pv를 통해 상태를 Available로 바꿈. 
    2. 아무것도 하지 않고, kubectl edit pv를 통해 상태를 Available로 바꿈. 

    첫번째는 PV와 연결된 볼륨(파드가 아니라 네트워크 스토리지 개념!)으로 들어가, 볼륨에 저장된 데이터를 모두 삭제한다. 그리고 Available로 상태를 바꾸게 되면 PV - PVC는 바인딩 되게 된다. PV에 저장된 데이터가 모두 없어졌기 때문에 PV를 사용하는 파드는 '새로운 PV'를 사용하는 것처럼 느낄 것이다.

    반면 두번째 방법은 데이터를 그대로 남겨둔 채로 다시 PV - PVC가 바인딩 되어서 파드가 사용하게 된다. 따라서 이 PV를 사용하는 파드는 이전 데이터를 그대로 사용할 수 있다.


    6.6 PV와 동적 프로비저닝

    PVC가 PV를 사용하려면, 클러스터 관리자가 미리 PV를 만들어두어야 한다. 이런 부분은 꽤 번거로울 수 있는데, 쿠버네티스는 각 스토리지별 Provisinor를 이용해서 동적으로 요청이 올 때 마다 PV를 만들 수 있다. 이 부분은 다음과 같이 할 수 있다.

    1. 사용자는 먼저 프로비저너를 쿠버네티스 클러스터에 배포한다. (Longhorn 같은 것들)
    2. StorageClass를 선언한다.
    3. PVC가 StorageClass를 참조한다. 그러면 StorageClass의 Provisinor는 PV를 동적으로 프로비저닝한다. 

    이 방법의 장점은 PV가 부족할 일이 없다는 것이다. 다만 PV는 부족하지 않을 수 있지만, 남은 스토리지 용량이 부족할 수는 있다. 


    6.6.1 StorageClass 리소스를 통한 사용 가능한 스토리지 유형 정의하기

    PVC가 생성될 때 마다 PV는 프로비저닝 되어야 한다. 쿠버네티스가 이렇게 동작할 수 있도록, StorageClass를 만들어야 한다. 그리고 StorageClass에 Provisionor를 설정해야한다. 

    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    metadata:
      name: c6-14
    provisioner: driver.longhorn.io
    parameters: # 프로비저너에게 요청할 때, 아래 파라메터가 전달됨. 
      numberOfReplicas: "3"
      staleReplicaTimeout: "30"
      fromBackup: ""
      fsType: "ext4"
      dataLocality: "disabled"

    다음과 같이 동작한다

    1. PVC는 생성될 때, 어떤 StorageClass를 참조할지 선택함.
    2. 쿠버네티스는 PVC에게 기록된 StorageClass를 보고 Provision을 요청함.
    3. StorageClass는 Parameter에 있는 값을 Provisinor에 넘기면서 실제 스토리지 프로비저닝을 요청함.
    4. 생성된 스토리지를 PV와 바인딩 한다. 그리고 PV - PVC를 바인딩함.

    6.6.2 PVC에서 스토리지 클래스 요청하기

    앞서 이야기 한 것처럼 PVC는 매니페스트 파일에 어떤 StorageClass를 참조할지 선언할 수 있다. 쿠버네티스는 PVC에 선언된 StorageClass를 참조해서 프로비저너에게 요청하게 된다. 예를 들어 아래 매니페스트 파일에서는 'fast'라는 StorageClass에게 PV 생성 요청을 한다. 

    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: c6-15
    spec:
      storageClassName: fast
      resources:
        requests:
          storage: 100Mi
      accessModes:
        - ReadWriteOnce

     


    StorageClass 사용하는 방법 이해하기 

    클러스터 관리자는 서로 다른 특성을 가진 StorageClass를 여러 개 생성할 수 있다. 그리고 개발자는 StorageClass 중에 자신에게 가장 적합한 StorageClass를 사용하도록 한다. 예를 들어 아래를 고려해볼 수 있다. 

    • A StorageClass : Read/Write는 빠르지만 횟수가 제한됨. 
    • B StorageClass : Read/Wirte는 느리지만 반영구적으로 사용 가능함. 

    개발자는 이처럼 StorageClass의 특성을 살펴보고, 필요한 StorageClass에게 PV 프로비저닝을 요청해서 사용하도록 한다. 


    6.6.3 스토리지 클래스를 지정하지 않은 동적 프로비저닝

    이 곳에서는 스토리지 클래스를 지정하지 않았을 때, PVC가 어떤 형식으로 동적 프로비저닝을 하는지 살펴본다. 

     

    Default StorageClass

    Default StorageClass가 존재하는데, 기본적으로는 가장 먼저 생성된 StorageClass가 Default가 된다. 조회를 해보면 이름 옆에 (default)라고 명확하게 알려준다. Default StorageClass는 StorageClass의 매니페스트 파일에서 어노테이션에 storageclass.kubernetes.io/is-default-class: "true"를 가지고 있는 녀석이 된다. 

    $ kubectl get sc
    >>>
    NAME                 PROVISIONER          RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
    longhorn (default)   driver.longhorn.io   Delete          Immediate           true                   21d
    
    
    storageclass.kubernetes.io/is-default-class: "true"

    default StorageClass는 PVC에서 명시적으로 어떤 StorageClass를 사용하지 않았을 때, 자동으로 default StorageClass를 이용해 PV를 생성한다. 

     

    스토리지 클래스를 지정하지 않고 PVC 생성하기

    PVC에서 StorageClass를 지정하지 않고 PVC를 생성했다면, 이 때 PV 프로비저닝을 위해서 Default StorageClass를 사용한다. 

     

    스토리지 클래스를 ""로 지정하고 PVC 생성하기

    스토리지 클래스의 이름을 ""로 지정하고 PVC를 생성한다면, 동적 프로비저닝을 하지 않겠다는 의미다. 이것은 미리 만들어져 있는 PV와 PVC를 바인딩 시키겠다는 의미다.

    kind: PersistentVolumeClaim
    spec:
      storageClassName: ""   # 새로운 PVC를 동적 프로비저닝 하지 않음.

    PV 동적 프로비저닝의 전체 그림 이해하기

    파드와 Persistent Storage를 연결하는 최적의 방법은 PV + PVC를 이용하는 것이다. 개발자는 PVC를 이용해 동적 프로비저닝을 요청한다. 그러면 각 Provisionor는 Persistent Storage를 프로비저닝 해서 PVC - PV를 바인딩 시켜준다. 그리고 개발자는 파드에 PVC를 마운트 해서 사용하면 된다.

     

     

     

     

     

     

     

     

     

     

     

     

     

    요약

    여러 노드에서 사용할 수 있는 PV라도 다른 네임스페이스에 있는 파드들에서 같이 사용할 수는 없음.

    RWO / ROX / RWX의 단위는 노드다. RWX는 여러 노드에서 읽기/쓰기가 가능함을 의미함.

     

     

     

     

     

    댓글

    Designed by JB FACTORY