본문 바로가기
Kubernetes/PKOS

[보완] 쿠버네티스 스토리지와 CSI Driver

by isn`t 2023. 4. 8.

*본 게시글의 내용은 개인 학습내용과 더불어 가시다님의 노션 페이지와 스터디 자료인 '24단계 실습으로 정복하는 쿠버네티스 도서' 를 기반으로 작성하였습니다.

Stateful vs Stateless

Stateful 애플리케이션은 상태(State)가 있다. 애플리케이션 실행에 필요한 데이터를 로컬 또는 외부 스토리지에 저장한다. 예를 들어, DB 서버0는 데이터를 스토리지에 저장하고, 이전에 처리한 요청에 대한 상태 정보를 가지고 있어야 한다. 이러한 Stateful 애플리케이션에는 "순서"가 있고, "이전 상태"에 따라 어떤 동작을 할 지 결정된다.

반면, Stateless 애플리케이션은 상태에 의존하지 않는다. 애플리케이션 실행에 필요한 모든 데이터를 어딘가에 저장했다가 불러오는 것이 아니라, 매 Reqeust마다 새로 생성한다. 예를 들어, 정적 웹 사이트는 각각의 요청에 대해 새로운 HTML 페이지를 생성하여 반환한다. 구글에서 검색어를 입력하고 엔터를 눌렀을 때, 모종의 사유로 응답 페이지가 로드되던 중에 연결이 끊어지면, 이전 요청에 대한 상태가 저장되지 않으므로 검색을 다시 해서 새로운 요청을 보내야 한다. 된다. 그리고 이전 상태와 관계없이 새 응답을 다시 받아온다.

Stateful 애플리케이션은 상태를 보관하고 이를 참조하여 순서가 있는 작업을 수행한다.
Stateless 애플리케이션은 상태에 의존하지 않고, 각 요청에 대해 매번 새로운 결과를 생성한다.

Pod와 State

원레 컨테이너 환경에서는 Stateless한 애플리케이션을 가정하고 사용했다. 이식성과 유연함을 고려하여 언제든 종료되고 새로 생겨나며 아름답게 오케스트레이션 되도록. 후에 점차 컨테이너의 사용이 확산되면서 Stateful한 애플리케이션을 운영하면서도 상태를 저장하며 컨테이너의 유연하고 빠른 속도의 운영을 누리고자 하는 니즈가 생겨났고, 최근에는 Stateful한 애플리케이션도 쿠버네티스 환경에서 운영하는 경우를 많이 볼 수 있다.

파드가 종료되고 새로 생겨나도 이전에 하던 작업을 그대로 이어서 할 수 있도록 Stateful한 환경에서 상태 데이터가 보존될 수 있도록 쿠버네티스에서도 스토리지를 이용한다. 쿠버네티스의 볼륨은 데이터를 안정적으로 보관하기 위해 k8s cluster와 분리하여 관리된다.

크게 나누자면 내부망에서 관리하는 볼륨과, 외부망에서 관리하는 볼륨으로 나눌 수 있다.

  • 내부망의 경우 : k8s를 구성하는 node의 로컬 볼륨이나 NFS, 온프레미스 솔루션(ceph)
  • 외부망의 경우 : AWS, GCP, Azure와 같은 클라우드 스토리지

이렇게 볼륨을 마련하고 관리자는 PV를 생성하여 저장용량과 Access Mode를 정의하게 되고,
사용자는 원하는 용량과 Access Mode를 기입하여 PVC를 만들며,
k8s는 이 요청을 충족하는 볼륨을 찾아 적절한 PV와 연결해주고 파드와 바인딩하여 파드에서 볼륨에 접근 가능토록 한다.

이런 방식으로 볼륨을 관리하게 되면 볼륨이 필요할 때 마다 PV를 만들고, PV와 연결하기 위해 용량과 Access Mode를 확인해서 맞춰주어야 하므로 번거로움이 있다.
따라서 파드가 생성될 때 자동으로 볼륨을 마운트하고 파드에서 볼륨을 사용할 수 있도록 하는 기능을 동적 프로비저닝(Dynamic Provisioning)이라 한다.

쿠버네티스 스토리지

CSI Driver(Container Storage Interface Driver)

쿠버네티스와 같은 컨테이너 오케스트레이션 시스템에서 스토리지 연결을 위한 표준 인터페이스이다. 각 스토리지 시스템의 연결을 위한 CSI Driver가 설치됨으로서 쿠버네티스에서는 스토리지 시스템과 통신을 할 수 있게 되고, 파드에 연결할 스토리지를 프로비저닝하고, 마운트, 관리, 삭제할 수 있게 된다.

EBS나 EFS를 쿠버네티스에서 사용하고자 하는 경우, Provisioner가 쿠버네티스 소스코드 내부에 구현되어 있다면 이는 쿠버네티스 릴리즈에 종속된다. 따라서 프로비저너의 신규 기능이 추가되어 이를 사용하고자 한다면 쿠버네티스의 버전이 업그레이드 해야 하는 제약이 생긴다.

때문에 쿠버네티스 내부에 저장된 in-tree provisioner를 걷어낸 뒤 별도의 컨트롤러를 두고 원하는 유형의 스토리지를 연결을 담당하도록 하며, 동적 프로비저닝이 가능하도록 쿠버네티스 클러스터와 스토리지 간 인터페이스를 추상화 한 것이 CSI Driver이다. EBS / EFS CSI Driver

예를 들어 EBS CSI Driver는 쿠버네티스 클러스터 내에 파드로 배포되어 동작하며, 쿠버네티스 클러스터가 PV로 EBS를 사용하면서 EBS 볼륨의 수명 주기를 관리하기 위해서 EBS CSI Driver는 사용자를 대신하여 AWS API를 호출한다. 따라서 이 때 EBS CSI Driver가 AWS API call을 날리기 위한 IAM 권한이 부여되어야 한다.

AWS EFS Driver

[공식 문서](## Prerequisite)에서도 다양한 사전 고려사항을 소개하고 있지만, 눈여겨 볼 점은 아래와 같다.

  • EFS CSI Driver는 Windows 컨테이너와 호환되지 않는다.
  • EKS Fargate 사용시 Dynamic Provisioning을 사용할 수 없다. Static Provisioning만 가능.
kubectl get pod -n kube-system | grep efs
efs-csi-controller-76fff8f8cb-ms9ls             3/3     Running   0             14m
efs-csi-node-cr6rf                              3/3     Running   0             14m
efs-csi-node-lhqtk                              3/3     Running   0             14m
efs-csi-node-v96rk                              3/3     Running   0             14m

EFS CSI Driver를 쿠버네티스 클러스터에 배포하면 위와 같은 구조를 갖는다.
1개의 컨트롤러와 각 노드별로 배포되는 csi-node.

EFS-CSI-Controller

efs-csi-controller는 Kubernetes 클러스터 내에서 Amazon EFS 파일 시스템을 동적으로 프로비저닝하고 관리한다. Kubernetes API 서버와 상호 작용하며 Amazon EFS 파일 시스템과 관련한 작업을 처리한다. 예를 들어, efs-csi-controller는 PVC와 EFS간 연결을 관리하고, 생성/삭제한다.

EFS-CSI-Node

데몬셋으로 모든 노드에 배포되며, EFS와 각 노드의 Pod간 연결을 관리한다. Pod가 생성될 때 EFS와 마운트하고 Pod가 삭제될 때 연결을 해제한다. Pod와 EFS간 연결 상태를 지속적으로 모니터링하고, 연결이 끊어져도 EFS-CSI-Node에 의해 자동으로 재연결이 수행된다.

아래 매니페스트를 이용하여 테스트를 진행하였다.

---
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: efs-sc
provisioner: efs.csi.aws.com
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: efs-pv
spec:
  capacity:
    storage: 5Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: efs-sc
  csi:
    driver: efs.csi.aws.com
    volumeHandle: fs-06340e6a6f1ff9f8f
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: efs-claim
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: efs-sc
  resources:
    requests:
      storage: 5Gi

그리고 아래와 같이 파드를 배포하였다.
파드 1에서는 /data/out1.txt에, 파드 2에서는 /data/out2.txt에 5초마다 내용을 기록한다.

apiVersion: v1
kind: Pod
metadata:
  name: app1
spec:
  containers:
  - name: app1
    image: busybox
    command: ["/bin/sh"]
    args: ["-c", "while true; do echo $(date -u) >> /data/out1.txt; sleep 5; done"]
    volumeMounts:
    - name: persistent-storage
      mountPath: /data
  volumes:
  - name: persistent-storage
    persistentVolumeClaim:
      claimName: efs-claim
---
apiVersion: v1
kind: Pod
metadata:
  name: app2
spec:
  containers:
  - name: app2
    image: busybox
    command: ["/bin/sh"]
    args: ["-c", "while true; do echo $(date -u) >> /data/out2.txt; sleep 5; done"]
    volumeMounts:
    - name: persistent-storage
      mountPath: /data
  volumes:
  - name: persistent-storage
    persistentVolumeClaim:
      claimName: efs-claim
$ kubectl exec -ti app1 -- sh -c "df -hT -t nfs4"
Filesystem           Type            Size      Used Available Use% Mounted on
127.0.0.1:/          nfs4            8.0E         0      8.0E   0% /data

테스트 파드에 연결된 EFS의 정보를 보면 nfs 프로토콜로 /data 에 마운트 된 것을 확인할 수 있다.

특이한 점은 EFS의 용량이 8.0E로 표시된다는 점이다. AWS는 EFS에 페타바이트 규모의 데이터를 저장할 수 있다고 가이드하고 있으며, EFS는 사용한 만큼 과금되고 저장된 데이터의 용량에 맞게 스토리지의 사이즈가 확장/축소되기 때문에, 상한 단위인 페타바이트를 포함할 수 있는 엑사바이트로 표현한 것으로 보인다.

$ kubectl exec -ti app1 -- tail -f /data/out1.txt
Fri Apr 7 18:41:37 UTC 2023
Fri Apr 7 18:41:42 UTC 2023
...

$ kubectl exec -ti app2 -- tail -f /data/out2.txt
Fri Apr 7 18:41:43 UTC 2023
Fri Apr 7 18:41:48 UTC 2023
...

결과를 확인해보면, 각 파드에서 정상적으로 데이터를 기록하였고

kubectl exec -ti app1 -- tail -f /data/out2.txt
Fri Apr 7 18:42:18 UTC 2023
Fri Apr 7 18:42:23 UTC 2023
...

kubectl exec -ti app2 -- tail -f /data/out1.txt
Fri Apr 7 18:42:12 UTC 2023
Fri Apr 7 18:42:17 UTC 2023
...

공유 스토리지인 EFS에 연결되어 있으므로 app1 파드에서 out2.txt를, app2 파드에서 out1.txt를 조회할 수 있다.

Parameters

(출처 : https://github.com/kubernetes-sigs/aws-efs-csi-driver)

EFS CSI Driver의 깃헙 내용을 살펴보면, stroageclass를 구성할 때 아래와 같은 파라미터를 추가할 수 있다.

  • provisioningMode : 현재는 efs-ap 단일 값만 지원하며, 액세스 포인트를 사용한다.
  • fileSystemId : 액세스 포인트가 생성될 파일 시스템 ID
  • directoryPerms : 액세스 포인트로 루트 디렉토리 생성시 적용할 디렉토리 권한
  • uid : 액세스 포인트 루트 디렉토리 생성시 적용할 POSIX User ID
  • gid, gidRangeStart, gidRangeEnd : 액세스 포인트 루트 디렉토리 생성시 적용할 POSIX User ID와 범위
  • basePath : 동적 프로비저닝시 액세스포인트가 생성되는 경로. 지정하지 않으면 파일시스템 상의 루트 아래에 액세스포인트가 생성된다.
  • az : cross account mount 설정.

Summary

기존에도 CSI 드라이버를 설치해서 EBS, EFS를 파드에 마운트하여 사용해보았지만 CSI Driver의 구성과 동작 원리를 비교하고 살펴보면서 사용하지는 않았다. 그저 필요하다니까 설치해서 사용하고, 필요한 파라미터 붙여넣어서 어떻게든 잘 쓰는 것에 만족했던 경향이 있었던 것 같다.

앞서 2주차 내용을 정리하면서 스토리지 부분은 여러번 사용해보고 실습만 간단히 하고 네트워크 부분만 정리하면서 넘어갔었는데, 이번 스토리지 파트를 추가로 정리하면서 CSI Driver의 탄생 배경과 동작 방식에 대해서 이해할 수 있는 좋은 기회가 되었다.

어제 있었던 EKS Techtalk에서, EKS에서 EFS를 사용할 경우 multi region DR 구성을 위한 아키텍처 와 주의점을 배웠는데, 조만간 여유가 된다면 EFS 액세스 포인트를 이용해서 동적 프로비저닝하는 부분과 함께 추가로 핸즈온해보고 글을 작성해보면 좋을 것 같다.