들어가기전에
앞서 블로깅에서 사용해왔던 디프롤이먼트는 모두 상태가 없는(stateless) 애플리케이션이었습니다. 즉, 디플로이먼트의 각 포드는 별도의 데이터를 가지고 있지 않았으며, 단순히 요청에 대한 응답만을 반환했습니다. 하지만 데이터베이스처럼 포드 내부에서 특정 데이터를 보유해야 하는, 상태가 있는(stateful) 애플리케이션의 경우에는 데이터를 어떻게 관리할지 고민해야 합니다.
도커의 볼륨처럼 쿠버네티스도 자체에서 호스트에 위치한 디렉터리를 각 포드와 공유함으로써 데이터를 보존하는 것이 가능합니다. 그렇지만 여러 개의 서버로 구성된 쿠버네티스와 같은 클러스터 환경에서는 이 방법이 적합하지 않을 수 있습니다. 쿠버네티스는 워커 노드 중 하나를 선택해 포드를 할당하는데, 특정 노드에서만 데이터를 보관해 저장하면 포드가 다른 노드로 옮겨갔을 때 해당 데이터를 사용할 수 없게 됩니다. 따라서 특정 노드에서만 포드를 실행해야 하는 상황이 생길 수도 있습니다.
이를 해결할 수 있는 일반적인 방법은 어느 노드에서도 접근해 사용할 수 있는 퍼시스턴트 볼륨(Persistent Volume)을 사용하는 것입니다. 퍼시스턴트 볼륨은 워커 노드들이 네트워크상에서 스토리지를 마운트해 영속적으로 데이터를 저장할 수 있는 볼륨을 의미합니다. 따라서 포드에 장애가 생겨 다른 노드로 옮겨가더라도 해당 노드에서 퍼시스턴트 볼륨에 네트워크로 연결해 데이터를 계속해서 사용할 수 있습니다. 네트워크로 연결해 사용할 수 있는 퍼시스턴트 볼륨의 대표적인 예로는 NFS, AWS의 EBS(Elastic Block Store), Ceph, GlusterFS 등이 있습니다.
이번 블로깅에서는 상태를 가지는 포드의 데이터를 보존하기 위한 쿠버네티스의 오브젝트인 퍼시스턴트 볼륨(Persistent Volume : PV), 퍼시스턴트 볼륨 클레임(Persistent Volume Claim: PVC)에 대해서 공부해봅시다.
로컬 볼륨 : hostPath, emptyDir
hostPath는 호스트와 볼륨을 공유하기 위해서 사용하고, emptyDir은 포드의 컨테이너 간에 볼륨을 공유하기 위해서 사용합니다.
워커 노드의 로컬 디렉터리를 볼륨으로 사용 : hostPath
포드의 데이터를 보존할 수 있는 가장 간단한 방법은 호스트의 디렉터리를 포드와 공유해 데이터를 저장하는 것입니다. 호스트와 디렉터리를 공유하는 포드를 생성하기 위해 아래의 내용으로 yaml 파일을 작성해 보겠습니다.
# vi hostpath-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: hostpath-pod
spec:
containers:
- name: my-container
image: busybox
args: [ "tail", "-f", "/dev/null" ]
volumeMounts:
- name: my-hostpath-volume
mountPath: /etc/data
volumes:
- name: my-hostpath-volume
hostPath:
path: /tmp
volumes 항목에 볼륨을 정의한 뒤, 이를 포드를 정의하는 containers 항목에서 참조해 사용합니다. 위의 yaml 파일에서는 볼륨에서 hostPath 항목을 정의함으로써 호스트의 /tmp를 포드의 /etc/data에 마운트했습니다.
포드를 생성한 뒤 포드의 컨테이너 내부로 들어가 /etc/data 디렉터리에 파일을 생성하면 호스트의 /tmp 디렉터리에 파일이 저장됩니다. 포드 컨테이너의 /etc/data와 호스트의 /tmp는 동일한 디렉터리로써 사용되는 것입니다.
$ kubectl apply -f hostpath-pod.yaml
pod/hostpath-pod created
실습을 해봤지만 잘 안됐음..
하지만 이런 방식의 데이터 보존은 바람직하지 않습니다. 디플로이먼트의 포드에 장애가 생겨 다른 노드로 포드가 옮겨갔을 경우, 이전 노드에 저장된 데이터를 사용할 수 없기 때문입니다. hostPath 방식의 볼륨을 반드시 사용해야 한다면 스케줄링을 이용해 특정 노드에만 포드를 배치하는 방법도 생각해 볼 수 있지만, 이 방법 또한 호스트 서버에 장애가 생기면 데이터를 잃게 된다는 단점이 있습니다.
그러나 hostPath 볼륨은 모든 노드에 배치해야 하는 특수한 포드의 경우에 유용하게 사용할 수 있습니다. 모니터링 툴인 CAdvisor는 호스트의 디렉터리와 도커 소켓 등을 컨테이너 내부로 공유해 모니터링 데이터를 수집했습니다. 이 모니터링 툴을 쿠버네티스의 모든 워커 노드에 배포해야 한다면 hostPath를 사용하는 것이 좋지만, 이런 경우를 제외하면 hostPath를 사용하는 것은 보안 및 활용성 측면에서 그다지 바람직하지 않습니다.
포드 내의 컨테이너 간 임시 데이터 공유 : emptyDir
emptyDir 볼륨은 포드의 데이터를 영속적으로 보존하기 위해 외부 볼륨을 사용하는 것이 아닌, 포드가 실행되는 도중에만 필요한 휘발성 데이터를 각 컨테이너가 함께 사용할 수 있도록 임시 저장 공간을 생성합니다. emptyDir 이라는 이름이 의미하는 것처럼 emptyDir 디렉터리는 비어있는 상태로 생성되면 포드가 삭제되면 emptyDir에 저장돼 있던 데이터도 함께 삭제됩니다.
아파치 웹 서버를 실행하는 포드인데 아파치 웹 서버의 루트 디렉터리를 emptyDir에 마운트함과 동시에 이 디렉터리를 content-creator 컨테이너의 /data 디렉터리와 공유하는 yaml 파일을 작성해봅시다.
# vi emptydir-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: emptydir-pod
spec:
containers:
- name: content-creator
image: alicek106/alpine-wget:latest
args: ["tail", "-f", "/dev/null"]
volumeMounts:
- name: my-emptydir-volume
mountPath: /data
- name: apache-webserver
image: httpd:2
volumeMounts:
- name: my-emptydir-volume
mountPath: /usr/local/apache2/htdocs/
volumes:
- name: my-emptydir-volume
emptyDir: {}
emptyDir은 한 컨테이너가 파일을 관리하고 한 컨테이너가 그 파일을 사용하는 경우에 유용하게 사용할 수 있습니다. content-creator 컨테이너에 내부로 들어가 /data 디렉터리에 웹 컨텐츠를 생성하면 아파치 웹 서버 컨테이너의 htdocs 디렉터리에도 동일하게 웹 컨텐츠 파일이 생성될 것이고, 웹 서버에 의해 외부에 제공될 것입니다.
$ kubectl apply -f emptydir-pod.yaml
pod/emptydir-pod created
$ kubectl exec -it emptydir-pod -c content-creator sh
/ # echo Hello, Kubernetes! >> /data/test.html
/ # exit
$ kubectl describe pod emptydir-pod | grep IP
IP 확인
$ kubectl run -i --tty --rm debug \
--image=alicek106/ubuntu:curl --restart=Never -- curl {ip주소}/test.html
Hello, Kubernetes!
네트워크 볼륨
쿠버네티스에서는 별도의 플러그인을 설치하지 않아도 다양한 종류의 네트워크 볼륨을 포드에 마운트할 수 있습니다. 온프레미스 환경에서도 구축할 수 있는 NFS, iSCSI, GlusterFS, Ceph와 같은 네트워크 볼륨뿐만 아니라 AWS의 EBS, GCP의 gcePersistentDisk와 같은 클라우드 플랫폼을 포드에 마운트할 수도 있습니다.
네트워크 볼륨의 위치는 특별히 정해진 것이 없으며, 네트워크로 접근할 수만 있으면 쿠버네티스 클러스터 내부, 외부 어느 곳에 존재해도 크게 상관 없습니다. 단, AWS의 EBS와 같은 클라우드에 종속적인 볼륨을 사용하려면 AWS의 EBS와 같은 클라우드에 종속적인 볼륨을 사용하려면 AWS에서 쿠버네티스 클러스터를 생성할 때 특정 클라우드를 위한 옵션이 별도로 설정돼 있어야 합니다.
네트워크 볼륨의 종류는 매우 많기 때문에 어느 것을 선택해 사용해야 할지 고민이 될 수 있습니다. 네트워크 볼륨의 선택 기준은 일반적으로 데이터의 읽기 및 쓰기 속도, 마운트 방식(1:1 또는 1:N), 네트워크 볼륨 솔루션 구축 비용 등을 고려할 수 있습니다. 정해진 답은 없기 때문에 성능을 벤치마크 해 본 뒤 가장 적합한 솔루션을 사용하는 것이 좋습니다.
NFS를 네트워크 볼륨으로 사용하기
NFS(Network File System)는 대부분의 운영 체제에서 사용할 수 있는 네트워크 스토리지로, 여러 개의 클라이언트가 동시에 마운트해 사용할 수 있다는 특징이 있습니다. NFS는 여러 개의 스토리지를 클러스터링하는 다른 솔루션에 비해 안정성이 떨어질 수는 있으나, 하나의 서버만으로 간편하게 사용할 수 있으며, NFS를 마치 로컬 스토리지처럼 사용할 수 있다는 장점이 있습니다.
NFS를 사용하려면 NFS 서버와 NFS 클라이언트가 각각 필요합니다. NFS 서버는 영속적인 데이터가 실제로 저장되는 네트워크 스토리지 서버이고, NFS 클라이언트는 NFS 서버에 마운트해 스토리지에 파일을 읽고 쓰는 역할입니다. NFS 클라이언트는 워커 노드의 기능을 사용할 것이므로 NFS 서버만 별도로 구축하면 됩니다.
NFS의 기능을 간단히 사용해보기 위해 쿠버네티스 클러스터 내부에서 임시 NFS 서버를 하나 생성해 보겠습니다.
# vi nfs-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nfs-server
spec:
selector:
matchLabels:
role: nfs-server
template:
metadata:
labels:
role: nfs-server
spec:
containers:
- name: nfs-server
image: gcr.io/google_containers/volume-nfs:0.8
ports:
- name: nfs
containerPort: 2049
- name: mountd
containerPort: 20048
- name: rpcbind
containerPort: 111
securityContext:
privileged: true
# vi nfs-service.yaml
apiVersion: v1
kind: Service
metadata:
name: nfs-service
spec:
ports:
- name: nfs
port: 2049
- name: mountd
port: 20048
- name: rpcbind
port: 111
selector:
role: nfs-server
$ kubectl apply -f nfs-deploy.yaml
$ kubectl apply -f nfs-service.yaml
NFS 서버를 위한 디플로이먼트와 서비스를 생성했다면 다음으로 해당 NFS 서버의 볼륨을 포드에서 마운트해 데이터를 영속적으로 저장할 수 있습니다. NFS 서버를 컨테이너에 마운트하는 포드를 새롭게 생성해보겠습니다.
# vi nfs-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: nfs-pod
spec:
containers:
- name: nfs-mount-container
image: busybox
args: [ "tail", "-f", "/dev/null" ]
volumeMounts:
- name: nfs-volume
mountPath: /mnt # 포드 컨테이너 내부의 /mnt 디렉터리에 마운트
volumes:
- name: nfs-volume
nfs:
path: /
server: {NFS_SERVICE_IP}
mountPath를 /mnt로 설정했기 때문에 NFS 서버의 네트워크 볼륨은 포드 컨테이너의 /mnt 디렉터리에 마운트될 것입니다. 즉, 컨테이너 내부에서 /mnt 디렉터리에 파일을 저장하면 실제로는 NFS 서버에 데이터가 저장됩니다. 또한, volumes 항목에서 nfs라는 항목을 정의함으로써 NFS 서버의 볼륨을 사용한다고 명시했습니다.
위의 yaml 파일에서 주의해야 할 점은 server 항목이 nfs-service라는 서비스의 DNS 이름이 아닌 {NFS_SERVICE_IP}로 설정돼 있다는 것입니다. NFS 볼륨의 마운트는 컨테이너 내부가 아닌 워커 노드에서 발생하므로 서비스의 DNS 이름으로 NFS 서버에 접근할 수 없습니다. 노드에서는 포드의 IP로 통신은 할 수 있지만, 쿠버네티스의 DNS를 사용하도록 설정돼 있지는 않기 때문입니다. 따라서 NFS 서비스의 CLuster IP를 직접 얻은 뒤, yaml 파일에 사용하는 방식으로 포드를 생성해보겠습니다.
# NFS 서버에 접근하기 위한 서비스의 Cluster IP를 얻음
$ export NFS_CLUSTER_IP=$(kubectl get svc/nfs-service -o jsonpath='{.spec.clusterIP}')
$ echo $NFS_CLUSTER_IP
# nfs-pod의 server 항목을 NFS_CLUSTER_IP로 교체해 생성합니다.
$ cat nfs-pod.yaml | sed "s/{NFS_SERVICE_IP}/$NFS_CLUSTER_IP/g" | kubectl apply -f -
참고로 kubectl get 명령어의 -o jsonpath는 리소스의 특정 정보만 가져올 수 있는 편리한 옵션입니다. 예를 들어 nfs-service 리소스의 내용을 json으로 출력해 보면,
$ kubectl get svc nfs-service -o json
$ kubectl get svc nfs-service -o jsonpath='{.spec.clusterIP}'
-o jsonpath='{.spec.clusterIP}' 옵션은 해당 리소스의 JSON 내용 중에서 spec.clusterIP 항목만을 출력합니다. jq와 같은 파싱 도구와 함께 사용하면 더욱 편하게 사용할 수 있습니다.
PV, PVC를 이용한 볼륨 관리
퍼시스턴트 볼륨과 퍼시스턴트 볼륨 클레임을 사용하는 이유
쿠버네티스에서 지원하는 대부분의 볼륨 타입은 포드나 디플로이먼트의 yaml 파일에서 직접 정의해 사용할 수 있습니다. 전에 실습해봤던 NFS는 포드의 yaml 파일에 nfs라는 항목을 정의했고, NFS 서버의 엔드포인트도 함께 명시했습니다.
그런데 실제로 애플리케이션을 개발한 뒤 yaml 파일로 배포할 때는 이러한 방식이 바람직하지 않을 수 있습니다.
예를 들어 MySQL 디플로이먼트를 배포하기 위한 yaml 파일을 작성한다고 가정해보겠습니다. MySQL은 상태를 가지는 애플리케이션이기 때문에 반드시 영속적 스토리지를 마운트해 데이터를 보관해야 합니다. 따라서 쿠버네티스에서 사용할 수 있는 여러 네트워크 볼륨 종류 중 NFS를 사용하기로 했고, MySQL의 디플로이먼트를 정의하는 yaml 파일에 nfs 항목과 함께 NFS 서버의 정보를 기입했습니다.
부서 내에서만 MySQL 디플로이먼트를 사용할 것이라면 문제 없겠지만, 이 yaml 파일을 다른 개발 부서에 배포하거나 웹상에 공개해야 한다면 문제가 될 수 있습니다. MySQL 디플로이먼트의 yaml 파일에 네트워크 볼륨으로서 NFS를 고정적으로 명시해뒀기 때문에 해당 yaml 파일로 MySQL을 생성하려면 반드시 NFS를 사용해야 합니다. 게다가 MySQL의 데이터를 보관하기 위해 iSCSI나 GlusterFS를 사용하고 싶다면 해당 네트워크 볼륨 타입을 명시하는 별도의 yaml 파일을 여러 개 만들어 배포해야 합니다. 즉, 볼륨과 애플리케이션의 정의가 서로 밀접하게 연관돼 있어 서로 분리할 수 없는 상황이 되어 버립니다.
이런 불편함을 해결하기 위해 쿠버네티스에서는 퍼시스턴트 볼륨(Persistent Volume : PV)과 퍼시스턴트 볼륨 클레임(Persistent Volume Claim : PVC)이라는 오브젝트를 제공합니다. 이 두 개의 오브젝트는 포드가 볼륨의 세부적인 사항을 몰라도 볼륨을 사용할 수 있도록 추상화해주는 역할을 담당합니다. 즉, 포드를 생성하는 yaml 파일 입장에서는 네트워크 볼륨이 NFS인지, AWS의 EBS인지 상관없이 볼륨을 사용할 수 있도록 하는 것이 핵심입니다. PV, PVC를 사용하는 흐름은 아래와 같습니다.
쿠버네티스 클러스터를 관리하는 인프라 관리자와 애플리케이션을 배포하려는 사용자가 나뉘어 있다고 가정해봅시다. 인프라 관리자는 NFS, Ceph와 같은 스토리지에 접근해 사용할 수 있으며, 이를 쿠버네티스로 가져오는 역할을 담당합니다. 이때, 사용자가 디플로이먼트의 포드에 볼륨을 마운트해 사용하려면 아래의 과정을 거쳐야 합니다.
- 인프라 관리자는 네트워크 볼륨의 정보를 이용해 퍼시스턴트 볼륨 리소스를 미리 생성해 돕니다. 네트워크 볼륨의 정보에는 NFS나 iSCSI와 같은 스토리지 서버에 마운트하기 위한 엔드포인트가 포함될 수 있습니다.
- 사용자는 포드를 정의하는 yaml 파일에 '이 포드는 데이터를 영속적으로 저장해야 하므로 마운트할 수 있는 외부 볼륨이 필요하다'라는 의미의 퍼시스턴트 볼륨 클레임을 명시하고, 해당 퍼시스턴트 볼륨 클레임을 생성합니다.
- 쿠버네티스는 기존에 인프라 관리자가 생성해 뒀던 퍼시스턴트 볼륨의 속성과 사용자가 요청한 퍼시스턴트 볼륨 클레임의 요구 사항이 일치한다면 두 개의 리소스를 매칭 시켜 바인드합니다. 포드가 이 퍼시스턴트 볼륨 클레임을 사용함으로써 포드의 컨테이너 내부에 볼륨이 마운트 된 상태로 생성됩니다.
여기서 중요한 부분은 사용자가 디플로이먼트의 yaml 파일에 볼륨의 상세한 스펙을 정의하지 않아도 된다는 것입니다. 이러한 방식을 사용하면 애플리케이션을 배포하는 yaml 파일을 좀 더 보편적인 방식으로 작성할 수 있습니다.
위의 그림은 퍼시스턴트 볼륨을 사용할 때와 그렇지 않을 때의 yaml 파일입니다. 디플로이먼트를 생성하는 yaml 파일에 nfs 항목을 정의하는 대신, persistentVolumeClaim 항목을 사용해 볼륨의 사용 여부만 나타내면 볼륨의 종류에 상관없이 쿠버네티스는 이를 매칭시켜 마운트합니다.
퍼시스턴트 볼륨과 퍼시스턴트 볼륨 클레임 사용하기
persistentvolume, persistentvolumeclaim을 사용할수도 있지만, 너무 길어 pv, pvc를 많이 사용합니다.
$ kubectl get pv
$ kubectl get pvc
AWS에서 제공하는 클라우드 플랫폼의 볼륨(AWS EBS)을 사용해보겠습니다. AWS에서 kops로 쿠버네티스를 설치했다면 퍼시스턴트 볼륨을 EBS와 연동해 사용할 수 있습니다. 설치 참고
AWS에서 EBS를 퍼시스턴트 볼륨으로 사용하기
aws cli를 사용하여 EBS 볼륨을 만듭니다.
$ sudo apt install jq -y
# aws configure시에 json 형식의 포맷으로 설정되있어야됨.
$ export VOLUME_ID=$(aws ec2 create-volume --size 5 \
--region ap-northeast-2 \
--availability-zone ap-northeast-2a \
--volume-type gp2 \
--tag-specifications \
'ResourceType=volume,Tags=[{Key=KubernetesCluster,Value=ihp001.k8s.local}]' \
| jq '.VolumeId' -r)
$ echo $VOLUME_ID
생성한 EBS 볼륨을 통해 쿠버네티스의 퍼티스턴트 볼륨을 생성해 보겠습니다. 아래의 yaml 파일을 만듭니다.
# vi ebs-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: ebs-pv
spec:
capacity:
storage: 5Gi # 볼륨의 크기 5G
accessModes:
- ReadWriteOnce # 하나의 포드에 의해서만 마운트
awsElasticBlockStore:
fsType: ext4
volumeID: <VOLUME_ID>
위에서 만들었던 환경 변수 VOLUME_ID를 이용해 퍼시스턴트 볼륨을 생성해 보겠습니다.
$ cat ebs-pv.yaml | sed"s/<VOLUME_ID>/$VOLUME_ID/g" | kubectl apply -f -
$ kubectl get pv
퍼시스턴트 볼륨은 네잌스페이스에 속하지 않는 클러스터 단위의 오브젝트이므로 네임스페이스에 상관없이 모든 퍼시스턴트 볼륨이 출력됩니다.
이번에는 pvc와 포드를 함께 생성해 보겠습니다. 아래의 yaml 파일은 my-ebs-pvc라는 퍼시스턴트 볼륨 클레임을 먼저 생성한 뒤, 이를 포드의 volumes 항목에서 사용함으로써 포드 내부에 EBS 볼륨을 마운트합니다.
# vi ebs-pod-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-ebs-pvc # 1. my-ebs-pvc라는 이름의 pvc 를 생성합니다.
spec:
storageClassName: ""
accessModes:
- ReadWriteOnce # 2.1 속성이 ReadWriteOnce인 퍼시스턴트 볼륨과 연결합니다.
resources:
requests:
storage: 5Gi # 2.2 볼륨 크기가 최소 5Gi인 퍼시스턴트 볼륨과 연결합니다.
---
apiVersion: v1
kind: Pod
metadata:
name: ebs-mount-container
spec:
containers:
- name: ebs-mount-container
image: busybox
args: [ "tail", "-f", "/dev/null" ]
volumeMounts:
- name: ebs-volume
mountPath: /mnt
volumes:
- name : ebs-volume
persistentVolumeClaim:
claimName: my-ebs-pvc # 3. my-ebs-pvc라는 이름의 pvc를 사용합니다.
pvc를 정의하는 yaml 파일의 accessModes와 resources 항목은 볼륨의 요구 사항으로, 해당 조건을 만족하는 pv와 연결돼야 한다는 의미입니다.
$ kubectl apply -f ebs-pod-pvc.yaml
$ kubectl get pv,pvc
$ kubectl get pods
pv, pvc의 상태가 bound로 설정됐다면 두 리소스가 성공적으로 연결된 것입니다. 포드가 정상적으로 생성되어 Running 상태라면 EBS 볼륨 또한 포드 내부에 정상적으로 마운트 됐다는 의미입니다. 포드 내부에는 5G 크기의 EBS 볼륨이 마운트되어 있습니다.
$ kubectl exec ebs-mount-container -- df -h | grep /mnt
참고로 pv는 ns에 속하지 않는 클러스터 단위의 오브젝트이고, pvc는 ns에 속하는 오브젝트입니다.
pv의 사용 방법을 정리해보면 아래의 그림과 같습니다.
- 포드의 데이터를 영속적으로 저장하기 위해 AWS에서 EBS 볼륨을 생성했습니다.
- ebs-pv.yaml 파일로 1번에서 생성한 EBS 볼륨을 쿠버네티스에서 pv로 등록합니다. ebs-pv.yaml 파일의 awsElasticBlockStore 항목에 EBS 볼륨 ID를 명시했습니다.
- ebs-pod-pvc.yaml 파일에서 pvc를 먼저 정의해 생성했습니다. pvc에는 원하는 볼륨의 조건을 나열했습니다.
- 2번에서 생성한 pv의 속성이 3번에서 생성한 pvc 요구 조건과 일치하기 때문에 두 리소스가 연결됩니다. kubectl get pv, pvc출력 결과에서 리소스의 상태가 연결 상태로 바뀐 것을 확인했습니다.
- ebs-pod-pvc.yaml 파일에 정의된 포드는 4번에서 생성한 pvc를 사용하도록 설정돼 있습니다. 최종적으로 EBS 볼륨이 컨테이너 내부에 마운트되게 됩니다.
퍼시스턴트 볼륨을 선택하기 위한 조건 명시
accessModes와 볼륨 크기, 스토리지클래스, 라벨 셀렉터를 이용한 퍼시스턴트 볼륨 선택
accessModes는 퍼시스턴트 볼륨과 퍼시스턴트 볼륨 클레임을 생성할 떄 설정할 수 있는 속성으로, 볼륨에 대해 읽기 및 쓰기 작업이 가능한지, 여러 개의 인스턴스에 의해 마운트 될 수 있는지 등을 의미합니다.
- ReadWriteOnce(RWO) : 1:1 마운트, 읽기 및 쓰기
- ReadOnlyMany(ROX) : 1:N 마운트 가능, 읽기 전용
- ReadWriteMany(RWX) : 1:N 마운트 가능, 읽기 및 쓰기 가능
EBS 볼륨은 기본적으로 읽기, 쓰기가 모두 가능하며 1:1 관계의 마운트만 가능하기 떄문에 ReadWriteOnce를 사용해야 합니다. 그렇지만 만약 1:N 마운트가 가능한 NFS 서버를 퍼시스턴트 볼륨으로 생성하려면 ReadWriteMany를 사용하는것이 맞습니다.
이전 pvc yaml 파일에서 resources.requests.storage 항목을 5Gi로 설정했었습니다. 이 조건이 pv와 부합했기에 정상적으로 바인드될 수 있었습니다.
그 외에도 스토리지 클래스나 라벨 셀렉터를 이용해 퍼시스턴트 볼륨의 선택을 좀 더 세분화할 수 있습니다. 스토리지 클래스틑 볼륨의 대표 속성 등을 나타내는 것으로, 퍼시스턴트 볼륨을 생성할 때 클래스를 설정하면 해당 클래스를 요청하는 퍼시스턴트 볼륨 클레임과 연결해 바인드합니다.
# vi ebs-pv-storageclass.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: ebs-pv-custom-cs
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
storageClassName: my-ebs-volume
awsElasticBlockStore:
fsType: ext4
volumeID: <VOLUME_ID>
# vi ebs-pod-pvc-custom-sc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-ebs-pvc-custom-sc
spec:
storageClassName: my-ebs-volume
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: Pod
metadata:
name: ebs-mount-container-custom-sc
spec:
containers:
- name: ebs-mount-container
image: busybox
args: [ "tail", "-f", "/dev/null" ]
volumeMounts:
- name: ebs-volume
mountPath: /mnt
volumes:
- name : ebs-volume
persistentVolumeClaim:
claimName: my-ebs-pvc-custom-sc
위에 2개의 yaml 파일에서 storageClassName이라는 항목에 my-ebs-volume이라는 값을 입력했습니다. 이러한 경우 스토리지 클래스의 이름이 일치하는 퍼시스턴트 볼륨과 퍼시스턴트 볼륨 클레임이 서로 연결됩니다.
# vi ebs-pv-label.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: ebs-pv-label
labels:
region: ap-northeast-2a
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
awsElasticBlockStore:
fsType: ext4
volumeID: <VOLUME_ID>
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-ebs-pvc-selector
spec:
selector:
matchLabels:
region: ap-northeast-2a
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: Pod
metadata:
name: ebs-mount-container-label
spec:
containers:
- name: ebs-mount-container
image: busybox
args: [ "tail", "-f", "/dev/null" ]
volumeMounts:
- name: ebs-volume
mountPath: /mnt
volumes:
- name : ebs-volume
persistentVolumeClaim:
claimName: my-ebs-pvc-selector
이렇게 라벨 셀렉터를 사용할 수도 있습니다. 이전에 서비스와 디플로이먼트를 서로 연결할 떄 라벨 셀렉터를 사용했던 것처럼 퍼시스턴트 볼륨 클레임에 라벨 셀렉터인 matchLabels 항목을 정의함으로써 특정 퍼시스턴트 볼륨과 바인드하는 것도 가능합니다.
퍼시스턴트 볼륨의 라이프사이클과 Reclaim Policy
pv를 생성한 뒤, kubectl get pv 명령어로 목록을 확인해 보면 status 라는 항목을 볼 수 있습니다. status 항목은 pv가 사용가능한지, pvc와 연결됐는지 등을 의미합니다. pv를 생성한 직후는 status가 Available으로 설정되어 있었습니다.
pvc를 새로 생성해 바인드했을 때는 status 항목이 Bound로 바뀌었습니다.
여기서 pvc를 삭제(pod, pvc)하면 pv의 상태가 Available이 아닌 Released라는 상태로 변경됩니다. Released는 해당 pv의 사용이 끝났다는 것을 의미하며, Released 상태에 있는 pv는 다시 사용할 수 없습니다. 실제 데이터가 볼륨 안에 보존돼 있기 때문에 퍼시스턴트 볼륨을 삭제한 뒤 다시 생성하면 Available. 상태의 볼륨을 다시 사용할 수 있습니다.
$ kubectl delete -f ebs-pod-pvc.yaml
$ kubectl get pv
$ kubectl delete -f ebs-pv.yaml
$ cat ebs-pv.yaml | sed "s/<VOLUME_ID>/$VOLUME_ID/g" | kubectl apply -f -
$ kubectl get pv
pvc를 삭제했을 때, pv 데이터를 어덯게 처리할 것인지 별도로 정의할 수 있습니다. pv의 사용이 끝났을 때 해당 볼륨을 어떻게 초기화할 것인지 별도로 설정할 수 있는데, 이를 Reclaim Policy라고 부릅니다. Reclaim Policy에는 크게 Retain, Delete, Recycle 방식이 있습니다.
쿠버네티스는 기본적으로 pv 데이터를 보존하는 방식은 Retain을 사용합니다. 그래서 kubectl get pv 명령어에서 출력되는 RECLAIM POLICY 항목의 Retain으로 보일 것입니다.
pv, pvc 정상적으로 연결돼 있으며, 볼륨이 포드의 컨테이너 내부에 마운트된 상황에서는 Storage Object in Use Protection이라는 기능이 적용됩니다. pv, pvc가 연결된 상황에서는 pv, pvc를 삭제해도 삭제되지 않습니다.
만약 pv의 Reclaim Policy를 Delete로 설정해 생성했다면 pv의 사용이 끝난 뒤에 자동으로 pv가 삭제되며, 연결된 외부의 스토리지도 삭제됩니다. Reclaim Policy를 Delete로 설정하는 yaml 파일입니다.
# vi ebs-pv-delete.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: ebs-pv-delete
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
awsElasticBlockStore:
fsType: ext4
volumeID: <VOLUME_ID>
persistentVolumeReclaimPolicy: Delete
# Reclaim Policy가 Delete인 pv 생성
$ cat ebs-pv-delete.yaml | sed "s/<VOLUME_ID>/$VOLUME_ID/g" | kubectl apply -f -
$ kubectl get pv
# po, pvc 생성
$ kubectl apply -f ebs-pod-pvc.yaml
$ kubectl get po,pv,pvc
# 시간조금 지나서
# pvc 삭제
$ kubectl delete -f ebs-pod-pvc.yaml
$ kubectl get pv,pvc
Reclaim Policy가 Delete로 설정됐기 때문에 pvc가 삭제됨과 동시에 pv도 함께 삭제됩니다. EBS 볼륨도 한꺼번에 삭제되기 때문에 볼륨에 저장돼 있던 파일들이 모두 유실된다는 것을 조심해야 합니다.
pv의 Reclaim Policy의 동작 원리를 정리해보면, 연결된 pvc를 삭제함으로써 pv 사용을 종료하게 되면 Reclaim Policy가 Retain인 경우는 pv가 Released 상태로 변경되고, Reclaim Policy가 Delete인 경우는 pv, EBS 볼륨이 함께 삭제됩니다.
StorageClass와 Dynamic Provisioning
다이나믹 프로비저닝과 스토리지 클래스
다이나믹 프로비저닝은 pvc에서 요구하는 조건과 일치하는 pv가 존재하지 않는다면 자동으로 pv와 외부 스토리지를 함께 프로비저닝 하는 기능입니다. 따라서 다이나믹 프로비저닝을 사용하면 EBS와 같은 외부 스토리지를 직접 미리 생성해 둘 필요가 없습니다.
다이나믹 프로비저닝은 스토리지 클래스의 정보를 참고해 외부 스토리지를 생성합니다.
- fast라는 이름의 스토리지 클래스에는 SDD를 생성하라는 설정을, slow라는 스토리지 클래스에는 HDD를 생성하라는 설정을 미리 정의헀다고 가정합니다.
- pvc 특정 스토리지 클래스를 명시해 성성합니다. pvc의 AccesMode, Capacity 등의 조건과 일치하는 pv가 존재하지 않습니다.
- 조건에 일치하는 pv를 새롭게 만들기 위해 스토리지 클래스에 정의된 속성에 따라서 외부 스토리지를 생성합니다.
- 새롭게 생성된 외부 스토리지는 쿠버네티스의 pv로 등록되고, pvc와 바인딩 됩니다.
다이나믹 프로비저닝은 모든 쿠버네티스 클러스터에서 범용적으로 사용할 수 있는 것은 아니며, 다이나믹 프로비저닝 기능이 지원되는 스토리지 프로비저너가 미리 활성화돼 있어야 합니다(클라우드 플랫폼은 자동으로 프로비저닝 됨).
AWS에서 다이나믹 프로비저닝 사용하기
지금까지 만들어진 오브젝트들을 전부 삭제해줍니다.
$ kubectl delete pv,pvc,po,deploy --all
스토리지 클래스 또한 쿠버네티스 오브젝트이기 때문에 kubectl get으로 확인할 수 있습니다.
$ kubectl get sc
AWS EBS의 HDD, SSD 두 가지에 대한 스토리지 클래스를 생성합니다.
# vi storageclass-slow.yaml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: slow
provisioner: kubernetes.io/aws-ebs
parameters:
type: st1
fsType: ext4
zones: ap-northeast-2a # 쿠버네티스 클러스터가 위치한 가용영역
# vi storageclass-fast.yaml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: fast
provisioner: kubernetes.io/aws-ebs
parameters:
type: gp2
fsType: ext4
zones: ap-northeast-2a # 쿠버네티스 클러스터가 위치한 가용영역
provisioner 항목에는 AWS의 쿠버네티스에서 사용할 수 있는 EBS 동적 프로비저너인 kubernetes.io/aws-ebs를 설정했습니다. type 항목은 EBS가 어떤 종유인지 나타내는 것으로, 앞에서 말헀던 st1, gp2, sc12, io1 등을 사용할 수 있습니다.
작성된 yaml 파일로 스토리지 클래스를 생성합니다.
$ kubectl apply -f storageclass-slow.yaml
$ kubectl apply -f storageclass-fast.yaml
$ kubectl get sc
지금 생성한 스토리지 클래스를 사용하는 pvc를 생성함으로써 다이나믹 프로비저닝을 발생시켜 봅시다. pvc yaml 파일을 작성합니다.
# vi pvc-fast-sc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-fast-sc
spec:
storageClassName: fast
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
$ kubectl apply -f pvc-fast-sc.yaml
$ kubectl get pv,pvc
만약 다이나믹 프로비저닝을 아예 사용하지 않으려면 반드시 storageClassName:""와 같이 yaml 파일의 storageClassName에 ""와 같이 명시적으로 공백을 설정해줍니다.
다이나믹 프로비저닝은 pv의 Reclaim Policy가 자동으로 Delete로 설정된다는 특징을 가지고 있습니다. 동적으로 생성되는 pv의 Reclaim Policy 속성은 스토리지 클래스에 설정된 reclaimPolicy 항목을 상속받는데, 스토리지 클래스의 reclaimPolicy가 기본적으로 Delete로 설정되기 때문입니다.
$ kubectl get sc fast -o yaml
# reclaimPolicy가 Delete임을 알 수 있음
따라서 pvc를 삭제하면 EBS 볼륨 또한 삭제됩니다. 다이나믹 프로비저닝을 사용할 때 Delete가 아닌 Retain 정책을 사용하고 싶다면 스토리지 클래스를 정의하는 yaml 파일에 reclaimPolicy: Retain을 명시하거나, kubectl edit 또는 patch 등의 명령어로 pv의 속성을 직접 변경해서 사용해야 합니다.
다아니믹 프로비저닝에서 특정 스토리지 클래스를 기본값으로 사용
스토리지 클래스를 생성할 때, annotation을 추가하면 해당 스토리지 클래스를 기본적으로 사용합니다.
# vi storageclass-default.yaml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: generic
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: kubernetes.io/aws-ebs
parameters:
type: gp2
fsType: ext4
zones: ap-northeast-2a # 쿠버네티스 클러스터가 위치한 가용 영역을 입력합니다.
$ kubectl apply -f storageclass-default.yaml
$ kubectl get sc
스토리지 클래스를 명시하지 않으면 자동으로 기본 스토리지 클래스를 통해 다이나믹 프로비저닝이 수행되고, storageClassName의 값을 ""으로 일부러 설정하면 다이나믹 프로비저닝이 발생하지 않습니다.
공부한 오브젝트들을 전부 삭제합니다.
$ kubectl delete po,deploy,svc,pv,pvc,sc --all