이번 포스트에서는 Kubernetes의 Persistent Volume을 지원하기 위해서 EKS에서 사용 가능한 옵션을 살펴보겠습니다.

 

 

목차

  1. Kubernetes 스토리지 옵션
  2. EKS의 스토리지 옵션
  3. AKS의 스토리지 옵션
  4. 실습 환경과 사전 정보 확인
  5. Amazon EBS CSI Driver 사용
  6. Amazon EFS CSI Driver 사용

 

 

1. Kubernetes 스토리지 옵션

Kubernetes 환경에서 실행되는 파드(컨테이너)는 컨테이너 이미지가 자체가 특별한 형태(격리되고 제한된 리소스를 가진)의 프로세스로 실행되는 것으로 이해할 수 있습니다. 이때 컨테이너 이미지에 존재하지 않는 추가적인 스토리지가 필요할 수 있는데 Kubernetes에서는 이를 Volume으로 제공할 수 있습니다.

 

Kubernetes의 Volumes은 일반적으로 이야기하는 스토리지 보다는 더 큰 개념으로 configMap, secret, 임시 저장 공간, 영구적 저장 공간을 포함합니다.

https://kubernetes.io/docs/concepts/storage/volumes/

 

Kubernetes의 Volume을 Ephemeral volume과 Persistent volume으로 나눌 수 있습니다.

 

Ephemeral volume은 파드의 수명주기를 가지고, Persistent volume은 파드의 수명주기와 관계 없이 존재할 수 있습니다. 즉, 파드가 삭제되면 kubernetes는 ephemeral volume을 제거하지만 persistent volume은 제거하지 않습니다. 또한 어떤 volume이건 파드의 수명주기 혹은 그 이상을 가지기 때문에 "컨테이너"를 재시작하더라도 volume의 데이터는 유지됩니다.

 

또한 한 가지 중요한 점은 volume은 pod spec에 존재하기 때문에, 파드 내에 존재하는 컨테이너 간에는 공유가 가능한 점이 있습니다.

 

아래로 ephemeral volume의 종류를 확인 할 수 있습니다.

https://kubernetes.io/docs/concepts/storage/ephemeral-volumes/

 

 

아마 처음 Kubernetes를 접하시는 분들은 emptyDir의 의미에 대해 헷갈릴 수 있는데, 이를 실습을 통해 알아 보겠습니다.

먼저 비교를 위해서 volume 정의가 없는 파드를 실행해보고, 컨테이너의 재시작에서 데이터가 유지되는지 확인해보겠습니다.

참고: https://kubernetes.io/docs/tasks/configure-pod-container/configure-volume-storage/

# redis 파드 생성
$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: redis
spec:
  terminationGracePeriodSeconds: 0
  containers:
  - name: redis
    image: redis
EOF

# redis 파드 내에 파일 작성
$ kubectl exec -it redis -- pwd
/data
$ kubectl exec -it redis -- sh -c "echo hello > /data/hello.txt"
$ kubectl exec -it redis -- cat /data/hello.txt
hello

# ps 설치 (컨테이너가 실행된 PID 확인을 위함)
$ kubectl exec -it redis -- sh -c "apt update && apt install procps -y"
<생략>
$ kubectl exec -it redis -- ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
redis          1  0.2  0.2 133636 15600 ?        Ssl  14:35   0:00 redis-server
root         233  0.0  0.0   8088  3916 pts/0    Rs+  14:37   0:00 ps aux

# redis 프로세스 강제 종료
$ kubectl exec -it redis -- kill 1

# container 가 restart 됨
$ kubectl get pod
NAME    READY   STATUS    RESTARTS      AGE
redis   1/1     Running   1 (45s ago)   2m52s

# redis 파드 내에 파일 확인
$ kubectl exec -it redis -- cat /data/hello.txt
cat: /data/hello.txt: No such file or directory
$ kubectl exec -it redis -- ls -l /data
total 0

# 파드 삭제
$ kubectl delete pod redis

 

volume이 없이는 실행 중인 컨테이너에서 파일을 쓰는 건은 단순히 컨테이너가 가진 Runtime이 유효한 layer를 쓰는 것에 불과합니다.

 

아래는 emptyDir을 통한 테스트 입니다. 결과를 명확하게 보기 위해서 이번에는 deployment로 배포하겠습니다.

# redis 파드 생성 with emptyDir
$ cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      terminationGracePeriodSeconds: 0
      containers:
      - name: redis
        image: redis
        volumeMounts:
        - name: redis-storage
          mountPath: /data/redis
      volumes:
      - name: redis-storage
        emptyDir: {}
EOF

# redis 파드 내에 파일 작성
$ kubectl get po
NAME                     READY   STATUS    RESTARTS   AGE
redis-78fdb689f4-pbmz7   1/1     Running   0          3s
$ kubectl exec -it redis-78fdb689f4-pbmz7 -- pwd
/data
$ kubectl exec -it redis-78fdb689f4-pbmz7 -- sh -c "echo hello > /data/redis/hello.txt"
$ kubectl exec -it redis-78fdb689f4-pbmz7 -- cat /data/redis/hello.txt
hello

# ps 설치
$ kubectl exec -it redis-78fdb689f4-pbmz7 -- sh -c "apt update && apt install procps -y"
<생략>
$ kubectl exec -it redis-78fdb689f4-pbmz7 -- ps aux

# redis 프로세스 강제 종료 
$ kubectl exec -it redis-78fdb689f4-pbmz7 -- kill 1

# 컨테이너가 재시작됨
$ kubectl get pod
NAME                     READY   STATUS    RESTARTS     AGE
redis-78fdb689f4-pbmz7   1/1     Running   1 (4s ago)   88s

# 컨테이너 스토리지를 사용하는 것과 다르게 파드 내에 파일이 유지되엇습니다.
$ kubectl exec -it redis-78fdb689f4-pbmz7 -- cat /data/redis/hello.txt
hello
$ kubectl exec -it redis-78fdb689f4-pbmz7 -- ls -l /data/redis
total 4
-rw-r--r-- 1 redis root 6 Feb 19 14:46 hello.txt

# 파드 삭제 후 파일 확인
$ kubectl delete pod redis-78fdb689f4-pbmz7
pod "redis-78fdb689f4-pbmz7" deleted

# 신규 파드가 배포되었습니다.
$ kubectl get po
NAME                     READY   STATUS    RESTARTS   AGE
redis-78fdb689f4-8thct   1/1     Running   0          4s

# redis 파드 내에 파일을 확인해보면 파드가 종료되면서 없어진 것을 알 수 있습니다.
$ kubectl exec -it redis-78fdb689f4-8thct  -- cat /data/redis/hello.txt
cat: /data/redis/hello.txt: No such file or directory
command terminated with exit code 1
$ kubectl exec -it redis-78fdb689f4-8thct  -- ls -l /data/redis
total 0

# 파드 삭제
kubectl delete pod redis

 

결국, 중요한 점은 emptyDir은 Ephemeral volume의 일종이기 때문에 파드의 수명주기 동안 유지된다는 점입니다.

 

 

Persistent Volume은 Ephemeral Volume과 다르게 파드의 수명주기와 관련없이 유지될 수 있습니다.

아래는 persistent volume에 대한 문서로, 아마 Persistent volume, Persistent volume Claim과 같은 용어는 이미 익숙하실 것이라 생각합니다.

https://kubernetes.io/docs/concepts/storage/persistent-volumes/

 

