본 게시물은 CloudNet@에서 진행하는 AEWS (Amazon EKS Workshop Study) 스터디에 참여하며 정리한 내용입니다.

 

0. 실습 환경

이번 실습에서는 명령어 콘솔로 원클릭 배포를 해보고자 한다. 다음 명령어를 사용하여 실습 환경을 준비하였다. 그 전에 CloudFormation에 해당하는 eks-oneclick2.yaml 파일이 이전 EKS Networking 실습때 사용한 스크립트와 어떤 차이가 있는지 간략히 살펴보자. diff 명령을 통해 이전 실습 파일인 eks-oneclick.yaml과 이번 실습 파일인 eks-oneclick2.yaml 파일을 비교해보았다. "diff A B"와 같이 실행하였을 때 > 로 나오면 B 파일에 추가된 내용을 이야기하며 < 로 나오면 A 파일에 추가된 내용이라고 알려준다. 아래 결과를 통해 살펴볼 때, 이번 스토리지 실습을 위해 Amazon Elastic File System (EFS) 사용을 위한 보안 그룹 및 마운트 대상을 지정하고, bastion에 해당하는 EC2 VM에 amazon-efs-utils 및 df-pv 도구를 미리 설치 및 이전 EKS Networking 실습 확인하였던 AWS Load Balancer Controller를 위한 IRSA 생성까지를 더 준비하는 것으로 확인된다.

$ diff eks-oneclick.yaml eks-oneclick2.yaml
287a288,330
> # EFS
>   EFSSG:
>     Type: AWS::EC2::SecurityGroup
>     Properties:
>       VpcId: !Ref EksVPC
>       GroupDescription: EFS Security Group
>       Tags:
>       - Key : Name
>         Value : !Sub ${ClusterBaseName}-EFS
>       SecurityGroupIngress:
>       - IpProtocol: tcp
>         FromPort: '2049'
>         ToPort: '2049'
>         CidrIp: !Ref VpcBlock
>
>   ElasticFileSystem:
>     Type: AWS::EFS::FileSystem
>     Properties:
>       FileSystemTags:
>         - Key: Name
>           Value: !Sub ${ClusterBaseName}-EFS
>   ElasticFileSystemMountTarget0:
>     Type: AWS::EFS::MountTarget
>     Properties:
>       FileSystemId: !Ref ElasticFileSystem
>       SecurityGroups:
>       - !Ref EFSSG
>       SubnetId: !Ref PublicSubnet1
>   ElasticFileSystemMountTarget1:
>     Type: AWS::EFS::MountTarget
>     Properties:
>       FileSystemId: !Ref ElasticFileSystem
>       SecurityGroups:
>       - !Ref EFSSG
>       SubnetId: !Ref PublicSubnet2
>   ElasticFileSystemMountTarget2:
>     Type: AWS::EFS::MountTarget
>     Properties:
>       FileSystemId: !Ref ElasticFileSystem
>       SecurityGroups:
>       - !Ref EFSSG
>       SubnetId: !Ref PublicSubnet3
>
311c354
<           Value: !Sub ${ClusterBaseName}-bastion-EC2
---
>           Value: !Sub ${ClusterBaseName}-bastion
329c372
<             hostnamectl --static set-hostname "${ClusterBaseName}-bastion-EC2"
---
>             hostnamectl --static set-hostname "${ClusterBaseName}-bastion"
345c388
<             yum -y install tree jq git htop
---
>             yum -y install tree jq git htop amazon-efs-utils
396c439
<             kubectl krew install ctx ns get-all neat # ktop df-pv mtail tree
---
>             kubectl krew install ctx ns get-all neat df-pv # ktop mtail tree
411a455
>             echo "export AWS_REGION=$AWS_DEFAULT_REGION" >> /etc/profile
438a483,499
>             # Setting EFS Filesystem
>             mkdir -p /mnt/myefs
>             echo "export EfsFsId=${ElasticFileSystem}" >> /etc/profile
>             #mount -t nfs4 -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport ${ElasticFileSystem}.efs.ap-northeast-2.amazonaws.com:/ /mnt/myefs
>
>             # Install VS Code in the browser https://github.com/coder/code-server
>             # curl -fOL https://github.com/coder/code-server/releases/download/v4.22.1/code-server_4.22.1_amd64.deb
>             # mkdir -p /root/.config/code-server
>             # cat << EOT > /root/.config/code-server/config.yaml
>             # bind-addr: 0.0.0.0:80
>             # auth: password
>             # password: Cloudnet@AEWS2!
>             # cert: false
>             # EOT
>             # dpkg -i code-server_4.22.1_amd64.deb
>             # systemctl enable --now code-server@$USER
>
440c501
<             eksctl create cluster --name $CLUSTER_NAME --region=$AWS_DEFAULT_REGION --nodegroup-name=ng1 --node-type=${WorkerNodeInstanceType} --nodes ${WorkerNodeCount} --node-volume-size=${WorkerNodeVolumesize} --vpc-public-subnets "$PubSubnet1","$PubSubnet2","$PubSubnet3" --version ${KubernetesVersion} --ssh-access --ssh-public-key /root/.ssh/id_rsa.pub --with-oidc --external-dns-access --full-ecr-access --dry-run > myeks.yaml
---
>             eksctl create cluster --name $CLUSTER_NAME --region=$AWS_DEFAULT_REGION --nodegroup-name=ng1 --node-type=${WorkerNodeInstanceType} --nodes ${WorkerNodeCount} --node-volume-size=${WorkerNodeVolumesize} --vpc-public-subnets "$PubSubnet1","$PubSubnet2","$PubSubnet3" --version ${KubernetesVersion} --max-pods-per-node 100 --ssh-access --ssh-public-key /root/.ssh/id_rsa.pub --with-oidc --external-dns-access --full-ecr-access --dry-run > myeks.yaml
455a517,525
>             cat <<EOT > irsa.yaml
>               serviceAccounts:
>               - metadata:
>                   name: aws-load-balancer-controller
>                   namespace: kube-system
>                 wellKnownPolicies:
>                   awsLoadBalancerController: true
>             EOT
>             sed -i -n -e '/withOIDC/r irsa.yaml' -e '1,$p' myeks.yaml

 

다음과 같이 명령어로 실행하여 배포를 진행하였다.

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

# CloudFormation 스택 배포
aws cloudformation deploy --template-file eks-oneclick2.yaml --stack-name myeks --parameter-overrides KeyName=kp-ian SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32  MyIamUserAccessKeyID=`cat ~/.aws/credentials | grep aws_access_key_id | awk '{print $3}'` MyIamUserSecretAccessKey=`cat ~/.aws/credentials | grep aws_secret_access_key | awk '{print $3}'` ClusterBaseName=myeks --region ap-northeast-2

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

# 작업용 EC2 SSH 접속
ssh -i ~/.ssh/kp-ian.pem ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)
or
ssh -i ~/.ssh/kp-ian.pem root@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)
~ password: qwe123

 

기본 설정 및 EFS를 확인해보자.

# default 네임스페이스 적용
kubectl ns default

# EFS 확인 : AWS 관리콘솔 EFS 확인해보자
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
rm -f /mnt/myefs/memo.txt

# 스토리지클래스 및 CSI 노드 확인
kubectl get sc
kubectl get sc gp2 -o yaml | yh
kubectl get csinodes

# 노드 정보 확인
kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone

 

EFS 마운트를 성송하여 파일 쓰기 및 파일 내용 확인, 파일 삭제가 가능하였으며

 

스토리지 클래스 및 CSI 노드 정보 확인 또한 완료하였다.

 

이후 원활한 실습을 위해 아래를 참고하여 AWS LB/ExternalDNS, kube-ops-view를 구성하자.

# AWS LB Controller
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

