Post

06. Volumes: Attaching Disk Storage to Containers

k8s-06.jpeg The complete picture of dynamic provisioning of PersistentVolumes. (출처: https://livebook.manning.com/book/kubernetes-in-action/chapter-6)

컨테이너가 재시작되면 기존 작업 내역이 모두 사라지게 될 수 있으므로, 컨테이너의 작업 내역을 저장하고 같은 pod 내의 다른 컨테이너가 함께 사용하는 저장 공간이다.

6.1 Introducing volumes


Pod 의 한 구성 부분으로 pod spec 에 정의되어 있다. 또 standalone Kubernetes object 가 아니라서 독립적으로 생성되거나 삭제될 수 없다.

Pod 내의 모든 컨테이너로부터 접근이 가능하지만, 그렇게 하려면 mount 해줘야 한다.

6.1.1 Volume 예시

6.1.2 Available volume types

종류가 굉장히 많은데, 다 알지는 않아도 괜찮다고 한다.

Volume types

  • emptyDir: 빈 폴더로 일시적인 데이터 저장에 사용
  • hostPath: 노드의 파일 시스템에 있는 디렉토리를 사용할 때
  • gitRepo: Git repository 를 checkout 하여 사용
  • nfs: NFS share
  • gcePersistentDisk, awsElasticBlockStore, azureDisk: 클라우드가 제공하는 스토리지
  • configMap, secret downwardAPI: 리소스나 클러스터 정보를 pod 에게 알려주기 위해 사용하는 volume
  • persistentVolumeClaim: pre- or dynamically provisioned persistent storage

6.2 Volume 을 이용한 컨테이너간 데이터 공유


6.2.1 emptyDir volume

emptyDir volume 을 사용하면 빈 디렉토리로 시작한다. Pod 안의 컨테이너는 해당 디렉토리에 자유롭게 쓸 (write) 수 있다. 대신 pod 이 지워지면 volume 도 함께 지워지므로, 썼던 내용은 모두 사라진다.

주로 컨테이너들 간에 파일을 공유할 때 유용한데, 한 컨테이너만 사용하는 경우에도 컨테이너가 일시적으로 디스크에 파일을 저장해야하는 경우에도 유용하게 사용할 수 있다. (sort operation on a large dataset that doesn’t fit into memory) 더불어 어떤 경우에는 컨테이너의 파일시스템을 사용할 수 없는 (not writable) 경우도 있다.

생성하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: v1
kind: Pod
metadata:
  name: fortune
spec:
  containers:
  - image: luksa/fortune
    name: html-generator
    volumeMounts:
    - name: html
      mountPath: /var/htdocs        # 컨테이너 내의 /var/htdocs 에 mount
  - image: nginx:alpine
    name: web-server
    volumeMounts:
    - name: html
      mountPath: /usr/share/nginx/html  # 컨테이너 내의 해당 디렉토리에 mount
      readOnly: true
    ports:
    - containerPort: 80
      protocol: TCP
  volumes:
  - name: html      # 이 volume 을 두 컨테이너가 공유한다
    emptyDir: {}

html-generator 에서 생성한 파일을 volume 에 쓰면 web-server 가 내용을 읽고 서빙해준다.

emptyDir 를 사용하면 실제 노드의 디스크에 volume 이 생성된다. 그래서 노드의 디스크 타입에 따라 성능이 달라질 수 있다. .volumes.emptyDirmedium 을 설정하게 되면 tmpfs 파일시스템 (메모리 위의 파일시스템) 을 사용할 수도 있다.

1
2
3
4
volumes:
- name: html
  emptyDir:
    medium: Memory

6.2.2 Git 레포를 사용하기

gitRepo volume 은 emptyDir volume 인데 시작할 때 git repository 의 내용이 채워진다. (컨테이너가 생성되기 전에)

Git repository 가 업데이트 되더라도, volume 은 업데이트 되지 않는다. 단 pod 이 재시작 되는 등 volume 이 다시 생성되게 되면 repo 의 변경사항이 반영된다. 단점이 있다면, repo 에 push 해서 변경사항이 생길 때마다 restart 해야한다.

생성하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: v1
kind: Pod
metadata:
  name: gitrepo-pod
spec:
  containers:
  - image: nginx:alpine
    name: web-server
    volumeMounts:
    - name: html
      mountPath: /usr/share/nginx/html
      readOnly: true
    ports:
    - containerPort: 80
      protocol: TCP
  volumes:
  - name: html
    gitRepo:
      repository: https://github.com/luksa/kubia-website-example.git  # 어떤 repo 를 사용할지
      revision: master      # master branch 사용
      directory: .          # volume 의 root 디렉토리에 clone

자동 업데이트

Sidecar container 를 사용해서 자동으로 repo 와 동기화시킬 수 있다.

Sidecar container: Container that augments the operation of the main container of the pod

Docker Hub 에서 git sync 를 검색하면 많은 이미지들이 나올 것이다. 해당 이미지를 YAML 에 추가해서 자동으로 sync 하도록 설정하고 pod 를 만들어주면 될 것이다.

Private repository

현재 private repository 는 clone 이 안 된다. gitRepo 옵션을 간단하게 유지하고 싶다고 한다.

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.

(어쩐지, 위 예시 코드 작동 안하더라.)

6.3 노드의 파일 시스템에 접근하기


보통 pod 들은 자신이 어떤 노드 위에서 돌아가고 있는지 알 필요가 없기 때문에 노드의 파일 시스템에 접근할 일이 없다. 하지만 시스템 레벨의 pod 들은 (DaemonSet 이라던가) 노드의 파일을 읽어야 하는 경우가 생길 수도 있다. 이런 경우에는 hostPath 볼륨을 사용한다.

6.3.1 hostPath volume

노드의 파일 시스템에 있는 한 파일이나 디렉토리를 매핑해준다. 같은 노드에 있는 pod 들이 같은 경로의 hostPath volume 을 사용하고 있다면 같은 파일을 보게 된다.

hostPath volume 은 앞서 살펴본 emptyDir, gitRepo 와는 다르게 pod 이 지워진다고 해서 삭제되지는 않는다. (컨테이너가 디렉토리에 뭔가 write 했다면 그것이 노드에 남아있는 듯 하다. 다른 pod 이 같은 노드로 스케쥴링 되고 같은 경로로 mount 하면 작업하고 남은 것들을 확인할 수 있는 듯)

그렇다고 해서 pod 들 간에 파일을 공유하는 목적으로 사용해서는 안 된다. 어느 노드에 스케쥴링 될지 알 수 없다.

6.3.2 System pods that use hostPath volumes

kube-system namespace 의 몇 pod 들에 대해 kubectl describe 를 해보면 hostPath volume 을 사용중인 것을 알 수 있다. 다른 pod 도 사용하고 있을 수도 있는데, 주로 자신의 데이터를 디렉토리에 쓰기보다는 노드의 정보를 가져와서 사용하기 위해 hostPath volume 을 사용한다.

Single-node 클러스터에서는 hostPath volume 을 persistent storage 로 사용할 수 있다. (Minikube) 아무튼 multi-node 클러스터에서는 여러 pod 들 간에 데이터를 공유하거나 persistent storage 로 사용해서는 안된다.

6.4 Persistent storage


Pod 들 간에 데이터를 공유해야 한다면, 위에서 언급한 volume type 들은 사용할 수 없다. 클러스터 내의 임의의 노드에서 접근이 가능해야하기 때문에 NAS (network attached storage) 급의 저장장치가 필요하다.

6.4.1 GCE Persistent Disk in a pod volume

생략.

GCE Persistent Disk 를 생성하고, 이를 mount 한 pod 에서 DB 에 뭔가 write 한 뒤, pod 를 삭제하고 재생성하여 find 해보면 존재한다는 내용이다.

6.4.2 Other types of volumes with underlying persistent storage

  • AWS: awsElasticBlockStore
  • Azure: azureFile, azureDisk

굉장히 다양한 종류의 volume type 을 지원하긴 하는데, 이걸 모두 알 필요는 당연히 없다. 또한 infrastructure 와 관련된 저장소 타입에 대한 정보를 개발자가 알아야할 이유도 없다. Kubernetes 는 이런 정보를 추상화시키는 것이 목적이다. 특정 저장소 정보를 YAML 파일에 기록하게 되면 해당 클러스터의 하드웨어 인프라와 지나치게 연결되어, 다른 클러스터에서 돌아가지 않게 된다. 위에서 제시한 persistent volume 들은 하나의 방법이긴 하지만 최선은 아니다.

6.5 스토리지 기술과 pod 를 분리하기


개발자는 물리적 저장소에 대한 정보를 몰라야 한다! 그것은 클러스터 관리자가 할 일이다.

6.5.1 PersistentVolumes and PersistentVolumeClaims

개발자는 그저 저장소를 달라고 요청을 하고, 요청을 받았을 때 어떤 저장소를 줄지는 클러스터 관리자가 설정한다.

이를 위해 PersistentVolume (PV) 과 PersistentVolumeClaim (PVC) 이라는 리소스가 생겨났다.

개발자가 직접 저장소를 pod 에 mount 하는 것이 아니라, 클러스터 관리자가 실제 저장소를 만들어두고 쿠버네티스에 PersistentVolume 이라는 리소스로 등록해 두는 것이다. (이때 저장소의 크기와 권한을 설정할 수 있다)

이제 클러스터의 사용자가 저장소가 필요하다면, PVC manifest 를 만들어서 최소 용량과 필요한 권한을 명시한다. 이 정보를 이용하여 쿠버네티스 API 에 요청하면 해당 정보에 맞는 PV 를 찾아 PVC 를 연결시켜준다. 이제 PVC 는 마치 pod 안에 mount 된 volume 처럼 사용할 수 있다. 그리고 PVC 가 지워지기 전까지 연결된 해당 PV 는 다른 PVC 가 사용할 수 없다.

6.5.2 PersistentVolume 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: PersistentVolume
metadata:
  name: mongodb-pv
spec:
  capacity:       # 크기
    storage: 1Gi
  accessModes:
    - ReadWriteOnce   # 하나의 PVC 만 읽거나 쓴다
    - ReadOnlyMany    # 여러 PVC 가 읽기만 한다
  persistentVolumeReclaimPolicy: Retain   # PVC 와 연결이 해제되면 지우거나 삭제하지 않고 유지한다
  hostPath:
    path: /tmp/mongodb

크기와, 접근 방식, 그리고 reclaim 될 때의 동작, 그리고 실제 물리적인 저장소 정보 (위 예시에서는 hostPath) 를 명시해줘야 한다.

참고로 PV 는 namespace 를 갖지 않는다. Cluster-level 리소스이다. 반면 PVC 는 namespace 를 갖는다.

6.5.3 PersistentVolumeClaim 을 생성하여 PersistentVolume 연결하기

Pod 에서 직접 접속해서는 안되고, (혹시 가능할까 ㅋㅋ) claim 을 먼저 해야한다. 그런데 claim 은 pod 생성과 독립적인 과정이다. Pod 에 종속적인 작업이 된다면 pod 이 re-스케쥴링 되거나 재시작될 때 또 claim 을 해야한다. 애초에 목적이 persistent volume 을 만드는 것이므로 pod 생성과는 독립적인 것이 자연스럽다. 재시작 되어도 저장소는 그대로 있어야지!

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mongodb-pvc
spec:
  resources:
    requests:
      storage: 1Gi    # 1GB 를 요청한다
  accessModes:        # 연결할 PV 는 아래 옵션으로 접근이 가능해야 한다
  - ReadWriteOnce
  storageClassName: ""  # 추후 dynamic provisioning 에서 설명

PVC 를 생성하면 적합한 PV 를 찾아 bind 해준다. PVC 에서 요청한 공간보다 PV 의 용량이 커야 하고, PVC 에서 요구한 accessModes 를 PV 또한 지원해야 한다.

Access Modes

  • RWO: ReadWriteOnce
  • ROX: ReadOnlyMany
  • RWX: ReadWriteMany

추가로, OnceMany 를 구분하는 기준은 노드 이다. Pod 의 개수가 아니다.

6.5.4 Pod 안에서 PersistentVolumeClaim 사용하기

PV 를 직접 연결하지 말고, PVC 를 reference 하도록 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1
kind: Pod
metadata:
  name: mongodb 
spec:
  containers:
  - image: mongo
    name: mongodb
    volumeMounts:
    - name: mongodb-data
      mountPath: /data/db
    ports:
    - containerPort: 27017
      protocol: TCP
  volumes:
  - name: mongodb-data
    persistentVolumeClaim:
      claimName: mongodb-pvc    # PVC 의 이름

6.5.5 PersistentVolume, PersistentVolumeClaim 의 장점

일단 개발자 입장에서는 개발자가 클러스터 환경에 대해 이해하고 있지 않아도 돼서 간편하다. 또한 PVC 를 만들기 위해 사용한 YAML 파일은 클러스터 환경과 관계없이 동작하게 되었다. 단지 PV 가 어떤 요구사항을 충족해야 하는지만 설명하면 된다.

(PV 는 여전히 클러스터 환경에 영향을 받는 것 같은데 이는 어쩔 수 없는 것인가…)

6.5.6 PersistentVolume 정리하고 재사용하기

Pod 과 PVC 를 지우고 kubectl get pv 를 해보면 Released 상태로 바뀐 것을 확인할 수 있다. 다시 PVC 를 생성하고 kubectl get pv 를 해보면 상태가 변하지 않았다. kubectl get pvc 를 해보면 Pending 상태인 것도 확인할 수 있다.

이는 PV 를 한 번 사용한 뒤 클러스터 관리자가 해당 PV 를 정리할 기회를 주기 위해서이다. PV 는 namespace 에 종속되지 않기 때문에 다른 namespace 의 PVC 에서도 bind 요청을 보낼 수 있다. 하지만 PV 를 정리하지 않고 그냥 연결해주게 되면 전에 연결되었던 다른 namespace 의 pod 에서 작업했던 내용이 그대로 남아있을 것이다. (다른 namespace 면 분리하고 싶은 것이니 작업 내용이 보이지 않아야 한다)

수동으로 reclaim 하기

위와 같은 현상은 persistentVolumeReclaimPolicy: Retain 으로 설정했기 때문에 일어났다. 수동으로 reclaim 하려면 PV 를 지우고 다시 생성하는 방법밖에 없다고 한다. PV 안에 있던 파일은 꺼내올 수 있다.

자동으로 reclaim 하기

다른 reclaim policy 로는 Recycle, Delete 가 있다. Recycle 은 지우고 새로 만들어서 다른 PVC 가 사용할 수 있도록 한다. Delete 는 그냥 지워버린다.

6.6 Dynamic provisioning of PersistentVolumes


위에서 살펴본 방식대로 PVC 를 사용하는 것도 편하지만, 여전히 클러스터 관리자가 PV 를 생성해야한다는 점에서 불편하다. 쿠버네티스는 PV 의 dynamic provisioning 을 제공하여 스토리지를 자동으로 만들어준다.

클러스터 관리자는 PV 를 만들지 않고 PV provisioner 를 만들어 두고, StorageClass object 를 여러 개 정의해둘 수 있다. 그러면 사용자는 어떤 타입의 PV 를 원하는지 설정할 수 있다.

StorageClass objects aren’t namespaced!

그러므로 관리자가 PV 를 여러 개 만들어 두지 않아도 되고, 한 두개 정도 StorageClass 를 정의해 두면 PVC 를 통해 요청이 들어올 때 쿠버네티스 시스템이 알아서 생성해준다. 이렇게 하면 PV 가 부족할 일은 없을 것이다. (하지만 저장 공간은 가득 찰 수도 있다)

6.6.1 StorageClass 리소스로 이용 가능한 스토리지 종류 정의하기

1
2
3
4
5
6
7
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast
provisioner: k8s.io/minikube-hostpath # volume plugin
parameters:
  type: pd-ssd    # provisioner 로 전달되는 parameter

PVC 가 StorageClass 에 요청을 할 때 어떤 provisioner 를 사용할지 설정할 수 있다. Provisioner plugin 마다 parameter 가 조금씩 다를 수 있다.

6.6.2 PersistentVolumeClaim 에서 StorageClass 요청하기

1
2
3
4
5
6
7
8
...
spec:
  storageClassName: fast    # StorageClass 이름
  resources:
    requests:
      storage: 100Mi
  accessModes:
  - ReadWriteOnce

StorageClass 를 명시할 수 있다. PVC 를 생성하면 StorageClass 가 PV 를 만들어 준다. 만약 존재하지 않는 StorageClass 를 입력하면 ProvisioningFailed 에러가 발생한다.

StorageClass 사용법 이해하기

클러스터 관리자가 다양한 StorageClass 를 만들어둘 수 있다. 개발자는 그 중 적절한 StorageClass 를 골라서 사용하면 된다.

PVC 를 설정할 때 StorageClass 의 이름으로 하기 때문에 클라우드 환경과 무관하게 해당 StorageClass 이름이 있다면 동작하게 된다. (portable!)

(아무튼 StorageClass 는 수동으로 만들어야 하는 것인가)

6.6.3 Dynamic provisioning without specifying a storage class

kubectl get sc 를 해보면 standard 라는 이름을 가진 StorageClass 가 있는 것을 확인할 수 있다. PVC 를 생성할 때 storageClassName 값을 주지 않고 생성하면 기본값인 standard 를 이용하게 된다.

그래서 위에서 storageClassName: "" 로 설정한 이유는 이렇게 empty string 을 넣어주지 않으면 standard StorageClass 를 이용하여 새로운 PV 를 만들기 때문이다. Empty string 을 넣어주면 bind 할 PV 를 이미 존재하는 PV 중에서 찾는다.

Complete picture of dynamic provisioning

정리하면, pod 에 persistent storage 를 붙이는 최고의 방법은 PVC (with or without StorageClass) 만 생성하는 것이다. 나머지는 dynamic provisioner 가 해결해준다.

  1. 클러스터 관리자가 PV provisioner 를 설정한다.
  2. 관리자가 StorageClass 를 만들어 둔다. 그리고 필요하다면 하나를 기본값으로 설정한다.
  3. 사용자는 (StorageClass 를 이용하여) PVC 을 생성한다.
  4. K8s 가 해당 StorageClass 의 provisioner 에게 새로운 PV 를 만들 것을 요청한다. 이 때 PVC 의 요구 사항 (용량, 접근 모드) 과 StorageClass 에 적은 parameter 가 함께 전달된다.
  5. Provisioner 가 실제 저장소를 만들고 PV 를 만들어 PVC 에 bind 한다.
  6. 사용자가 해당 PVC 를 이름으로 reference 하는 pod 를 생성한다.

Discussion & Additional Topics

What is NFS?

  • Network File System
  • https://en.wikipedia.org/wiki/Network_File_System

Why do PersistentVolum(Claim) access modes pertain to the number of worker nodes, not to the number of pods?

  • 왜 이렇게 했지?
  • (추정) 클라우드에서 volume 이라는 시스템 자체가 노드 단위로 컨트롤 되기 때문에?

올바른 PersistentVolume 의 사용법?

  • Reclaim policy 가 Retain, Recycle, Delete 뿐이면 재사용이 안된다는 것인데, 그렇다면 PVC 를 생성해서 작업을 하는 동안 계속 연결해야 하고 pod 들의 작업이 끝나야지만 지우라는 건가?
  • PVC 를 지워서 연결은 끊었지만 나중에 다시 필요하다면…? 애초에 그런 상황을 만들지 말고 PVC 를 계속 유지?
  • 그런 듯 하다.

hostPath use case

  • GPU 가 필요한 노드에서 DaemonSet 으로 노드에 Nvidia driver 를 설치하고 driver path 를 hostPath 로 잡는 경우가 있다.

Storage vs Disk vs Volume vs Filesystem

  • 스토리지: 추상화된 저장소
  • 디스크: 저장소의 물리적 구현체 (HDD, SSD)
  • 볼륨: 디스크의 한 구획 (파티션, C:, D;)
  • 파일시스템: 디스크에 파일을 저장하는 소프트웨어 (ext, NTFS etc.)
This post is licensed under CC BY 4.0 by the author.