이후 다뤄지는 EKS의 스토리지 옵션은 이러한 Persistent Voluem을 제공하는 AWS의 스토리지 서비스와 그 구현에 대한 내용입니다.

 

 

2. EKS의 스토리지 옵션

먼저 AWS의 스토리지 옵션에는 아래와 같은 선택지가 있습니다.

출처: https://aws.amazon.com/ko/getting-started/decision-guides/storage-on-aws-how-to-choose/

 

일반적인 스토리지의 구분을 block storage, file storage, object storage 유형으로 나눠 본다면, AW에서는 cache를 스토리지 옵션으로 제공하고 있습니다.

Block 스토리지에 대응하는 Amazon EBS, File 스토리지의 Amazon EFS, 그리고 Object 스토리지를 위한 Amazon S3 가 있습니다.

 

이 중 EKS에서 제공하는 옵션으로 Amazon EBS, Amazon EFS을 중점으로 살펴보겠습니다.

 

기타 전체 스토리지 옵션은 아래에서 살펴보실 수 있습니다. 본 포스트에서는 다루지 않지만, Windows 를 위한 Amazon FSx, 그리고 Amazon S3도 CSI driver로 제공하고 있는 걸로 알 수 있습니다.

https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/storage.html

 

 

3. AKS의 스토리지 옵션

Azure에서도 Block, File, Object에 대응하는 스토리지 옵션을 가지고 있습니다.

 

다만 Azure에서 File, Object, Queue, Table 범주의 스토리지는 개별 상품으로 제공하는 것이 아닌 스토리지 계정(Storage Account)이란 상위 개념으로 두고, 하위에 각 스토리지에 대한 기능을 제공하고 있습니다.

이로써 전반적인 보안/네트워크, 데이터 관리 등에 대한 전역 설정은 스토리지 계정에 남겨두고, 각 스토리지에서는 스토리지게 국한된 기능을 설정하거나 사용하도록 하는 계층적 구조를 가지고 있습니다.

 

Azure에서의 Block 스토리지는 Azure Disk로, File 스토리지는 Azure File, Object 스토리지는 Azure Blob Storage로 제공합니다.

AKS에서는 이들 스토리지 옵션을 위해 CSI Driver를 제공하며, 클러스터를 설치하면 기본적으로는 Azure Disk CSI driver와 Azure File CSI driver는 enable 되어 있지만 Azure Blob Storage CSI Driver는 기본적으로 disable되어 있어 필요하면 추가로 enable을 해야합니다.

 

AKS의 CSI driver 제공에 대해서 아래 문서를 참고하실 수 있습니다.

https://learn.microsoft.com/ko-kr/azure/aks/csi-storage-drivers

 

또한 EKS에 대응하는 AKS 스토리지 옵션을 설명한 아래 문서를 참고하실 수 있습니다.

https://learn.microsoft.com/ko-kr/azure/architecture/aws-professional/eks-to-aks/storage

 

 

3. 실습 환경과 사전 정보 확인

아래와 같이 실습 환경을 구성하도록 하겠습니다.

 

앞선 실습과 다른 점은 각 Public Subnet에 ENI를 추가하여 EFS를 사용할 수 있도록 구성했습니다.

 

CloudFormation을 통해서 아래와 같이 배포를 진행합니다.

아래에서 CloudFormation 배포 명령에서 개인 환경에 알맞게 KeyName을 변경하시면 됩니다.

# yaml 파일 다운로드
curl -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/K8S/myeks-3week.yaml

# 배포
aws cloudformation deploy --template-file ./myeks-3week.yaml \
--stack-name myeks --parameter-overrides KeyName=ekskey SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32 --region ap-northeast-2

# CloudFormation 스택 배포 완료 후 운영서버 EC2 IP 출력
aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[*].OutputValue' --output text

# 운영서버 EC2 에 SSH 접속
ssh -i <ssh 키파일> ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)

 

웹 콘솔에서 확인해보면 각 서브넷에 EFS 용 Network interface가 생성된 것을 알 수 있습니다.

 

 

기본 실습 환경이 배포되면 아래와 같이 EKS를 배포합니다.

# 환경 변수 선언
export CLUSTER_NAME=myeks

# myeks-VPC/Subnet 정보 확인 및 변수 지정
export VPCID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=$CLUSTER_NAME-VPC" --query 'Vpcs[*].VpcId' --output text)
echo $VPCID

export PubSubnet1=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PublicSubnet1" --query "Subnets[0].[SubnetId]" --output text)
export PubSubnet2=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PublicSubnet2" --query "Subnets[0].[SubnetId]" --output text)
export PubSubnet3=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PublicSubnet3" --query "Subnets[0].[SubnetId]" --output text)
echo $PubSubnet1 $PubSubnet2 $PubSubnet3

SSHKEYNAME=ekskey # 개인 Key Pair 이름으로 변경

 

환경 변수를 바탕으로 clusterConfig를 생성합니다.

cat << EOF > myeks.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: myeks
  region: ap-northeast-2
  version: "1.31"

iam:
  withOIDC: true # enables the IAM OIDC provider as well as IRSA for the Amazon CNI plugin

  serviceAccounts: # service accounts to create in the cluster. See IAM Service Accounts
  - metadata:
      name: aws-load-balancer-controller
      namespace: kube-system
    wellKnownPolicies:
      awsLoadBalancerController: true

vpc:
  cidr: 192.168.0.0/16
  clusterEndpoints:
    privateAccess: true # if you only want to allow private access to the cluster
    publicAccess: true # if you want to allow public access to the cluster
  id: $VPCID
  subnets:
    public:
      ap-northeast-2a:
        az: ap-northeast-2a
        cidr: 192.168.1.0/24
        id: $PubSubnet1
      ap-northeast-2b:
        az: ap-northeast-2b
        cidr: 192.168.2.0/24
        id: $PubSubnet2
      ap-northeast-2c:
        az: ap-northeast-2c
        cidr: 192.168.3.0/24
        id: $PubSubnet3

addons:
  - name: vpc-cni # no version is specified so it deploys the default version
    version: latest # auto discovers the latest available
    attachPolicyARNs: # attach IAM policies to the add-on's service account
      - arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy
    configurationValues: |-
      enableNetworkPolicy: "true"

  - name: kube-proxy
    version: latest

  - name: coredns
    version: latest

  - name: metrics-server
    version: latest

managedNodeGroups:
- amiFamily: AmazonLinux2023
  desiredCapacity: 3
  iam:
    withAddonPolicies:
      certManager: true # Enable cert-manager
      externalDNS: true # Enable ExternalDNS
  instanceType: t3.medium
  preBootstrapCommands:
    # install additional packages
    - "dnf install nvme-cli links tree tcpdump sysstat ipvsadm ipset bind-utils htop -y"
  labels:
    alpha.eksctl.io/cluster-name: myeks
    alpha.eksctl.io/nodegroup-name: ng1
  maxPodsPerNode: 100
  maxSize: 3
  minSize: 3
  name: ng1
  ssh:
    allow: true
    publicKeyName: $SSHKEYNAME
  tags:
    alpha.eksctl.io/nodegroup-name: ng1
    alpha.eksctl.io/nodegroup-type: managed
  volumeIOPS: 3000
  volumeSize: 120
  volumeThroughput: 125
  volumeType: gp3
EOF

 

이제 EKS를 생성합니다.

eksctl create cluster -f myeks.yaml --verbose 4

 

EKS 클러스터에서 스토리지와 관련하여 기본 설정을 확인해보도록 하겠습니다.

 