# ExternalDNS
MyDomain=sdndev.net
MyDnsHostedZoneId=$(aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." --query "HostedZones[0].Id" --output text)
echo $MyDomain, $MyDnsHostedZoneId

curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/aews/externaldns.yaml
sed -i "s/0.13.4/0.14.0/g" externaldns.yaml
MyDomain=$MyDomain MyDnsHostedZoneId=$MyDnsHostedZoneId envsubst < externaldns.yaml | kubectl apply -f -

# kube-ops-view
helm repo add geek-cookbook https://geek-cookbook.github.io/charts/
helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 --set env.TZ="Asia/Seoul" --namespace kube-system
kubectl patch svc -n kube-system kube-ops-view -p '{"spec":{"type":"LoadBalancer"}}'
kubectl annotate service kube-ops-view -n kube-system "external-dns.alpha.kubernetes.io/hostname=kubeopsview.$MyDomain"
echo -e "Kube Ops View URL = http://kubeopsview.$MyDomain:8080/#scale=1.5"

 

1. 스토리지 이해

파드 내부 데이터는 파드가 정지가 되면 모두 삭제가 된다. 이유는 컨테이너가 구동될 때 임시 파일 시스템을 사용하기 때문과 동시에 개별 파드는 모두 상태가 없는 (Stateless) 애플리케이션이기 때문이다. 따라서 상태가 있는 (Stateful) 애플리케이션을 구축하기 위해서는 데이터 보존이 필요하며 쿠버네티스 환경에서는 이를 퍼시스턴트 볼륨 (Persistent Volume, PV)라는 개념으로 부르고 있다.

(출처: https://aws.amazon.com/ko/blogs/tech/persistent-storage-for-kubernetes/ )

 

정확하게는 PV와 PVC 2개의 개념으로 나뉜다. PV는 볼륨 입장에서 물리 저장소 공간을 바라본다고 할 수 있고, PVC (Persistent Volume Claim)은 파드에서 PV를 사용한다고 클레임/주장할 수 있도록 하는 개념이라고 할 수 있겠다. 이를 스토리지 클래스라는 체계적인 단위로 프로비저닝을 하여 관리하는 방식으로 쿠버네티스에서는 권장하고 있다. 또한 퍼시스턴트 볼륨 사용이 끝났을 때 해당 볼륨은 어떻게 초기화할 것인지 별도로 설정할 수 있는데, 쿠버네티스에서는 이를 Reclaim Policy라고 부른다. Reclaim Policy에는 크게 Retain(보존), Delete(삭제, 즉 EBS 볼륨도 삭제됨) 방식이 있다고 한다.

(출처: https://aws.amazon.com/ko/blogs/tech/persistent-storage-for-kubernetes/ )

 

이전 EKS 네트워킹 스터디에서 CNI가 있는 것처럼 스토리지에는 CSI (Container Storage Interface)가 있다. https://kubernetes-csi.github.io/docs/ 에 있는 내용을 번역해보았다:

CSI( 컨테이너 스토리지 인터페이스)는 Kubernetes와 같은 CO(컨테이너 오케스트레이션 시스템)의 컨테이너화된 워크로드에 임의의 블록 및 파일 스토리지 시스템을 노출하기 위한 표준이다. CSI 제3자 스토리지 제공자를 사용하면 핵심 Kubernetes 코드를 건드릴 필요 없이 Kubernetes에서 새로운 스토리지 시스템을 노출하는 플러그인을 작성하고 배포할 수 있다.

 

그렇다면 임시 파일 시스템을 사용할 때와 스토리지를 사용할 때를 비교해보도록 하자.

 

a. 기본 컨테이너 환경에서 임시 파일 시스템 사용

# 파드 배포 - date 명령어로 현재 시간을 10초 간격으로 /home/pod-out.txt 파일에 저장
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
kubectl exec busybox -- tail -f /home/pod-out.txt

# 파드 삭제 후 다시 생성 후 파일 정보 확인
kubectl delete pod busybox
kubectl apply -f date-busybox-pod.yaml
kubectl exec busybox -- cat /home/pod-out.txt
kubectl exec busybox -- tail -f /home/pod-out.txt

# 실습 완료 후 삭제
kubectl delete pod busybox

파드를 지우고 다시 접속하여 cat으로 조회해보면 이전 내용이 보이지 않는 것을 확인할 수 있다.

 

b. 호스트 Path 를 사용하는 PV/PVC : local-path-provisioner 스토리지 클래스로 배포하여 실습해보자.

링크에 보면 장단점이 잘 명시되어 있다. 번역한 내용을 아래에 붙인다.

장점
 - HostPath 또는 local 을 사용하여 볼륨을 동적으로 프로비저닝한다.
 - 현재 Kubernetes 로컬 볼륨 프로비저너는 로컬 볼륨에 대한 동적 프로비저닝을 수행할 수 없다.로컬 기반 영구 볼륨은 실험적인 기능이다.

단점
 - 현재 볼륨 용량 제한을 지원하지 않는다.용량 제한을 무시한다.
# 배포
curl -s -O https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml
kubectl apply -f local-path-storage.yaml

# 확인
kubectl get-all -n local-path-storage
kubectl get pod -n local-path-storage -owide
kubectl describe cm -n local-path-storage local-path-config
kubectl get sc
kubectl get sc local-path
NAME         PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
local-path   rancher.io/local-path   Delete          WaitForFirstConsumer   false

 

 

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

# PVC 확인
kubectl get pvc
kubectl describe pvc

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

# 파드 확인
kubectl get pod,pv,pvc
kubectl describe pv    # Node Affinity 확인
kubectl exec -it app -- tail -f /data/out.txt

# 파드 삭제 후 PV/PVC 확인
kubectl delete pod app
kubectl get pod,pv,pvc
for node in $N1 $N2 $N3; do ssh ec2-user@$node tree /opt/local-path-provisioner; done

# 파드 다시 실행
kubectl apply -f localpath2.yaml
 
# 확인
kubectl exec -it app -- head /data/out.txt
kubectl exec -it app -- tail -f /data/out.txt

 

 

예상한 대로 Node Affinity를 가진 상태에서 파드를 다시 시작하더라도 동일 경로에 있는 파일을 액세스하는 것을 확인할 수 있었다. 다음 실습을 위해 파드와 만든 PVC를 삭제하자.

# 파드와 PVC 삭제 
kubectl delete pod app
kubectl get pv,pvc
kubectl delete pvc localpath-claim

# 확인
kubectl get pv
for node in $N1 $N2 $N3; do ssh ec2-user@$node tree /opt/local-path-provisioner; done

2. AWS EBS Controller

지난 실습에서 살펴보았던 AWS LoadBalancer Controller (6번 항목)과 비슷하다고도 볼 수 있을텐데, EKS에서 EBS와 같은 AWS 스토리지를 다룰 때 AWS API를 호출하면서 AWS 스토리지를 관리하는 CSI Controller와 관련해 EBS에 해당하는 부분이 바로 AWS EBS Controller라 할 수 있겠다. 따라서 Amazon EKS add-on 형태로 Amazon EBS CSI driver를 설치하여 사용하며, 관련한 자세한 내용은 AWS 문서를 통해서도 확인 가능하다.

 

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

# 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
NAMESPACE	    NAME				            ROLE ARN
kube-system 	ebs-csi-controller-sa		arn:aws:iam::911283464785:role/AmazonEKS_EBS_CSI_DriverRole
...

# Amazon EBS CSI driver addon 추가
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

# 확인
eksctl get addon --cluster ${CLUSTER_NAME}
kubectl get deploy,ds -l=app.kubernetes.io/name=aws-ebs-csi-driver -n kube-system
kubectl get pod -n kube-system -l 'app in (ebs-csi-controller,ebs-csi-node)'
kubectl get pod -n kube-system -l app.kubernetes.io/component=csi-driver

# 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

# csinodes 확인
kubectl get csinodes

# gp3 스토리지 클래스 생성 - Link
kubectl get sc
cat <<EOT > gp3-sc.yaml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: gp3
allowVolumeExpansion: true
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
parameters:
  type: gp3
  #iops: "5000"
  #throughput: "250"
  allowAutoIOPSPerGBIncrease: 'true'
  encrypted: 'true'
  fsType: xfs # 기본값이 ext4
EOT
kubectl apply -f gp3-sc.yaml
kubectl get sc
kubectl describe sc gp3 | grep Parameters

 

IRSA 서비스 계정을 생성하고 gp3라는 스토리지 클래스를 생성해 보았다.

 

이제 PVC/PV 파트 테스트를 해보자.

# 워커노드의 EBS 볼륨 확인 : tag(키/값) 필터링 - 링크
aws ec2 describe-volumes --filters Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node --output table
aws ec2 describe-volumes --filters Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node --query "Volumes[*].Attachments" | jq
aws ec2 describe-volumes --filters Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node --query "Volumes[*].{ID:VolumeId,Tag:Tags}" | jq
aws ec2 describe-volumes --filters Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node --query "Volumes[].[VolumeId, VolumeType, Attachments[].[InstanceId, State][]][]" | jq
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

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

# PVC, 파드 확인
kubectl get pvc,pv,pod
kubectl get VolumeAttachment

# 추가된 EBS 볼륨 상세 정보 확인 
aws ec2 describe-volumes --volume-ids $(kubectl get pv -o jsonpath="{.items[0].spec.csi.volumeHandle}") | jq

# PV 상세 확인 : nodeAffinity 내용의 의미는?
kubectl get pv -o yaml | yh
...
    nodeAffinity:
      required:
        nodeSelectorTerms:
        - matchExpressions:
          - key: topology.ebs.csi.aws.com/zone
            operator: In
            values:
            - ap-northeast-2b
...

kubectl get node --label-columns=topology.ebs.csi.aws.com/zone,topology.kubernetes.io/zone
kubectl describe node | more

# 파일 내용 추가 저장 확인
kubectl exec app -- tail -f /data/out.txt

# 아래 명령어는 확인까지 다소 시간이 소요됨
kubectl df-pv

## 파드 내에서 볼륨 정보 확인
kubectl exec -it app -- sh -c 'df -hT --type=overlay'
kubectl exec -it app -- sh -c 'df -hT --type=xfs'

 

실제 확인을 해보니 gp3 스토리지 클래스에 대해 PVC 클레임이 완료되고 /data 마운트 포인트에 대해 PV 활용을 하는 파드에서 확인이 잘 이루어졌다.

 

볼륨 증가 또한 가능한데, 이 때 늘릴 수는 있으나 줄일 수는 없다는 점을 참고하도록 하자. 자세한 부분은 링크를 통해 내용을 참고한다. 아래 실습을 통해 10G로 볼륨 크기가 늘어남을 확인할 수 있었다.

# 현재 pv 의 이름을 기준하여 4G > 10G 로 증가 : .spec.resources.requests.storage의 4Gi 를 10Gi로 변경
kubectl get pvc ebs-claim -o jsonpath={.spec.resources.requests.storage} ; echo
kubectl get pvc ebs-claim -o jsonpath={.status.capacity.storage} ; echo
kubectl patch pvc ebs-claim -p '{"spec":{"resources":{"requests":{"storage":"10Gi"}}}}'

# 확인 : 볼륨 용량 수정 반영이 되어야 되니, 수치 반영이 조금 느릴수 있다
kubectl exec -it app -- sh -c 'df -hT --type=xfs'
kubectl df-pv
aws ec2 describe-volumes --volume-ids $(kubectl get pv -o jsonpath="{.items[0].spec.csi.volumeHandle}") | jq

 

실습을 완료한 다음에는 관련 파드 및 PVC를 삭제하도록 하자.

kubectl delete pod app & kubectl delete pvc ebs-claim

3. AWS Volume SnapShots Controller

볼륨에 대한 스냅샷은 특정 시점에 볼륨 상태가 어떠했는지 일종의 백업 형태로 바라볼 수 있겠다. 블로그에도 언급된 AWS Volume SnapShots Controller 부분을 실습해보도록 하자.

# (참고) EBS CSI Driver에 snapshots 기능 포함 될 것으로 보임
kubectl describe pod -n kube-system -l app=ebs-csi-controller

# Install Snapshot CRDs
curl -s -O https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshots.yaml
curl -s -O https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshotclasses.yaml
curl -s -O https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshotcontents.yaml
kubectl apply -f snapshot.storage.k8s.io_volumesnapshots.yaml,snapshot.storage.k8s.io_volumesnapshotclasses.yaml,snapshot.storage.k8s.io_volumesnapshotcontents.yaml
kubectl get crd | grep snapshot
kubectl api-resources  | grep snapshot

# Install Common Snapshot Controller
curl -s -O https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/deploy/kubernetes/snapshot-controller/rbac-snapshot-controller.yaml
curl -s -O https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/deploy/kubernetes/snapshot-controller/setup-snapshot-controller.yaml
kubectl apply -f rbac-snapshot-controller.yaml,setup-snapshot-controller.yaml
kubectl get deploy -n kube-system snapshot-controller
kubectl get pod -n kube-system -l app=snapshot-controller

# Install Snapshotclass
curl -s -O https://raw.githubusercontent.com/kubernetes-sigs/aws-ebs-csi-driver/master/examples/kubernetes/snapshot/manifests/classes/snapshotclass.yaml
kubectl apply -f snapshotclass.yaml
kubectl get vsclass # 혹은 volumesnapshotclasses

 

아래와 같이 테스트 PVC/파드를 먼저 생성해본다.

# PVC 생성
kubectl apply -f awsebs-pvc.yaml

# 파드 생성
kubectl apply -f awsebs-pod.yaml

# 파일 내용 추가 저장 확인
kubectl exec app -- tail -f /data/out.txt

# VolumeSnapshot 생성 : Create a VolumeSnapshot referencing the PersistentVolumeClaim name >> EBS 스냅샷 확인
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/3/ebs-volume-snapshot.yaml
cat ebs-volume-snapshot.yaml | yh
kubectl apply -f ebs-volume-snapshot.yaml

# VolumeSnapshot 확인
kubectl get volumesnapshot
kubectl get volumesnapshot ebs-volume-snapshot -o jsonpath={.status.boundVolumeSnapshotContentName} ; echo
kubectl describe volumesnapshot.snapshot.storage.k8s.io ebs-volume-snapshot
kubectl get volumesnapshotcontents

# VolumeSnapshot ID 확인 
kubectl get volumesnapshotcontents -o jsonpath='{.items[*].status.snapshotHandle}' ; echo

# AWS EBS 스냅샷 확인
aws ec2 describe-snapshots --owner-ids self | jq
aws ec2 describe-snapshots --owner-ids self --query 'Snapshots[]' --output table

# app & pvc 제거 : 강제로 장애 재현
kubectl delete pod app && kubectl delete pvc ebs-claim

 

강제로 장애를 재현하였는데 이 때 스냅샷을 통해 PVC로 복원이 가능하다.

# 스냅샷에서 PVC 로 복원
kubectl get pvc,pv
cat <<EOT > ebs-snapshot-restored-claim.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ebs-snapshot-restored-claim
spec:
  storageClassName: gp3
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 4Gi
  dataSource:
    name: ebs-volume-snapshot
    kind: VolumeSnapshot
    apiGroup: snapshot.storage.k8s.io
EOT
cat ebs-snapshot-restored-claim.yaml | yh
kubectl apply -f ebs-snapshot-restored-claim.yaml

# 확인
kubectl get pvc,pv

# 파드 생성
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/3/ebs-snapshot-restored-pod.yaml
cat ebs-snapshot-restored-pod.yaml | yh
kubectl apply -f ebs-snapshot-restored-pod.yaml

실제 실습을 할 때에는 충분히 out.txt 파일에 데이터가 기록된 상태에서 볼륨 스냅샷을 생성하는 것이 중요하다. 본인이 실습할 때에는 볼륨 스냅샷 생성을 할 때 out.txt 파일에 아무것도 기록되어 있지 않은 상태에서 진행하였기에 스냅샷 복원 후 이전 데이터가 모두 보이지 않는 것이 정상인 상황이었다.

실습을 완료하였다면 관련 리소스를 삭제하도록 하자.

kubectl delete pod app && kubectl delete pvc ebs-snapshot-restored-claim && kubectl delete volumesnapshots ebs-volume-snapshot

4. AWS EFS Controller

EFS를 EKS에서 사용하면 여러 AZ에 걸쳐 데이터를 저장하여 높은 수준의 가용성과 내구성을 제공할 수 있다.

아래 명령어를 참고하여 EFS Controller를 직접 설치해보았다.

# EFS 정보 확인 
aws efs describe-file-systems --query "FileSystems[*].FileSystemId" --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::${ACCOUNT_ID}:policy/AmazonEKS_EFS_CSI_Driver_Policy \
  --approve

# ISRA 확인
kubectl get sa -n kube-system efs-csi-controller-sa -o yaml | head -5
eksctl get iamserviceaccount --cluster myeks

# EFS Controller 설치
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=${ACCOUNT_ID}.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

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

 

그 다음, EFS 파일 시스템을 다수의 파드가 사용하도록 설정해보자.

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

# 실습 코드 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

# 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
kubectl df-pv

# 파드 정보 확인 : PV에 5Gi 와 파드 내에서 확인한 NFS4 볼륨 크리 8.0E의 차이는 무엇? 파드에 6Gi 이상 저장 가능한가?
kubectl get pods
kubectl exec -ti app1 -- sh -c "df -hT -t nfs4"
kubectl exec -ti app2 -- 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

# 공유 저장소 저장 동작 확인
tree /mnt/myefs              # 작업용EC2에서 확인
tail -f /mnt/myefs/out1.txt  # 작업용EC2에서 확인
kubectl exec -ti app1 -- tail -f /data/out1.txt
kubectl exec -ti app2 -- tail -f /data/out2.txt

실습을 완료한 다음에는 꼭 삭제를 하도록 하자.

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

5. EKS Persistent Volumes for Instance Store & Add NodeGroup

새로 노드 그룹을 생성해보는 과정을 실습해보도록 하자. 인스턴스 스토어는 EC2 스토리지 (EBS) 정보에 출력되지는 않는다고 한다.

# 인스턴스 스토어 볼륨이 있는 c5 모든 타입의 스토리지 크기
aws ec2 describe-instance-types \
 --filters "Name=instance-type,Values=c5*" "Name=instance-storage-supported,Values=true" \
 --query "InstanceTypes[].[InstanceType, InstanceStorageInfo.TotalSizeInGB]" \
 --output table
--------------------------
|  DescribeInstanceTypes |
+---------------+--------+
|  c5d.large    |  50    |
|  c5d.12xlarge |  1800  |
...

# 신규 노드 그룹 생성
eksctl create nodegroup --help
eksctl create nodegroup -c $CLUSTER_NAME -r $AWS_DEFAULT_REGION --subnet-ids "$PubSubnet1","$PubSubnet2","$PubSubnet3" --ssh-access \
  -n ng2 -t c5d.large -N 1 -m 1 -M 1 --node-volume-size=30 --node-labels disk=nvme --max-pods-per-node 100 --dry-run > myng2.yaml

cat <<EOT > nvme.yaml
  preBootstrapCommands:
    - |
      # Install Tools
      yum install nvme-cli links tree jq tcpdump sysstat -y

      # Filesystem & Mount
      mkfs -t xfs /dev/nvme1n1
      mkdir /data
      mount /dev/nvme1n1 /data

      # Get disk UUID
      uuid=\$(blkid -o value -s UUID mount /dev/nvme1n1 /data) 

      # Mount the disk during a reboot
      echo /dev/nvme1n1 /data xfs defaults,noatime 0 2 >> /etc/fstab
EOT
sed -i -n -e '/volumeType/r nvme.yaml' -e '1,$p' myng2.yaml
eksctl create nodegroup -f myng2.yaml

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

# 워커 노드 SSH 접속
N4=<각자 자신의 워커 노드4번 Private IP 지정>
ssh ec2-user@$N4 hostname

# 확인
ssh ec2-user@$N4 sudo nvme list
ssh ec2-user@$N4 sudo lsblk -e 7 -d
ssh ec2-user@$N4 sudo df -hT -t xfs
ssh ec2-user@$N4 sudo tree /data
ssh ec2-user@$N4 sudo cat /etc/fstab

# (옵션) max-pod 확인
kubectl describe node -l disk=nvme | grep Allocatable: -A7
Allocatable:
  attachable-volumes-aws-ebs:  25
  cpu:                         1930m
  ephemeral-storage:           27905944324
  hugepages-1Gi:               0
  hugepages-2Mi:               0
  memory:                      3097552Ki
  pods:                        100

# (옵션) kubelet 데몬 파라미터 확인 : --max-pods=29 --max-pods=100
ssh ec2-user@$N4 sudo ps -ef | grep kubelet
root      2972     1  0 16:03 ?        00:00:09 /usr/bin/kubelet --config /etc/kubernetes/kubelet/kubelet-config.json --kubeconfig /var/lib/kubelet/kubeconfig --container-runtime-endpoint unix:///run/containerd/containerd.sock --image-credential-provider-config /etc/eks/image-credential-provider/config.json --image-credential-provider-bin-dir /etc/eks/image-credential-provider --node-ip=192.168.3.131 --pod-infra-container-image=602401143452.dkr.ecr.ap-northeast-2.amazonaws.com/eks/pause:3.5 --v=2 --cloud-provider=aws --container-runtime=remote --node-labels=eks.amazonaws.com/sourceLaunchTemplateVersion=1,alpha.eksctl.io/cluster-name=myeks,alpha.eksctl.io/nodegroup-name=ng2,disk=nvme,eks.amazonaws.com/nodegroup-image=ami-0da378ed846e950a4,eks.amazonaws.com/capacityType=ON_DEMAND,eks.amazonaws.com/nodegroup=ng2,eks.amazonaws.com/sourceLaunchTemplateId=lt-030e6043923ce712b --max-pods=29 --max-pods=100

 

 

 

스토리지 클래스를 재생성하고 Read 성능을 측정해보자.

# 기존 삭제
#curl -s -O https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml
cd
kubectl delete -f local-path-storage.yaml

#
sed -i 's/opt/data/g' local-path-storage.yaml
kubectl apply -f local-path-storage.yaml

# 모니터링
watch 'kubectl get pod -owide;echo;kubectl get pv,pvc'
ssh ec2-user@$N4 iostat -xmdz 1 -p nvme1n1

# 측정 : Read
#curl -s -O https://raw.githubusercontent.com/wikibook/kubepractice/main/ch10/fio-read.fio
kubestr fio -f fio-read.fio -s local-path --size 10G --nodeselector disk=nvme

 

성능 측정을 해보니 20,308.923828 IOPS를 얻을 수가 있었다. 일반 EBS는 약 3,000 IOPS가 나온다고 한다. 즉 인스턴스 스토어가 약 7배 빠르다. 실습을 다한 후에는 자원을 꼭 삭제하자.

kubectl delete -f local-path-storage.yaml

eksctl delete nodegroup -c $CLUSTER_NAME -n ng2

 

6. 노드 그룹 

EKS 워크샵에 보면 Graviton (ARM) 인스턴스에 대해 설명하는 내용이 있다.Graviton 인스턴스의 경우 Processor family에 'g'라는 글씨를 추가로 붙여 인스턴스 유형을 나타내며 EKS 환경에서 Graviton에 해당하는 노드 그룹을 생성하여 관리를 할 수가 있다.

 

kubectl get nodes -L kubernetes.io/arch

# 신규 노드 그룹 생성
eksctl create nodegroup --help
eksctl create nodegroup -c $CLUSTER_NAME -r $AWS_DEFAULT_REGION --subnet-ids "$PubSubnet1","$PubSubnet2","$PubSubnet3" --ssh-access \
  -n ng3 -t t4g.medium -N 1 -m 1 -M 1 --node-volume-size=30 --node-labels family=graviton --dry-run > myng3.yaml
eksctl create nodegroup -f myng3.yaml

# 확인
kubectl get nodes --label-columns eks.amazonaws.com/nodegroup,kubernetes.io/arch
kubectl describe nodes --selector family=graviton
aws eks describe-nodegroup --cluster-name $CLUSTER_NAME --nodegroup-name ng3 | jq .nodegroup.taints

# taints 셋팅 -> 적용에 2~3분 정도 시간 소요
aws eks update-nodegroup-config --cluster-name $CLUSTER_NAME --nodegroup-name ng3 --taints "addOrUpdateTaints=[{key=frontend, value=true, effect=NO_EXECUTE}]"

# 확인
kubectl describe nodes --selector family=graviton | grep Taints
aws eks describe-nodegroup --cluster-name $CLUSTER_NAME --nodegroup-name ng3 | jq .nodegroup.taints

 

위 실습 스크립트와 같이 Graviton 노드 그룹을 생성한 다음, 생성한 Graviton 노드 그룹에 taint를 설정한다. Taint는 굳이 번역하자면 일종의 '오점'이 되는데, 즉 설정된 노드에 일종의 오점을 지정하여 파드를 배치할 때 이 Taint(오점)을 용인한다는 Toleration 옵션을 설정해야 Taint가 있는 노드에 배치가 될 수가 있다.

 

2-3분 지난 후 Taint 결과를 확인해보자. 참고로 Taint는 다음과 같은 속성이 있다고 한다.

# NO_SCHEDULE - This corresponds to the Kubernetes NoSchedule taint effect. This configures the managed node group with a taint that repels all pods that don't have a matching toleration. All running pods are not evicted from the manage node group's nodes.
# NO_EXECUTE - This corresponds to the Kubernetes NoExecute taint effect. Allows nodes configured with this taint to not only repel newly scheduled pods but also evicts any running pods without a matching toleration.
# PREFER_NO_SCHEDULE - This corresponds to the Kubernetes PreferNoSchedule taint effect. If possible, EKS avoids scheduling Pods that do not tolerate this taint onto the node.

 

실제 Graviton 노드 그룹에 파드를 배치시켜보자.

cat << EOT > busybox.yaml
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"
  tolerations:
    - effect: NoExecute
      key: frontend
      operator: Exists
EOT
kubectl apply -f busybox.yaml

# 파드가 배포된 노드 정보 확인
kubectl get pod -owide

 

아래 스크린샷과 같이 파드에 배포할 때 toletations 속성을 지정하고 이에 따라 파드가 배포된 노드가 Gravition 노드 그룹임을 확인할 수 있었다.

 

이렇게 노드 그룹을 사용하여 특정 노드 그룹 Family에 대해 관리가 가능하다. 실습을 완료한 다음에는 자원을 삭제한다.

kubectl delete pod busybox
eksctl delete nodegroup -c $CLUSTER_NAME -n ng3

 

스팟 인스턴스 또한 노드 그룹 설정을 통해 사용할 수 있다 (관련 EKS 워크샵 내용: 링크). 사용 가능한 스팟 인스턴스를 확인하는 도구로 ec2-instance-selector가 있다. 이를 설치하여 실행해보자.

 

# ec2-instance-selector 설치
curl -Lo ec2-instance-selector https://github.com/aws/amazon-ec2-instance-selector/releases/download/v2.4.1/ec2-instance-selector-`uname | tr '[:upper:]' '[:lower:]'`-amd64 && chmod +x ec2-instance-selector
mv ec2-instance-selector /usr/local/bin/
ec2-instance-selector --version

# 사용
ec2-instance-selector --vcpus 2 --memory 4 --gpus 0 --current-generation -a x86_64 --deny-list 't.*' --output table-wide

스팟 인스턴스 노드 그룹을 생성하기 위해서는 노드 그룹에 해당하는 Role의 ARN 정보를 가져올 필요가 있다. 이 정보는 AWS 콘솔에서 IAM -> Roles에 가서 NodeInstance와 같이 필터링을 하여 Role 이름을 확인할 수 있다.

 

kubectl get nodes -l eks.amazonaws.com/capacityType=ON_DEMAND
kubectl get nodes -L eks.amazonaws.com/capacityType
NAME                                              STATUS   ROLES    AGE   VERSION               CAPACITYTYPE
ip-192-168-1-65.ap-northeast-2.compute.internal   Ready    <none>   75m   v1.28.5-eks-5e0fdde   ON_DEMAND
ip-192-168-2-89.ap-northeast-2.compute.internal   Ready    <none>   75m   v1.28.5-eks-5e0fdde   ON_DEMAND
ip-192-168-3-39.ap-northeast-2.compute.internal   Ready    <none>   75m   v1.28.5-eks-5e0fdde   ON_DEMAND

# 생성 : 아래 node-role 은 각자 자신의 노드롤 ARN을 입력하자 ($ACCOUNT_ID로 대체)
# role AWSServiceRoleForAmazonEKSNodegroup 테스트해보자
aws eks create-nodegroup \
  --cluster-name $CLUSTER_NAME \
  --nodegroup-name managed-spot \
  --subnets $PubSubnet1 $PubSubnet2 $PubSubnet3 \
  --node-role arn:aws:iam::$ACCOUNT_ID:role/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-wvZ2FX2m79Vv \
  --instance-types c5.large c5d.large c5a.large \
  --capacity-type SPOT \
  --scaling-config minSize=2,maxSize=3,desiredSize=2 \
  --disk-size 20
  
  aws eks wait nodegroup-active --cluster-name $CLUSTER_NAME --nodegroup-name managed-spot
  
  kubectl get nodes -L eks.amazonaws.com/capacityType,eks.amazonaws.com/nodegroup

 

AWS 콘솔에서도 결과를 확인할 수 있다.

 

스팟 인스턴스에 파드를 생성하고 삭제하는 것으로 실습을 완료해보자.

#
cat << EOT > busybox.yaml
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"
  nodeSelector:
    eks.amazonaws.com/capacityType: SPOT
EOT
kubectl apply -f busybox.yaml

# 파드가 배포된 노드 정보 확인
kubectl get pod -owide

# 삭제
kubectl delete pod busybox
eksctl delete nodegroup -c $CLUSTER_NAME -n managed-spot

 

nodeSelector에 의해 스팟 인스턴스를 지정하였고 이에 따라 위애 생성 요청을 한 파드가 스팟 인스턴스 노드에서 실행되고 있음을 확인하였다.

 

 

항상 그렇듯이 실습을 모두 완료한 이후에는 생성된 전체 자원을 꼭 삭제하도록 하자.

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

 

본 게시물은 CloudNet@에서 진행하는 AEWS (Amazon EKS Workshop Study) 스터디에 참여하며 정리한 내용입니다.

 

1. VPC CNI 소개

그 전에 먼저 CNI에 대해 살펴보자. https://cni.dev 홈페이지 내용을 번역기로 해석한 내용을 아래에 붙여본다.

클라우드 네이티브 컴퓨팅 기반 CNI( Container Network Interface ) 프로젝트는 지원되는 여러 플러그인과 함께 Linux 및 Windows 컨테이너에서 네트워크 인터페이스를 구성하기 위한 플러그인을 작성하기 위한 사양 및 라이브러리로 구성됩니다. CNI는 컨테이너의 네트워크 연결과 컨테이너 삭제 시 할당된 리소스 제거에만 관심이 있습니다. 이러한 초점 때문에 CNI는 광범위한 지원을 제공하며 사양 구현이 간단합니다.

 

이러한 CNI를 쓰는 대표적인 오픈 소스가 바로 쿠버네티스(K8s)로, CNI는 K8s 네트워크 환경을 구성해주는 역할을 하며 링크와 같이 다양한 플러그인이 존재한다. Amazon EKS에서는 amazon-vpc-cni-k8s를 사용하여 쿠버네티스에서 AWS에 있는 Elastic Network Interfaces를 통한 파드 네트워킹을 지원한다. amazon-vpc-cni-k8s가 파드 IP를 할당할 때 파드의 IP 네트워크 내역과 노드(워커) IP 대역이 같아서 직접 통신이 가능하다. 아래 그림에서 볼 수 있듯이, AWS VPC CNI에서는 IP 대역이 변화하지 않는다. 또한 Calico CNI 등 일반적으로 K8s CNI는 오버레이(VXLAN, IP-IP 등) 통신을 하고, AWS VPC CNI는 동일 대역으로 직접 통신을 한다. 

 

동일 대역에서 직접 통신이 이루어지므로 사용 가능한 IP 개수에 제약이 있는 점 또한 참고할 필요가 있겠다. 자세한 내용을 링크에 언급되어 있다고 하니 참고하자. 우선 설치된 EKS 환경에서 CNI 기본 정보를 확인해보자. 명령어는 다음과 같다.

# CNI 정보 확인
kubectl describe daemonset aws-node --namespace kube-system | grep Image | cut -d "/" -f 2

# 노드 IP 확인
aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,PrivateIPAdd:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output table

# 파드 IP 확인
kubectl get pod -n kube-system -o=custom-columns=NAME:.metadata.name,IP:.status.podIP,STATUS:.status.phase

 

실행 결과는 다음과 같다. 구성된 환경에서 amazon-vpc-cni-k8s는 최신인 v1.16.4를 사용하고 있으며, 192.168.1.0/24, 192.168.2.0/24, 192.168.3.0/24 3개의 VPC가 구성된 환경에서 노드 IP 주소 및 파드 IP 주소가 동일한 IP 대역을 사용하고 있음을 확인할 수 있었다.

2. 노드 기본 네트워크 정보 확인

노드에서 기본 네트워크 정보가 어떻게 되어 있는지를 보도록 하자. 

 

# coredns 파드 IP 정보 확인
kubectl get pod -n kube-system -l k8s-app=kube-dns -owide

# 노드의 라우팅 정보 확인 >> EC2 네트워크 정보의 '보조 프라이빗 IPv4 주소'와 비교해보자
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c route; echo; done

 

coredns가 총 2 개 pod로 내포된 상태라 AZ1과 2에 존재하며, 파드 IP 주소 및 라우팅 테이블 IP 주소 모두 노드 IP 주소로 된 것을 확인할 수 있다. AWS 콘솔에서 AZ1에 있는 워커 노드의 Secondary private IPv4 주소와 같음을 확인할 수 있다.

 

이번에는 nicolaka/netshoot 테스트용 pod를 생성하여 라우팅 정보를 확인해보도록 하자.

# [터미널1~3] 노드 모니터링
ssh ec2-user@$N1
watch -d "ip link | egrep 'eth|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"

ssh ec2-user@$N2
watch -d "ip link | egrep 'eth|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"

ssh ec2-user@$N3
watch -d "ip link | egrep 'eth|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"

# 테스트용 파드 netshoot-pod 생성
cat <<EOF | kubectl create -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: netshoot-pod
spec:
  replicas: 3
  selector:
    matchLabels:
      app: netshoot-pod
  template:
    metadata:
      labels:
        app: netshoot-pod
    spec:
      containers:
      - name: netshoot-pod
        image: nicolaka/netshoot
        command: ["tail"]
        args: ["-f", "/dev/null"]
      terminationGracePeriodSeconds: 0
EOF

# 파드 이름 변수 지정
PODNAME1=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[0].metadata.name})
PODNAME2=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[1].metadata.name})
PODNAME3=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[2].metadata.name})

