본문 바로가기
카테고리 없음

[AEWS-3기] Storage - (1) 쿠버네티스의 스토리지

by james_janghun 2025. 2. 19.

 

이 글은 가시다님과 함께하는 AEWS 3기의 내용을 정리한 것입니다

 

오늘은 Storage에 대해서 정리합니다.

 

EBS에 관하여

AWS에서 스토리지 서비스라고 하면 EBS, EFS, S3정도가 생각난다.

그 중 EBS는 Elastic Block Store 서비스로 블록스토리지를 이야기한다. 쉽게말하면 외장하드디스크 이다.

 

AWS 공식문서에서 EBS는 다음과 같이 소개한다.

 

Amazon EBS gp3 볼륨은 범용 SSD 기반 EBS 볼륨의 최근 세대로 고객은 저장소 용량에 관계없이 성능을 프로비저닝하는 동시에 기존 gp2 볼륨보다 GB당 가격을 최대 20% 감축할 수 있습니다. gp3 볼륨을 통해 고객은 블록 스토리지 용량을 추가로 프로비저닝하지 않고도 IOPS(초당 입출력 연산)와 처리량을 확장할 수 있습니다. 즉 고객은 필요한 스토리지에 대해서만 비용을 지불하면 됩니다.

새로운 gp3 볼륨은 볼륨 크기에 관계없이 3,000 IOPS의 예측 가능한 기본 성능을 제공하도록 설계되었습니다. 애플리케이션에 기준보다 더 많은 성능이 필요한 사용 사례의 경우, 용량을 추가할 필요 없이 필요한 IOPS 또는 처리량을 프로비저닝하기만 하면 됩니다. gp3 볼륨은 MySQL, Cassandra, 가상 데스크톱, Hadoop 분석 클러스터를 포함하여 낮은 비용으로 고성능이 필요한 다양한 애플리케이션에 이상적입니다. 

 

EBS에서 제공하는 볼륨 유형은 크게 gp3, gp2가 있다. gp는 General Purpose의 약자이다.

이 둘을 비교한 내용은 AWS 공식문서에서 먼저 살펴보면 다음과 같다.

https://docs.aws.amazon.com/ko_kr/emr/latest/ManagementGuide/emr-plan-storage-compare-volume-types.html

 

Amazon EBS 볼륨 유형 gp2 및 gp3 비교 - Amazon EMR

이 페이지에 작업이 필요하다는 점을 알려 주셔서 감사합니다. 실망시켜 드려 죄송합니다. 잠깐 시간을 내어 설명서를 향상시킬 수 있는 방법에 대해 말씀해 주십시오.

docs.aws.amazon.com

 

 

단순하게 미료만 해봐도 볼륨당 최대 처리량이 4배, gp3는 기본으로 3000IOPS 제공, gp2는 GB당 3IOPS를 기본으로 제공하기 때문에 전반적으로 gp3가 우수하다. 심지어 가격도 보면 gp3가 0.08달러인데 비해 gp2는 0.10 달러로 GiB당 0.02달러가 차이난다. 

이게 작아보여도 늘 큰게 1000GiB당 20달러가 차이난다는 것이다.

서비스를 제공할때 1000GiB는 정말 작은 수준이라는걸 알것이다. 볼륨의 용량만해도 엄청나게 차이가 난다.

 

따라서 gp3가 gp2보다 비용도 저렴한데 더 우수하다고 볼 수 있다.

 

EKS에서의 스토리지

1. Temporary filesystem

Pod의 기본 저장방식은 Temporary filesystem으로 즉 파드 정지 = 데이터 삭제를 의미

Temporary filesystem으로 기본적으로 파드 내부의 데이터는 파드가 정지되면 모두 삭제된다. 마치 일회용품 같은 것이다.

파드 내에 여러 개의 컨테이너가 있다면 해당 컨테이너가 각각 내부적으로 임시볼륨을 만들어서 사용하게 된다. 이 볼륨 자체도 Temporary filesystem이 적용되므로 파드가 삭제시 모두 삭제되는 문제가 있었다.

 

그림에서 왼쪽은 파드내에 1개의 컨테이너의 임시 볼륨을 나타내고, 오른쪽은 여러개의 컨테이너의 임시볼륨을 나타낸다.

 

https://aws.amazon.com/ko/blogs/tech/persistent-storage-for-kubernetes/

 

이런 임시 저장소를 emptydir이라고 하고 있다. 

확인을 위해서 redis 파드를 만들어본다.

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: redis
spec:
  terminationGracePeriodSeconds: 0
  containers:
  - name: redis
    image: redis
EOF

 

redis 파드 내 hello라는 내용의 /data/hello.txt를 만든다.

