[EKS@CloudNet] EKS Storage & Nodegroup

2024. 3. 23. 16:28EKS@CloudNet

- CloudNet에서 주관하는 EKS 스터디 2기 내용입니다

- 매번 좋은 스터디를 진행해 주시는 CloudNet 팀 감사드립니다

 

 

0. Setup 

- 기존과 비슷하게 가시다님이 배포해주신 원클릭 배포 파일을 사용하지만, IAM의 serviceAccount와 maxPodPerNode (node당 최대 pod 갯수) 내용이 추가되어 있다. 

 

 

- CloudFormation에 yaml을 어떻게 적용할지 모른다면 우선 AWS 콘솔의 CloudFormation 페이지에 들어가서, "스택 생성" 을 누르면 yaml을 업로드하고 기존과 같이 스택을 생성할 수 있다.

 

 

 

- EFS filesystem 확인

가시다님이 미리 $EfsFsId 로 EFS 콘솔의 ID를 지정해놓으셨다. 따라서 아래의 명령어를 이용해서 원하는 디렉토리에 마운트하고, 내용을 확인할 수 있다.

echo $EfsFsId
mount -t efs -o tls $EfsFsId:/ /mnt/myefs
df -hT --type nfs4

echo "efs file test" > /mnt/myefs/memo.txt
cat /mnt/myefs/memo.txt

 

실행 후, /mnt/myefs 디렉토리에 마운트된 것을 확인한다.

 

 

- 스토리지 클래스/ CSI 노드 확인

스토리지 클래스: Dynamic Provisioning 을 실행할 때 필요한 디스크 타입을 지정할 수 있다. 사용할 수 있는 다양한 디스크 타입은 이 블로그 에서 자세히 확인할 수 있다.

 

이 중에 emptyDir와 hostPath가 헷갈렸는데, 

emptyDir: Pod의 볼륨으로 pod와 수명을 같이하므로 pod이 내려갈 때 휘발된다!

hostPath: 노드의 로컬 볼륨으로, pod가 내려갈 때 휘발되지는 않지만 호스트 노드의 파일 시스템 경로를 사용하여 컨테이너 내 디렉토리에 마운트하는 방식으로 pod과 다른 노드의 파일시스템에는 접근할 수 없다

 

Dynamic Provisioning: pod에 스토리지를 연동하려면 매 pod마다 물리적인 볼륨, PV, PVC 를 생성해야 하는데, 이 과정을 k8s가 알아서 해 주는 방식이다.

 

CSI(Container Storage Interface) 노드:

별도의 controller pod을 통해서 dynamic provisioning을 사용할 수 있게 한다. 

 

 

 

아래의 코드를 이용하면 우리가 스토리지 클래스로 EBS를 사용하고 있고, 사용하고 있는 CSI 노드의 정보도 확인할 수 있다. 

kubectl get sc
kubectl get sc gp2 -o yaml | yh
kubectl get csinodes

 

- 지난번과 동일하게 노드 IP를 확인하고 이후 노드 접근에 용이하게 PrivateIP 변수로 지정해준다.

N1=$(kubectl get node --label-columns=topology.kubernetes.io/zone --selector=topology.kubernetes.io/zone=ap-northeast-2a -o jsonpath={.items[0].status.addresses[0].address})
N2=$(kubectl get node --label-columns=topology.kubernetes.io/zone --selector=topology.kubernetes.io/zone=ap-northeast-2b -o jsonpath={.items[0].status.addresses[0].address})
N3=$(kubectl get node --label-columns=topology.kubernetes.io/zone --selector=topology.kubernetes.io/zone=ap-northeast-2c -o jsonpath={.items[0].status.addresses[0].address})
echo "export N1=$N1" >> /etc/profile
echo "export N2=$N2" >> /etc/profile
echo "export N3=$N3" >> /etc/profile
echo $N1, $N2, $N3

# 노드 보안그룹 ID 확인
NGSGID=$(aws ec2 describe-security-groups --filters Name=group-name,Values=*ng1* --query "SecurityGroups[*].[GroupId]" --output text)
aws ec2 authorize-security-group-ingress --group-id $NGSGID --protocol '-1' --cidr 192.168.1.100/32

 