앞서 EKS 생성에서 확인한 바와 같이 EKS를 기본 생성했을 때는 CSI Driver는 설치되어 있지 않습니다. 그렇기 때문에 EKS에서는 Add-on을 설치해야 합니다.

$kubectl get po -A
NAMESPACE     NAME                              READY   STATUS    RESTARTS   AGE
default       nsenter-58d5bd                    1/1     Running   0          42s
kube-system   aws-node-9rz7w                    2/2     Running   0          7m23s
kube-system   aws-node-nfxqk                    2/2     Running   0          7m26s
kube-system   aws-node-x4sbw                    2/2     Running   0          7m22s
kube-system   coredns-86f5954566-bjxj2          1/1     Running   0          13m
kube-system   coredns-86f5954566-qf5b6          1/1     Running   0          13m
kube-system   kube-proxy-8jpdb                  1/1     Running   0          7m22s
kube-system   kube-proxy-bmrk9                  1/1     Running   0          7m23s
kube-system   kube-proxy-jmqzl                  1/1     Running   0          7m26s
kube-system   metrics-server-6bf5998d9c-4lg9h   1/1     Running   0          13m
kube-system   metrics-server-6bf5998d9c-8xszh   1/1     Running   0          13m

$ kubectl get ds -A
NAMESPACE     NAME         DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
kube-system   aws-node     3         3         3       3            3           <none>          14m
kube-system   kube-proxy   3         3         3       3            3           <none>          14m

 

EKS를 생성한 시점 built-in Storage Class를 확인해보면 gp2가 생성되어 있습니다.

kubectl get storageclass
NAME   PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
gp2    kubernetes.io/aws-ebs   Delete          WaitForFirstConsumer   false                  17m

kubectl describe storageclass gp2
Name:            gp2
IsDefaultClass:  No
Annotations:     kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"storage.k8s.io/v1","kind":"StorageClass","metadata":{"annotations":{},"name":"gp2"},"parameters":{"fsType":"ext4","type":"gp2"},"provisioner":"kubernetes.io/aws-ebs","volumeBindingMode":"WaitForFirstConsumer"}

Provisioner:           kubernetes.io/aws-ebs
Parameters:            fsType=ext4,type=gp2
AllowVolumeExpansion:  <unset>
MountOptions:          <none>
ReclaimPolicy:         Delete
VolumeBindingMode:     WaitForFirstConsumer
Events:                <none>

 

gp2를 바탕으로 pvc와 파드를 생성해보겠습니다.

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: gp2-ebs-claim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  storageClassName: gp2
EOF


# 파드 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: app
spec:
  terminationGracePeriodSeconds: 3
  containers:
  - name: app
    image: centos
    command: ["/bin/sh"]
    args: ["-c", "while true; do echo \$(date -u) >> /data/out.txt; sleep 5; done"]
    volumeMounts:
    - name: persistent-storage
      mountPath: /data
  volumes:
  - name: persistent-storage
    persistentVolumeClaim:
      claimName: gp2-ebs-claim
EOF

 

혹시나 intree가 동작하는 건가 생각했는데, Provisioner를 보면 ebs.csi.aws.com으로 처리되는 것으로 보입니다.

(아마 intree volume이 deprecated 되어 이제는 모든 intree volume 형식도 csi가 처리하도록 변경된 것일 수 있습니다)

$ kubectl get po,pvc,pv
NAME                 READY   STATUS    RESTARTS   AGE
pod/app              0/1     Pending   0          8s
pod/nsenter-58d5bd   1/1     Running   0          6m6s

NAME                                  STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
persistentvolumeclaim/gp2-ebs-claim   Pending                                      gp2            <unset>                 25s
$ kubectl describe pvc gp2-ebs-claim
Name:          gp2-ebs-claim
Namespace:     default
StorageClass:  gp2
Status:        Pending
Volume:
Labels:        <none>
Annotations:   volume.beta.kubernetes.io/storage-provisioner: ebs.csi.aws.com
               volume.kubernetes.io/selected-node: ip-192-168-1-6.ap-northeast-2.compute.internal
               volume.kubernetes.io/storage-provisioner: ebs.csi.aws.com
Finalizers:    [kubernetes.io/pvc-protection]
Capacity:
Access Modes:
VolumeMode:    Filesystem
Used By:       app
Events:
  Type    Reason                Age                From                         Message
  ----    ------                ----               ----                         -------
  Normal  WaitForFirstConsumer  26s (x3 over 42s)  persistentvolume-controller  waiting for first consumer to be created before binding
  Normal  ExternalProvisioning  11s (x3 over 25s)  persistentvolume-controller  Waiting for a volume to be created either by the external provisioner 'ebs.csi.aws.com' or manually by the system administrator. If volume creation is delayed, please verify that the provisioner is running and correctly registered.

 

각 노드에 대응하는 csinodes 정보가 생성되어 있습니다.

CSINode 오브젝트에 대한 문서를 확인해보면 CSI driver를 설치하면 해당 노드에 CSI 관련 정보가 CSINode에 들어가는 것이라고 합니다.

https://kubernetes-csi.github.io/docs/csi-node-object.html

$ kubectl get csinodes
NAME                                               DRIVERS   AGE
ip-192-168-1-6.ap-northeast-2.compute.internal     0         20m
ip-192-168-2-172.ap-northeast-2.compute.internal   0         20m
ip-192-168-3-246.ap-northeast-2.compute.internal   0         20m

$ kubectl get csinodes ip-192-168-1-6.ap-northeast-2.compute.internal -oyaml
apiVersion: storage.k8s.io/v1
kind: CSINode
metadata:
  annotations:
    storage.alpha.kubernetes.io/migrated-plugins: kubernetes.io/aws-ebs,kubernetes.io/azure-disk,kubernetes.io/azure-file,kubernetes.io/cinder,kubernetes.io/gce-pd,kubernetes.io/portworx-volume,kubernetes.io/vsphere-volume
  creationTimestamp: "2025-02-22T13:43:45Z"
  name: ip-192-168-1-6.ap-northeast-2.compute.internal
  ownerReferences:
  - apiVersion: v1
    kind: Node
    name: ip-192-168-1-6.ap-northeast-2.compute.internal
    uid: d5f55b6f-d90c-4e5e-b984-6dedfa396116
  resourceVersion: "2233"
  uid: da7b254b-9dff-451d-9abf-02edc9c31eac
spec:
  drivers: null

 

이후 CSI driver 설치한 이후 비교를 위해서 정보를 남겨 두겠습니다. 참고로 위에서 보면 spec.drivers가 null로 보입니다.

 

AKS는 기본 생성 시점에 2개의 CSI driver가 설치되고, 아래와 같이 drivers에 등록이 된걸로 보입니다.

$ kubectl get csinodes
NAME                                DRIVERS   AGE
aks-nodepool1-76251328-vmss000008   2         49s
aks-nodepool1-76251328-vmss000009   2         47s

$ kubectl get csinodes aks-nodepool1-76251328-vmss000008 -oyaml
apiVersion: storage.k8s.io/v1
kind: CSINode
metadata:
  annotations:
    storage.alpha.kubernetes.io/migrated-plugins: kubernetes.io/aws-ebs,kubernetes.io/azure-disk,kubernetes.io/azure-file,kubernetes.io/cinder,kubernetes.io/gce-pd,kubernetes.io/portworx-volume,kubernetes.io/vsphere-volume
  creationTimestamp: "2025-02-22T14:10:54Z"
  name: aks-nodepool1-76251328-vmss000008
  ownerReferences:
  - apiVersion: v1
    kind: Node
    name: aks-nodepool1-76251328-vmss000008
    uid: 1704269d-1aca-40fc-bb47-70675ec1cbcf
  resourceVersion: "768823"
  uid: 6d5e60da-5cc3-473f-b8c0-ffa880be9f3a
