Kubernetes를 배워보자 10일차 - Volume, emptyDir, hostPath, Dynamic Provisioning, StorageClass

0

kubernetes

목록 보기
10/13

Volume

pod의 하나의 개별 host와 같다. CPU와 Memory를 할당받고 개개의 네트워크 인터페이스를 가지며, 하나의 독립적인 host machine처럼 동작한다. disk의 경우도 마찬가지이다. 각 pod들은 개별 filesystem을 가지기 때문에 pod가 재시작되면 filesystem이 다시 만들어진다. 이는 container를 동작할 때 filesystem이 구축되기 때문이다.

그런데, 여기서 발생하는 한 가지 문제점이 바로 disk관리이다. pod가 재시작되기 전에 운용중이던 data들이 pod의 filesystem에 저장되었다면 이는 disk에 저장된 것과 같다. 그러나, pod가 재시작되면 filesystem이 다시 구축되므로 이전 정보가 유지되지않고 사라진다. 이러한 문제는 docker container에서도 발생하여 volume이라는 개념을 도출하였다.

kubernetes에서도 마찬가로 volume이라는 리소스를 만들어 disk 데이터를 유지할 수 있다. volume은 pod와 같은 리소스는 아니지만, pod가 kubectl create로 만들어질 때 생성되며, kubectl delete로 삭제할 때 같이 사라진다. 때문에 pod가 재시작되어도 volume에 지정한 filesystem은 유지되며, 이전 data가 남아있는 것이다.

1. Volume에 대한 소개

Volume은 pod의 컴포넌트로, pod spec에 함께 정의된다. 따라서 volume은 kubernetes object가 아니며, pod와 따로 만들어지거나 삭제되지 않는다. 오직 pod안의 모든 container에 대해서만 voulum이 이용가능하며, volume에 접근하기 위해서는 마운트되어야 한다. volume 마운트는 filesystem 어디로 하든 상관없다.

다음의 예와 같이 하나의 pod안에 3개의 container가 있다고 하자. 첫번째 container는 /var/htdocs의 파일을 읽어 웹서버로 제공하고 /var/logs에 로그를 적는다. 두번째 container는 /var/html에 html파일을 생성하여 넣는다. 세번째 container는 /var/logs에 있는 log파일을 읽어 분석한다.

문제는 이 3개의 container들끼리 파일시스템을 공유하고 있지 않기 때문에, 이들이 읽고 쓰고하는 파일들이 서로 독립되어 있다는 것이다. 따라서 제대로 동작할 수가 없다.

이를 위해서 2개의 volume을 만들고 하나는 /var/htdocs, /var/html에 대한 volume을 담당하도록 하고, 하나는 /var/logs에 대한 volume을 담당하도록 하자. 이렇게 만들면 하나의 pod에서 여러 개의 container들이 filesystem을 공유하기 때문에 문제없이 프로그램이 구동될 수 있다.

즉, html 파일에 대한 volume은 publicHtml이 담당하도록 하고, log에 대한 volume은 logVol이 담당하도록 하는 것이다. 단, 조심해야할 것이 있는데, 위의 그림에서 보듯이 3번째 container는 logVol volume만 마운팅하고 있는 것을 알 수 있다. 따라서 3번째 container는 logVol volume이 마운팅하고 잇는 /var/log filesystem에만 접근이 가능하고 /var/html, /var/htdocs에는 접근이 불가능하다.

마찬가지로 두번째 container는 publicHtml volume만 마운팅해서 /var/html, /var/htdocs만 접근가능하지 /var/logs에는 접근이 불가능하다.

이는 이들이 모두 같은 pod에 있어도 각 container가 마운트한 volume에 따라서 공유할 수 있는 영역이 달라진다는 것을 알 수 있다.

volume은 pod에 붙어있는 것이기 때문에 라이프사이클이 pod와 동일하다. 따라서 pod가 존재할 동안에만 file이 존재하는 것이다. 그러나 volume type이 어떤 것이냐에 따라서 pod와 volume이 사라지더라도 file이 남아있는 경우가 있다. 이 경우에는 pod가 내려가도 데이터가 남는 경우이다.

2. Volume type

volume type은 굉장히 여러 가지가 있다. 이 중에 몇개는 general하게 쓰이지만 몇 가지는 특정 기술에 종속되어 사용된다. 이들을 모두 알 필요는 없고, 중요한 몇 가지만 계속 사용하게 된다.

  1. emptyDir: 간단한 빈 데이터로 일시적인 데이터를 저장하기 위해서 사용한다.
  2. hostPath: pod가 배치된 worker node의 filesystem에 디렉터리를 마운팅하기 위해 사용한다.
  3. gitRepo: git repository의 내용을 체크 아웃함으로서 volume을 시작한다.
  4. nfs: NFS가 pod에 마운트된다.
  5. gcePersistentDisk, awsElasticBlockStore, azureDist: 특정 Cloud공급업체에서 제공하는 storage를 마운팅한다.
  6. cinder, cephfs, iscsi, flocker, glusterfs, quobyte, rbd, flexVolume, vsphere: network storage의 다른 타입을 마운팅한다.
  7. configMap, secret, downwardAPI: 특별한 volum타입으로 해당 pod에 특정 kubernetes pod를 노출시킨다.
  8. persistentVolumeClaim: 정적 또는 동적 프로비저닝 persistent storage를 사용하기 위한 방법, 이에 대해서는 나중에 더 자세히 알아보도록 하자.