# 파드 확인
kubectl get pod -o wide
kubectl get pod -o=custom-columns=NAME:.metadata.name,IP:.status.podIP

# 노드에 라우팅 정보 확인
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c route; echo; done

 

파드가 생성되면 워커노드에 eniY@ifN 이 추가되고 라우팅 테이블에도 정보가 추가됨을 확인할 수 있다.

 

이렇게 추가된 eniY에 대해 자세히 정보를 확인해볼 수도 있다. 워커노드에 접속한 다음, 마지막에 생성된 네트워크 인터페이스에 대한 네임스페이스 정보를 파악하여, 해당 네임스페이스로 진입하여 네트워크 정보를 살펴보는 것도 도움이 되겠다.

# 노드3에서 네트워크 인터페이스 정보 확인
ssh ec2-user@$N3
----------------
ip -br -c addr show
ip -c link
ip -c addr
ip route # 혹은 route -n

# 마지막 생성된 네임스페이스 정보 출력 -t net(네트워크 타입)
sudo lsns -o PID,COMMAND -t net | awk 'NR>2 {print $1}' | tail -n 1

# 마지막 생성된 네임스페이스 net PID 정보 출력 -t net(네트워크 타입)를 변수 지정
MyPID=$(sudo lsns -o PID,COMMAND -t net | awk 'NR>2 {print $1}' | tail -n 1)