- 이후 실습을 위해 지난 시간에 다룬 AWS LoadBalancer도 helm으로 설치해 준다

helm repo add eks https://aws.github.io/eks-charts
helm repo update
helm install aws-load-balancer-controller eks/aws-load-balancer-controller -n kube-system --set clusterName=$CLUSTER_NAME \
  --set serviceAccount.create=false --set serviceAccount.name=aws-load-balancer-controller

 

 

 

 

1. 스토리지: Pod, PVC, PV

- Pod은 기본적으로 상태가 없는(stateless) 하므로, 데이터를 보관하기에 부적합하다. 상술했듯 Pod가 날아가면 내부의 데이터는 모두 날아가기 때문에 보존이 필요한 데이터는 외부 스토리지를 이용해야 한다. 

 

- 이때 사용하는 외부 스토리지가 PV(Persistent Volume) 이다. PV는 stateful하고 Pod와 별개이므로 pod가 내려가도 정보를 저장할 수 있다. 노드의 hostPath가 노드 단위에서의 자원 관리였다면, PV는 클러스터 단위의 자원으로 스토리지 그 자체이다. node1의 Pod A가 node2의 PV B에 접근할 수 있다 (hostPath는 pod과 hostPath가 다른 노드에 있다면 접근이 불가능하다!)

# skeleton yaml for PV from k8s.docs

apiVersion: v1
kind: PersistentVolume
metadata:
  name: foo-pv
spec:
  storageClassName: ""
  claimRef:
    name: foo-pvc
    namespace: foo

 

 

 

 

- PV에 사용자가 하는 요청이 PVC(Persistent Volume Claim)이다. PVC에 따라 쿠버네티스는 적정한 PV를 찾아 PVC에 할당해주고, 최종적으로 Pod에 붙인다. 이때 PV와 PVC는 일대일 대응관계이다.

# skeleton yaml form PVC from k8s.docs

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: myclaim
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  resources:
    requests:
      storage: 8Gi
  storageClassName: slow

 

 

 

- hostPath vs PV-PVC에 대해 직관적으로 설명되어 있는 그림이다. 

https://kubetm.github.io/k8s/03-beginner-basic-resource/volume/

 

실습: 기본 컨테이너 환경의 임시 파일시스템 사용

- busybox 파드를 하나 만들어서 10초 간격으로 현재 시간을 내부 스토리지에 저장하도록 한다. 그리고 터미널을 하나 더 띄워서 저장된 컨텐츠를 모니터링한다. pod의 yaml과 command는 아래와 같다.

 

apiVersion: v1
kind: Pod
metadata: 
  name: busybox
spec: 
  terminationGracePeriodSeconds: 3
  containers: 
  - name: busybox
    image: busybox
    command: 
    - "/bin/sh"
    - "-c"
    - "while true; do date >> /home/pod-out.txt; cd /home; sync; sync; sleep 10; done"

 

# 터미널 1 : pod 생성
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/3/date-busybox-pod.yaml
cat date-busybox-pod.yaml | yh
kubectl apply -f date-busybox-pod.yaml

kubectl get pod

# 터미널 2: 모니터링
kubectl exec busybox -- tail -f /home/pod-out.txt

 

 

10초마다 한번씩 시간이 잘 기록되었다.

 

 

 

 

- 그러면 pod을 삭제했다 다시 생성해도 이전 기록이 남아 있을까? 삭제했다가 재생성한 뒤 해당 txt file을 조회해보면 컨텐츠가 사라진 것을 확인할 수 있다.

# 터미널 1: pod 삭제 후 재생성
kubectl delete pod busybox
kubectl apply -f date-busybox-pod.yaml

# 터미널 2: 확인
kubectl exec busybox -- tail -f /home/pod-out.txt

# 실습 후 삭제
kubectl delete pod busybox

 

 

 

 

 

 

실습: PV/PVC 사용

- 이번에는 pod안의 데이터를 보존하기 위해 local-path-provisioner를 사용하는 PV/PVC를 함께 배포해 차이를 보자. Dynamic Provisioning을 사용하기 위해 StorageClass에서 제공하는 Local Path Provisioner를 사용하는데, 둘의 차이와 장단점을 정리해 보았다.

hostPath Local Path Provider
- 간단하고 직관적이다
- host node에 직접 접근하므로 빠르다