하나의 pod에는 여러 개의 volume을 가질 수 있고, 이 여러 개의 volume은 여러 개의 volume type을 가진다.

4. Volume을 사용하여 Container간의 data를 공유

가장 간단한 volume type은 emptyDir이다. emptyDir은 말 그대로 빈 디렉터리를 container에 제공하여 container가 해당 디렉터리에 파일을 쓰는 것이다. 단, volume의 생명주기는 pod에 연결되어있기 때문에 pod가 다운되면 emptyDir의 데이터도 사라진다.

예제를 하나 만들어보도록 하자. container 하나는 web server로 index.html 파일을 읽어서 화면에 보여주고, 두번째 container는 반복적으로 10초 마다 랜덤한 string을 얻어와서 index.html에 써주도록 하는 것이다.

즉, 두번째 container가 랜덤한 글을써서 index.html에 써주고 첫번째 container가 웹서버로 index.html을 보여주는 것이다.

이를 위해서는 첫번째와 두 번째 container의 index.html를 서로 공유해야한다는 것이다. 이를 위해서 volume을 만들어주어야 하는 것이다.

웹서버를 제공하는 첫 번째 container는 nginx이면 충분하다. 두 번째 container는 다음의 script를 실행하도록 한다.

#!/bin/bash
trap "exit" SIGINT
while :
do
  echo $(date) Writing fortune to /var/htdocs/index.html
  /usr/games/fortune > /var/htdocs/index.html
  sleep 10
done

/var/htdocsindex.html을 반복해서 쓰는 것이다.

이제 pod를 만들면 다음과 같다.

apiVersion: v1
kind: Pod
metadata:
  name: fortune
spec:
  containers:
  - image: luksa/fortune                   
    name: html-generator                   
    volumeMounts:                          
    - name: html                           
      mountPath: /var/htdocs               
  - image: nginx:alpine                    
    name: web-server                       
    volumeMounts:                          
    - name: html                           
      mountPath: /usr/share/nginx/html     
      readOnly: true                       
    ports:
    - containerPort: 80
      protocol: TCP
  volumes:                                 
  - name: html                             
    emptyDir: {}                           

html-generator가 10초마다 랜덤한 글을 /var/htdocs/index.html에 써주는 container이다. web-server/usr/share/nginx/html에서 index.html파일을 읽어 웹으로 제공해주는 container이다.

재밌는 것은 html-generatorweb-server가 공유해야할 filesystem path가 다르지만, volume이 html으로 같기 때문에 이들은 서로 다른 filesystem path에 같은 공간을 보고 있는 것이다.

|----------------|                                 |-volume(html)-|
| html-generator | --- /usr/share/nginx/html/ ---> |              |
|----------------|                                 |  index.html  |
                                                   |              |
|----------------|                                 |              |
|   web-server   | ---       /var/htdocs      ---> |--------------|
|----------------|

즉, single emptyDir volume인 html이 두 container에 다른 path로 마운팅된 것이다.

이제 pod를 만들고 web-server를 외부에 노출시켜 접속할 수 있도록 하자.

kubectl create -f empty-dir-pod.yaml
pod/fortune created

kubectl port-forward fortune 8080:80
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80

다음의 curl을 통해서 web-server에 접근할 수 있다.

curl localhost:8080
When I was younger, I could remember anything, whether it had happened
or not; but my faculties are decaying now and soon I shall be so I
cannot remember any but the things that never happened.  It is sad to
go to pieces like this but we all have to do it.
		-- Mark Twain

curl localhost:8080
Your reasoning is excellent -- it's only your basic assumptions that are wrong.

10초마다 새로운 명언들이 index.html에 쓰여지기 때문에 매번 다른 응답이 올 것이다.

emptyDir은 일시적으로 데이터를 pod가 설치된 worker node disk에 저장하기 때문에 worker node의 disk속도 영향을 받는다. 따라서 kubernetes에게 해당 volume을 tmpfs filesystem을 쓰도록 지시할 수 있는데, 가령 Memory에 쓰도록 해서 속도의 이점을 얻도록 할 수 있다. medium에 저장장치를 써주면 된다.

volumes:
  - name: html
    emptyDir:
      medium: Memory

한 가지 명심해야할 것은 emptyDir은 pod의 lifecycle을 따라가기 때문에 pod가 다운되거나 삭제되면 volume도 삭제되어 파일을 잃는다. 따라서 temporary하게 데이터를 서로 공유할 때만 사용하는 것이 좋다.

5. Accessing files on the worker node's filesystem