spec:
  drivers:
  - name: file.csi.azure.com
    nodeID: aks-nodepool1-76251328-vmss000008
    topologyKeys: null
  - allocatable:
      count: 8
    name: disk.csi.azure.com
    nodeID: aks-nodepool1-76251328-vmss000008
    topologyKeys:
    - topology.disk.csi.azure.com/zone

 

마지막으로 앞서 생성이 실패한 리소스는 삭제하고 다음 실습으로 넘어가서 EBS CSI driver를 살펴보겠습니다.

$ kubectl delete po app
pod "app" deleted
$ kubectl delete pvc gp2-ebs-claim
persistentvolumeclaim "gp2-ebs-claim" deleted

 

 

4. Amazon EBS CSI Driver 사용

EBS CSI Driver를 사용하기 위해서 Addon을 설치 하겠습니다. 또한 ebs-csi-controller에서 사용하는 권한을 위해서 AmazonEBSCSIDriverPolicy를 사용해 IRSA 설정을 하였습니다.

# 아래는 aws-ebs-csi-driver 전체 버전 정보와 기본 설치 버전(True) 정보 확인
$ aws eks describe-addon-versions \
    --addon-name aws-ebs-csi-driver \
    --kubernetes-version 1.31 \
    --query "addons[].addonVersions[].[addonVersion, compatibilities[].defaultVersion]" \
    --output text

# IRSA 설정 : AWS관리형 정책 AmazonEBSCSIDriverPolicy 사용
$ eksctl create iamserviceaccount \
  --name ebs-csi-controller-sa \
  --namespace kube-system \
  --cluster ${CLUSTER_NAME} \
  --attach-policy-arn arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy \
  --approve \
  --role-only \
  --role-name AmazonEKS_EBS_CSI_DriverRole

# IRSA 확인
$ eksctl get iamserviceaccount --cluster ${CLUSTER_NAME}
NAMESPACE       NAME                            ROLE ARN
kube-system     aws-load-balancer-controller    arn:aws:iam::430118812536:role/eksctl-myeks-addon-iamserviceaccount-kube-sys-Role1-KfYBX6UfNuOM
kube-system     ebs-csi-controller-sa           arn:aws:iam::430118812536:role/AmazonEKS_EBS_CSI_DriverRole

# Amazon EBS CSI driver addon 배포(설치)
$ export ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
$ eksctl create addon --name aws-ebs-csi-driver --cluster ${CLUSTER_NAME} --service-account-role-arn arn:aws:iam::${ACCOUNT_ID}:role/AmazonEKS_EBS_CSI_DriverRole --force
2025-02-22 23:17:25 [ℹ]  Kubernetes version "1.31" in use by cluster "myeks"
2025-02-22 23:17:26 [ℹ]  IRSA is set for "aws-ebs-csi-driver" addon; will use this to configure IAM permissions
2025-02-22 23:17:26 [!]  the recommended way to provide IAM permissions for "aws-ebs-csi-driver" addon is via pod identity associations; after addon creation is completed, run `eksctl utils migrate-to-pod-identity`
2025-02-22 23:17:26 [ℹ]  using provided ServiceAccountRoleARN "arn:aws:iam::430118812536:role/AmazonEKS_EBS_CSI_DriverRole"
2025-02-22 23:17:26 [ℹ]  creating addon

 

 

Addon 설치가 완료되고 정보를 살펴보겠습니다.

# 확인
$ eksctl get addon --cluster ${CLUSTER_NAME}
2025-02-22 23:18:10 [ℹ]  Kubernetes version "1.31" in use by cluster "myeks"
2025-02-22 23:18:10 [ℹ]  getting all addons
2025-02-22 23:18:11 [ℹ]  to see issues for an addon run `eksctl get addon --name <addon-name> --cluster <cluster-name>`
NAME                    VERSION                 STATUS          ISSUES  IAMROLE                                                                  UPDATE AVAILABLE CONFIGURATION VALUES            POD IDENTITY ASSOCIATION ROLES
aws-ebs-csi-driver      v1.39.0-eksbuild.1      CREATING        0       arn:aws:iam::430118812536:role/AmazonEKS_EBS_CSI_DriverRole
coredns                 v1.11.4-eksbuild.2      ACTIVE          0
kube-proxy              v1.31.3-eksbuild.2      ACTIVE          0
metrics-server          v0.7.2-eksbuild.2       ACTIVE          0
vpc-cni                 v1.19.2-eksbuild.5      ACTIVE          0       arn:aws:iam::430118812536:role/eksctl-myeks-addon-vpc-cni-Role1-Y1231NkEEKPX                              enableNetworkPolicy: "true"

$ kubectl get deploy,ds -l=app.kubernetes.io/name=aws-ebs-csi-driver -n kube-system
NAME                                 READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/ebs-csi-controller   2/2     2            2           2m15s

NAME                                  DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR              AGE
daemonset.apps/ebs-csi-node           3         3         3       3            3           kubernetes.io/os=linux     2m16s
daemonset.apps/ebs-csi-node-windows   0         0         0       0            0           kubernetes.io/os=windows   2m16s


# ebs-csi-controller 파드에 6개 컨테이너 확인
kubectl get pod -n kube-system -l app=ebs-csi-controller -o jsonpath='{.items[0].spec.containers[*].name}' ; echo
ebs-plugin csi-provisioner csi-attacher csi-snapshotter csi-resizer liveness-probe

 

디플로이먼트와 데몬셋 정보를 볼때 ebs-csi-controller 자체가 사용자 노드 그룹에서 실행되는 것을 알 수 있습니다. 그러하므로 별도의 권한을 주어서 컨트롤러가 직접 스토리지를 관리할 수 있도록 권한 할당하는 방식으로 진행됩니다.

 

 

다만 AKS의 CSI controller는 컨트롤 플레인에 위치하고 있고 CSI driver 역할을 하는 컴포넌트만 워커 노드에 데몬셋으로 실행됩니다. 또한 컨트롤 프레인의 CSI controller는 AKS 클러스터의 Identity를 통해 ARM(Azure Resource Manager)으로 요청을 하게 되어 있습니다.

$ kubectl get deploy,ds -n kube-system
NAME                                 READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/coredns              2/2     2            2           8d
deployment.apps/coredns-autoscaler   1/1     1            1           8d
deployment.apps/konnectivity-agent   2/2     2            2           8d
deployment.apps/metrics-server       2/2     2            2           8d

NAME                                        DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
daemonset.apps/azure-ip-masq-agent          2         2         2       2            2           <none>          8d
daemonset.apps/cloud-node-manager           2         2         2       2            2           <none>          8d
daemonset.apps/cloud-node-manager-windows   0         0         0       0            0           <none>          8d
daemonset.apps/csi-azuredisk-node           2         2         2       2            2           <none>          8d
daemonset.apps/csi-azuredisk-node-win       0         0         0       0            0           <none>          8d
daemonset.apps/csi-azurefile-node           2         2         2       2            2           <none>          8d
daemonset.apps/csi-azurefile-node-win       0         0         0       0            0           <none>          8d
daemonset.apps/kube-proxy                   2         2         2       2            2           <none>          8d