- 다른 node의 hostpath에는 접근할 수 없다
- dynamic provisioning이 가능하여 가용성과 안정성이 높다
- 다른 node의 데이터에 접근할 수 있다

- 네트워크 스토리지에 비해 속도가 느릴 수 있다

 

실습을 위해 local-path-provisioner 스토리지 클래스를 배포하고, pv, pvc, 그리고 pvc를 사용하는 pod 까지 생성한다.

 

 

 

 

# StorageClass local-path-storage 배포
curl -s -O https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml
kubectl apply -f local-path-storage.yaml

# PVC 생성
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/3/localpath1.yaml
cat localpath1.yaml | yh
kubectl apply -f localpath1.yaml

# Pod 생성
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/3/localpath2.yaml
cat localpath2.yaml | yh
kubectl apply -f localpath2.yaml

 

5초마다 현재시간이 찍히는 것을 확인할 수 있고, 현재 호스트에 붙도록 node Affinity가 설정되어 있다.

 

 

 

 

 

 

 

그러면 이제 pod을 내렸다 다시 올리고 데이터가 남아있는지 확인해보자.

# pod tkrwp
kubectl delete pod app

# 잠시 기다리기 #

# pod 재생성
kubectl apply -f localpath2.yaml

# 터미널 2: pv 확인
kubectl exec -it app -- head /data/out.txt

 

 

 

 

pod가 내려갔을 때 시간 기록이 잠시 끊겼지만 데이터는 잘 남아있는 것을 확인할 수 있다.

 

 

 

2. AWS EBS controller

EBS(Elastic Block Store) 는 AWS의 영구적인 블록 스토리지 서비스이다. S3이 객체 스토리지인 것과 가장 큰 차이이다. 

블록 스토리지는 DB, 파일시스템, OS등 구조적 데이터 저장에 용이하고, 객체 스토리지는 파일, 이미지, 오디오 등 비 구조적 데이터 저장에 주로 사용된다. 아래 표는 S3과 EBS의 차이를 간략하게 정리한 것이다.

  S3 EBS
데이터 형식 객체 스토리지 블록 스토리지
용도 비구조 데이터-파일, 이미지, 오디오 구조 데이터-DB, 파일 시스템, OS
액세스 방식 키로 식별, http 액세스 블록 단위 액세스, 파일 시스템 CRUD

 

 

 

 

아래 그림은 AWS EBS와 pod가 연결되는 방식에 대한 도식화이다.

https://malwareanalysis.tistory.com/598

- EBS에 액세스하는 pod와 EBS는 같은 AZ에 있어야 한다.

- 인스턴스와 연결되는 PV, PVC는 이때 EBS를 이어주는 중간다리 역할을 하므로 accessMode를 ReadWriteOnce로 설정해야 한다.

 

 

 

 

실습: EBS 생성

- 아래 코드를 이용하면 1.28 버전 EKS에 맞는 EBS 드라이버를 설치하고, 확인할 수 있다.

aws eks describe-addon-versions \
    --addon-name aws-ebs-csi-driver \
    --kubernetes-version 1.28 \
    --query "addons[].addonVersions[].[addonVersion, compatibilities[].defaultVersion]" \
    --output text

 

- 실제 1.28버전만 설치되어 있음을 확인할 수 있다.

 

 

 

- 해당 클러스터에서 EBS를 관리하는 권한을 가진 Service Account를 생성해준다. 

# ISRA 설정 : 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
  
# ISRA 확인
eksctl get iamserviceaccount --cluster myeks

 

 

 

 

- 또한 EBS-CSI-Controller pod에 EBS/CSI 관련 컨테이너 6개가 들어와 있는 것을 볼 수 있다

(ebs-plubin, csi-orovisioner, cst-attacher, csi-snapshotter, csi-resizer, liveness-probe)

 

 

 

gp3 type의 스토리지를 생성해준다. gp3은 볼륨 크기와 무관하게 볼륨 전체의 iops와, throughput을 지정할 수 있다. 또한 가격적인 면에서도 기존 gp2보다 매력적이다. 기존 gp2는 GB당 3iops로 일정하게 확장되어 용량이 크지 않지만 ips가 많이 필요한 작업에는 적합하지 않았다. 하이퍼커넥트 기술블로그에 gp3 스토리지에 대해 더 자세히 설명되어 있다. 

 

 

 