대부분의 pod들은 자신이 있는 host node의 존재를 알 지 못해야한다. 따라서 host node의 파일시스템에 접근하지 못해야하는데, 특별한 system-level pods(가령 damonSet)의 경우는 node의 host filesystem에 접근할 수 있어야 한다. 이 경우에 hostPath volume을 사용한다.

hostPath volume은 node의 특정한 파일 또는, 특정한 디렉터리에 접근할 수 있도록 한다. 같은 node에서 동작 중인 pod들은 같은 path에 접근이 가능하다. 가령 다음의 예를 보도록 하자.

node1에 있는 두 pod가 hostPath volume으로 /some/path/on/host를 사용한다면 pod1, pod2가 모두 /some/path/on/host에 접근할 수 있다.

hostPath volume은 host node에 데이터를 저장하므로 emptyDir과는 달리 pod가 삭제되어도, 재시작되어도 데이터가 삭제되지 않고 남아있다. 즉, persistent data를 만든다는 것이다.

만약 hostPath volume이 persistent data를 만든다고 database로 쓰려고 한다면 이는, 다시 생각해보아야 한다. 왜냐하면 hostPath volume은 node의 filesystem에 의존하기 때문에 pod가 reschedule되어서 다른 node에 할당되면 이전에 저장했던 데이터가 없기 때문이다. 때문에 일반적인 pod에는 hostPath volume을 사용하지 않는 것이다.

가장 대표적으로 사용되는 예가 DaemonSet이다. fluentd라는 app의 경우 각 node마다 하니씩 생성되는 DaemonSet으로 배포된다. 그리고서는 각 node의 hostPath volume에 접근해서 log파일을 읽고 정제 후 원하는 위치로 전달한다.

한 가지 예시를 보면 다음과 같다.

Name:           fluentd-cloud-logging-gke-kubia-default-pool-4ebc2f1e-9a3e
Namespace:      kube-system
...
Volumes:
  varlog:
    Type:       HostPath (bare host directory volume)
    Path:       /var/log
  varlibdockercontainers:
    Type:       HostPath (bare host directory volume)
    Path:       /var/lib/docker/containers

각 node의 /var/log, /var/lib/docker/containers에 접근하는 것을 볼 수 있다. 여기에 lo파일들이 위치해있기 때문이다. 이렇게 특별한 이유로 사용하는 app들 이외에는 hostPath volume을 사용할 이유가 없다.

data persistent를 위해서 aws나 gcp를 통해서 volume을 직접 설정할 수 있다.

apiVersion: v1
kind: Pod
metadata:
  name: mongodb
spec:
  volumes:
  - name: mongodb-data
    awsElasticBlockStore:
      volumeId: my-volume
      fsType: ext4
  containers:
  - ...

위의 예제는 volume을 통해 AWS EBS에 접근하는 방법을 알려준다.

그러나 문제는 aws나 gcp라는 cloud 공급자에게 의존하는 volume이 만들어진다는 것이다.

Pod(Volume) -> GCP,AWS DB

만약 database 공급자를 aws에서 gcp로 바꾸려고 한다면 pod내의 volume을 모두 변경해야한다. 또는 특정 서버의 database로 변경하려고해도 volume을 변경해야하는 일이 생긴다.

kubernetes에서는 이러한 문제를 해결하기위하여 database와 database를 사용하려는 volume을 분리하도록 하는데, 이 개념이 바로 pv와 pvc이다.

6. Persistent Storage in kuberntes

volume을 직접적으로 특정한 infrastructure에 의존한다면, kubernetes resource(pod, service etc)들이 특정 infra구조에 한정될 수 밖에 없는 문제가 생긴다. 이렇게 volume을 특정 infrastructure에 의존하지 않도록 하기위해서 kubernetes에서는 Persistent-VolumePersistentVolumeClaims가 존재한다.

PersistentVolume(pv)kube-apiserver로 제어받는 kubernetes resource이므로 kubectl로 create, update, delete가 가능하다.

PersistentVolume은 object로 storage를 나타내며 pod에 붙어 storage에 연결하도록 한다. 위에서 배운 emptyDir, hostPath volume의 경우는 pod에 붙어있는 volume이기 때문에 pod가 다운되면 volume도 다운되고 데이터를 잃는다. 반면 PersistentVolume은 pod와는 독립적인 kubernetes resource이기 때문에 pod가 다운되어도 volume이 남아있고 data도 남아있게 된다.

명심할 것은 PersistentVolumes는 boject로 etcd datastore의 entry이지, actual disk를 의미하는 것이 아니다.

간단하게 PersistentVolume type은 두 가지로 정리할 수 있다.
1. PersistentVolume backend 기술(ex, EBS volumes, Azure Disk, cephfs etc)
2. access mode로 ReadWriteOnce가 있다.