kubectl exec -it redis -- pwd
kubectl exec -it redis -- sh -c "echo hello > /data/hello.txt"
kubectl exec -it redis -- cat /data/hello.txt

ps를 설치하고 pod 내 프로세스 1을 삭제하게되면 pod가 재시작된다.

kubectl exec -it redis -- sh -c "apt update && apt install procps -y"
kubectl exec -it redis -- ps aux

# kill1으로 파드를 재시작한다.
kubectl exec -it redis -- kill 1
kubectl get pod

 

redis 파드 내에 파일을 확인한다.

kubectl exec -it redis -- cat /data/hello.txt
kubectl exec -it redis -- ls -l /data

 

pod가 재시작되면 이전에 생성한 hello.txt 파일이 사라진 것을 확인할 수 있다.

 

 

2. PV(Persistent Volume) / PVC(Persistent Volume Claim)

쿠버네티스에서는 PV가 도커 볼륨과 같이 외부 볼륨을 의미한다

그래서 데이터를 보존하기 위해 Stateful한 볼륨을 만들어서 파드 외부에 저장하는 방식을 고안했다.

그래서 나온게 PV(Persistent Volume), PVC(Persistent Volume Claim)이다. 이 둘은 한쌍으로 PV는 어느 노드에서도 연결하여 사용이 가능하도록 만들어진 볼륨이고 PVC는 PV를 생성하기 위한 명령서랑 비슷한 것으로 하나로 묶어서 보면된다. 왜냐하면 기본적으로 pod는 PV를 직접 호출하는게 아니라 PVC에 의해서 PV를 가져오기 때문이다. 

 

하지만 PV는 NFS, AWS EBS, Ceph과 같이 우리가 흔히 이용하는 온프레미스의 디스크나 클라우드 내의 디스크를 PV로 만들어서 쿠버네티스 상에 인식을 시켜주는 것으로 추상적인 개념이다. 실질적인 디스크는 아래와 같이 존재해야한다.

  • CSI(Container Storage Interface) → (예: Amazon EFS , Amazon EBS , Amazon FSx 등)
  • ISCSI(SCSI over IP) 스토리지
  • 노드에 마운트된 LOCAL 저장 장치
  • NFS(Network File System) 스토리지

 

https://aws.amazon.com/ko/blogs/tech/persistent-storage-for-kubernetes/

 

다만 파드가 여러 사유로 삭제되고 다시 생성되는 경우나, 신규로 비슷한 파드를 찍어내는 경우 등등 파드가 생성될 때 자동으로 볼륨을 마운트하는 기능이 필요했다. 이를 동적 프로비저닝(Dynamic Provisioning)이라고 한다.

 

따라서 동적 프로비저닝을 사용하면 PV 객체를 직접 생성할 필요없이, PVC를 통해서 자동으로 PV를 찍어낸다.

이를 구현하는게 Storage Class이다. Storage Class는 일종의 볼륨 추상화 그룹으로 해당 스토리지 클래스에 정의된 곳에서 자동으로 PV를 생성할 수 있도록 구성한 것이다.

 

Local Path Provisioner

- 디렉터리명을 다이나믹하게 지정하면서 StorageClass를 사용해 동적으로 PV를 사용할 수 있다.

 

설치

 kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.31/deploy/local-path-storage.yaml

 

생성하는 리소스는 다음과 같이 namespace, serviceaccount, role, clusterrole, rolebinding, clusterrolebinidng, deployment, storageclass, configmap을 배포합니다.

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

PVC 생성

local-path라는 storageclass를 기반으로 하는 PVC를 먼저 생성한다.

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

 

파드 생성

volume에서 PVC로 localpath-claim을 사용하는 파드를 생성한다. 이 파드는 지정된 명령에 따라서 date -u 명령을 5초마다 기록하게 된다.

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: localpath-claim
EOF

 

실제로 확인해보면 다음과 같이 /data/out.txt에 5초마다 date -u가 기록되는 것을 확인할 수 있다.

kubectl exec -it app -- tail -f /data/out.txt

 

tree 명령어를 통해서 out.txt파일을 확인해보면 노드 1에서 조회되는 것을 확인할 수 있다.

위치는 /opt/local-path-provisioner로 pvc 이름과 함께 out.txt를 확인할 수 있다. 실제로 파일 내용을 확인해봐도 다음과 같이 5초마다 찍히고 있는것을 확인할 수 있다.

 

PV에 대해서 descirbe 해보면 path에서 해당 정보를 역시 확인할 수 있었다.

 

파드 삭제

이제 파드를 삭제한 뒤 다시 생성해보자. 파드를 삭제해도 해당 pv 정보는 살아있는 것을 확인할 수 있다.

 