# PID 정보로 파드 정보 확인
sudo nsenter -t $MyPID -n ip -c addr
sudo nsenter -t $MyPID -n ip -c route

exit
----------------

 

 

또한 직접 테스트용 파드로 접속하여 확인해볼 수도 있다.

# 테스트용 파드 접속(exec) 후 Shell 실행
kubectl exec -it $PODNAME1 -- zsh

# 아래부터는 pod-1 Shell 에서 실행 : 네트워크 정보 확인
----------------------------
ip -c addr
ip -c route
route -n
ping -c 1 <pod-2 IP>
ps
cat /etc/resolv.conf
exit
----------------------------

 

3. 노드 간 파드 통신

AWS VPC CNI를 사용하는 EKS 디폴트 환경에서는 별도의 오버레이 통신 기술 없이, VPC Native하게 파드 간 직접 통신이 가능하다.

테스트를 하기 위해 tcpdump를 각 워커 노드에 실행한 상태에서 ping을 통해 확인해보았다. 여기서 한 가지, 파드 생성시 순서를 atomic하게 보장하지 않으므로 워커노드 1,2,3 번호와 PODIP 1,2,3 순서가 동일하지 않을 수 있다는 점을 꼭 참고하자.

# 파드 IP 변수 지정
PODIP1=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[0].status.podIP})
PODIP2=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[1].status.podIP})
PODIP3=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[2].status.podIP})