PersistentVolumeClaim은 pod안에서 하나의 volume처럼 사용되어, PersistentVolime으로 무엇을 사용하고 얼만큼의 사이즈를 사용할 지 결정하면 된다. 사용자는 PersistentVolumeClaim을 kubernetes API server에 제출하면 kubernetes에서 적절한 volume을 claim에 바인딩해준다. PersistentVolume과 claim이 바인딩되면 해당 volume에 연결된 것으로 사용할 수 있다.

7. PersistentVolume type에 대한 소개

PersistentVolume은 외부 storage에 대한 pointer일 뿐이다. 외부 storage가 EBS면 PersistentVolumeEBS에 맞춰서 만들어야 하고, NFS라면 이에 맞게 만들어야한다. 이 덕분에 pod에서 volume으로 storage를 지정하지 않아도 되어서, pod와 pv가 분리되어 있을 수 있는 것이다.

PersistentVolume은 다음의 2가지 장점을 가진다.

  1. PersistentVolume은 pod의 생명주기에 붙어있지 않기 때문에, 만약 PersistentVolume object가 붙어있는 pod가 다운되어도 volume은 살아남는다.
  2. PersistentVolume은 cluster 전반적으로 배포되기 때문에 모든 node에서 pod들이 접근할 수 있다.

PersistentVolume을 만들 때 access mode를 설정할 수 있는데, 다음의 특징이 있다.
1. ReadWriteOnce: 같은 시간에 오직 하나의 node만이 volume에 read/write를 하도록 한다.
2. ReadOnlyMany: 같은 시간에 여러 node에서 volume에 read만을 하도록 한다.
3. ReadWriteMany: 같은 시간에 여러 node에서 volume에 ready/write할 수 있도록 한다.

이 중 하나의 access mode를 PV에 설정해주어야 한다.

이제 PsersistentVolume을 만들어보도록 하자.

  • pv-hostpath.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-hostpath
spec:
  accessModes:
    - ReadWriteOnce
  capacity:
    storage: 1Gi
  persistentVolumeReclaimPolicy: Retain
  hostPath:
    path: "/mnt/data"

다음은 hostPath/mnt/data에 데이터를 저장하는 PV를 만든 것이다. 용량은 1Gi를 가지며 ReadWriteOnce이다. PV이지만 해당 node의 /mnt/data에 데이터를 적재하므로 hostDir volume과 비슷한 특성을 가진다. 다만 pod가 다운되어도 연결된 node가 바뀌지 않으므로 /mnt/data는 계속있게 된다. 그러나 해당 방법은 매우 좋지 않은 방법이고, nfs를 통해서 /mnt/data를 모든 노드가 공유하도록 만드는 방법이 있다.

persistentVolumeReclaimPolicy: Retain를 두어서 pv와 pvc의 연결이 끊어졌을 때 데이터를 삭제할 지, 유지할 지 등을 결정할 수 있다.

주의할 것은 pv는 namespace가 없다. 즉, namespace에 종속되고 관리되지 않는다.

위의 그림처럼 pv는 namespace에 종속되지 않지만 pvc는 namespace에 종속되는 것을 볼 수 있다.

8. PersistentVolumeClaim

PV를 만들어서 data를 저장하고, 가져올 공간을 마련했지만 이 공간을 사용하기 위해서는 claim을 만들어야 한다. 이 claim이 바로 PersistentVolumeClaim인데 PVC는 pod와 분리된 객체로, pod가 다시 스케줄링되어도 여전히 사용가능한 상태로 남아있게 된다. 즉, 라이프사이클을 공유하지 않는다.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-hostpath
spec:
  resources:
    requests:
      storage: 1Gi
  accessModes:
  - ReadWriteOnce
  storageClassName: ""

pvc에서도 storage를 정해주어야 하는데, pod에서 pv volume을 얼마나 사용할 것인지 결정하는 것이다. accessModeReadWriteOnce로 단일 객체만이 해당 pvc을 사용해서 read, write를 할 수 있다는 것이다. storageClassName""에 대해서는 추후에 알보도록 하자. 현재는 dynamic provisioning이라고만 알고 있도록 하자.

위의 두 pv와 pvc사이에는 직접적인 어떤 매칭키가 있지 않지만, 조건이 맞으므로 바인딩된다. 조건은 다음과 같다.
1. pv가 pvc의 storage 용량을 수용할 수 있다.
2. pv가 pvc에서 요구하는 accessModes를 지원한다.

selector를 이용하여 label로 pv와 pvc를 이어줄 수도 있고, storageClassName을 동일하게하여 매칭시킬 수 있다. 위의 예제에서는 default""을 사용하고 있어서 상관없이 바인딩된 것이다.

pv와 pvc는 기본적으로 one-on-one이다. 따라서, pv 하나당 pvc도 하나인데, 반대로 pvc에는 여러 Pod가 연결될 수 있다는 것을 명심하자.

pvc가 잘 바인딩되었는 지 확인해보도록 하자.

kubectl get pvc -A
NAMESPACE   NAME           STATUS   VOLUME        CAPACITY   ACCESS MODES   STORAGECLASS   AGE
default     pvc-hostpath   Bound    pv-hostpath   1Gi        RWO                           10s