파드 재생성

자 이제 파드를 다시 생성해보자.

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: localpath-claim
EOF

 

내용을 확인해보면 약 50초간의 데이터가 없다가 다시 생성된 것을 확인할 수 있다.

즉 같은 PV를 계속 공유한다는 것을 알 수 있었다.

kubectl exec -it app -- head /data/out.txt
kubectl exec -it app -- tail -f /data/out.txt

 

 

CSI(Container Storage Interface) Controller

쿠버네티스는 다양한 플랫폼을 지원하기 때문에 온프레미스부터 클라우드까지 다양한 벤더사의 플랫폼 위에서 사용된다.

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

 

Store application data for your cluster - Amazon EKS

Thanks for letting us know this page needs work. We're sorry we let you down. If you've got a moment, please tell us how we can make the documentation better.

docs.aws.amazon.com

 

따라서 다음과 같이 각 벤더사의 각 서비스에 맞는 CSI Controller 생태계가 존재하니 해당 내용을 한번 확인해보고 플랫폼과 서비스를 생각해보면 좋을 것 같다.

 

aws-ebs-csi-controller 

AWS에서는 단연 EBS가 거의 보편적인 볼륨으로 사용된다. 따라서 EKS를 사용하는 운영자라면 EBS-CSI-Controller가 매우 중요할 것이다. 공식 github 주소는 https://github.com/kubernetes-sigs/aws-ebs-csi-driver 입니다.

 

EBS CSI Controller 동작 방식

AWS EBS CSI controller의 동작을 악분님께서 잘 설명하셔서 그림을 가져왔다.

https://malwareanalysis.tistory.com/598

 

그림의 내용은 다음과 같다.

1) API 서버는 csi-controller에 pod PV 요청을 한다.

 

2) Csi-controller는 API Server의 pod에 PV요청이 발생하면 AWS API를 호출한다.

- 여기서 CSI-controller는 각 벤더사 별로 존재한다.

 

3) AWS API는 EBS를 생성한다.

- 이때 당연히 Create Volume에 대한 AWS 권한이 필요하며, 적절한 IRSA가 동작해야한다. 생성된 EBS는 Server(EKS Node)레벨에서 device로 인식되어야 한다.

 

4) EKS API서버는 kubelet에 적절한 device 볼륨에 대해서 마운트 요청을 날린다.

 

5) kubelet은 csi-node를 통해  pod에 해당 device를 마운트하라고 요청한다.

 

6) csi-node는 인식된 device정보를 확인하고, 요청과 일치하는지 파악한 후 요청된 device에 대하여 요청된 pod에 마운트 시킨다.

 

EBS CSI Controller 설치

eksctl로 간단하게 EKS에서 사용할 IRSA를 생성한다. IRSA는 Kubernetes의 Service Account와 AWS의 IAM Role을 연결하는 것으로 쿠버네티스의 SA가 AWS를 통제할 수 있는 역할을 Assume하는 기능을 한다.

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

 

 

AWS 내에서 실제로 보면 이 역할이다.

 