# 파드1 Shell 에서 파드2로 ping 테스트
kubectl exec -it $PODNAME1 -- ping -c 2 $PODIP2

# 파드2 Shell 에서 파드3로 ping 테스트
kubectl exec -it $PODNAME2 -- ping -c 2 $PODIP3

# 파드3 Shell 에서 파드1로 ping 테스트
kubectl exec -it $PODNAME3 -- ping -c 2 $PODIP1

# 워커 노드 EC2 : TCPDUMP 확인
sudo tcpdump -i any -nn icmp

 

 

이번에는 eniY 어댑터에 tcpdump를 실행해 확인해보면 해당 eniY 어댑터를 통해 패킷이 이동함을 확인할 수 있다. 또한 워커 노드1에서 실제 ip route 경로를 확인해보자. 디폴트 네트워크 정보를 살펴보면 eth0를 통해 빠져나감을 확인할 수 있다.

ifconfig | grep eni
sudo tcpdump -i eniYYYYYYYYYY -nn icmp

[워커 노드1]
# routing policy database management 확인
ip rule

# routing table management 확인
ip route show table local

# 디폴트 네트워크 정보를 eth0 을 통해서 빠져나감을 확인하자.
ip route show table main
default via 192.168.1.1 dev eth0

 

4. 파드에서 외부 통신

파드에서 외부와 통신하는 흐름을 자세히 살펴보면 iptables에 SNAT을 통해 노트의 eth0 IP로 변경되어 외부와 통신이 이루어지는 것을 확인할 수 있다.