$ kubectl get po -n kube-system csi-azuredisk-node-4fjbg -o jsonpath='{.spec.containers[*].name}'; echo
liveness-probe node-driver-registrar azuredisk

 

 

앞서 살펴본 csinodes를 살펴보면 DRIVERS 값이 1로 확인되고, csinode를 살펴보면 spec에 driver와 allocatable(attach 가능한 수)이 확인됩니다.

# csinodes 확인
$ kubectl get csinodes
NAME                                               DRIVERS   AGE
ip-192-168-1-6.ap-northeast-2.compute.internal     1         37m
ip-192-168-2-172.ap-northeast-2.compute.internal   1         37m
ip-192-168-3-246.ap-northeast-2.compute.internal   1         37m

$ kubectl get csinodes ip-192-168-1-6.ap-northeast-2.compute.internal -oyaml
apiVersion: storage.k8s.io/v1
kind: CSINode
metadata:
  annotations:
    storage.alpha.kubernetes.io/migrated-plugins: kubernetes.io/aws-ebs,kubernetes.io/azure-disk,kubernetes.io/azure-file,kubernetes.io/cinder,kubernetes.io/gce-pd,kubernetes.io/portworx-volume,kubernetes.io/vsphere-volume
  creationTimestamp: "2025-02-22T13:43:45Z"
  name: ip-192-168-1-6.ap-northeast-2.compute.internal
  ownerReferences:
  - apiVersion: v1
    kind: Node
    name: ip-192-168-1-6.ap-northeast-2.compute.internal
    uid: d5f55b6f-d90c-4e5e-b984-6dedfa396116
  resourceVersion: "9341"
  uid: da7b254b-9dff-451d-9abf-02edc9c31eac
spec:
  drivers:
  - allocatable:
      count: 26
    name: ebs.csi.aws.com
    nodeID: i-0c573120ce2302472
    topologyKeys:
    - kubernetes.io/os
    - topology.ebs.csi.aws.com/zone
    - topology.kubernetes.io/zone

$  kubectl get csidrivers
NAME              ATTACHREQUIRED   PODINFOONMOUNT   STORAGECAPACITY   TOKENREQUESTS   REQUIRESREPUBLISH   MODES        AGE
ebs.csi.aws.com   true             false            false             <unset>         false               Persistent   5m17s
efs.csi.aws.com   false            false            false             <unset>         false               Persistent   48m

$ kubectl describe csidrivers ebs.csi.aws.com
Name:         ebs.csi.aws.com
Namespace:
Labels:       app.kubernetes.io/component=csi-driver
              app.kubernetes.io/managed-by=EKS
              app.kubernetes.io/name=aws-ebs-csi-driver
              app.kubernetes.io/version=1.39.0
Annotations:  <none>
API Version:  storage.k8s.io/v1
Kind:         CSIDriver
Metadata:
  Creation Timestamp:  2025-02-22T14:17:33Z
  Resource Version:    9277
  UID:                 62f100ee-d3ad-463c-8674-23e4af00b280
Spec:
  Attach Required:     true
  Fs Group Policy:     ReadWriteOnceWithFSType
  Pod Info On Mount:   false
  Requires Republish:  false
  Se Linux Mount:      false
  Storage Capacity:    false
  Volume Lifecycle Modes:
    Persistent
Events:  <none>

 

참고로 부착 가능한 EBS 수량은 아래와 같이 변경 할 수 있습니다.

# 노드에 최대 EBS 부착 수량 변경
$ cat << EOF > node-attachments.yaml
"node":
  "volumeAttachLimit": 31
  "enableMetrics": true
EOF

$ aws eks update-addon --cluster-name ${CLUSTER_NAME} --addon-name aws-ebs-csi-driver \
  --addon-version v1.39.0-eksbuild.1 --configuration-values 'file://node-attachments.yaml'
{
    "update": {
        "id": "ab0de1c6-be8a-306f-8504-a6bbe332cb28",
        "status": "InProgress",
        "type": "AddonUpdate",
        "params": [
            {
                "type": "AddonVersion",
                "value": "v1.39.0-eksbuild.1"
            },
            {
                "type": "ConfigurationValues",
                "value": "\"node\":\n  \"volumeAttachLimit\": 31\n  \"enableMetrics\": true"
            }
        ],
        "createdAt": "2025-02-22T23:31:53.878000+09:00",
        "errors": []
    }
}


## 확인
$ kubectl get csinodes ip-192-168-1-6.ap-northeast-2.compute.internal -oyaml
apiVersion: storage.k8s.io/v1
kind: CSINode
metadata:
  annotations:
    storage.alpha.kubernetes.io/migrated-plugins: kubernetes.io/aws-ebs,kubernetes.io/azure-disk,kubernetes.io/azure-file,kubernetes.io/cinder,kubernetes.io/gce-pd,kubernetes.io/portworx-volume,kubernetes.io/vsphere-volume
  creationTimestamp: "2025-02-22T13:43:45Z"
  name: ip-192-168-1-6.ap-northeast-2.compute.internal
  ownerReferences:
  - apiVersion: v1
    kind: Node
    name: ip-192-168-1-6.ap-northeast-2.compute.internal
    uid: d5f55b6f-d90c-4e5e-b984-6dedfa396116
  resourceVersion: "13130"
  uid: da7b254b-9dff-451d-9abf-02edc9c31eac
spec:
  drivers:
  - allocatable:
      count: 31
    name: ebs.csi.aws.com
    nodeID: i-0c573120ce2302472
    topologyKeys:
    - kubernetes.io/os
    - topology.ebs.csi.aws.com/zone
    - topology.kubernetes.io/zone

 

이제 스토리지 클래스를 생성하여 샘플 파드를 실행해보겠습니다.

# gp3 스토리지 클래스 생성
$ kubectl get sc
cat <<EOF | kubectl apply -f -
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: gp3
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
allowVolumeExpansion: true
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
parameters:
  type: gp3
  #iops: "5000"
  #throughput: "250"
  allowAutoIOPSPerGBIncrease: 'true'
  encrypted: 'true'
  fsType: xfs # 기본값이 ext4
EOF

$ kubectl get storageclass
NAME            PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
gp2             kubernetes.io/aws-ebs   Delete          WaitForFirstConsumer   false                  71m
gp3 (default)   ebs.csi.aws.com         Delete          WaitForFirstConsumer   true                   16s

$ kubectl describe sc gp3 | grep Parameters
Parameters:            allowAutoIOPSPerGBIncrease=true,encrypted=true,fsType=xfs,type=gp3

 

참고로 스토리지 클래스에 사용되는 파라미터는 아래 문서를 참고할 수 있습니다.

https://github.com/kubernetes-sigs/aws-ebs-csi-driver/blob/master/docs/parameters.md

 

 

이제 PVC와 파드를 배포해 실제 정상 동작 여부를 살펴보겠습니다.

# 워커노드의 EBS 볼륨 확인 : tag(키/값) 필터링 - 링크
aws ec2 describe-volumes --filters Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node --query "Volumes[].{VolumeId: VolumeId, VolumeType: VolumeType, InstanceId: Attachments[0].InstanceId, State: Attachments[0].State}" | jq

