2021. 10. 9. 20:18ㆍDevOps/Docker & Kubernetes
Overview
이전 포스팅들에서 파드와, 파드와 상호작용하는 다른 쿠버네티스 리소스들에 대해서 살펴보았습니다. 이번 포스팅에서는 다시 파드로 돌아가서, 파드 안에 있는 컨테이너들이 외부의 Disk Storage나 Shared Storage를 사용하는 방법에 대해서 살펴보려고 합니다.
파드는 하나의 Logical Host 단위로 동작하기 때문에 파드 내에서 돌아가는 프로세스들은 CPU, RAM, Network Interface등을 공유합니다. 그렇지만 파드 내에 있는 개별적인 컨테이너들은 대체적으로 격리된 파일 시스템(isolated filesystem)을 갖는데, 그 이유는 파일 시스템이 컨테이너의 이미지로부터 오기 때문입니다. 컨테이너가 시작될 때, 이미지에 정의된 파일들이 빌드 타임에 추가됩니다. 이로 미루어볼 때, livenessProbe Failed등의 이유로 인해 파드 내의 컨테이너가 재시작될 경우, 이전 컨테이너의 파일 시스템을 사용할 수 없고 다시 초기화된 상태를 사용해야 함을 알 수 있습니다.
하지만 특정 케이스에서, 컨테이너가 재시작했을 때, 이전 컨테이너의 상태로부터 시작하게 하고 싶은 경우가 있습니다. (you do want to preserve the directories that hold actual data). 쿠버네티스는 이러한 경우를 위해 "volume"이라는 기능을 제공합니다. 볼륨은 파드의 일부로써, 파드 내의 여러 컨테이너들 사이에서 공유가 가능하고, 파드와 동일한 라이프사이클을 갖습니다.
Introducing Volumes
쿠버네티스 볼륨은 파드의 컴포넌트중의 하나로 Standalong K8S Object가 아닙니다. (그 자체로 생성되거나 삭제할 수 없습니다.) 볼륨은 파드 내의 모든 컨테이너에서 사용할 수 있지만, 볼륨에 접근하려는 컨테이너는 반드시 각 컨테이너에 해당 볼륨을 마운트해야 합니다.
리눅스 파일 시스템은 파일 트리의 임의의 위치(arbitrary location)에서 마운트 하는 것을 허용하기 때문에 다음과 같이 publicHtml 볼륨을 WebServer 컨테이너의 var/htdocs/로, ContentAgent 컨테이너의 var/html/로 마운트할 수 있습니다. 두 디렉터리는 모두 쿠버네티스 볼륨을 마운트하기 때문에 하나의 디렉터리에서 해당 볼륨에 데이터를 업로드하면 다른 디렉터리에서도 이 데이터에 접근할 수 있게 됩니다.
Using Volumes to share data between containers
Using an emptyDir Volume
쿠버네티스 볼륨에는 여러가지 타입이 있지만, 가장 단순한 타입은 emptyDir 타입입니다. 말 그대로 emptyDirectory에서 시작한다는 의미로 뒤에서 살펴볼 GitRepository와 같은 다른 볼륨타입들은 모두 이 EmptyDir위에서 생성됩니다. (build upon it)
다음의 예제를 통해 볼륨을 사용해서 컨테이너간에 데이터를 공유하는 방법을 살펴보도록 하겠습니다 luksa/fortune 이미지를 사용하면 10초마다 랜덤 QA문장을 /var/htdocs아래의 index.html에 생성해줍니다. nginx:alpine이미지를 사용하면 /usr/share/nginx/html아래에 있는 index.html을 80번 포트에 서빙하고, 쿠버네티스 emptyDir 볼륨을 /var/htdocs, /usr/share/nginx/html 디렉터리에 마운트 시켜서 luksa/fortune 컨테이너에서 생성된 html파일을 nginx 이미지에서 사용할 수 있도록 합니다.
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: {}
kubectl apply -f 커맨드를 사용하여 위의 파드를 생성하고 port-forward를 통해 8080포트와 fortune 파드의 80번 포트를 매핑시켜줍니다. 매핑후에 curl을 사용하여 8080에 요청을 보내면, 10초마다 다른 QA Text가 리턴되는 것을 확인할 수 있습니다.
실제로 두 컨테이너에 각각 들어가서 파일을 확인해보면 두 컨테이너가 동일한 index.html을 가지고 있는 것을 확인할 수 있습니다. 즉, Shared Volume이 제대로 작동한다는 의미입니다.
Using a Git repository as the starting point for a volume (deprecated in v1.11)
gitRepo 볼륨도 emptyDir과 거의 비슷하게 동작합니다. 다만 initial State가 empty가 아닌 git repository의 상태라는 것이 차이점입니다. 사용자가 gitRepo 볼륨을 생성하면 파드내의 컨테이너가 볼륨을 마운트하기전에 쿠버네티스는 emptyDir 볼륨을 만들고 그 안에 git Repository를 clone 합니다. 즉 initial State가 클론한 git Repository의 상태라는 것입니다.
(gitRepo는 v.1.11부터 사용이 중단되었으며 이를 사용하기 위해서는 emtyDir는 마운트하고 여기에 직접 git을 이용해서 repo를 복제하고 EmptyDir를 파드 컨테이너에 마운트해야 합니다.)
Accessing Files on the worker node's fileSystem
대부분의 파드는 호스트 노드에 대해서 몰라야 합니다. 때문에 호스트 노드의 파일 시스템에도 접근할 일이 없어야 합니다. 하지만 특정한 시스템 레벨의 파드들은 노드의 파일을 읽거나 노드의 파일 시스템에 접근해야 할 수 있습니다. 이때 hostPath 타입의 볼륨을 사용합니다.
경고:
HostPath 볼륨에는 많은 보안 위험이 있으며, 가능하면 HostPath를 사용하지 않는 것이 좋다. HostPath 볼륨을 사용해야 하는 경우, 필요한 파일 또는 디렉터리로만 범위를 지정하고 ReadOnly로 마운트해야 한다. - official docs
hostPath 타입은 Persistent Volume의 일종으로, 파드가 종료되어도 그 상태를 유지합니다. (노드의 파일시스템을 사용하기 때문에) 하지만, 여러 보안상의 이슈들로 인해서 가능하면 HostPath를 사용하지 않는 것이 좋습니다. minikube를 사용해서 클러스터 환경을 구성한 경우 kube-controller-manager-minikube 파드는 HostPath를 통해서 certiciates, log등의 노드의 파일 시스템에 직접 접근하는 것을 확인할 수 있습니다.
Using Persistent Storage
위에서 설명한 hostPath타입의 볼륨은 일종의 Persistent Storage이긴 하지만, 보안상의 이유로 인해 권장되는 타입이 아니며, "특정 노드에 한해서" 공유되는 것이기 때문에 다른 노드에 배포된 파드는 동일한 스토리지에 접근할 수 없습니다.
서로 다른 노드들에 있는 파드들이 동일한 파일 시스템을 마운트하여 접근하기 위해서는 Network-Attachted Storage(NAS)타입의 볼륨이 필요하고, 쿠버네티스는 이러한 네트워크 기반 파일 시스템을 사용할 수 있는 볼륨타입을 지원합니다.
예시에는 gcePersistentDisk타입을 명시했지만 AWS를 사용하는 경우, awsElasticBlockStore 타입을, Azure를 사용하는 경우 azureFile이나 azureDisk타입을 사용할 수 있습니다. 해당 타입을 사용하여 볼륨을 마운트하면 해당 볼륨은 아래 그림과 같이 NAS 파일 스토리지를 Reference하고 있기 때문에 서로 다른 노드의 서로 다른 파드에서 동일한 파일 시스템에 접근해서 Read/Write등의 opration을 사용할 수 있습니다.
Decoupling pods from the underlying storage technology
위에서 GCE, EBS등을 사용해서 Persistent Storage를 Volume에 직접 연결하는 방법을 살펴보았습니다. 하지만 이를 사용하기 위해서는 쿠버네티스를 사용해서 애플리케이션을 배포하려는 개발자가 실제 NAS infrastructure를 이해해야 하고, 이를 직접 연결해야 합니다. 이는 쿠버네티스의 Basic Idea, 실제 Infrastructure를 개발자로부터 숨기고자 하는 것과 어긋납니다.
쿠버네티스가 지향하는 이상적인 상황에서, 쿠버네티스를 사용해서 애플리케이션을 배포하려는 개발자는 내부적으로 어떤 스토리지 볼륨을 사용하는지 알 필요가 없어야 합니다. 즉 AWS EBS를 사용하는지, GCP의 GCE를 사용하는지를 모른 상태로 스토리지를 사용하고 배포할 수 있어야 한다는 의미입니다. 이는 클린아키텍쳐의 관점에서 "추상화"를 제공함으로써, 개발자가 세부 구현(어떤 플랫폼의 어떤 스토리지를 사용할 것인지)에 신경쓰지 않고 인터페이스(앞으로 설명할 PersistentVolumes, PersistentVolumeClaims)만 사용할 수 있도록 책임과 구현을 분리하는 것을 의미합니다.
쿠버네티스는 이러한 추상화를 가능하게 하기 위해서 PersistentVolumes와 PersistentVolumeClaims를 제공합니다.
간단한 동작원리는 다음과 같습니다. "cluster administrator"는 실제로 쿠버네티스가 배포된 환경과 스토리지를 알고 있으므로, 이 정보를 활용해서 PersistentVolume(PV)을 생성합니다. 그러면 Application을 Deploy하려는 개발자는 이 PersistentVolume이 어떤 플랫폼에 어떤 스토리지를 사용해서 배포되어있는지는 모르지만, 자신이 원하는 네트워크 스토리지의 용량등을 정의한 yaml파일을 통해 PersistentVolumeClaim(PVC)을 생성합니다. 쿠버네티스는 PVC가 생성되면 해당 스펙에 맞는 PV를 찾아서 PVC에 바인딩하게 되고 개발자가 생성한 Pod는 이 PV에 바인딩된 PVC를 Referencing하는 볼륨을 생성하게 됩니다. (PVC는 실제로 파드에서 볼륨처럼 사용할 수 있게 됩니다)
Creating a PersistentVolume
PV(실제로는 administrator가 생성함)를 생성하는 것은 간단합니다. 다음과 같은 형식의 yaml파일을 가지고 쿠버네티스 API Server에 Request요청을 보내면 됩니다. accessMode 옵션을 통해 단일 노드에서만 읽기 / 쓰기 연산이 가능한지, 멀티 노드에서 동시에 읽기가 가능한지등의 접근성 여부를 설정할 수 있고, persistentVolumeReclaimPolicy를 통해 해당 PV를 사용하는 PVC가 제거되었을때 해당 볼륨을 유지할 것인지(Retain), 재활용할 것인지(Recycle), 제거할 것인지(Delete)를 설정할 수 있습니다.
apiVersion: v1
kind: PersistentVolume
metadata:
name: mongodb-pv
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
- ReadOnlyMany
persistentVolumeReclaimPolicy: Retain
gcePersistentDisk:
pdName: mongodb
fsType: ext4
PV는 Cluter-Level Resource이므로 특정 namespace에 바인딩 되지 않습니다
PVC는 특정 namespace에 바인딩되는 리소스입니다.
Claiming a PersistentVolume by creating a PersistentVolumeClaim
PV를 생성했으면 이를 사용하기 위해서 PVC를 설정해야 합니다. 이렇게 PVC를 설정하는 것을 PV를 Claim(요구)한다 라고 하며, 이 PV를 요구하는 것은 파드를 생성하는 것과 완전히 별개의 프로세스로 동작합니다. 그 이유는 파드가 Rescheduled되었을 때에도 PVC는 살아있어야 하는, 공유 자원이기 때문입니다
다음과 같이 yaml파일을 통해서 PVC를 생성할 수 있으며 PVC가 생성되는 즉시 쿠버네티스는 위의 Claim(요구사항)을 만족할 수 있는 PV를 찾아서 그 스펙대로 바인딩해줍니다. PVC는 특정한 네임스페이스 내에서만 생성될 수 있는데, 그래야지만 특정 네임스페이스 안에 있는 파드에 바인딩될 수 있기 때문입니다.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mongodb-pvc
spec:
resources:
requests:
storage: 1Gi
accessModes:
- ReadWriteOnce
storageClassName: ""
실제로 이렇게 생성된 PVC는 볼륨을 사용하듯이 사용할 수 있습니다.
apiVersion: v1
kind: Pod
metadata:
name: mongodb
spec:
containers:
- image: mongo
name: mongodb
volumeMounts:
- name: mongodb-data
mountPath: /data/db
ports:
- containerPorts: 27017
protocol: TCP
volumes:
- name: mongodb-data
persistentVolumeClaim:
claimName: mongodb-pvc
Recycling PersistentVolumes
위의 예제에서 persistentVolumeReclaimPolicy를 Retain으로 설정했기 때문에 위에서 생성한 mongodb-pvc를 지워도 pvsms "Available"상태가 아닌 "Release" 상태로 남아있게 됩니다
Retain 옵션은 해당 Claim이 종료되어도 그 Volume과 Contents를 Retain한다는 의미이기 때문에 PV가 여전히 남아있는 것입니다. 이를 지우기 위해서는 Manual하게 PV Resource를 지우고 재생성해야 합니다. 다른 방법으로는 초기에 Retain 옵션이 아닌 Recycle, Delete 옵션을 설정하는 방법이 있는데 Recycle 옵션으로 설정하게 되면 해당 PV가 서로 다른 PVC에 의해 재사용될 수 있고, Delete 옵션으로 설정하게 되면 해당 PVC가 종료되는 시점에 해당 PV도 제거하게 됩니다.
Dynamic Provisioning of PersistentVolumes
PV와 PVC를 사용하면 개발자가 클러스터의 CloudProvider와 Infrastructure를 알지 못해도 인터페이스를 이용해서 쉽게 NFS를 사용할 수 있습니다. 하지만 이 방법 또한 여전히 System Administrator가 manual하게 storage를 provision해야 한다는 한계점이 있습니다. 다행히도 쿠버네티스는 이러한 작업을 "Dynamic Provisioning"을 통해 자동으로 관리할 수 있는 방법을 제공합니다.
administrator는 PV를 생성하는 대신에 Persistent Volume Provisioner를 배포하고 StorageClass Object를 정의하기만 하면, 이 PV를 사용하는 개발자가 해당 StorageClass로 PVC를 생성하는 간단한 방식으로 NFS를 사용할 수 있습니다.
PV와 비슷하게, StorageClass도 namspace에 속하지 않습니다.
Defining the available storage types through StorageClass
StorageClass도 PersistentVolume과 같이 별도의 Resource로 생성해주어야 합니다. 이름을 설정해주고 provisioner(아래 예제에서는 GCP를 사용하므로 gce-pd provisioner를 사용했습니다. 쿠버네티스는 GCP, AWS, Azuer등의 CloudProvider의 Provisioner를 제공합니다.)를 설정해주기만 하면 됩니다. (스토리지 클래스 오브젝트가 생성될 때 reclaimPolicy의 Default값은 Delete 입니다)
Requesting the storage class in PersistentVolumeClaim
PVC는 기존 방식과 사용방법이 거의 동일합니다. spec에 storageClassName 필드만 추가로 입력해주면 됩니다. 위에서 StorageClass의 이름을 "fast"로 설정하였기 때문에 해당 StorageClass를 사용하기 위해서는 storageClassName: fast로 설정해주면 됩니다. 이 Claim을 생성하게 되면 provisioner에 의해 fast StorageClass리소스를 Reference하고 있는 PV가 생성됩니다.
Dynamic Provisioning without specifying a storage class
storageClassName을 빈스트링("")으로 명시하게 되면 dynamic provisioning을 사용하지 않고, pre-provisioned된 PV를 사용하도록 합니다. dynamic provisioning 대신에 manual하게 provision된 리소스를 사용하도록 하고 싶을 때 이러한 방법을 사용합니다.
Reference
https://kubernetes.io/ko/docs/concepts/storage/volumes/
https://kubernetes.io/ko/docs/concepts/storage/dynamic-provisioning/
'DevOps > Docker & Kubernetes' 카테고리의 다른 글
[Kubernetes in Action] Accessing Pod Metadata and Other Resources (0) | 2021.12.12 |
---|---|
[Kubernetes in Action] ConfigMaps and Secrets (0) | 2021.10.30 |
[Kubernetes in Action] Services (0) | 2021.10.02 |
[Kubernetes in Action] Replication and Other Controllers (0) | 2021.09.26 |
[Kubernetes in Action] Pods (0) | 2021.09.25 |