이제 pvc와 대응하는 pod을 생성해서 사용할 수 있다.

# 워커노드에서 파드에 추가한 EBS 볼륨 확인
aws ec2 describe-volumes --filters Name=tag:ebs.csi.aws.com/cluster,Values=true --output table
aws ec2 describe-volumes --filters Name=tag:ebs.csi.aws.com/cluster,Values=true --query "Volumes[*].{ID:VolumeId,Tag:Tags}" | jq
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

# 워커노드에서 파드에 추가한 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

# PVC 생성
cat <<EOT > awsebs-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ebs-claim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 4Gi
  storageClassName: gp3
EOT
kubectl apply -f awsebs-pvc.yaml
kubectl get pvc,pv

# 파드 생성
cat <<EOT > awsebs-pod.yaml
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
EOT
kubectl apply -f awsebs-pod.yaml

 

 

 

gp3 스토리지가 각각 pod에 대응하여 attach되었음을 확인할 수 있다.

 

 

 

3. AWS EFS Controller

EFS(Elastic File System) 는 AWS의 클라우드 기반 스토리지 서비스이다. EBS가 리전기반인것과 달리 EFS는 클라우드 기반이기 때문에 여러 AZ의 pod에서 접근 및 수정이 가능하다. 아래 그림을 보면 알 수 있듯 각 pod에 연결된 pvc가 EFS Provisioner로 관리되므로 EFS 볼륨에 여러 pod가 액세스할 수 있다.

https://dev.to/awscommunity-asean/aws-eks-with-efs-csi-driver-and-irsa-using-cdk-dgc

 

- S3, EBS, EFS의 차이에 대해서는 SmileShark 블로그 에 더 잘 정리되어 있다.

 

 

 

실습: EFS 생성 및 사용

- EFS 관리를 위한 IAM 을 생성해준다. 

 - EBS와 유사하게 IRSA를 설정해주고 확인한다.

 

 

 

-helm으로 EFS 컨트롤러를 설치한다.

helm repo add aws-efs-csi-driver https://kubernetes-sigs.github.io/aws-efs-csi-driver/
helm repo update
helm upgrade -i aws-efs-csi-driver aws-efs-csi-driver/aws-efs-csi-driver \
    --namespace kube-system \
    --set image.repository=602401143452.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/eks/aws-efs-csi-driver \
    --set controller.serviceAccount.create=false \
    --set controller.serviceAccount.name=efs-csi-controller-sa

 

 

 

 

- aws 콘솔에서 EFS 메뉴를 찾아 들어가면 GUI로 EFS 관련 정보를 확인할 수 있다.

 

 

- 해당 콘솔에 들어오면 EFS가 탑재되는 대상 IP를 알 수 있다.

 

 

 

- 생성한 EFS에 여러 pod가 접근하도록 설정할 수 있다.

PV를 생성하는데, 이때 PV는 스토리지를 새로 만드는 것이 아니라 volumeHandle을 자신의 EFS 파일시스템 ID로 변경해줌으로서 PVC-EFS 사이를 이어주는 중간다리만 하는 것으로 이해했다.

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

# EFS 스토리지클래스 생성 및 확인
cat storageclass.yaml | yh
kubectl apply -f storageclass.yaml
kubectl get sc efs-sc

# 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 | yh
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-05699d3c12ef609e2

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

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

# 파드 생성 및 연동 : 파드 내에 /data 데이터는 EFS를 사용
cat pod1.yaml pod2.yaml | yh
kubectl apply -f pod1.yaml,pod2.yaml

 

 

이제 EFS 에 여러 pod가 접근할 수 있다. 

 

 

 

잊지 말아요 자원 삭제

eksctl delete cluster --name $CLUSTER_NAME && aws cloudformation delete-stack --stack-name $CLUSTER_NAME

 

우리의 지갑은 소중하니까요

 

 

 

 

CKA 준비와 실습 스터디를 병행하니 실무적인 접근과 fundamental한 접근을 동시에 학습할 수 있어서 시야가 넓어지는 것 같다.

읽어주셔서 감사합니다!