[
  {
    "VolumeId": "vol-0d330ee4d601b3f61",
    "VolumeType": "gp3",
    "InstanceId": "i-09fae9da74f42fff6",
    "State": "attached"
  },
  {
    "VolumeId": "vol-0be8852c50d581f9c",
    "VolumeType": "gp3",
    "InstanceId": "i-09378c64bd018dfbb",
    "State": "attached"
  },
  {
    "VolumeId": "vol-0612ef77c2ebebb49",
    "VolumeType": "gp3",
    "InstanceId": "i-0c573120ce2302472",
    "State": "attached"
  }
]

# 워커노드에서 파드에 추가한 EBS 볼륨 확인
aws ec2 describe-volumes --filters Name=tag:ebs.csi.aws.com/cluster,Values=true --query "Volumes[].{VolumeId: VolumeId, VolumeType: VolumeType, InstanceId: Attachments[0].InstanceId, State: Attachments[0].State}" | jq

[]

# PVC 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ebs-claim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 4Gi
  storageClassName: gp3
EOF

# WaitForFirstConsumer 이므로 파드가 없이는 Pending 상태로 머문다.
kubectl get pvc,pv
NAME                              STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
persistentvolumeclaim/ebs-claim   Pending                                      gp3            <unset>                 2s

# 파드 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: app
spec:
  terminationGracePeriodSeconds: 3
  containers:
  - name: app
    image: centos
    command: ["/bin/sh"]
    args: ["-c", "while true; do echo \$(date -u) >> /data/out.txt; sleep 5; done"]
    volumeMounts:
    - name: persistent-storage
      mountPath: /data
  volumes:
  - name: persistent-storage
    persistentVolumeClaim:
      claimName: ebs-claim
EOF

# 워커노드에서 파드에 추가한 EBS 볼륨 모니터링
while true; do aws ec2 describe-volumes --filters Name=tag:ebs.csi.aws.com/cluster,Values=true --query "Volumes[].{VolumeId: VolumeId, VolumeType: VolumeType, InstanceId: Attachments[0].InstanceId, State: Attachments[0].State}" --output text; date; sleep 1; done

# Pod가 생성되는 시점
Sat Feb 22 23:50:11 KST 2025
None    None    vol-0dfe08ec2df70ab74   gp3
Sat Feb 22 23:50:14 KST 2025
i-0c573120ce2302472     attaching       vol-0dfe08ec2df70ab74   gp3
Sat Feb 22 23:50:17 KST 2025
i-0c573120ce2302472     attached        vol-0dfe08ec2df70ab74   gp3
Sat Feb 22 23:50:20 KST 2025
i-0c573120ce2302472     attached        vol-0dfe08ec2df70ab74   gp3
...
# PVC가 삭제되는 시점
Sun Feb 23 00:07:36 KST 2025
i-0c573120ce2302472     attached        vol-0dfe08ec2df70ab74   gp3
Sun Feb 23 00:07:39 KST 2025
i-0c573120ce2302472     detaching       vol-0dfe08ec2df70ab74   gp3
Sun Feb 23 00:07:42 KST 2025
None    None    vol-0dfe08ec2df70ab74   gp3
Sun Feb 23 00:07:45 KST 2025


# PVC, 파드 확인
$ kubectl get pod,pvc,pv
NAME                 READY   STATUS    RESTARTS   AGE
pod/app              1/1     Running   0          54s
pod/nsenter-58d5bd   1/1     Running   0          60m

NAME                              STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
persistentvolumeclaim/ebs-claim   Bound    pvc-9d54f6eb-4458-40a3-9855-df4b48daeeef   4Gi        RWO            gp3            <unset>
     112s

NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM               STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
persistentvolume/pvc-9d54f6eb-4458-40a3-9855-df4b48daeeef   4Gi        RWO            Delete           Bound    default/ebs-claim   gp3
 <unset>                          52s

$ kubectl get VolumeAttachment
NAME                                                                   ATTACHER          PV                                         NODE
                                   ATTACHED   AGE
csi-d4e4fc85a15fcc5c44af222599968b28287cb967ee7d363e11aa070b74840ac0   ebs.csi.aws.com   pvc-9d54f6eb-4458-40a3-9855-df4b48daeeef   ip-192-168-1-6.ap-northeast-2.compute.internal   true       56s


# 파일 내용 추가 저장 확인
kubectl exec app -- tail -f /data/out.txt
Sat Feb 22 14:51:17 UTC 2025
Sat Feb 22 14:51:22 UTC 2025
Sat Feb 22 14:51:27 UTC 2025
Sat Feb 22 14:51:32 UTC 2025
Sat Feb 22 14:51:37 UTC 2025
Sat Feb 22 14:51:42 UTC 2025
Sat Feb 22 14:51:47 UTC 2025
Sat Feb 22 14:51:52 UTC 2025
Sat Feb 22 14:51:57 UTC 2025
Sat Feb 22 14:52:02 UTC 2025

 

생성된 EBS 볼륨의 volumeHandle을 확인해 웹 콘솔에서 확인할 수 있습니다.

# 추가된 EBS 볼륨 상세 정보 확인
$ kubectl get pv -o jsonpath="{.items[0].spec.csi.volumeHandle}"
vol-0dfe08ec2df70ab74

 

 

웹 콘솔을 살펴보면 아래와 같이 확인됩니다.

 

 

PV 정보를 확인해보면 nodeAffinity가 지정되어 있고, ap-northeast-2a로 지정이 되어 있는 것을 알 수 있습니다. 파드 배포된 노드를 확인해보면 ip-192-168-1-6.ap-northeast-2.compute.interna 이고, 해당 노드의 zone 정보인 ap-northeast-2a 일치하는 것을 알 수 있습니다.

# PV 상세 확인
$ kubectl get pv -o yaml
apiVersion: v1
items:
- apiVersion: v1
  kind: PersistentVolume
  metadata:
    annotations:
      pv.kubernetes.io/provisioned-by: ebs.csi.aws.com
      volume.kubernetes.io/provisioner-deletion-secret-name: ""
      volume.kubernetes.io/provisioner-deletion-secret-namespace: ""
    creationTimestamp: "2025-02-22T14:50:12Z"
    finalizers:
    - external-provisioner.volume.kubernetes.io/finalizer
    - kubernetes.io/pv-protection
    - external-attacher/ebs-csi-aws-com
    name: pvc-9d54f6eb-4458-40a3-9855-df4b48daeeef
    resourceVersion: "17654"
    uid: 657630c7-f924-4b00-af0b-38af7bbddc00
  spec:
    accessModes:
    - ReadWriteOnce
    capacity:
      storage: 4Gi
    claimRef:
      apiVersion: v1
      kind: PersistentVolumeClaim
      name: ebs-claim
      namespace: default
      resourceVersion: "17628"
      uid: 9d54f6eb-4458-40a3-9855-df4b48daeeef
    csi:
      driver: ebs.csi.aws.com
      fsType: xfs
      volumeAttributes:
        storage.kubernetes.io/csiProvisionerIdentity: 1740233857923-9409-ebs.csi.aws.com
      volumeHandle: vol-0dfe08ec2df70ab74
    nodeAffinity:
      required:
        nodeSelectorTerms:
        - matchExpressions:
          - key: topology.kubernetes.io/zone
            operator: In
            values:
            - ap-northeast-2a
    persistentVolumeReclaimPolicy: Delete
    storageClassName: gp3
    volumeMode: Filesystem
  status:
    lastPhaseTransitionTime: "2025-02-22T14:50:12Z"
    phase: Bound
kind: List
metadata:
  resourceVersion: ""