성공적으로 바인딩되었다.

pv도 확인해보도록 하자.

kubectl get pv -A
NAME          CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                  STORAGECLASS   REASON   AGE
pv-hostpath   1Gi        RWO            Retain           Bound    default/pvc-hostpath                           2m48s

성공적으로 바인딩되었다. pv의 CLAIM부분을 보면 default/pvc-hostpath라고 써있는데 이는 pv에 바인딩된 pvc를 말한다. default는 pvc의 namespace이다. 앞서 언급했듯이 pv는 cluster scope이기 때문에 namespace가 없고 pvc는 pod와 같은 resource로 분류되기 때문에 namespace가 있다는 것을 유념하자.

9. pod에 바인딩하기

PV를 pod에 연결하기 위해서는 PVC를 pod의 volume에 연결해야한다. 이를 위해서 pod의 volume에 pvc이름을 적어주면 된다.

  • pod-with-pvc.yaml
apiVersion: v1
kind: Pod
metadata:
  name: pod-nginx
spec:
  containers:
  - image: nginx:latest
    name: nginx
    volumeMounts:
    - name: hostpath-data
      mountPath: /data/db
    ports:
    - containerPort: 27017
      protocol: TCP
  volumes:
  - name: hostpath-data
    persistentVolumeClaim:
      claimName: pvc-hostpath

간단히 설명해보면 containers안에 volumeMounts는 container가 pod에서 사용할 volume path를 말한다. 즉, containerpod/data/db에 접근할 수 있게되는 것이다. 다음으로 volumes는 pod와 pvc를 연결하는 구간으로 persistentVolumeClaim에 pvc이름을 적어주면 된다. 이렇게 되면 container에서는 /data/db로 데이터를 저장하면 pvc-hostpath에 연결된 pv에 데이터가 저장된다. 우리의 경우는 pv가 /mnt/data이므로 pod-nginx에서 저장한 /data/db/mnt/data로 저장된다.

다음의 pod를 만들어보도록 하자.

ubectl get po -A
NAMESPACE     NAME                                       READY   STATUS    RESTARTS       AGE
default       pod-nginx                                  1/1     Running   0              3m2s

다음으로 pod-nginx pod안에 들어가서 /data/db에 데이터를 만들어보도록 하자.

kubectl exec -it pod-nginx -- bash
root@pod-nginx:/# cd /data/db/
root@pod-nginx:/data/db# echo "hello world" >> temp.txt 

다음으로 host로 돌아와서 /mnt/data로 들어가면 우리가 만든 temp.txt가 있다.

root@ubuntu20:/mnt/data# cat temp.txt 
hello world

잘 연결된 것을 볼 수 있다.

이렇게 pvc와 pv를 사용하면 pod를 개발하는 개발자는 pv에 어떠한 volume을 사용하는 지 알 필요가 없다. 즉, coupling이 사라지게 된다. 이전에 pod에 특정 volume을 입히는 작업은 pod를 개발하는 개발자가 volume이 무엇인지 명시해야하고 알았어야만 했다. 가령, AWS, GCE, hostpath마다 다른 volume 정의를 만들어주야 하고, 매번 배포되는 os에 맞게 volume형상을 따로 적어주야 하는 것이다. 그러나 이렇게 pv, pvc를 분리하게 되면 개발자는 내부의 storage 기술이 무엇이 사용되고 있는 지 알 필요가 없다.

추가적으로 pod와 pvc는 어떠한 infrastructure를 사용할 지 명시하지 않기 때문에 어떠한 kubernetes cluster이던지 이식이 가능하고 인프라에 종속되지 않는다.

마지막으로 pv와 pvc는 pod에 종속되지 않아서, 나중에 해당 pod가 불필요해지면 pod만 삭제하면 되고 pv, pvc는 다른 pod에서 할당할 수 있게 재활용도 가능하다.

10. PV 재활용하기

재밌는 실험을 하나 해보도록 하자.

pod와 pvc를 없애보도록 하자.

ubectl delete -f ./pod-with-pvc.yaml 
pod "pod-nginx" deleted 
root@ubuntu20:/home/gyu/study_of_kubernetes/6_volume# kubectl delete -f ./pvc-hostpath.yaml 
persistentvolumeclaim "pvc-hostpath" deleteds

다음으로 pvc를 곧 바로 다시 만들어보도록 하자.

kubectl create -f ./pvc-hostpath.yaml 
persistentvolumeclaim/pvc-hostpath created

그 다음 pvc를 상태를 확인해보도록 하자. 우리의 예상대로는 pv가 이미있고 pvc가 생성되면 pvc-pv가 연결될 것이라 생각한다. 그러나 결과는 다음과 같다.

kubectl get pvc -A
NAMESPACE   NAME           STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
default     pvc-hostpath   Pending                                                     4s                         

status가 Pending이라는 것일 알 수 있다. 왜 일까??

pv의 상태를 확인해보도록 하자.