아래와 같은 권한이 필요해요. 물론 이것을 세부적으로 통제하는 회사면 Resource부분을 아주 타이트하게 관리해야한다. 그럼 대신에 정책 업데이트를 수동으로 해야한다.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeAvailabilityZones",
                "ec2:DescribeInstances",
                "ec2:DescribeSnapshots",
                "ec2:DescribeTags",
                "ec2:DescribeVolumes",
                "ec2:DescribeVolumesModifications"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:CreateSnapshot",
                "ec2:ModifyVolume"
            ],
            "Resource": "arn:aws:ec2:*:*:volume/*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:AttachVolume",
                "ec2:DetachVolume"
            ],
            "Resource": [
                "arn:aws:ec2:*:*:volume/*",
                "arn:aws:ec2:*:*:instance/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:CreateVolume",
                "ec2:EnableFastSnapshotRestores"
            ],
            "Resource": "arn:aws:ec2:*:*:snapshot/*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:CreateTags"
            ],
            "Resource": [
                "arn:aws:ec2:*:*:volume/*",
                "arn:aws:ec2:*:*:snapshot/*"
            ],
            "Condition": {
                "StringEquals": {
                    "ec2:CreateAction": [
                        "CreateVolume",
                        "CreateSnapshot"
                    ]
                }
            }
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DeleteTags"
            ],
            "Resource": [
                "arn:aws:ec2:*:*:volume/*",
                "arn:aws:ec2:*:*:snapshot/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:CreateVolume"
            ],
            "Resource": "arn:aws:ec2:*:*:volume/*",
            "Condition": {
                "StringLike": {
                    "aws:RequestTag/ebs.csi.aws.com/cluster": "true"
                }
            }
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:CreateVolume"
            ],
            "Resource": "arn:aws:ec2:*:*:volume/*",
            "Condition": {
                "StringLike": {
                    "aws:RequestTag/CSIVolumeName": "*"
                }
            }
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DeleteVolume"
            ],
            "Resource": "arn:aws:ec2:*:*:volume/*",
            "Condition": {
                "StringLike": {
                    "ec2:ResourceTag/ebs.csi.aws.com/cluster": "true"
                }
            }
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DeleteVolume"
            ],
            "Resource": "arn:aws:ec2:*:*:volume/*",
            "Condition": {
                "StringLike": {
                    "ec2:ResourceTag/CSIVolumeName": "*"
                }
            }
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DeleteVolume"
            ],
            "Resource": "arn:aws:ec2:*:*:volume/*",
            "Condition": {
                "StringLike": {
                    "ec2:ResourceTag/kubernetes.io/created-for/pvc/name": "*"
                }
            }
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:CreateSnapshot"
            ],
            "Resource": "arn:aws:ec2:*:*:snapshot/*",
            "Condition": {
                "StringLike": {
                    "aws:RequestTag/CSIVolumeSnapshotName": "*"
                }
            }
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:CreateSnapshot"
            ],
            "Resource": "arn:aws:ec2:*:*:snapshot/*",
            "Condition": {
                "StringLike": {
                    "aws:RequestTag/ebs.csi.aws.com/cluster": "true"
                }
            }
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DeleteSnapshot"
            ],
            "Resource": "arn:aws:ec2:*:*:snapshot/*",
            "Condition": {
                "StringLike": {
                    "ec2:ResourceTag/CSIVolumeSnapshotName": "*"
                }
            }
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DeleteSnapshot"
            ],
            "Resource": "arn:aws:ec2:*:*:snapshot/*",
            "Condition": {
                "StringLike": {
                    "ec2:ResourceTag/ebs.csi.aws.com/cluster": "true"
                }
            }
        }
    ]
}

 

 

깃허브에 들어가보니 2025-01-13일 자로 API 요청에 대한 수정사항이 있다. 다만 이 내용은 AmazonEBSCSIDriverPolicy 라는 기본 AWS 관리정책을 사용한다면 자동 업데이트 된다고 한다. 만약 custom해서 정책을 사용하시는 분들은 해당 policy를 추가하기 바란다.

https://github.com/kubernetes-sigs/aws-ebs-csi-driver/issues/2190

 

[ACTION REQUIRED] Update to the EBS CSI Driver IAM Policy · Issue #2190 · kubernetes-sigs/aws-ebs-csi-driver

Updates 2025-01-13: AWS has deployed the update to the AmazonEBSCSIDriverPolicy managed policy to all AWS partitions. Any driver installation using this policy should automatically be updated and n...

github.com

 

 

 

커스텀 policy를 이용하는 운영자라면 이 내용을 반드시 넣어줘야 한다고 한다.

{
    "Effect": "Allow",
    "Action": "ec2:CreateVolume",
    "Resource": "arn:*:ec2:*:*:snapshot/*"
}

 

 

이제 역할을 만들어주고 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
kubectl get sa -n kube-system ebs-csi-controller-sa -o yaml | head -5

 

이렇게 Service account 가 생성되고,

 

Describe 해보면 annotation에 Role이 보인다.

 

Add on도 잘 보이는 것을 확인할 수 있다.

 

gp3 스토리지 클래스 생성

파라미터에서 gp3를 지정하고 iops, throughput 등 다양한 ebs 옵션을 지정할 수 있다.

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 sc
kubectl describe sc gp3 | grep Parameters

PVC 생성

PVC 생성 시 StorageClass를 gp3로 지정했다.

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

 

pod 생성

pod 생성시 volumes에 PVC 정보를 앞서 생성한 evs-claim이라는 이름으로 넣어준다. 또한 volume의 이름인 persistent-storage를

그대로 volumemounts 위치에 이름에다가 넣어준다.

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

 

생성되는 pv, pvc랑 마운트정보를 확인하고 EVS 볼륨 상세 정보를 통해서 EBS정보를 확인해보자.

아까 등록한 정보와 일치한다.

aws ec2 describe-volumes --volume-ids $(kubectl get pv -o jsonpath="{.items[0].spec.csi.volumeHandle}") | jq

 

 

볼륨을 증가할 필요가 있다면 다음 명령어로 증가시킬 수 있다.

현재 4Gi를 10Gi로 변경해보자.

kubectl patch pvc ebs-claim -p '{"spec":{"resources":{"requests":{"storage":"10Gi"}}}}'