$ kubectl get po -owide
NAME             READY   STATUS    RESTARTS   AGE    IP              NODE                                             NOMINATED NODE   READINESS GATES
app              1/1     Running   0          4m4s   192.168.1.252   ip-192-168-1-6.ap-northeast-2.compute.internal   <none>           <none>
nsenter-58d5bd   1/1     Running   0          63m    192.168.1.6     ip-192-168-1-6.ap-northeast-2.compute.internal   <none>           <none>

$ kubectl get node --label-columns=topology.ebs.csi.aws.com/zone,topology.k8s.aws/zone-id
NAME                                               STATUS   ROLES    AGE   VERSION               ZONE              ZONE-ID
ip-192-168-1-6.ap-northeast-2.compute.internal     Ready    <none>   68m   v1.31.5-eks-5d632ec   ap-northeast-2a   apne2-az1
ip-192-168-2-172.ap-northeast-2.compute.internal   Ready    <none>   68m   v1.31.5-eks-5d632ec   ap-northeast-2b   apne2-az2
ip-192-168-3-246.ap-northeast-2.compute.internal   Ready    <none>   68m   v1.31.5-eks-5d632ec   ap-northeast-2c   apne2-az3

 

일련의 과정을 다시 살펴보면 WaitForFirstConsumer로 지정된 PVC는 파드의 스케줄링이 된 이후 PV가 생성되며, PV는 파드가 스케줄링 된 노드의 토폴로지 정보를 따라 nodeAffinity로 동일한 토폴로지에 배포되는 것을 알 수 있습니다.

 

생성된 리소스를 삭제하고 실습을 마무리하겠습니다.

kubectl delete pod app & kubectl delete pvc ebs-claim

 

 

5. Amazon EFS CSI Driver 사용

Amazon EFS CSI Driver를 사용하기 위해서 addon을 설치하겠습니다.

# EFS 정보 확인 
aws efs describe-file-systems --query "FileSystems[*].FileSystemId" --output text

# 아래는 aws-efs-csi-driver 전체 버전 정보와 기본 설치 버전(True) 정보 확인
aws eks describe-addon-versions \
    --addon-name aws-efs-csi-driver \
    --kubernetes-version 1.31 \
    --query "addons[].addonVersions[].[addonVersion, compatibilities[].defaultVersion]" \
    --output text

# IAM 정책 생성
curl -s -O https://raw.githubusercontent.com/kubernetes-sigs/aws-efs-csi-driver/master/docs/iam-policy-example.json
aws iam create-policy --policy-name AmazonEKS_EFS_CSI_Driver_Policy --policy-document file://iam-policy-example.json

# ISRA 설정 : 고객관리형 정책 AmazonEKS_EFS_CSI_Driver_Policy 사용
eksctl create iamserviceaccount \
  --name efs-csi-controller-sa \
  --namespace kube-system \
  --cluster ${CLUSTER_NAME} \
  --attach-policy-arn arn:aws:iam::aws:policy/service-role/AmazonEFSCSIDriverPolicy \
  --approve \
  --role-only \
  --role-name AmazonEKS_EFS_CSI_DriverRole

# ISRA 확인
eksctl get iamserviceaccount --cluster ${CLUSTER_NAME}

NAMESPACE       NAME                            ROLE ARN
kube-system     aws-load-balancer-controller    arn:aws:iam::430118812536:role/eksctl-myeks-addon-iamserviceaccount-kube-sys-Role1-KfYBX6UfNuOM
kube-system     ebs-csi-controller-sa           arn:aws:iam::430118812536:role/AmazonEKS_EBS_CSI_DriverRole
kube-system     efs-csi-controller-sa           arn:aws:iam::430118812536:role/AmazonEKS_EFS_CSI_DriverRole

# Amazon EFS CSI driver addon 배포(설치)
export ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
eksctl create addon --name aws-efs-csi-driver --cluster ${CLUSTER_NAME} --service-account-role-arn arn:aws:iam::${ACCOUNT_ID}:role/AmazonEKS_EFS_CSI_DriverRole --force

2025-02-23 00:15:55 [ℹ]  Kubernetes version "1.31" in use by cluster "myeks"
2025-02-23 00:15:56 [ℹ]  IRSA is set for "aws-efs-csi-driver" addon; will use this to configure IAM permissions
2025-02-23 00:15:56 [!]  the recommended way to provide IAM permissions for "aws-efs-csi-driver" addon is via pod identity associations; after addon creation is completed, run `eksctl utils migrate-to-pod-identity`
2025-02-23 00:15:56 [ℹ]  using provided ServiceAccountRoleARN "arn:aws:iam::430118812536:role/AmazonEKS_EFS_CSI_DriverRole"
2025-02-23 00:15:56 [ℹ]  creating addon


# 확인
eksctl get addon --cluster ${CLUSTER_NAME}

2025-02-23 00:16:20 [ℹ]  Kubernetes version "1.31" in use by cluster "myeks"
2025-02-23 00:16:20 [ℹ]  getting all addons
2025-02-23 00:16:22 [ℹ]  to see issues for an addon run `eksctl get addon --name <addon-name> --cluster <cluster-name>`
NAME                    VERSION                 STATUS          ISSUES  IAMROLE                                                                  UPDATE AVAILABLE CONFIGURATION VALUES                                            POD IDENTITY ASSOCIATION ROLES
aws-ebs-csi-driver      v1.39.0-eksbuild.1      ACTIVE          0                                                                                "node":
  "volumeAttachLimit": 31
  "enableMetrics": true
aws-efs-csi-driver      v2.1.4-eksbuild.1       CREATING        0       arn:aws:iam::430118812536:role/AmazonEKS_EFS_CSI_DriverRole
coredns                 v1.11.4-eksbuild.2      ACTIVE          0
kube-proxy              v1.31.3-eksbuild.2      ACTIVE          0
metrics-server          v0.7.2-eksbuild.2       ACTIVE          0
vpc-cni                 v1.19.2-eksbuild.5      ACTIVE          0       arn:aws:iam::430118812536:role/eksctl-myeks-addon-vpc-cni-Role1-Y1231NkEEKPX                              enableNetworkPolicy: "true"

kubectl get pod -n kube-system -l "app.kubernetes.io/name=aws-efs-csi-driver,app.kubernetes.io/instance=aws-efs-csi-driver"

NAME                                  READY   STATUS    RESTARTS   AGE
efs-csi-controller-64fc4bc65d-64wjt   3/3     Running   0          38s
efs-csi-controller-64fc4bc65d-bnfvp   3/3     Running   0          38s
efs-csi-node-5sp5l                    3/3     Running   0          39s
efs-csi-node-62kwv                    3/3     Running   0          39s
efs-csi-node-7mw5g                    3/3     Running   0          39s


kubectl get pod -n kube-system -l app=efs-csi-controller -o jsonpath='{.items[0].spec.containers[*].name}' ; echo

efs-plugin csi-provisioner liveness-probe

kubectl get csidrivers efs.csi.aws.com -o yaml

apiVersion: storage.k8s.io/v1
kind: CSIDriver
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"storage.k8s.io/v1","kind":"CSIDriver","metadata":{"annotations":{},"name":"efs.csi.aws.com"},"spec":{"attachRequired":false}}
  creationTimestamp: "2025-02-22T13:34:14Z"
  name: efs.csi.aws.com
  resourceVersion: "24198"
  uid: 09e4a6b6-5b58-44a0-8c46-0da5716364d1