kubectl get pv -A
NAME          CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS     CLAIM                  STORAGECLASS   REASON   AGE
pv-hostpath   1Gi        RWO            Retain           Released   default/pvc-hostpath                           25m

pv-hostpath의 상태가 Released이다. pv와 pvc가 연결되기 위해서는 pv가 Available상태여야 한다. Released상태는 이전에 pv가 pvc와 연결되었었고, 그 정보를 policy에 맞게 정리해야한다는 말이다. 이 때문에 정리가 완료되기 전까지는 pv에 pvc가 연결되지 않는다.

우리는 pv에 persistentVolumeReclaimPolicy: RetainRetain으로 하였다. 이는 pv를 삭제하여도, 또는 Relased상태가 되어도 데이터를 남기겠다는 것이다. Retain의 경우에 PV를 reclaim하는 유일한 방법은 직접(manually) pv를 삭제하고 다시 만드는 수 밖에 없다. 따라서, 이전에 있던 데이터를 admin이 삭제하거나 남길 수 있는 것이다.

반먄 자동으로 reclaim할 수도 있는데, RecycleDelete reclaim policy를 사용하면 된다. 참고로 Recylce은 nfs나 hostpath와 같은 local filesystem에서만 사용가능하고 Delete는 aws, gce와 같은 infrastructure를 사용할 때만 가능하다.

  1. Recycle: pv와 pvc의 연결이 끊어지면, 내부의 데이터를 모두 삭제한다. 단, pv가 지정하고 있는 path를 삭제하진 않는다. 즉, /mnt/data라면 내부의 모든 파일을 삭제하지만 해당 파일시스템을 호스팅하고 있는 host를 삭제하진 않는다. 즉, 프로비저닝된 스토리지 자체는 삭제하지 안흔다. Released이후에 pvc를 만들면 바로 연결된다. 단, 이 Recycle은 현재 deprecated되었으며, dynamic provisioning 방법을 쓰도록 권장된다. 이는 아래에 나온다.
  2. Delete: pv와 pvc의 연결이 끊어지면, 내부의 데이터를 모두 삭제한다. 단, pv와 연결된 infrastructure까지 모두 삭제한다. 가령, pv에 aws의 ebs가 연결되어 있었다면, pvc연결이 끊어지면 Released가 되면 내부 데이터도 삭제하고 ebs도 삭제한다.

11. Dynamic provisioning of PersistentVolumes

pv, pvc를 사용하여 developer가 actual한 storage에 대해서 자세하게 알지 못해도 쉽게 application을 구성할 수 있도록 하였다. 그러나 pv, pvc를 사용한다해도 아직 cluster관리자가 actual한 storage를 셋업하고 준비해야하는 문제가 생긴다. 이러한 작업을 자동으로 관리하고 설정할 수 있도록 해주는 것이 바로 dynamic provisioning이다.

dynamic provisining은 cluster관리자가 특정 pv를 만드는 것이 아니라 PV provisioner와 하나 이상의 StorageClass 객체를 만들어 사용하면 된다. StorageClass는 어떤 타입의 PV를 사용할 것인지 선택하도록 하는 것으로 PVC에 StorageClass를 지정하면 provisioner가 PV를 프로비저닝할 때 작업을 취한다.

참고로, PV와 같이 StorageClass 역시도 namespace가 없다.

현재 대부분의 infrastructure(AWS, GCP, AZURE)에서는 provisioner를 제공해주고 있다. 다만, on-premises로 kubernetes를 배포하는 경우에는 provisioner가 없으니 만들어야한다.

dynamic provisioning을 사용하면 PV를 따로 만들 필요없이, StorageClass를 먼저 정의한 다음에 PVC를 통해서, provisioner가 PV를 동적으로 만들어내는 것이다.

그름으로 정리하면 다음과 같다.

우리는 local hostPath volume을 사용할 것이기 때문에, local provisioner를 설치해주도록 하자.

참고: https://github.com/rancher/local-path-provisioner

kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.25/deploy/local-path-storage.yaml

namespace/local-path-storage created
serviceaccount/local-path-provisioner-service-account created
role.rbac.authorization.k8s.io/local-path-provisioner-role created
clusterrole.rbac.authorization.k8s.io/local-path-provisioner-role created
rolebinding.rbac.authorization.k8s.io/local-path-provisioner-bind created
clusterrolebinding.rbac.authorization.k8s.io/local-path-provisioner-bind created
deployment.apps/local-path-provisioner created
storageclass.storage.k8s.io/local-path created
configmap/local-path-config created

잘 설치되었는 지 확인해보도록 하자.

kubectl get po -n local-path-storage local-path-provisioner-97d4f8b59-tcn7d 
NAME                                     READY   STATUS    RESTARTS   AGE
local-path-provisioner-97d4f8b59-tcn7d   1/1     Running   0          50s

잘 설치된 것을 확인할 수 있다. 현재 설치된 local-path-provisioner의 yaml파일을 살펴보면 다음과 같이 StorageClass가 정의된 것을 볼 수 있다.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-path
provisioner: rancher.io/local-path
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Delete

