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가 남아있는 것이다.
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가 내려가도 데이터가 남는 경우이다.
volume type은 굉장히 여러 가지가 있다. 이 중에 몇개는 general하게 쓰이지만 몇 가지는 특정 기술에 종속되어 사용된다. 이들을 모두 알 필요는 없고, 중요한 몇 가지만 계속 사용하게 된다.
emptyDir
: 간단한 빈 데이터로 일시적인 데이터를 저장하기 위해서 사용한다.hostPath
: pod가 배치된 worker node의 filesystem에 디렉터리를 마운팅하기 위해 사용한다.gitRepo
: git repository의 내용을 체크 아웃함으로서 volume을 시작한다.nfs
: NFS
가 pod에 마운트된다.gcePersistentDisk
, awsElasticBlockStore
, azureDist
: 특정 Cloud공급업체에서 제공하는 storage를 마운팅한다.cinder
, cephfs
, iscsi
, flocker
, glusterfs
, quobyte
, rbd
, flexVolume
, vsphere
: network storage의 다른 타입을 마운팅한다.configMap
, secret
, downwardAPI
: 특별한 volum타입으로 해당 pod에 특정 kubernetes pod를 노출시킨다.persistentVolumeClaim
: 정적 또는 동적 프로비저닝 persistent storage를 사용하기 위한 방법, 이에 대해서는 나중에 더 자세히 알아보도록 하자.하나의 pod에는 여러 개의 volume을 가질 수 있고, 이 여러 개의 volume은 여러 개의 volume type을 가진다.
가장 간단한 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/htdocs
에 index.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-generator
와 web-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하게 데이터를 서로 공유할 때만 사용하는 것이 좋다.
대부분의 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이다.
volume을 직접적으로 특정한 infrastructure에 의존한다면, kubernetes resource(pod, service etc)들이 특정 infra구조에 한정될 수 밖에 없는 문제가 생긴다. 이렇게 volume을 특정 infrastructure에 의존하지 않도록 하기위해서 kubernetes에서는 Persistent-Volume
과 PersistentVolumeClaims
가 존재한다.
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에 연결된 것으로 사용할 수 있다.
PersistentVolume
은 외부 storage에 대한 pointer일 뿐이다. 외부 storage가 EBS면 PersistentVolume
는 EBS
에 맞춰서 만들어야 하고, NFS
라면 이에 맞게 만들어야한다. 이 덕분에 pod에서 volume으로 storage를 지정하지 않아도 되어서, pod와 pv가 분리되어 있을 수 있는 것이다.
PersistentVolume
은 다음의 2가지 장점을 가진다.
PersistentVolume
은 pod의 생명주기에 붙어있지 않기 때문에, 만약 PersistentVolume
object가 붙어있는 pod가 다운되어도 volume은 살아남는다.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
을 만들어보도록 하자.
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에 종속되는 것을 볼 수 있다.
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을 얼마나 사용할 것인지 결정하는 것이다. accessMode
는 ReadWriteOnce
로 단일 객체만이 해당 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가 있다는 것을 유념하자.
PV를 pod에 연결하기 위해서는 PVC를 pod의 volume에 연결해야한다. 이를 위해서 pod의 volume에 pvc이름을 적어주면 된다.
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를 말한다. 즉, container
가 pod
의 /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에서 할당할 수 있게 재활용도 가능하다.
재밌는 실험을 하나 해보도록 하자.
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: Retain
를 Retain
으로 하였다. 이는 pv를 삭제하여도, 또는 Relased
상태가 되어도 데이터를 남기겠다는 것이다. Retain
의 경우에 PV를 reclaim하는 유일한 방법은 직접(manually) pv를 삭제하고 다시 만드는 수 밖에 없다. 따라서, 이전에 있던 데이터를 admin이 삭제하거나 남길 수 있는 것이다.
반먄 자동으로 reclaim할 수도 있는데, Recycle
과 Delete
reclaim policy를 사용하면 된다. 참고로 Recylce
은 nfs나 hostpath와 같은 local filesystem에서만 사용가능하고 Delete
는 aws, gce와 같은 infrastructure를 사용할 때만 가능하다.
/mnt/data
라면 내부의 모든 파일을 삭제하지만 해당 파일시스템을 호스팅하고 있는 host를 삭제하진 않는다. 즉, 프로비저닝된 스토리지 자체는 삭제하지 안흔다. Released
이후에 pvc를 만들면 바로 연결된다. 단, 이 Recycle
은 현재 deprecated되었으며, dynamic provisioning 방법을 쓰도록 권장된다. 이는 아래에 나온다. Released
가 되면 내부 데이터도 삭제하고 ebs도 삭제한다. 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
가 정의된 것을 볼 수 있다. provisioner
로 rancher.io/local-path
가 지정되어있다. 이는 volume plugin이 PV를 만들기 위해서 사용할 provisioner
를 지정하는 것이다.
volumeBindingMode
는 pv와 pvc를 연결시키는 타이밍을 의미하는데, Immediate
로 지정하면 바로 연결되고, 위와 같이 WaitForFirstConsumer
로 지정하면 pvc를 사용하는 pod가 생길 때까지 pvc와 pv의 연결을 지연한다.
다음으로 pvc를 만들어보도록 하자.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: local-path-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 128Mi
이전에 만들었던 pvc와 동일하지만 다른 점이 있다면 storageClassName
이다. 우리가 위에서 정의한 StorageClass
인 local-path
를 지정하도록 하자. 이렇게 만들면 provisioner가 pvc를 보고 자동으로 pv를 만드는 것이다.
다음으로 pod를 만들어보자.
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이 성공한 것을 볼 수 있다.
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-path
를 storageClassName
으로 사용한 pvc.yaml
을 다음과 같이 바꿔보도록 하자.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: local-path-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 128Mi
storageClassName
을 삭제한 것을 볼 수 있다. 다음으로 이전에 만들었던 pv-hostpath.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-hostpath
와 local-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를 지정하는 것이 좋다.
조금 복잡한데 정리하면 다음과 같다.
StorageClassName
이 빈 경우 둘 다 빈 storage class인 상태로 연결이 가능하다.StorageClassName
이 빈 경우 default storageclass가 지정된다. ""
으로 지정하도록 하자. 이렇게 하면 default storage가 적용되지 않아 dynamic provisioning을 막을 수 있다.정리하자면 기본적으로 pv와 pvc 모두 StorageClassName: ""
으로 적어주고, dynamic provisioning일 때만 특정 StorageClass를 지정하도록 하자. default storageclass에 의지하는 것은 조금 위험한 로직이기 때문이다.