spec:
  attachRequired: false
  fsGroupPolicy: ReadWriteOnceWithFSType
  podInfoOnMount: false
  requiresRepublish: false
  seLinuxMount: false
  storageCapacity: false
  volumeLifecycleModes:
  - Persistent

 

앞서 EBS CSI Driver와 유사한 결과이므로 부연 설명 없이 EFS를 사용하는 샘플 파드를 배포해 보게습니다.

# 모니터링
watch 'kubectl get sc efs-sc; echo; kubectl get pod,pvc,pv'

# 실습을 위한 코드 clone
git clone https://github.com/kubernetes-sigs/aws-efs-csi-driver.git /root/efs-csi
cd /root/efs-csi/examples/kubernetes/multiple_pods/specs && tree
.
├── claim.yaml
├── pod1.yaml
├── pod2.yaml
├── pv.yaml
└── storageclass.yaml

0 directories, 5 files

# EFS 스토리지클래스 생성 및 확인
cat storageclass.yaml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: efs-sc
provisioner: efs.csi.aws.com

kubectl apply -f storageclass.yaml
kubectl get sc efs-sc
NAME     PROVISIONER       RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
efs-sc   efs.csi.aws.com   Delete          Immediate           false                  4s

# PV 생성 및 확인 : volumeHandle을 자신의 EFS 파일시스템ID로 변경
EfsFsId=$(aws efs describe-file-systems --query "FileSystems[*].FileSystemId" --output text)
sed -i "s/fs-4af69aab/$EfsFsId/g" pv.yaml
cat pv.yaml
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-0395692e3bac346bc # 변경되어 있어야 함

kubectl apply -f pv.yaml
kubectl get pv; kubectl describe pv

# PVC 생성 및 확인
cat claim.yaml
kubectl apply -f claim.yaml
kubectl get pvc

# 파드 생성 및 연동 : 파드 내에 /data 데이터는 EFS를 사용
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: app1
  labels:
    app: my-app
spec:
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector:
            matchExpressions:
              - key: app
                operator: In
                values:
                  - my-app
          topologyKey: "kubernetes.io/hostname"
  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
  labels:
    app: my-app
spec:
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector:
            matchExpressions:
              - key: app
                operator: In
                values:
                  - my-app
          topologyKey: "kubernetes.io/hostname"
  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
EOF


# 파드가 각 다른 노드에서 실행 중입니다.
kubectl get po -owide
NAME             READY   STATUS    RESTARTS   AGE     IP              NODE                                               NOMINATED NODE   READINESS GATES
app1             1/1     Running   0          62s     192.168.1.11    ip-192-168-1-6.ap-northeast-2.compute.internal     <none>           <none>
app2             1/1     Running   0          62s     192.168.2.19    ip-192-168-2-172.ap-northeast-2.compute.internal   <none>           <none>

# 공유 저장소 저장 동작 확인
kubectl exec -ti app1 -- ls -l /data
total 8
-rw-r--r--    1 root     root           812 Feb 22 15:41 out1.txt
-rw-r--r--    1 root     root           522 Feb 22 15:41 out2.txt

 

각 노드에서 EFS에 대한 정보를 바탕으로 dig를 수행해보면 아래와 같이 private ip가 확인됩니다.

# EFS 정보
aws efs describe-file-systems --query "FileSystems[*].FileSystemId" --output text
fs-0395692e3bac346bc

# NIC 정보
aws efs describe-mount-targets --file-system-id $(aws efs describe-file-systems --query "FileSystems[*].FileSystemId" --output text) --query "MountTargets[*].IpAddress" --output text
192.168.3.14    192.168.1.59    192.168.2.142

# 노드에서 확인
[root@ip-192-168-1-6 /]# dig +short fs-0395692e3bac346bc.efs.ap-northeast-2.amazonaws.com
192.168.1.59

[root@ip-192-168-1-6 /]# findmnt -t nfs4
TARGET                                                                                            SOURCE      FSTYPE OPTIONS
/var/lib/kubelet/pods/0239b41c-81fa-42bc-86f8-8c98de0fd54d/volumes/kubernetes.io~csi/efs-pv/mount 127.0.0.1:/ nfs4   rw,relatime,vers=4.1,rsize=1048576,wsize=1048576,namle
[root@ip-192-168-1-6 /]#


[root@ip-192-168-2-172 /]# dig +short fs-0395692e3bac346bc.efs.ap-northeast-2.amazonaws.com
192.168.2.142

[root@ip-192-168-2-172 /]# findmnt -t nfs4
TARGET                                                                                            SOURCE      FSTYPE OPTIONS
/var/lib/kubelet/pods/35a9db83-e58b-4a9f-a367-a2399c4e810b/volumes/kubernetes.io~csi/efs-pv/mount 127.0.0.1:/ nfs4   rw,relatime,vers=4.1,rsize=1048576,wsize=1048576,namle

 

노드에서 dns query 결과를 보면 각 EFS에 대해서 각 서브넷에 연결된 private IP로 응답하는 것을 볼 수 있는데 이 IP로 직접 연결된 것은 아니고, nfs의 연결이 127.0.0.1로 되어 있는 것을 알 수있습니다.

노드에 efs-proxy 라는 프로세스가 실행 중인 걸로 봐서는 이 프로세스가 연결에 어떤 역할을 하는 것이 아닌가 의심은 되는데, 공식 문서에도 정확한 내용은 확인 할 수 없었습니다. 다만 추정하기로는 EFS로 연결되는 각 서브넷의 ENI에 대해서 가용성을 보장하기 위해서 로컬 proxy로 연결하여 추상화 시키고, 이후 ENI 통신하도록 하는 것이 아닌가 생각이 됩니다.

[root@ip-192-168-1-6 /]# ss -tlpn
State           Recv-Q          Send-Q                   Local Address:Port                    Peer Address:Port         Process
LISTEN          0               4096                         127.0.0.1:61679                        0.0.0.0:*             users:(("aws-k8s-agent",pid=2984,fd=12))
LISTEN          0               4096                         127.0.0.1:10248                        0.0.0.0:*             users:(("kubelet",pid=2497,fd=14))
LISTEN          0               1024                         127.0.0.1:20828                        0.0.0.0:*             users:(("efs-proxy",pid=42230,fd=9))
LISTEN          0               4096                         127.0.0.1:39495             ..
[root@ip-192-168-1-6 /]# ps -ef |grep 42230
root       42230   34116  0 15:39 ?        00:00:00 /sbin/efs-proxy /var/run/efs/stunnel-config.fs-0395692e3bac346bc.var.lib.kubelet.pods.0239b41c-81fa-42bc-86f8-8c98de0fd54d.volumes.kubernetes.io~csi.efs-pv.mount.20828 --tls
root       48005   42982  0 15:53 ?        00:00:00 grep --color=auto 42230

 

마지막으로 리소스를 삭제하도록 하겠습니다.

# 쿠버네티스 리소스 삭제
kubectl delete pod app1 app2
kubectl delete pvc efs-claim && kubectl delete pv efs-pv && kubectl delete sc efs-sc

 

 

마무리

여기까지 해서 EKS의 스토리지 옵션을 살펴보았습니다. 물론 실습에서 다루지 않은 다른 옵션이 있으므로, 공식문서를 참고 부탁드립니다.

참고로 EKS와 CloudFormation으로 생성된 리소스는 [3-2] EKS 노드 그룹(https://a-person.tistory.com/34)에서 삭제하도록 하겠습니다.

 

+ Recent posts