local-path라는 StorageClass가 정의된 것을 볼 수 있다. provisionerrancher.io/local-path가 지정되어있다. 이는 volume plugin이 PV를 만들기 위해서 사용할 provisioner를 지정하는 것이다.

volumeBindingMode는 pv와 pvc를 연결시키는 타이밍을 의미하는데, Immediate로 지정하면 바로 연결되고, 위와 같이 WaitForFirstConsumer로 지정하면 pvc를 사용하는 pod가 생길 때까지 pvc와 pv의 연결을 지연한다.

다음으로 pvc를 만들어보도록 하자.

  • pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: local-path-pvc
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: local-path
  resources:
    requests:
      storage: 128Mi

이전에 만들었던 pvc와 동일하지만 다른 점이 있다면 storageClassName이다. 우리가 위에서 정의한 StorageClasslocal-path를 지정하도록 하자. 이렇게 만들면 provisioner가 pvc를 보고 자동으로 pv를 만드는 것이다.

다음으로 pod를 만들어보자.

  • pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: volume-test
spec:
  containers:
  - name: volume-test
    image: nginx:stable-alpine
    imagePullPolicy: IfNotPresent
    volumeMounts:
    - name: volv
      mountPath: /data
    ports:
    - containerPort: 80
  volumes:
  - name: volv
    persistentVolumeClaim:
      claimName: local-path-pvc

pod부분은 정말 달라진게 없다. pvc로 local-path-pvc를 지정하고 /data를 마운팅하는 것이 전부이다.

즉, dynamic provisioning를 사용하는 방법을 정리하면 다음과 같다.
1. provisioner 배포 (aws, gcp 등의 경우는 기본적으로 제공된다.)
2. StorageClass를 정의한다.
3. PVC에서 정의한 StorageClass를 지정하고, 배포한 provisioner를 지정한다.
4. pod에서 PVC를 지정하고 pod와 pvc를 모두 배포한다.

그럼 이제 pvc와 pod를 배포해보도록 하자.

kubectl create -f ./pvc.yaml 
kubectl create -f ./pod.yaml 

잘 배포되었는 지 확인해보도록 하자.

kubectl get pod volume-test 
NAME          READY   STATUS    RESTARTS   AGE
volume-test   1/1     Running   0          6m26s

kubectl get pvc local-path-pvc 
NAME             STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
local-path-pvc   Bound    pvc-4a364750-64f1-4363-bad0-5c89d60cab5c   128Mi      RWO            local-path     7m16s

pvc가 바인딩된 것을 볼 수 있다. 그렇다면 pv가 만들어진 것이다. 한 번 확인해보도록 하자.

kubectl get pv pvc-4a364750-64f1-4363-bad0-5c89d60cab5c 
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                    STORAGECLASS   REASON   AGE
pvc-4a364750-64f1-4363-bad0-5c89d60cab5c   128Mi      RWO            Delete           Bound    default/local-path-pvc   local-path              7m16s

pvc와 연결된 pv를 찾을 수 있다. provisioner 덕분에 dynamic하게 pv를 프로비저닝할 수 있었다.

그렇다면 진짜 pv처럼 동작하는 지 한 번 데이터를 저장해보도록 하자. pod를 만들 때 mountPath: /data로 데이터를 마운팅한 것을 알 수 있다. 해당 Path로 PV와 연결된다는 것을 알 수 있다.

다음의 명령어로 local-path-test pod의 /data경로 test 파일을 저장하도록 하자.

kubectl exec volume-test -- sh -c "echo local-path-test > /data/test"

이제 volume-test pod를 삭제하도록 하고, 다시 생성했을 때 우리가 만든 파일이 존재하는 지 확인해보도록 하자. 존재한다면 local-path에 연결된 것이다.

kubectl delete -f ./pod.yaml 
pod "volume-test" deleted

ubectl create -f ./pod.yaml 
pod/volume-test created

다음의 명령어로 이전에 만들었던 /data경로의 test파일이 아직 존재하는 지 확인해보도록 하자.

kubectl exec volume-test -- sh -c "cat /data/test"

local-path-test

local-path-test이 나오면 성공이다. dynamic provisioning이 성공한 것을 볼 수 있다.

12 StorageClass가 지정되지 않은 PVC

cluster내에는 default stroageclass를 지정할 수 있다. 이를 설정하는 방법은 StorageClass의 annotation으로 storageclass.beta.kubernetes.io/is-default-class: "true" 추가하면 된다. 실제 GKE에서 사용하는 standard storage class의 정의를 보면 다음의 annotation이 존재한다.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  annotations:
    storageclass.beta.kubernetes.io/is-default-class: "true"
  creationTimestamp: 2017-05-16T15:24:11Z
  labels:
    addonmanager.kubernetes.io/mode: EnsureExists
    kubernetes.io/cluster-service: "true"
  name: standard
  resourceVersion: "180"
  selfLink: /apis/storage.k8s.io/v1/storageclassesstandard
  uid: b6498511-3a4b-11e7-ba2c-42010a840014