(출처: https://github.com/aws/amazon-vpc-cni-k8s/blob/master/docs/cni-proposal.md )

 

워커노드 1을 기준으로 하여 확인해보니 외부 ping에 대해 192.168.1.6 IP 주소 요청이 eth0으로 192.168.1.112인 워커노드1 IP 주소로 변환되어 외부로 요청이 이루어지는 것을 확인할 수 있었다. 관련 iptables NAT 정보도 확인해보았다.

# 작업용 EC2 : pod-1 Shell 에서 외부로 ping
kubectl exec -it $PODNAME1 -- ping -c 1 www.google.com
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i curl -s ipinfo.io/ip; echo; echo; done
for i in $PODNAME1 $PODNAME2 $PODNAME3; do echo ">> Pod : $i <<"; kubectl exec -it $i -- curl -s ipinfo.io/ip; echo; echo; done

# 워커 노드 EC2 : TCPDUMP 확인
sudo tcpdump -i any -nn icmp
sudo tcpdump -i eth0 -nn icmp
sudo iptables -t nat -S | grep 'A AWS-SNAT-CHAIN'

 

실습을 다 한 이후에는 다음 실습을 위해 netshoot 파드를 삭제하도록 하자.

kubectl delete deploy netshoot-pod

 

5. 노드에 파드 생성 개수 제한

실습 내용을 쉽게 확인하기 위해 kube-ops-view를 설치하면 좋다. 다음 명령어를 사용해 설치 가능하다.

# kube-ops-view
helm repo add geek-cookbook https://geek-cookbook.github.io/charts/
helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 --set env.TZ="Asia/Seoul" --namespace kube-system
kubectl patch svc -n kube-system kube-ops-view -p '{"spec":{"type":"LoadBalancer"}}'

# kube-ops-view 접속 URL 확인 (1.5 배율)
kubectl get svc -n kube-system kube-ops-view -o jsonpath={.status.loadBalancer.ingress[0].hostname} | awk '{ print "KUBE-OPS-VIEW URL = http://"$1":8080/#scale=1.5"}'

 

이 공식이 제일 중요하다고 생각된다.

 

최대 파드 생성 갯수 : (Number of network interfaces for the instance type × (the number of IP addresses per network interface - 1)) + 2

 

t3.medium 사용할 때 워커 노드의 인스턴스 정보를 확인해보자.

 

# t3 타입의 정보(필터) 확인
aws ec2 describe-instance-types --filters Name=instance-type,Values=t3.* \
 --query "InstanceTypes[].{Type: InstanceType, MaxENI: NetworkInfo.MaximumNetworkInterfaces, IPv4addr: NetworkInfo.Ipv4AddressesPerInterface}" \
 --output table
 
 # c5 타입의 정보(필터) 확인
aws ec2 describe-instance-types --filters Name=instance-type,Values=c5*.* \
 --query "InstanceTypes[].{Type: InstanceType, MaxENI: NetworkInfo.MaximumNetworkInterfaces, IPv4addr: NetworkInfo.Ipv4AddressesPerInterface}" \
 --output table

 

# 파드 사용 가능 계산 예시 : aws-node 와 kube-proxy 파드는 host-networking 사용으로 IP 2개 남음
((MaxENI * (IPv4addr-1)) + 2)
t3.medium 경우 : ((3 * (6 - 1) + 2 ) = 17개 >> aws-node 와 kube-proxy 2개 제외하면 15개

# 워커노드 상세 정보 확인 : 노드 상세 정보의 Allocatable 에 pods 에 17개 정보 확인
kubectl describe node | grep Allocatable: -A6

 

 

최대 개수 제한이 걸려 생성되지 않는 상황도 직접 확인해보았다.

# 워커 노드 EC2 - 모니터링
while true; do ip -br -c addr show && echo "--------------" ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; done

# 작업용 EC2 - 터미널1
watch -d 'kubectl get pods -o wide'

# 작업용 EC2 - 터미널2
# 디플로이먼트 생성
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/2/nginx-dp.yaml
kubectl apply -f nginx-dp.yaml

# 파드 확인
kubectl get pod -o wide
kubectl get pod -o=custom-columns=NAME:.metadata.name,IP:.status.podIP

# 파드 증가 테스트 >> 파드 정상 생성 확인, 워커 노드에서 eth, eni 갯수 확인 >> 어떤일이 벌어졌는가?
kubectl scale deployment nginx-deployment --replicas=50

 

 

다음 실습 전에 생성한 배포를 삭제하도록 하자.

kubectl delete deployment nginx-deployment

 

이에 대한 해결 방안으로는 Prefix Delegation, WARM & MIN IP/Prefix Targets, Custom Network 등이 있다고 하니 참고하자.

6. AWS LoadBalancer Controller

서비스에 대한 설명은 https://kubernetes.io/ko/docs/concepts/services-networking/service/ 를 참고하는 것으로 하자. AWS LoadBalancer Controller를 사용하는 모드에서는 Load Balancer Controller 파드를 통해 파드 IP 등 지속적인 정보 제공을 하는 역할을 담당하고 EKS Cluster와 상태를 sync하는 역할 또한 수행한다. 다음 그림은 NLB IP 모드로 동작하는 상황을 보여준다.

 

https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/aws-load-balancer-controller.html 문서에 언급된 내용에 따라 다음 명령어를 참고하여 IRSA로 AWS LoadBalancer Controller를 배포하자.

# OIDC 확인
aws eks describe-cluster --name $CLUSTER_NAME --query "cluster.identity.oidc.issuer" --output text
aws iam list-open-id-connect-providers | jq

# IAM Policy (AWSLoadBalancerControllerIAMPolicy) 생성
curl -O https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.5.4/docs/install/iam_policy.json
aws iam create-policy --policy-name AWSLoadBalancerControllerIAMPolicy --policy-document file://iam_policy.json

# 혹시 이미 IAM 정책이 있지만 예전 정책일 경우 아래 처럼 최신 업데이트 할 것
# aws iam update-policy ~~~

# 생성된 IAM Policy Arn 확인
aws iam list-policies --scope Local | jq
aws iam get-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy | jq
aws iam get-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --query 'Policy.Arn'

# AWS Load Balancer Controller를 위한 ServiceAccount를 생성 >> 자동으로 매칭되는 IAM Role 을 CloudFormation 으로 생성됨!
# IAM 역할 생성. AWS Load Balancer Controller의 kube-system 네임스페이스에 aws-load-balancer-controller라는 Kubernetes 서비스 계정을 생성하고 IAM 역할의 이름으로 Kubernetes 서비스 계정에 주석을 답니다
eksctl create iamserviceaccount --cluster=$CLUSTER_NAME --namespace=kube-system --name=aws-load-balancer-controller --role-name AmazonEKSLoadBalancerControllerRole \
--attach-policy-arn=arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --override-existing-serviceaccounts --approve

## IRSA 정보 확인
eksctl get iamserviceaccount --cluster $CLUSTER_NAME

## 서비스 어카운트 확인
kubectl get serviceaccounts -n kube-system aws-load-balancer-controller -o yaml | yh

# Helm Chart 설치
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

## 설치 확인 : aws-load-balancer-controller:v2.7.1
kubectl get crd
kubectl get deployment -n kube-system aws-load-balancer-controller
kubectl describe deploy -n kube-system aws-load-balancer-controller
kubectl describe deploy -n kube-system aws-load-balancer-controller | grep 'Service Account'
  Service Account:  aws-load-balancer-controller
 
# 클러스터롤, 롤 확인
kubectl describe clusterrolebindings.rbac.authorization.k8s.io aws-load-balancer-controller-rolebinding
kubectl describe clusterroles.rbac.authorization.k8s.io aws-load-balancer-controller-role

 

이 부분이 진행되어야 7번 Ingress 실습이 가능해진다.

7. Ingress

인그레스는 클러스터 내부의 서비스 (ClusterIP, NodePort, Loadbalancer)를 외부로 노출(HTTP/HTTPS)할 수 있도록 돕는 역할을 하며, 일종의 웹 프록시 역할을 수행한다. AWS Load Balancer Controller + Ingress (ALB) IP 모드 동작을 AWS VPC CNI에서 확인해보자.

 

# 게임 파드와 Service, Ingress 배포
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/3/ingress1.yaml
cat ingress1.yaml | yh
kubectl apply -f ingress1.yaml

# 모니터링
watch -d kubectl get pod,ingress,svc,ep -n game-2048

# 생성 확인
kubectl get-all -n game-2048
kubectl get ingress,svc,ep,pod -n game-2048
kubectl get targetgroupbindings -n game-2048
NAME                               SERVICE-NAME   SERVICE-PORT   TARGET-TYPE   AGE
k8s-game2048-service2-e48050abac   service-2048   80             ip            87s

# ALB 생성 확인
aws elbv2 describe-load-balancers --query 'LoadBalancers[?contains(LoadBalancerName, `k8s-game2048`) == `true`]' | jq
ALB_ARN=$(aws elbv2 describe-load-balancers --query 'LoadBalancers[?contains(LoadBalancerName, `k8s-game2048`) == `true`].LoadBalancerArn' | jq -r '.[0]')
aws elbv2 describe-target-groups --load-balancer-arn $ALB_ARN
TARGET_GROUP_ARN=$(aws elbv2 describe-target-groups --load-balancer-arn $ALB_ARN | jq -r '.TargetGroups[0].TargetGroupArn')
aws elbv2 describe-target-health --target-group-arn $TARGET_GROUP_ARN | jq

# Ingress 확인
kubectl describe ingress -n game-2048 ingress-2048
kubectl get ingress -n game-2048 ingress-2048 -o jsonpath="{.status.loadBalancer.ingress[*].hostname}{'\n'}"

# 게임 접속 : ALB 주소로 웹 접속
kubectl get ingress -n game-2048 ingress-2048 -o jsonpath={.status.loadBalancer.ingress[0].hostname} | awk '{ print "Game URL = http://"$1 }'

# 파드 IP 확인
kubectl get pod -n game-2048 -owide

 

 

8. 추가 과제: Optimize webSocket applications scaling with API Gateway on Amazon EKS

https://aws.amazon.com/ko/blogs/containers/optimize-websocket-applications-scaling-with-api-gateway-on-amazon-eks/ 블로그에 있는 내용을 실습해보았다. Websocket 기술의 경우 클라이언트와 서버간 실시간 양방향 데이터 교환을 용이하게 하기 위해 사용하는 프로토콜 중 하나이다. 원래 애플리케이션을 최소한으로 변경하면서 장기 실행 클라이언트에 대해서도 자동 크기 조정을 달성할 수 있도록 웹 애플리케이션을 활용하는 부분을 확인해보도록 하자.

 

직접 해보았는데 아쉽게 웹 소켓 연결까지는 되는데 "hello"라고 입력하여도 응답이 오지는 않는다. 기록을 위해 아래와 같이 남겨둔다.

 

필요 환경 준비

- EKS 클러스터 및 ECR 레지스트리는 쉽게 준비 가능하며 AWS Load Balancer Controller 또한 위 6번 내용을 참고하면 된다. wscat을 설치하기 위해서는 node를 설치해야 하는지라 아래 부분을 참고하여 진행하도록 하자.

 

wscat 설치하기

- https://docs.aws.amazon.com/ko_kr/sdk-for-javascript/v2/developer-guide/setting-up-node-on-ec2-instance.html 내용에 따라 설치한다. 다만 최신 lts는 현재 AMI에서 동작하지 않을 수 있으므로 17 정도 버전으로 설치하도록 하자.

- wscat 설치는 " npm install -g wscat " 명령어를 통해 설치가 가능하다.

 

IRSA 정책 생성

https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/associate-service-account-role.html 내용을 참고하여 생성해보았다. 다만 처음에 생성한 것이 아닌 아래 6번 과정을 통해 생성이 이루어진 API Gateway 리소스에 대해 정책을 만든 후 해당 정책을 연결하였다. 다음 명령어를 사용하였다.

 

eksctl create iamserviceaccount --name my-service-account --namespace default --cluster myeks --role-name my-role --attach-policy-arn arn:aws:iam::$ACCOUNT_ID:policy/APIGatewayAllPolicy --approve

 

실습 과정은 다음과 같다.

 

1. 프로젝트 다운로드

git clone https://github.com/aws-samples/websocket-eks

 

2. ECR 레지스트리 생성하여 URL 가져오기

 

3. 해당 ECR 레지스트리에서 push command 설명을 참고하여 클론한 폴더 내 존재하는 Dockerfile로 컨테이너화한 후 업로드를 시킨다. 이후 레지스트리 URL을 반영하여 서비스를 배포한 후 결과를 확인한다.

Replace <ecr image> with the location of your image in deployment-Service.yml
kubectl apply -f deployment-Service.yml
Run the following command to confirm the deployment was created successfully
kubectl get deployment websocket-microservice

 

4. Nodeport를 생성한다.

kubectl apply -f nodePort-Service.yml
Run the following command to confirm the service was created successfully
kubectl get service websocket-restapp-nodeport-service

 

5. ALB 인그레스를 생성한다.

kubectl apply -f albIngress.yml
Run the following command to get the address for the ALB
kubectl get ingress ingress-websocket-restapp-service

 

6. 위 5번에서 반환된 URL을 사용하여 Cloudformation으로 웹 소켓을 생성한다.

aws cloudformation create-stack --stack-name websocket-api --template-body file://./websocket-api-gateway-cfn.yml  --parameters ParameterKey=IntegrationUri,ParameterValue=http://<ALB Address>

 

7. IRSA 서비스 계정을 생성하고 그 정보를 update-deployment-Service.yml 에 반영한 다음 적용한다.

kubectl apply -f update-deployment-Service.yml

8. 최종 확인을 해보자. CloudFormation에서 웹소켓 URL을 찾고 이 URL을 기반으로 접속을 한다.

wscat --connect wss://<Your API gateway url>

9. 최종 테스트: 블로그 내용대로라면 echo back이 되어야 하는데 그렇게 되지 않는다. 추후 재실행 / 디버깅을 해보고자 한다.

10. 실습 자원 제거

aws cloudformation delete-stack --stack-name websocket-api

kubectl delete -f albIngress.yml
kubectl delete -f nodePort-Service.yml
kubectl delete -f deployment-Service.yml
본 게시물은 CloudNet@에서 진행하는 AEWS (Amazon EKS Workshop Study) 스터디에 참여하며 정리한 내용입니다.

 

1. Amazon EKS 소개

Amazon EKS 사용자 가이드 공식 문서 (링크)에 따르면, Amazon Web Services (AWS)에서 제공하는 관리형 서비스로, Kubernetes (쿠버네티스) 컨트롤 플레인을 설치, 운영 및 유지 관리할 필요가 없는 "관리형 서비스"라고 설명되어 있다. 쿠버네티스 클러스터 아키텍처가 설명된 오픈 소스 쿠버네티스 문서 (링크)에 컨트롤 플레인에 대한 그림이 있다.

위 이미지에서 나온 컨트롤 플레인에 보면 cloud-controller-manager, etcd, kube-api-server, scheduler, Controller Manager 이렇게 5개 요소가 있는데, 해당 요소를 직접 설치하여 관리하는 대신 Amazon EKS라는 관리형 서비스를 생성하여 쿠버네티스 노드들을 활용하게 된다. 자세한 내용은 EKS 워크샵 설명(링크)에도 있으니 이를 참고하자.

 

쿠버네티스 오픈 소스는 https://github.com/kubernetes/kubernetes/releases 링크를 통해서도 새로 업데이트된 버전을 확인할 수가 있는데, 이 버전 번호에 대한 자세한 설명은 링크에서도 확인 가능하다.

 

x.y.z | x: 메이저 버전, y: 마이너 버전, z: 패치 버전

 

2. EKS 워크샵 실습 환경 소개 및 작업용 EC2 구성

본 스터디에서는 EKS 워크샵에 있는 "AWS 계정으로 시작"에 따라 스터디 시작 전 미리 AWS 계정을 준비하였으며, 실습 환경 구축에 보면 AWS Cloud9부터 kubectl 설치, eksctl 설치 등이 있는데 스터디장이신 가시다님께서 준비해 주신 AWS CloudFormation을 사용하여 편하게 스터디 참여를 할 수가 있었다. EKS 버전은 스터디를 진행하고 있는 2024년 3월을 기준으로 EKS 지원 add-on 및 K8s 생태계와 가장 잘 호환되고 검증된 애플리케이션이 많은 버전인 v1.28을 선택하여 진행하였다. 스터디 진행하는 AWS 환경을 이해해볼 겸 AWS 아키텍처 아이콘을 참고하여 아래와 같이 도식화해보았다.

 

아래와 같이 CloudFormation 템플릿을 다운로드 받은 다음에 AWS CLI (링크)를 사용해 실행해 보았다.

$ curl -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/K8S/myeks-1week.yaml
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 10373  100 10373    0     0   180k      0 --:--:-- --:--:-- --:--:--  180k
$ aws cloudformation deploy --template-file myeks-1week.yaml --stack-name myeks --parameter-overrides KeyName=kp-ian SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32 --region ap-northeast-2

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - myeks

 

이후 다음 명령어를 실행하면 IP 주소를 알 수 있으며, 이 IP 주소로 SSH를 실행하여 Shell 로 접속하여 이후 작업을 진행한다. 접속할 SSH ID 및 Password는 위 CloudFormation 템플릿 파일에 있으니 참고하자.

aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[*].OutputValue' --output text

 

작업용 EC2에 접속하였다면 IAM 사용자 자격 구성이 필요하다. 실습 편의를 위해 administrator 권한을 가진 IAM User의 자격 증명을 입력한다.

[root@myeks-host ~]# aws ec2 describe-instances

Unable to locate credentials. You can configure credentials by running "aws configure".
[root@myeks-host ~]# aws configure
AWS Access Key ID [None]: AKI..........
AWS Secret Access Key [None]: FQ.......................
Default region name [None]: ap-northeast-2
Default output format [None]: json
[root@myeks-host ~]# aws ec2 describe-instances
{
    "Reservations": [
        {
            "Groups": [],
            "Instances": [
                {
                    "AmiLaunchIndex": 0,
                    "ImageId": "ami-025cebb6913219d99",...........

 

3. eksctl로 클러스터 생성하기

EKS 워크샵 내용 (링크)에서는 yaml 파일을 작성하여 eksctl 명령어로 클러스터를 생성하는데, 기본적인 옵션을 직접 eksctl 명령어에 적절한 파라미터 형태로 전달하는 방식 또한 가능하여 스터디에서는 이 방법을 사용해 보았다. 필요한 옵션 값을 환경 변수로 저장하여 활용하였다.

3.1. 필요한 환경 변수 준비하기

$AWS_DEFAULT_REGION 및 $CLUSTER_NAME은 작업용 EC2에서 이미 준비가 되어 있다. 이를 확인해보고 나머지 환경 변수에 대해 설정해보았다.

[root@myeks-host ~]# echo $AWS_DEFAULT_REGION
ap-northeast-2
[root@myeks-host ~]# echo $CLUSTER_NAME
myeks
[root@myeks-host ~]# export VPCID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=$CLUSTER_NAME-VPC" | jq -r .Vpcs[].VpcId)
[root@myeks-host ~]# echo "export VPCID=$VPCID" >> /etc/profile
[root@myeks-host ~]# export PubSubnet1=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-PublicSubnet1" --query "Subnets[0].[SubnetId]" --output text)
rt PubSubnet2=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-PublicSubnet2" --query "Subnets[0].[SubnetId]" --output text)
echo "export PubSubnet1=$PubSubnet1" >> /etc/profile
echo "export PubSubnet2=$PubSubnet2" >> /etc/profile
[root@myeks-host ~]# export PubSubnet2=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-PublicSubnet2" --query "Subnets[0].[SubnetId]" --output text)
[root@myeks-host ~]# echo "export PubSubnet1=$PubSubnet1" >> /etc/profile
[root@myeks-host ~]# echo "export PubSubnet2=$PubSubnet2" >> /etc/profile
[root@myeks-host ~]# echo $VPCID
vpc-06019251cc08c519b
[root@myeks-host ~]# echo $PubSubnet1,$PubSubnet2
subnet-09c63523c434bcaec,subnet-0244ef5fa73c2f986

3.2. EKS 클러스터 생성하기

위 준비가 완료되었다면 아래 명령어를 통해 실행하면 된다.

eksctl create cluster --name $CLUSTER_NAME --region=$AWS_DEFAULT_REGION --nodegroup-name=$CLUSTER_NAME-nodegroup --node-type=t3.medium \
--node-volume-size=30 --vpc-public-subnets "$PubSubnet1,$PubSubnet2" --version 1.28 --ssh-access --external-dns-access --verbose 4​

 

총 15-20분 정도 소요되니 잠시 기다려보자. 기다리는 동안에 다른 터미널을 1개 더 열어 아래 명령어를 실행하면 클러스터가 생성되었는지 여부를 확인하는 데 도움이 된다.

while true; do aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,PrivateIPAdd:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text ; echo "------------------------------" ; sleep 1; done

클러스터 생성이 완료되면 터미널 상태는 다음과 같이 변경되고

AWS 콘솔에서도 EKS가 배포된 내용을 확인할 수 있다 (혹 콘솔에서 갱신이 안되었다면 새로고침 버튼을 클릭해보자).

 

EKS 클러스터 생성이 완료가 된 이후부터는 kubectl 명령어를 통해 EKS 클러스터에 여러 명령을 실행할 수 있게 된다. 스터디 중 여러 가지를 실행해보면서 많은 것들을 확인을 하였는데, 그 중 한 가지만 블로그에 정리해 보고자 한다.

4. 생성된 EKS 클러스터 확인 - 엔드포인트 액세스 변경 (Public -> Public and private)

EKS 클러스터 정보를 확인하기 위해서는 "kubectl cluster-info" 라는 명령어를 사용하면 된다.

(awesian@myeks:N/A) [root@myeks-host ~]# eksctl get nodegroup --cluster $CLUSTER_NAME --name $CLUSTER_NAME-nodegroup
CLUSTER NODEGROUP       STATUS  CREATED                 MIN SIZE        MAX SIZE        DESIRED CAPACITY        INSTANCE TYPE   IMAGE ID        ASG NAME              TYPE
myeks   myeks-nodegroup ACTIVE  2024-03-09T18:02:34Z    2               2               2                       t3.medium       AL2_x86_64      eks-myeks-nodegroup-eac71230-bb27-1b00-6c14-e2c96dfc5646       managed
(awesian@myeks:N/A) [root@myeks-host ~]# kubectl cluster-info
Kubernetes control plane is running at https://088CD22A78682CF5F017CFEE329E3C1A.gr7.ap-northeast-2.eks.amazonaws.com
CoreDNS is running at https://088CD22A78682CF5F017CFEE329E3C1A.gr7.ap-northeast-2.eks.amazonaws.com/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

뿐만 아니라 "eksctl get cluster" 명령어로도 확인이 가능하다. 한 가지 특이했던 점은 생성된 엔드포인트가 public이라는 것이다. Public이라는 의미는 해당 endpoint에 대한 네트워크 연결이 가능하다는 것을 의미하며, 해당 엔드포인트를 통해 Pod 생성 등을 진행하려면 추가 인증이 필요하나 간단한 version 확인은 public일 경우 별도의 인증을 거치지 않아도 위와 같이 생성한 EKS 클러스터에는 접근이 가능하였다.

콘솔에서 확인해 보더라도 API 서버 엔드포인트 액세스가 "Public"으로 나와있는 상황이다.

이를 "퍼블릭 및 프라이빗"으로 변경해보자. 변화 감지를 위해 총 3개의 터미널을 사용해보자. 2개 터미널은 모니터링 목적으로 사용한다.

# 터미널 A - 모니터링용 1
APIDNS=$(aws eks describe-cluster --name $CLUSTER_NAME | jq -r .cluster.endpoint | cut -d '/' -f 3)
dig +short $APIDNS
while true; do dig +short $APIDNS ; echo "------------------------------" ; date; sleep 1; done

# 터미널 B - 모니터링용 2
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-2c -o jsonpath={.items[0].status.addresses[0].address})
while true; do ssh ec2-user@$N1 sudo ss -tnp | egrep 'kubelet|kube-proxy' ; echo ; ssh ec2-user@$N2 sudo ss -tnp | egrep 'kubelet|kube-proxy' ; echo "------------------------------" ; date; sleep 1; done

# 터미널 C - Public(본인 접속 IP만 제한)+Private 로 변경. 설정 후 약 8-10분 정도 이후 다른 터미널에 변화 감지 가능
aws eks update-cluster-config --region $AWS_DEFAULT_REGION --name $CLUSTER_NAME --resources-vpc-config endpointPublicAccess=true,publicAccessCidrs="$(curl -s ipinfo.io/ip)/32",endpointPrivateAccess=true

기다려보면 터미널 A에서 2개의 public IP 로 보이던 부분이 갑자기 내부 네트워크 서브넷으로 변경된 부분을 확인할 수 있었다.

그런데 오른쪽은 변화가 없었는데, 이는 Public 및 Private 모두 활성화되다보니 기존 kube-proxy 및 kubelet이 이미 연결을 맺고 있는 네트워크 연결을 굳이 종료할 필요가 없어서가 아닌가 생각된다.

 

이후 "kubectl" 명령어를 실행하면 동작하지 않는다. 다음과 같이 실행해보면 오류 메시지와 함께 보이는 IP 주소가 Public IP가 아니라는 것을 확인할 수 있다. 즉, 클러스터 설정이 변경되면서 이제는 Private IP로 Endpoint를 반환하는 것이다.

(awesian@myeks:N/A) [root@myeks-host ~]# kubectl get node -v=6
I0310 03:44:52.743735   18383 loader.go:395] Config loaded from file:  /root/.kube/config
I0310 03:45:23.611890   18383 round_trippers.go:553] GET https://088CD22A78682CF5F017CFEE329E3C1A.gr7.ap-northeast-2.eks.amazonaws.com/api/v1/nodes?limit=500  in 30861 milliseconds
I0310 03:45:23.612005   18383 helpers.go:264] Connection error: Get https://088CD22A78682CF5F017CFEE329E3C1A.gr7.ap-northeast-2.eks.amazonaws.com/api/v1/nodes?limit=500: dial tcp 192.168.1.51:443: i/o timeout
Unable to connect to the server: dial tcp 192.168.1.51:443: i/o timeout
(awesian@myeks:N/A) [root@myeks-host ~]# kubectl cluster-info

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
Unable to connect to the server: dial tcp 192.168.2.122:443: i/o timeout

 

접속이 안된다는 것은 EKS Control plane 보안 그룹에서 서브넷에 접속 가능하도록 추가 설정을 해주어야 함을 의미한다. 다음 명령어를 통해 노드 보안 그룹에 myeks-host에서 노드(파드)에 접속 가능하도록 룰을 추가 설정하였다.

# EKS ControlPlane 보안그룹 ID 확인
aws ec2 describe-security-groups --filters Name=group-name,Values=*ControlPlaneSecurityGroup* --query "SecurityGroups[*].[GroupId]" --output text
CPSGID=$(aws ec2 describe-security-groups --filters Name=group-name,Values=*ControlPlaneSecurityGroup* --query "SecurityGroups[*].[GroupId]" --output text)
echo $CPSGID

# 노드 보안그룹에 myeks-host 에서 노드(파드)에 접속 가능하게 룰(Rule) 추가 설정
aws ec2 authorize-security-group-ingress --group-id $CPSGID --protocol '-1' --cidr 192.168.1.100/32

또한 kubelet과 kube-proxy도 private IP 주소로 접속하도록 설정을 변경해보자. 다음 명령어를 실행한다.

# kube-proxy rollout
kubectl rollout restart ds/kube-proxy -n kube-system

# kubelet 은 개별 노드에서 systemctl restart kubelet을 실행하는 형태로 적용. $N1과 $N2 환경 변수가 설정되어 있어야 함.
for i in $N1 $N2; do echo ">> node $i <<"; ssh ec2-user@$i sudo systemctl restart kubelet; echo; done

위 첫 번째 명령어를 실행한 다음에는 kube-proxy에 대한 연결이 private IP로 이루어지는 것을 볼 수 있으며,

두 번째 명령어가 잘 실행되면 kubelet 또한 연결이 private IP로 이루어지는 것을 볼 수 있다.

5. 리소스 정리

실습을 완료한 다음에는 반드시 리소스를 삭제하여 불필요한 비용 발생을 최소화하도록 하자.

  • Amazon EKS 클러스터 삭제하기 (약 10분 정도 소요): eksctl delete cluster --name $CLUSTER_NAME
  • 위 과정이 완료된 후 AWS CloudFormation 스택 삭제하기: aws cloudformation delete-stack --stack-name myeks

+ Recent posts