parameters:                                                 
  type: pd-standard                                              
provisioner: kubernetes.io/gce-pd  

default storageclass가 지정되면 PVC를 만들 때 storageClassName를 지정하지 않아도, 해당 StorageClass로 생성된다. 가령, GKE에서 다음과 같이 StorageClass를 지정하지 않으면 위의 default storageclass인 standard가 storage가 된다.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mongodb-pvc2
spec:                        
  resources:                 
    requests:                
      storage: 100Mi         
  accessModes:               
    - ReadWriteOnce          

위의 pvc를 배포하면 다음과 같이 pvc-pv가 연결된다.

kubectl get pvc mongodb-pvc2
NAME          STATUS   VOLUME         CAPACITY   ACCESSMODES   STORAGECLASS
mongodb-pvc2  Bound    pvc-95a5ec12   1Gi        RWO           standard

StorageClass를 지정하지 않아도 standard로 연결된 것을 알 수 있다.

그런데, 만약 default storageclass가 없다면 어떻게 될까? 그렇게 되면 StorageClass가 지정되지 않은 PV와 StorageaClass가 지정되지 않은 PVC끼리 연결된다. 가령, 위의 예제 중에서 local-pathstorageClassName으로 사용한 pvc.yaml을 다음과 같이 바꿔보도록 하자.

  • pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: local-path-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 128Mi

storageClassName을 삭제한 것을 볼 수 있다. 다음으로 이전에 만들었던 pv-hostpath.yaml파일을 배포하도록 하자.

  • pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-hostpath
  labels:
    type: hostpath
    env: prod
spec:
  accessModes:
    - ReadWriteOnce
  capacity:
    storage: 1Gi
  persistentVolumeReclaimPolicy: Recycle
  hostPath:
    path: "/mnt/data"

pv-hostpathlocal-path-pvc는 사실 아무런 연결고리가 없지만, 둘 다 StorageClass가 지정되지 않았고 default storageclass가 없기 때문에 문제없이 링킹될 수 있다.

실제로 pvc를 배포해보면 다음과 같이 pv-pvc가 연결된다.

kubectl get pv -A
NAME          CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                    STORAGECLASS   REASON   AGE
pv-hostpath   1Gi        RWO            Recycle          Bound    default/local-path-pvc                           25h

만약, default storageclass가 지정되어 있는 cluster이지만 pvc와 pv를 pre-provisioning하고 싶을 때가 있을 것이다. 이 때 마땅한 StorageClass가 없다고 빈 값으로 넣으면 안된다.

이유는 단순한데, default storageclass가 있으니 pvc의 Storageclass이름이 default storageclass가 되어버려서 자동으로 PV를 생성해버리기 때문이다. 즉, 이미 만들어놓은 PV에 pvc가 프로비저닝되지 않는 것이다.

이 현상을 해결하기 위해서 다음과 같이 pvc의 storageClassName을 설정하면 된다.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: local-path-pvc
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: ""
  resources:
    requests:
      storage: 128Mi

""storageClassName을 아예 안써놓고 비워두는 것과 완전히 다르다. """"이라는 storageClassName을 쓰겠다는 것이다. 따라서 default storageclass가 적용되지 않고 PV의 storageClassName""인 것만 pv-pvc연결이 되어 프로비저닝된다.

만약, default storageclass가 없는 cluster에서 미리 생성한 PV에 PVC를 연결하려면, 굳이 pv와 pvc를 storageClassName: ""으로 적어줄 필요가 없다. 어차피 default storageclass가 없어서 storageClassName이 없는 것과 storageClassName: ""쓴 것을 같은 것으로 치부한다.

단, 나중에 default storageclass가 추가되면 storageClassName 자체를 없애버린 pvc, pv에서는 동작이 달라질 수 있으니, pre-provisioning 할 때는 StorageClassName: ""으로 pv와 pvc를 지정하는 것이 좋다. 또는 명시적으로 pv를 지정하는 것이 좋다.

조금 복잡한데 정리하면 다음과 같다.

  1. default storageclass가 없으면, pv와 pvc의 StorageClassName이 빈 경우 둘 다 빈 storage class인 상태로 연결이 가능하다.
  2. default storageclass가 있다면, pv와 pvc의 StorageClassName이 빈 경우 default storageclass가 지정된다.
  3. default storageclass가 있다면 pv와 pvc를 연결하려는데 마땅한 StroageClassName이 없다면 ""으로 지정하도록 하자. 이렇게 하면 default storage가 적용되지 않아 dynamic provisioning을 막을 수 있다.

정리하자면 기본적으로 pv와 pvc 모두 StorageClassName: ""으로 적어주고, dynamic provisioning일 때만 특정 StorageClass를 지정하도록 하자. default storageclass에 의지하는 것은 조금 위험한 로직이기 때문이다.

0개의 댓글