이번 포스트에서는 EKS Upgrade를 실습을 통해서 알아보겠습니다.

 

본 실습은 EKS Workshop인 Amazon EKS Upgrades: Strategies and Best Practices 를 바탕으로 진행하였음을 알려드립니다.

해당 워크샵 링크는 아래와 같습니다.

https://catalog.us-east-1.prod.workshops.aws/workshops/693bdee4-bc31-41d5-841f-54e3e54f8f4a/en-US

 

목차

  1. EKS의 업그레이드와 전략
  2. 실습 환경 개요
  3. In-place 클러스터 업그레이드
    3.1. 컨트롤 플레인 업그레이드
    3.2. Addons 업그레이드
    3.3. 관리형 노드 그룹 업그레이드
    3.4. Karpenter 노드 업그레이드
    3.5. Self-managed 노드 업그레이드
    3.6. Fargate 노드 업그레이드
  4. Blue/Green 클러스터 업그레이드

 

1. EKS의 업그레이드와 전략

쿠버네티스의 버전은 semantic versioning을 따르며, 특정 버전을 x.y.z라고 할 때 각 major.minor.patch 버전을 의미합니다.

새로운 쿠버네티스 마이너 버전은 약 4개월 바다 릴리즈 되며, 모든 버전은 12개월 동안 표준 지원을 제공되고, 한시점에 3개의 마이너 버전에 대한 표준 지원을 제공합니다. 표준 지원을 제공한다는 의미는 해당 버전에 대해서 패치가 지원된다는 의미로 이해하실 수 있습니다.

 

Amazon EKS는 쿠버네티스의 릴리즈 사이클을 따릅니다만, 세부적으로는 조금 더 넓은 범위의 지원을 보장합니다. EKS에서 특정 버전이 릴리즈되면 14개월 간 표준 지원이 되며, 또한 총 4개의 마이너 버전에 대한 표준 지원을 제공합니다.

현재 지원하는 쿠버네티스 버전에 대해서 아래 Amazon EKS kubernetes 릴리즈 일정을 살펴보시기 바랍니다.

https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/kubernetes-versions.html#kubernetes-release-calendar

 

EKS는 표준 지원(Standard Support)이 지난 이후에도 12개월의 확장 지원(Extended Support)을 제공하지만 이는 비용이 추가 됩니다.

웹 콘솔에서 EKS 클러스터의 Overview>Kubernetes version settings>Manage를 통해 Upgrade policy를 선택할 수 있습니다.

 

이때 표준 지원을 선택하는 경우, 표준 지원 기간이 종료되면 자동 업그레이드 되는 점을 유의하셔야 합니다.

 

버전 업그레이드에 대해서 고민해야 될 부분은 특정 버전이 14개월 동안 표준 지원되기 때문에 14개월 뒤에 업그레이드를 한다고 생각하실 수도 있지만, 사실 1개 버전만 업그레이드 하는 경우, 다음 버전의 EOS가 곧 도래하기 때문에 몇단계를 더 업그레이드 해야만 다시 1년가량을 안정적으로 사용하실 수 있습니다.

 

예를 들어, 1.27 버전이 2023/05/24~2024/07/24 까지 표준 지원 기간이지만, 2024년 7월에 1.28로 업그레이드를 해도 2024/12/26일에 다시 EOS가 도래합니다. 그렇기 때문에 실제로는 1.27->1.28->1.29->1.30까지 업그레이드를 해야 이후 1년 정도 EOS 이슈가 없이 사용할 수 있습니다.

 

EKS 업그레이드 과정

EKS의 업그레이드 과정은 실제 업그레이드에 대한 검토와 백업과 같은 내용을 제외하고 클러스터 자체를 업그레이드 하는 작업에 대해서만 설명합니다.

 

전반적인 업그레이드 절차는 아래와 같이 이뤄집니다.

1) 컨트롤 플레인 업그레이드

2) Add-on 업그레이드

3) 데이터 플레인 업그레이드

 

이때, 데이터 플레인의 형태가 다양한 경우, 세부적으로 데이터 플레인의 업그레이드 방식이 달라질 수 있습니다.

 

EKS 업그레이드 전략

EKS의 업그레이드 전략은 In-place 업그레이드와 Blue/Green 업그레이드가 있습니다.

In-place 업그레이드는 현재 운영 중인 클러스터에서 버전을 업그레이드 하는 것을 의미하며, Blue/Green 업그레이드는 신규 클러스터(Green)를 생성해 워크로드를 생성한 뒤 신규 클러스터로 전환하는 방법을 의미합니다.

 

업그레이드에 대한 세부적인 정보를 아래와 같은 문서를 참고하시기 바랍니다.

  • Best Practices for Cluster Upgrades

https://docs.aws.amazon.com/eks/latest/best-practices/cluster-upgrades.html

  • Kubernetes cluster upgrade: the blue-green deployment strategy

https://aws.amazon.com/ko/blogs/containers/kubernetes-cluster-upgrade-the-blue-green-deployment-strategy/

 

 

또한 중요한 사항은 Kubernetes Version Skew 정책입니다.

https://kubernetes.io/releases/version-skew-policy/#supported-version-skew

 

Kubernetes version skew 정책의 의미는 주요 컴포넌트(ex. kube-apiserver, kubelet, etc) 간 버전 차이가 얼마나 허용되는지에 대한 규칙입니다. In-place 업그레이드에서 여러 버전을 순차적으로 업그레이드할 수 있는데, 컨트롤 플레인과 데이터 플레인 간 허용되는 버전 내에서 업그레이드를 고려해야 합니다.

예를 들어, kube-apiserver의 버전이 1.32일 때, 허용되는 kubelet, kube-proxy의 version skew는 1.29까지입니다. 그러하므로 1.29에서 컨트롤 플레인 버전을 1.32까지 업그레이드 할 수 있고, 이후 노드 그룹의 버전을 순차적으로 업그레이드 하시면 됩니다.

 

또한 Kubernetes 의 In-place 업그레이드는 단계적인 버전 업그레이드만 지원되는 점도 유의를 해야 합니다. 한번에 여러 버전을 업그레이드 할 수 없습니다.

 

업그레이드 전 사전 검토 과정에서는 Cluster Insight 의 Upgrade insight를 검토해보기 바랍니다.

여기서는 아래와 같이 Kubernetes version skew, 클러스터 상태, add-on 버전 호환성, Deprecated API 에 대한 검토가 이뤄지는 것을 알 수 있씁니다.

 

이후 실습을 통해서 상세한 내용을 살펴보겠습니다.

 

 

2. 실습 환경 개요

본 실습에서는 EKS 1.25 클러스터이며 Extended upgrade policy에 해당하는 것을 알 수 있습니다.

 

또한 Compute 정보를 살펴보면 다양한 데이터 플레인 형태를 가지고 있습니다. Nodes를 보면 2개의 Managed node가 있고, 2개의 Self-managed node가 있습니다(실제로 1개는 Karpenter 노드입니다). 그리고 Fargate 노드도 확인됩니다.

그러하므로 컨트롤 플레인을 업그레이드 한 뒤, 각 노드 그룹의 유형 별로 다른 업그레이드 방식을 실습을 통해 살펴보겠습니다.

 

워크샵에서는 웹 콘솔뿐 아니라 code-server를 제공하며, cod-server에 접속하면 terraform 파일(녹색), git-ops-repo에 대한 로컬 파일(빨간색)이 저장되어 있습니다. code-server 우측에는 Terminal이나 파일 편집(파란색)을 할 수 있습니다.

 

git-ops-repo는 code commit이 remote로 지정되어 있으며, argo CD가 구성되어 code commit 리파지터리를 바라보도록 설정되어 있습니다.

 

그리고 EKS에서는 argo CD에 의해서 app of apps 형태로 아래와 같은 파드들이 실행 중에 있습니다.

ec2-user:~/environment/terraform:$ kubectl get application -A
NAMESPACE   NAME        SYNC STATUS   HEALTH STATUS
argocd      apps        Synced        Healthy
argocd      assets      Synced        Healthy
argocd      carts       Synced        Healthy
argocd      catalog     Synced        Healthy
argocd      checkout    Synced        Healthy
argocd      karpenter   Synced        Healthy
argocd      orders      Synced        Healthy
argocd      other       Synced        Healthy
argocd      rabbitmq    Synced        Healthy
argocd      ui          OutOfSync     Healthy

ec2-user:~/environment/terraform:$ kubectl get po -A |grep -v kube-system
NAMESPACE     NAME                                                        READY   STATUS    RESTARTS       AGE
argocd        argo-cd-argocd-application-controller-0                     1/1     Running   0              2d8h
argocd        argo-cd-argocd-applicationset-controller-74d9c9c5c7-n5k95   1/1     Running   0              2d8h
argocd        argo-cd-argocd-dex-server-6dbbd57479-mst55                  1/1     Running   0              2d8h
argocd        argo-cd-argocd-notifications-controller-fb4b954d5-v9dw7     1/1     Running   0              2d8h
argocd        argo-cd-argocd-redis-76b4c599dc-c8d2j                       1/1     Running   0              2d8h
argocd        argo-cd-argocd-repo-server-6b777b579d-b7ssz                 1/1     Running   0              2d8h
argocd        argo-cd-argocd-server-86bdbd7b89-gzm7d                      1/1     Running   0              2d8h
assets        assets-7ccc84cb4d-2p284                                     1/1     Running   0              2d8h
carts         carts-7ddbc698d8-wl9k9                                      1/1     Running   1 (2d8h ago)   2d8h
carts         carts-dynamodb-6594f86bb9-8gwpf                             1/1     Running   0              2d8h
catalog       catalog-857f89d57d-nrnf7                                    1/1     Running   3 (2d8h ago)   2d8h
catalog       catalog-mysql-0                                             1/1     Running   0              2d8h
checkout      checkout-558f7777c-z5qvh                                    1/1     Running   0              17h
checkout      checkout-redis-f54bf7cb5-r2sdp                              1/1     Running   0              17h
karpenter     karpenter-74c6ffc5d9-8m6mc                                  1/1     Running   0              2d8h
karpenter     karpenter-74c6ffc5d9-nj7lc                                  1/1     Running   0              2d8h
orders        orders-5b97745747-7rwdl                                     1/1     Running   2 (2d8h ago)   2d8h
orders        orders-mysql-b9b997d9d-bnbmn                                1/1     Running   0              2d8h
rabbitmq      rabbitmq-0                                                  1/1     Running   0              2d8h
ui            ui-5dfb7d65fc-nfrjw                                         1/1     Running   0              2d8h

 

환경을 이해하기 위해 code-comit 으로 push를 해서 argo CD로 sync가 이뤄지도록 변경을 수행해보겠습니다.

# service를 nlb로 노출
cat << EOF > ~/environment/eks-gitops-repo/apps/ui/service-nlb.yaml
apiVersion: v1
kind: Service
metadata:
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
    service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
    service.beta.kubernetes.io/aws-load-balancer-type: external
  labels:
    app.kubernetes.io/instance: ui
    app.kubernetes.io/name: ui
  name: ui-nlb
  namespace: ui
spec:
  type: LoadBalancer
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app.kubernetes.io/instance: ui
    app.kubernetes.io/name: ui
EOF

cat << EOF > ~/environment/eks-gitops-repo/apps/ui/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: ui
resources:
  - namespace.yaml
  - configMap.yaml
  - serviceAccount.yaml
  - service.yaml
  - deployment.yaml
  - hpa.yaml
  - service-nlb.yaml
EOF

#
cd ~/environment/eks-gitops-repo/
git add apps/ui/service-nlb.yaml apps/ui/kustomization.yaml
git commit -m "Add to ui nlb"
git push
argocd app sync ui
...

# UI 접속 URL 확인 (1.5, 1.3 배율)
kubectl get svc -n ui ui-nlb -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' | awk '{ print "UI URL = http://"$1""}'

 

이제 실제 업그레이드를 수행해 보겠습니다. 클러스터 업그레이드 수단은 웹 콘솔, CLI, IaC 도구 등이 있을 수 있습니다. 본 실습에서는 Terraform을 통해서 모든 업그레이드를 진행합니다.

 

 

3. In-place 클러스터 업그레이드

3.1. 컨트롤 플레인 업그레이드

EKS의 컨트롤 플레인 업그레이드는 Blue/Green 업그레이드 방식으로 진행되는 것으로 알려져 있습니다. 업그레이드 과정에서 이슈가 발생하면 업그레이드는 Roll back되어 영향을 최소화 합니다. Rollback 되는 경우 실패 이유를 평가하여 문제를 해결하기 위한 지침을 제공하여, 문제를 해결하고 다시 업그레이드를 시도할 수 있습니다.

 

컨트롤 플레인 업그레이드에 앞서 서비스 호출을 모니터링 하겠습니다.

export UI_WEB=$(kubectl get svc -n ui ui-nlb -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'/actuator/health/liveness)

while true; do date; curl -s $UI_WEB; echo; aws eks describe-cluster --name eksworkshop-eksctl | egrep 'version|endpoint"|issuer|platformVersion'; echo ; sleep 2; echo; done

 

Terraform 코드에서 EKS의 버전 정보는 variables.tf 저장되어 있습니다. 여기서 cluster_version을 1.25에서 1.26으로 변경합니다.

variable "cluster_version" {
  description = "EKS cluster version."
  type        = string
  default     = "1.25"
}

variable "mng_cluster_version" {
  description = "EKS cluster mng version."
  type        = string
  default     = "1.25"
}


variable "ami_id" {
  description = "EKS AMI ID for node groups"
  type        = string
  default     = ""
}

 

터미널에서 아래와 같이 수행하면 대략 10분 내에 완료가 됩니다.

ec2-user:~/environment/terraform:$ terraform apply -auto-approve
aws_iam_user.argocd_user: Refreshing state... [id=argocd-user]
module.vpc.aws_vpc.this[0]: Refreshing state... [id=vpc-0cf5ec98d2e448575]
module.eks.data.aws_partition.current[0]: Reading...
data.aws_caller_identity.current: Reading...
...
Plan: 6 to add, 13 to change, 6 to destroy.
...
...
Apply complete! Resources: 6 added, 2 changed, 6 destroyed.

Outputs:

configure_kubectl = "aws eks --region us-west-2 update-kubeconfig --name eksworkshop-eksctl"

 

웹 콘솔에서도 업그레이드가 트리거 된 것을 확인 할 수 있습니다.

 

컨트롤 플레인 업그레이드 전/후 시간과 상태를 보면 아래와 같습니다.

# 업그레이드 전
Tue Apr  1 14:22:41 UTC 2025
{"status":"UP"}
        "version": "1.25",
        "endpoint": "https://C55B5928163C30776DEF011A92FE870C.gr7.us-west-2.eks.amazonaws.com",
                "issuer": "https://oidc.eks.us-west-2.amazonaws.com/id/C55B5928163C30776DEF011A92FE870C"
        "platformVersion": "eks.44",
...
Tue Apr  1 14:30:20 UTC 2025
{"status":"UP"}
        "version": "1.26",
        "endpoint": "https://C55B5928163C30776DEF011A92FE870C.gr7.us-west-2.eks.amazonaws.com",
                "issuer": "https://oidc.eks.us-west-2.amazonaws.com/id/C55B5928163C30776DEF011A92FE870C"
        "platformVersion": "eks.45",

 

업그레이드를 진행한 이후에 데이터 플레인의 버전과는 상이한 상태이지만, Kubernetes version skew에서는 문제가 없는 상황입니다. 아래와 같이 Upgrade insight를 확인해볼 수 있습니다.

 

3.2. Addons 업그레이드

이제 애드온 업그레이드를 진행하겠습니다.

eksctl 을 통해서 가능한 업그레이드 버전을 확인할 수 있습니다.

NAME                    VERSION                 STATUS  ISSUES  IAMROLE                                                                                         UPDATE AVAILABLE                                                                                                                                                   CONFIGURATION VALUES
aws-ebs-csi-driver      v1.41.0-eksbuild.1      ACTIVE  0       arn:aws:iam::181150650881:role/eksworkshop-eksctl-ebs-csi-driver-2025033005221599450000001d
coredns                 v1.8.7-eksbuild.10      ACTIVE  0                                                                                                       v1.9.3-eksbuild.22,v1.9.3-eksbuild.21,v1.9.3-eksbuild.19,v1.9.3-eksbuild.17,v1.9.3-eksbuild.15,v1.9.3-eksbuild.11,v1.9.3-eksbuild.10,v1.9.3-eksbuild.9,v1.9.3-eksbuild.7,v1.9.3-eksbuild.6,v1.9.3-eksbuild.5,v1.9.3-eksbuild.3,v1.9.3-eksbuild.2
kube-proxy              v1.25.16-eksbuild.8     ACTIVE  0                                                                                                       v1.26.15-eksbuild.24,v1.26.15-eksbuild.19,v1.26.15-eksbuild.18,v1.26.15-eksbuild.14,v1.26.15-eksbuild.10,v1.26.15-eksbuild.5,v1.26.15-eksbuild.2,v1.26.13-eksbuild.2,v1.26.11-eksbuild.4,v1.26.11-eksbuild.1,v1.26.9-eksbuild.2,v1.26.7-eksbuild.2,v1.26.6-eksbuild.2,v1.26.6-eksbuild.1,v1.26.4-eksbuild.1,v1.26.2-eksbuild.1
vpc-cni                 v1.19.3-eksbuild.1      ACTIVE  0

 

각 애드온은 아래 페이지에서 EKS 버전 별 호환 버전을 확인하실 수 있습니다.

 

또한 아래 명령으로 1.26에 호환되는 버전 정보를 확인 할 수 있습니다. 현재 VPC CNI와 EBS CSI driver는 최신 버전을 사용 중으로 coredns(v1.8.7-eksbuild.10), kube-proxy(v1.25.16-eksbuild.8)에 대해서 확인합니다.

ec2-user:~/environment/terraform:$ aws eks describe-addon-versions --addon-name coredns --kubernetes-version 1.26 --output table \
    --query "addons[].addonVersions[:10].{Version:addonVersion,DefaultVersion:compatibilities[0].defaultVersion}"
------------------------------------------
|          DescribeAddonVersions         |
+-----------------+----------------------+
| DefaultVersion  |       Version        |
+-----------------+----------------------+
|  False          |  v1.9.3-eksbuild.22  |
|  False          |  v1.9.3-eksbuild.21  |
|  False          |  v1.9.3-eksbuild.19  |
|  False          |  v1.9.3-eksbuild.17  |
|  False          |  v1.9.3-eksbuild.15  |
|  False          |  v1.9.3-eksbuild.11  |
|  False          |  v1.9.3-eksbuild.10  |
|  False          |  v1.9.3-eksbuild.9   |
|  True           |  v1.9.3-eksbuild.7   |
|  False          |  v1.9.3-eksbuild.6   |
+-----------------+----------------------+
ec2-user:~/environment/terraform:$ aws eks describe-addon-versions --addon-name kube-proxy --kubernetes-version 1.26 --output table \
    --query "addons[].addonVersions[:10].{Version:addonVersion,DefaultVersion:compatibilities[0].defaultVersion}"
--------------------------------------------
|           DescribeAddonVersions          |
+-----------------+------------------------+
| DefaultVersion  |        Version         |
+-----------------+------------------------+
|  False          |  v1.26.15-eksbuild.24  |
|  False          |  v1.26.15-eksbuild.19  |
|  False          |  v1.26.15-eksbuild.18  |
|  False          |  v1.26.15-eksbuild.14  |
|  False          |  v1.26.15-eksbuild.10  |
|  False          |  v1.26.15-eksbuild.5   |
|  False          |  v1.26.15-eksbuild.2   |
|  False          |  v1.26.13-eksbuild.2   |
|  False          |  v1.26.11-eksbuild.4   |
|  False          |  v1.26.11-eksbuild.1   |
+-----------------+------------------------+

 

테라폼 코드 중 addons.tf 를 열어 아래의 정보를 최신 버전으로 변경합니다.

  eks_addons = {
    coredns = {
      addon_version = "v1.8.7-eksbuild.10"
    }
    kube-proxy = {
      addon_version = "v1.25.16-eksbuild.8"
    }
    vpc-cni = {
      most_recent = true
    }
    aws-ebs-csi-driver = {
      service_account_role_arn = module.ebs_csi_driver_irsa.iam_role_arn
    }
  }

 

terraform 명령으로 업그레이드를 진행합니다.

ec2-user:~/environment/terraform:$ terraform apply -auto-approve
aws_iam_user.argocd_user: Refreshing state... [id=argocd-user]
data.aws_caller_identity.current: Reading...

...

Apply complete! Resources: 0 added, 2 changed, 0 destroyed.

Outputs:

configure_kubectl = "aws eks --region us-west-2 update-kubeconfig --name eksworkshop-eksctl"

 

대략 1분 30초 정도가 소요되었습니다.

Tue Apr  1 14:58:07 UTC 2025
{"status":"UP"}
        "version": "1.26",
        "endpoint": "https://C55B5928163C30776DEF011A92FE870C.gr7.us-west-2.eks.amazonaws.com",
                "issuer": "https://oidc.eks.us-west-2.amazonaws.com/id/C55B5928163C30776DEF011A92FE870C"
        "platformVersion": "eks.45",
...
Tue Apr  1 14:59:36 UTC 2025
{"status":"UP"}
        "version": "1.26",
        "endpoint": "https://C55B5928163C30776DEF011A92FE870C.gr7.us-west-2.eks.amazonaws.com",
                "issuer": "https://oidc.eks.us-west-2.amazonaws.com/id/C55B5928163C30776DEF011A92FE870C"
        "platformVersion": "eks.45",

 

관련 파드들이 롤링 업데이트 됩니다. 과정을 살펴보면 coredns는 pdb가 지정되어 있기 때문에 하나의 파드가 Running 상태가 된 이후 old파드가 Terminating되는 것을 알 수 있습니다. kube-proxy는 데몬 셋으로 종료 후 신규 파드로 생성됩니다.

ec2-user:~/environment:$ kubectl get pdb -n kube-system
NAME                           MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS   AGE
aws-load-balancer-controller   N/A             1                 1                     2d9h
coredns                        N/A             1                 1                     2d9h
ebs-csi-controller             N/A             1                 1                     2d9h

ec2-user:~/environment:$ kubectl get po -n kube-system -w 
NAME                                            READY   STATUS    RESTARTS   AGE
...
kube-proxy-rdhmw                                1/1     Terminating   0          2d9h
coredns-98f76fbc4-d7l7z                         1/1     Terminating   0          2d9h
kube-proxy-rdhmw                                0/1     Terminating   0          2d9h
kube-proxy-rdhmw                                0/1     Terminating   0          2d9h
kube-proxy-rdhmw                                0/1     Terminating   0          2d9h
coredns-58cc4d964b-5rbmb                        0/1     Pending       0          0s
coredns-58cc4d964b-5rbmb                        0/1     Pending       0          0s
coredns-58cc4d964b-5rbmb                        0/1     ContainerCreating   0          0s
coredns-58cc4d964b-d5zmg                        0/1     Pending             0          0s
coredns-58cc4d964b-d5zmg                        0/1     Pending             0          0s
kube-proxy-gbn46                                0/1     Pending             0          1s
kube-proxy-gbn46                                0/1     Pending             0          1s
coredns-58cc4d964b-d5zmg                        0/1     ContainerCreating   0          0s
kube-proxy-gbn46                                0/1     ContainerCreating   0          1s
coredns-98f76fbc4-d7l7z                         0/1     Terminating         0          2d9h
coredns-98f76fbc4-d7l7z                         0/1     Terminating         0          2d9h
coredns-98f76fbc4-d7l7z                         0/1     Terminating         0          2d9h
coredns-58cc4d964b-d5zmg                        0/1     Running             0          2s
coredns-58cc4d964b-d5zmg                        1/1     Running             0          2s
coredns-98f76fbc4-brtkn                         1/1     Terminating         0          2d9h
coredns-58cc4d964b-5rbmb                        0/1     Running             0          3s
coredns-58cc4d964b-5rbmb                        1/1     Running             0          3s
kube-proxy-gbn46                                1/1     Running             0          3s
kube-proxy-rkvpc                                1/1     Terminating         0          2d9h
coredns-98f76fbc4-brtkn                         0/1     Terminating         0          2d9h
coredns-98f76fbc4-brtkn                         0/1     Terminating         0          2d9h
coredns-98f76fbc4-brtkn                         0/1     Terminating         0          2d9h
kube-proxy-rkvpc                                0/1     Terminating         0          2d9h
kube-proxy-rkvpc                                0/1     Terminating         0          2d9h
kube-proxy-rkvpc                                0/1     Terminating         0          2d9h
kube-proxy-tt8mk                                0/1     Pending             0          0s
kube-proxy-tt8mk                                0/1     Pending             0          0s
kube-proxy-tt8mk                                0/1     ContainerCreating   0          0s
kube-proxy-tt8mk                                1/1     Running             0          2s
kube-proxy-psbfc                                1/1     Terminating         0          2d9h
kube-proxy-psbfc                                0/1     Terminating         0          2d9h
kube-proxy-psbfc                                0/1     Terminating         0          2d9h
kube-proxy-psbfc                                0/1     Terminating         0          2d9h
kube-proxy-vv6cz                                0/1     Pending             0          0s
kube-proxy-vv6cz                                0/1     Pending             0          0s
kube-proxy-vv6cz                                0/1     ContainerCreating   0          0s
kube-proxy-vv6cz                                1/1     Running             0          2s
kube-proxy-sv977                                1/1     Terminating         0          2d9h
kube-proxy-sv977                                0/1     Terminating         0          2d9h
kube-proxy-sv977                                0/1     Terminating         0          2d9h
kube-proxy-sv977                                0/1     Terminating         0          2d9h
kube-proxy-t9xxk                                0/1     Pending             0          0s
kube-proxy-t9xxk                                0/1     Pending             0          0s
kube-proxy-t9xxk                                0/1     ContainerCreating   0          0s
kube-proxy-t9xxk                                1/1     Running             0          2s
kube-proxy-5zz6t                                1/1     Terminating         0          17h
kube-proxy-5zz6t                                0/1     Terminating         0          17h
kube-proxy-5zz6t                                0/1     Terminating         0          17h
kube-proxy-5zz6t                                0/1     Terminating         0          17h
kube-proxy-zh6st                                0/1     Pending             0          0s
kube-proxy-zh6st                                0/1     Pending             0          0s
kube-proxy-zh6st                                0/1     ContainerCreating   0          0s
kube-proxy-zh6st                                1/1     Running             0          2s
kube-proxy-jbwlb                                1/1     Terminating         0          2d9h
kube-proxy-jbwlb                                0/1     Terminating         0          2d9h
kube-proxy-jbwlb                                0/1     Terminating         0          2d9h
kube-proxy-jbwlb                                0/1     Terminating         0          2d9h
kube-proxy-6jlqj                                0/1     Pending             0          0s
kube-proxy-6jlqj                                0/1     Pending             0          0s
kube-proxy-6jlqj                                0/1     ContainerCreating   0          0s
kube-proxy-6jlqj                                1/1     Running             0          2s

 

 

3.3. 관리형 노드 그룹 업그레이드

In-place 클러스터 업그레이드에서도 관리형 노드 그룹의 업그레이드를 In-place와 Blue/Green 업그레이드로 선택 진행할 수 있습니다.

 

관리형 노드 그룹 In-place 업그레이드

In-place 업그레이드는 점진적인 롤링 업그레이드로 구현되어, 새로운 노드가 먼저 ASG에 추가되고, 이후 구 노드는 cordon, drain, remove 되는 방식으로 진행됩니다.

이 과정을 설정 단계>확장 단계>업그레이드 단계>축소단계로 이해할 수 있습니다.

 

1) 설정 단계

최신 Lunch template 버전을 사용하도록 ASG를 업데이트하고 updateConfig 속성을 사용하여 병렬로 업그레이드할 노드의 최대 수를 결정.

참고로 updateConfig는 노드 그룹의 속성에서 확인할 수 있습니다.

 

이때, Update strategy의 Default는 새 노드를 먼저 추가 후 구 노드를 삭제하는 방식이고, Minimal 구 노드를 바로 삭제하는 방식입니다. 비용이 우선시 되는 노드 그룹은 Minimal을 선택할 수 있습니다.

 

2) 확장 단계

ASG의 Maximum size나 desired size 중 큰 값으로 증가시킵니다. 또한 배포된 가용 영역 수의 두배까지 증가합니다.

이 단계에서 노드그룹을 확장하면 구 노드에 대해서는 un-schedulable로 마크하고, node.kubernetes.io/exclude-from-external-load-balancers=true를 설정해 로드 밸러서에서 노들르 제거할 수 있도록 합니다.

 

3) 업그레이드 단계

노드에서 파드 drain 하고, 노드를 cordon합니다. 이후 ASG에 종료 요청을 보냅니다. Unavailable 단위로 진행할 수 있으며, 모든 구 노드가 삭제될 때까지 업그레이드 단계를 반복합니다.

 

4) 축소단계

ASG의 Maximum과 Desired 를 1씩 줄여서 업데이트가 시작되기 전의 값으로 돌아갑니다.

 

 

이제 in-place 업그레이드를 진행합니다.

variable.tf 파일의 관리형 노드 그룹에 대한 값을 1.26으로 변경합니다.

variable "cluster_version" {
  description = "EKS cluster version."
  type        = string
  default     = "1.26"
}

variable "mng_cluster_version" {
  description = "EKS cluster mng version."
  type        = string
  default     = "1.25" # <- 1.26 
}


variable "ami_id" {
  description = "EKS AMI ID for node groups"
  type        = string
  default     = ""
}

 

마찬가지로 terraform을 적용합니다.

ec2-user:~/environment/terraform:$ terraform apply -auto-approve
...

Apply complete! Resources: 3 added, 1 changed, 3 destroyed.

Outputs:

configure_kubectl = "aws eks --region us-west-2 update-kubeconfig --name eksworkshop-eksctl"
ec2-user:~/environment/terraform:$ 

 

업그레이드 과정에서 증가/축소 및 노드 상태를 확인하기 위해서 아래와 같이 모니터링을 하겠습니다.

while true; do date; kubectl get nodes -o wide --label-columns=eks.amazonaws.com/nodegroup,topology.kubernetes.io/zone |grep initial; sleep 5; echo; done

# 최초 
ec2-user:~/environment:$ while true; do date; kubectl get nodes -o wide --label-columns=eks.amazonaws.com/nodegroup,topology.kubernetes.io/zone |grep initial; sleep 5; echo; done
Tue Apr  1 15:49:52 UTC 2025
ip-10-0-12-239.us-west-2.compute.internal           Ready    <none>   2d10h   v1.25.16-eks-59bf375   10.0.12.239   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2a
ip-10-0-32-55.us-west-2.compute.internal            Ready    <none>   2d10h   v1.25.16-eks-59bf375   10.0.32.55    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2c


# 증가 단계 (4대의 노드가 추가 됨)
Tue Apr  1 15:53:06 UTC 2025
ip-10-0-12-239.us-west-2.compute.internal           Ready    <none>   2d10h   v1.25.16-eks-59bf375   10.0.12.239   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2a
ip-10-0-15-190.us-west-2.compute.internal           Ready    <none>   27s     v1.26.15-eks-59bf375   10.0.15.190   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2a
ip-10-0-28-191.us-west-2.compute.internal           Ready    <none>   2m27s   v1.26.15-eks-59bf375   10.0.28.191   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2b
ip-10-0-30-83.us-west-2.compute.internal            Ready    <none>   2m26s   v1.26.15-eks-59bf375   10.0.30.83    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2b
ip-10-0-32-55.us-west-2.compute.internal            Ready    <none>   2d10h   v1.25.16-eks-59bf375   10.0.32.55    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2c
ip-10-0-46-150.us-west-2.compute.internal           Ready    <none>   94s     v1.26.15-eks-59bf375   10.0.46.150   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2c


# 업그레이드 단계
# old node cordon
Tue Apr  1 15:53:12 UTC 2025
ip-10-0-12-239.us-west-2.compute.internal           Ready,SchedulingDisabled   <none>   2d10h   v1.25.16-eks-59bf375   10.0.12.239   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2a
ip-10-0-15-190.us-west-2.compute.internal           Ready                      <none>   33s     v1.26.15-eks-59bf375   10.0.15.190   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2a
ip-10-0-28-191.us-west-2.compute.internal           Ready                      <none>   2m33s   v1.26.15-eks-59bf375   10.0.28.191   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2b
ip-10-0-30-83.us-west-2.compute.internal            Ready                      <none>   2m32s   v1.26.15-eks-59bf375   10.0.30.83    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2b
ip-10-0-32-55.us-west-2.compute.internal            Ready,SchedulingDisabled   <none>   2d10h   v1.25.16-eks-59bf375   10.0.32.55    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2c
ip-10-0-46-150.us-west-2.compute.internal           Ready                      <none>   100s    v1.26.15-eks-59bf375   10.0.46.150   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2c

# x.x.x.239 old node NotReady
Tue Apr  1 15:56:07 UTC 2025
ip-10-0-12-239.us-west-2.compute.internal           NotReady,SchedulingDisabled   <none>   2d10h   v1.25.16-eks-59bf375   10.0.12.239   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2a
ip-10-0-15-190.us-west-2.compute.internal           Ready                         <none>   3m29s   v1.26.15-eks-59bf375   10.0.15.190   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2a
ip-10-0-28-191.us-west-2.compute.internal           Ready                         <none>   5m29s   v1.26.15-eks-59bf375   10.0.28.191   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2b
ip-10-0-30-83.us-west-2.compute.internal            Ready                         <none>   5m28s   v1.26.15-eks-59bf375   10.0.30.83    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2b
ip-10-0-32-55.us-west-2.compute.internal            Ready,SchedulingDisabled      <none>   2d10h   v1.25.16-eks-59bf375   10.0.32.55    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2c
ip-10-0-46-150.us-west-2.compute.internal           Ready                         <none>   4m36s   v1.26.15-eks-59bf375   10.0.46.150   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2c

# x.x.x.239 old node removed
Tue Apr  1 15:56:14 UTC 2025
ip-10-0-15-190.us-west-2.compute.internal           Ready                      <none>   3m35s   v1.26.15-eks-59bf375   10.0.15.190   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2a
ip-10-0-28-191.us-west-2.compute.internal           Ready                      <none>   5m35s   v1.26.15-eks-59bf375   10.0.28.191   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2b
ip-10-0-30-83.us-west-2.compute.internal            Ready                      <none>   5m34s   v1.26.15-eks-59bf375   10.0.30.83    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2b
ip-10-0-32-55.us-west-2.compute.internal            Ready,SchedulingDisabled   <none>   2d10h   v1.25.16-eks-59bf375   10.0.32.55    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2c
ip-10-0-46-150.us-west-2.compute.internal           Ready                      <none>   4m42s   v1.26.15-eks-59bf375   10.0.46.150   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2c

# x.x.x.55 old node Not ready
Tue Apr  1 15:59:08 UTC 2025
ip-10-0-15-190.us-west-2.compute.internal           Ready                         <none>   6m29s   v1.26.15-eks-59bf375   10.0.15.190   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2a
ip-10-0-28-191.us-west-2.compute.internal           Ready                         <none>   8m29s   v1.26.15-eks-59bf375   10.0.28.191   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2b
ip-10-0-30-83.us-west-2.compute.internal            Ready                         <none>   8m28s   v1.26.15-eks-59bf375   10.0.30.83    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2b
ip-10-0-32-55.us-west-2.compute.internal            NotReady,SchedulingDisabled   <none>   2d10h   v1.25.16-eks-59bf375   10.0.32.55    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2c
ip-10-0-46-150.us-west-2.compute.internal           Ready                         <none>   7m36s   v1.26.15-eks-59bf375   10.0.46.150   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2c

# all old nodes removed and all nodes are 1.26.15
Tue Apr  1 15:59:32 UTC 2025
ip-10-0-15-190.us-west-2.compute.internal           Ready    <none>   6m53s   v1.26.15-eks-59bf375   10.0.15.190   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2a
ip-10-0-28-191.us-west-2.compute.internal           Ready    <none>   8m53s   v1.26.15-eks-59bf375   10.0.28.191   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2b
ip-10-0-30-83.us-west-2.compute.internal            Ready    <none>   8m52s   v1.26.15-eks-59bf375   10.0.30.83    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2b
ip-10-0-46-150.us-west-2.compute.internal           Ready    <none>   8m      v1.26.15-eks-59bf375   10.0.46.150   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2c


# 축소 단계
# new node 도 cordon 상태로 빠짐
Tue Apr  1 16:00:29 UTC 2025
ip-10-0-15-190.us-west-2.compute.internal           Ready                      <none>   7m50s   v1.26.15-eks-59bf375   10.0.15.190   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2a
ip-10-0-28-191.us-west-2.compute.internal           Ready                      <none>   9m50s   v1.26.15-eks-59bf375   10.0.28.191   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2b
ip-10-0-30-83.us-west-2.compute.internal            Ready,SchedulingDisabled   <none>   9m49s   v1.26.15-eks-59bf375   10.0.30.83    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2b
ip-10-0-46-150.us-west-2.compute.internal           Ready                      <none>   8m57s   v1.26.15-eks-59bf375   10.0.46.150   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2c

# 노드 4대 -> 3대
Tue Apr  1 16:02:22 UTC 2025
ip-10-0-15-190.us-west-2.compute.internal           Ready    <none>   9m43s   v1.26.15-eks-59bf375   10.0.15.190   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2a
ip-10-0-28-191.us-west-2.compute.internal           Ready    <none>   11m     v1.26.15-eks-59bf375   10.0.28.191   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2b
ip-10-0-46-150.us-west-2.compute.internal           Ready    <none>   10m     v1.26.15-eks-59bf375   10.0.46.150   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2c

# 한대 더 corndon
Tue Apr  1 16:03:31 UTC 2025
ip-10-0-15-190.us-west-2.compute.internal           Ready,SchedulingDisabled   <none>   10m     v1.26.15-eks-59bf375   10.0.15.190   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2a
ip-10-0-28-191.us-west-2.compute.internal           Ready                      <none>   12m     v1.26.15-eks-59bf375   10.0.28.191   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2b
ip-10-0-46-150.us-west-2.compute.internal           Ready                      <none>   11m     v1.26.15-eks-59bf375   10.0.46.150   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2c

# 최종 2대, 1.26 버전으로 업그레이드 됨
Tue Apr  1 16:05:24 UTC 2025
ip-10-0-28-191.us-west-2.compute.internal           Ready    <none>   14m     v1.26.15-eks-59bf375   10.0.28.191   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2b
ip-10-0-46-150.us-west-2.compute.internal           Ready    <none>   13m     v1.26.15-eks-59bf375   10.0.46.150   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   initial-2025033005225054810000002a    us-west-2c

 

EKS의 업그레이드는 신규 노드가 추가되고 drain>cordon>drain으로 구 노드를 삭제하고, 최종 desired 수로 축소하는 방식으로 이뤄집니다. 이로 인해 결과적으로는 구 노드가 삭제되고, 신규 버전으로 생성된 신규 노드가 남는 방식으로 업그레이드가 진행됩니다.

 

2대 노드를 가진 노드 그룹의 업그레이드 시간은 대략 15분(15:49:52~16:05:24) 정도가 소요되었습니다.

 

관리형 노드 그룹 Blue/Green 업그레이드

관리형 노드 그룹도 Blue/Green 업그레이드 방식을 선택할 수 있습니다.

해당 실습에서 blue 노드 그룹은 특정 stateful 워크로드와 PV 사용으로 특정 가용 영역에만 프로비저닝이 되어 있습니다.

 

이 경우의 업그레이드 방식은 먼저 terraform 에 Green 관리형 노드 그룹을 생성하고, 이후 Blue 관리형 노드 그룹을 삭제하는 방식으로 진행됩니다.

먼저 base.tf에 Green 노드 그룹을 생성합니다.

  eks_managed_node_groups = {
    initial = {
      instance_types = ["m5.large", "m6a.large", "m6i.large"]
      min_size     = 2
      max_size     = 10
      desired_size = 2
      update_config = {
        max_unavailable_percentage = 35
      }
    }

    blue-mng={
      instance_types = ["m5.large", "m6a.large", "m6i.large"]
      cluster_version = "1.25"
      min_size     = 1
      max_size     = 2
      desired_size = 1
      update_config = {
        max_unavailable_percentage = 35
      }
      labels = {
        type = "OrdersMNG"
      }
      subnet_ids = [module.vpc.private_subnets[0]] # 해당 MNG은 프라이빗서브넷1 에서 동작(ebs pv 사용 중)
      taints = [
        {
          key    = "dedicated"
          value  = "OrdersApp"
          effect = "NO_SCHEDULE"
        }
      ]
    }

    green-mng={
      instance_types = ["m5.large", "m6a.large", "m6i.large"]
      subnet_ids = [module.vpc.private_subnets[0]]
      min_size     = 1
      max_size     = 2
      desired_size = 1
      update_config = {
        max_unavailable_percentage = 35
      }
      labels = {
        type = "OrdersMNG"
      }
      taints = [
        {
          key    = "dedicated"
          value  = "OrdersApp"
          effect = "NO_SCHEDULE"
        }
      ]
    }
  }

 

그리고 terraform apply -auto-approve을 수행하고, 노드가 증가한 상태를 확인 합니다. 동일한 가용 영역에 생성된 것을 확인할 수 있습니다.

ec2-user:~/environment:$ while true; do date; kubectl get nodes -o wide --label-columns=eks.amazonaws.com/nodegroup,topology.kubernetes.io/zone |egrep "green|blue"; sleep 5; echo; done
Tue Apr  1 16:24:49 UTC 2025
ip-10-0-3-145.us-west-2.compute.internal            Ready    <none>   2d11h   v1.25.16-eks-59bf375   10.0.3.145    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   blue-mng-2025033005225055020000002c   us-west-2a
...
Tue Apr  1 16:27:44 UTC 2025
ip-10-0-3-145.us-west-2.compute.internal            Ready    <none>   2d11h   v1.25.16-eks-59bf375   10.0.3.145    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   blue-mng-2025033005225055020000002c    us-west-2a
ip-10-0-3-227.us-west-2.compute.internal            Ready    <none>   40s     v1.26.15-eks-59bf375   10.0.3.227    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   green-mng-20250401162553095800000007   us-west-2a

 

이제 base.tf에서 blue 노드 그룹을 삭제하고 terraform apply -auto-approve을 수행합니다.

현재 blue 노드 그룹에는 orders 파드들이 실행 중인 것을 알 수 있습니다.

ec2-user:~/environment:$ kubectl get po -A -owide |grep ip-10-0-3-145.us-west-2.compute.internal
kube-system   aws-node-g9sk9                                              2/2     Running   0               2d11h   10.0.3.145    ip-10-0-3-145.us-west-2.compute.internal            <none>           <none>
kube-system   ebs-csi-node-8jqbj                                          3/3     Running   0               2d11h   10.0.0.162    ip-10-0-3-145.us-west-2.compute.internal            <none>           <none>
kube-system   efs-csi-node-x546f                                          3/3     Running   0               2d11h   10.0.3.145    ip-10-0-3-145.us-west-2.compute.internal            <none>           <none>
kube-system   kube-proxy-6jlqj                                            1/1     Running   0               92m     10.0.3.145    ip-10-0-3-145.us-west-2.compute.internal            <none>           <none>
orders        orders-5b97745747-7rwdl                                     1/1     Running   2 (2d10h ago)   2d11h   10.0.3.163    ip-10-0-3-145.us-west-2.compute.internal            <none>           <none>
orders        orders-mysql-b9b997d9d-bnbmn                                1/1     Running   0               2d11h   10.0.7.229    ip-10-0-3-145.us-west-2.compute.internal            <none>           <none>

 

노드와 이들 파드들을 모니터링 하겠습니다.

# 최초 상태
ec2-user:~/environment:$  while true; do date; kubectl get nodes -o wide --label-columns=eks.amazonaws.com/nodegroup,topology.kubernetes.io/zone |egrep "green|blue";echo;  kubectl get po -A -owide |grep orders; sleep 5; echo; done
Tue Apr  1 16:34:39 UTC 2025
ip-10-0-3-145.us-west-2.compute.internal            Ready    <none>   2d11h   v1.25.16-eks-59bf375   10.0.3.145    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   blue-mng-2025033005225055020000002c    us-west-2a
ip-10-0-3-227.us-west-2.compute.internal            Ready    <none>   7m35s   v1.26.15-eks-59bf375   10.0.3.227    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   green-mng-20250401162553095800000007   us-west-2a

orders        orders-5b97745747-7rwdl                                     1/1     Running   2 (2d11h ago)   2d11h   10.0.3.163    ip-10-0-3-145.us-west-2.compute.internal            <none>           <none>
orders        orders-mysql-b9b997d9d-bnbmn                                1/1     Running   0               2d11h   10.0.7.229    ip-10-0-3-145.us-west-2.compute.internal            <none>           <none>

# Blue 노드 cordon>drain으로 Green 노드로 이전함
Tue Apr  1 16:35:04 UTC 2025
ip-10-0-3-145.us-west-2.compute.internal            Ready    <none>   2d11h   v1.25.16-eks-59bf375   10.0.3.145    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   blue-mng-2025033005225055020000002c    us-west-2a
ip-10-0-3-227.us-west-2.compute.internal            Ready    <none>   8m      v1.26.15-eks-59bf375   10.0.3.227    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   green-mng-20250401162553095800000007   us-west-2a

orders        orders-5b97745747-7rwdl                                     1/1     Running   2 (2d11h ago)   2d11h   10.0.3.163    ip-10-0-3-145.us-west-2.compute.internal            <none>           <none>
orders        orders-mysql-b9b997d9d-bnbmn                                1/1     Running   0               2d11h   10.0.7.229    ip-10-0-3-145.us-west-2.compute.internal            <none>           <none>

Tue Apr  1 16:35:11 UTC 2025
ip-10-0-3-145.us-west-2.compute.internal            Ready,SchedulingDisabled   <none>   2d11h   v1.25.16-eks-59bf375   10.0.3.145    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   blue-mng-2025033005225055020000002c    us-west-2a
ip-10-0-3-227.us-west-2.compute.internal            Ready                      <none>   8m7s    v1.26.15-eks-59bf375   10.0.3.227    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   green-mng-20250401162553095800000007   us-west-2a

orders        orders-5b97745747-7ctj4                                     0/1     ContainerCreating   0               5s      <none>        ip-10-0-3-227.us-west-2.compute.internal            <none>           <none>
orders        orders-mysql-b9b997d9d-wc9vn                                0/1     ContainerCreating   0               5s      <none>        ip-10-0-3-227.us-west-2.compute.internal            <none>           <none>

# 최종 상태
Tue Apr  1 16:37:03 UTC 2025
ip-10-0-3-227.us-west-2.compute.internal            Ready    <none>   10m     v1.26.15-eks-59bf375   10.0.3.227    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25   green-mng-20250401162553095800000007   us-west-2a

orders        orders-5b97745747-7ctj4                                     1/1     Running   2 (81s ago)     118s    10.0.8.104    ip-10-0-3-227.us-west-2.compute.internal            <none>           <none>
orders        orders-mysql-b9b997d9d-wc9vn                                1/1     Running   0               118s    10.0.12.108   ip-10-0-3-227.us-west-2.compute.internal            <none>           <none>

 

관리형 노드 그룹에 대한 실습을 마무리 하겠습니다.

 

3.4. Karpenter 노드 업그레이드

앞서 살펴본 실습에서 EBS PV를 사용하는 경우와 같이 관리형 노드 그룹을 사용하는 경우에는 신규 노드 그룹을 생성하는 시점 고려하는 사항이 많습니다.

반면 Karpenter 노드는 내부적으로 신규 노드를 추가하는 시점 이러한 PV의 위치까지 고려하여 보다 사용 편의가 높습니다.

 

Karpenter에서는 노드 업그레이드를 위해서 Drift 혹은 TTL(expireAfter)과 같은 기능을 사용할 수 있습니다. 동작 방식이 다를 뿐 결과적으로는 EC2NodeClass에 업그레이드할 AMI로 변경하면, 원하는 사양으로 유도하거나 혹은 TTL이 지난 시점 변경되도록 하는 방식입니다.

 

그러하므로 이 실습은 terraform이 아닌 EC2NodeClass를 수정하는 방식을 사용합니다.

여기서는 code commit과 argoCD가 연결되어 있기 때문에, 로컬에서 수정하고 code commit으로 push한 뒤 karpenter 애플리케이션을 sync하도록 하겠습니다.

 

먼저 1.26에 해당하는 AMI ID를 확인합니다.

## AMI ID 확인
ec2-user:~/environment/terraform:$  aws ssm get-parameter --name /aws/service/eks/optimized-ami/1.26/amazon-linux-2/recommended/image_id \
    --region ${AWS_REGION} --query "Parameter.Value" --output text
ami-086414611b43bb691

 

이후 로컬의 eks-gitops-repo\apps\karpenter로 이동하여 default-ec2nc.yaml의 AMI ID를 확인한 AMI ID로 변경합니다.

apiVersion: karpenter.k8s.aws/v1beta1
kind: EC2NodeClass
metadata:
  name: default
spec:
  amiFamily: AL2
  amiSelectorTerms:
  - id: "ami-0ee947a6f4880da75" # Latest EKS 1.25 AMI
  role: karpenter-eksworkshop-eksctl
  securityGroupSelectorTerms:
  - tags:
      karpenter.sh/discovery: eksworkshop-eksctl
  subnetSelectorTerms:
  - tags:
      karpenter.sh/discovery: eksworkshop-eksctl
  tags:
    intent: apps
    managed-by: karpenter
    team: checkout

 

변경된 파일을 code commit으로 push하고, argo CD를 sync 합니다.

# 10분 소요 (예상) 실습 포함
cd ~/environment/eks-gitops-repo
git add apps/karpenter/default-ec2nc.yaml apps/karpenter/default-np.yaml
git commit -m "disruption changes"
git push --set-upstream origin main
argocd app sync karpenter

# 모니터링
while true; do date; kubectl get nodeclaim; echo ; kubectl get nodes -l team=checkout; echo ; kubectl get nodes -l team=checkout -o jsonpath="{range .items[*]}{.metadata.name} {.spec.taints}{\"\n\"}{end}"; echo ; kubectl get pods -n checkout -o wide; echo ; sleep 1; echo; done

# 최초 상태
ec2-user:~/environment:$ 
while true; do date; kubectl get nodeclaim; echo ; kubectl get nodes -l team=checkout; echo ; kubectec2-user:~/environment:$ while true; do date; kubectl get nodeclaim; echo ; kubectl get nodes -l team=checkout; echo ; kubectl get nodes -l team=checkout -o jsonpath="{range .items[*]}{.metadata.name} {.spec.taints}{\"\n\"}{end}"; echo ; kubectl get pods -n checkout -o wide; echo ; sleep 1; echo; done
Tue Apr  1 16:54:08 UTC 2025
NAME            TYPE       ZONE         NODE                                        READY   AGE
default-6css4   c4.large   us-west-2b   ip-10-0-24-100.us-west-2.compute.internal   True    19h

NAME                                        STATUS   ROLES    AGE   VERSION
ip-10-0-24-100.us-west-2.compute.internal   Ready    <none>   19h   v1.25.16-eks-59bf375

ip-10-0-24-100.us-west-2.compute.internal [{"effect":"NoSchedule","key":"dedicated","value":"CheckoutApp"}]

NAME                             READY   STATUS    RESTARTS   AGE   IP            NODE                                        NOMINATED NODE   READINESS GATES
checkout-558f7777c-z5qvh         1/1     Running   0          19h   10.0.29.195   ip-10-0-24-100.us-west-2.compute.internal   <none>           <none>
checkout-redis-f54bf7cb5-r2sdp   1/1     Running   0          19h   10.0.19.67    ip-10-0-24-100.us-west-2.compute.internal   <none>           <none>

# 신규 노드 생성
Tue Apr  1 16:58:58 UTC 2025
NAME            TYPE       ZONE         NODE                                        READY     AGE
default-6css4   c4.large   us-west-2b   ip-10-0-24-100.us-west-2.compute.internal   True      19h
default-pflq6   c4.large   us-west-2b                                               Unknown   3s

NAME                                        STATUS   ROLES    AGE   VERSION
ip-10-0-24-100.us-west-2.compute.internal   Ready    <none>   19h   v1.25.16-eks-59bf375

# 노드 Ready
Tue Apr  1 17:00:35 UTC 2025
NAME            TYPE       ZONE         NODE                                        READY   AGE
default-6css4   c4.large   us-west-2b   ip-10-0-24-100.us-west-2.compute.internal   True    19h
default-pflq6   c4.large   us-west-2b   ip-10-0-28-136.us-west-2.compute.internal   True    99s

NAME                                        STATUS   ROLES    AGE   VERSION
ip-10-0-24-100.us-west-2.compute.internal   Ready    <none>   19h   v1.25.16-eks-59bf375
ip-10-0-28-136.us-west-2.compute.internal   Ready    <none>   27s   v1.26.15-eks-59bf375

# 신규 노드로 파드 생성
Tue Apr  1 17:00:35 UTC 2025
NAME            TYPE       ZONE         NODE                                        READY   AGE
default-6css4   c4.large   us-west-2b   ip-10-0-24-100.us-west-2.compute.internal   True    19h
default-pflq6   c4.large   us-west-2b   ip-10-0-28-136.us-west-2.compute.internal   True    99s

NAME                                        STATUS   ROLES    AGE   VERSION
ip-10-0-24-100.us-west-2.compute.internal   Ready    <none>   19h   v1.25.16-eks-59bf375
ip-10-0-28-136.us-west-2.compute.internal   Ready    <none>   27s   v1.26.15-eks-59bf375

ip-10-0-24-100.us-west-2.compute.internal [{"effect":"NoSchedule","key":"dedicated","value":"CheckoutApp"},{"effect":"NoSchedule","key":"karpenter.sh/disruption","value":"disrupting"}]
ip-10-0-28-136.us-west-2.compute.internal [{"effect":"NoSchedule","key":"dedicated","value":"CheckoutApp"}]

NAME                             READY   STATUS    RESTARTS   AGE   IP            NODE                                        NOMINATED NODE   READINESS GATES
checkout-558f7777c-z5qvh         1/1     Running   0          19h   10.0.29.195   ip-10-0-24-100.us-west-2.compute.internal   <none>           <none>
checkout-redis-f54bf7cb5-r2sdp   1/1     Running   0          19h   10.0.19.67    ip-10-0-24-100.us-west-2.compute.internal   <none>           <none>


Tue Apr  1 17:00:41 UTC 2025
NAME            TYPE       ZONE         NODE                                        READY   AGE
default-6css4   c4.large   us-west-2b   ip-10-0-24-100.us-west-2.compute.internal   True    19h
default-pflq6   c4.large   us-west-2b   ip-10-0-28-136.us-west-2.compute.internal   True    105s

NAME                                        STATUS   ROLES    AGE   VERSION
ip-10-0-24-100.us-west-2.compute.internal   Ready    <none>   19h   v1.25.16-eks-59bf375
ip-10-0-28-136.us-west-2.compute.internal   Ready    <none>   33s   v1.26.15-eks-59bf375

ip-10-0-24-100.us-west-2.compute.internal [{"effect":"NoSchedule","key":"dedicated","value":"CheckoutApp"},{"effect":"NoSchedule","key":"karpenter.sh/disruption","value":"disrupting"}]
ip-10-0-28-136.us-west-2.compute.internal [{"effect":"NoSchedule","key":"dedicated","value":"CheckoutApp"}]

NAME                             READY   STATUS              RESTARTS   AGE   IP       NODE                                        NOMINATED NODE   READINESS GATES
checkout-558f7777c-hddnc         0/1     ContainerCreating   0          2s    <none>   ip-10-0-28-136.us-west-2.compute.internal   <none>           <none>
checkout-redis-f54bf7cb5-tqj6q   0/1     ContainerCreating   0          2s    <none>   ip-10-0-28-136.us-west-2.compute.internal   <none>           <none>

# 구 노드 사라짐
Tue Apr  1 17:00:46 UTC 2025
NAME            TYPE       ZONE         NODE                                        READY   AGE
default-pflq6   c4.large   us-west-2b   ip-10-0-28-136.us-west-2.compute.internal   True    109s

NAME                                        STATUS   ROLES    AGE   VERSION
ip-10-0-28-136.us-west-2.compute.internal   Ready    <none>   37s   v1.26.15-eks-59bf375

ip-10-0-28-136.us-west-2.compute.internal [{"effect":"NoSchedule","key":"dedicated","value":"CheckoutApp"}]

NAME                             READY   STATUS              RESTARTS   AGE   IP       NODE                                        NOMINATED NODE   READINESS GATES
checkout-558f7777c-hddnc         0/1     ContainerCreating   0          6s    <none>   ip-10-0-28-136.us-west-2.compute.internal   <none>           <none>
checkout-redis-f54bf7cb5-tqj6q   0/1     ContainerCreating   0          6s    <none>   ip-10-0-28-136.us-west-2.compute.internal   <none>           <none>

 

Karpenter 동작의 세부 과정은 로그를 통해서 확인할 수 있습니다.

kubectl -n karpenter logs deployment/karpenter -c controller --tail=33 -f
...

# drift 진행 > nodeClaim 생성 > nodeClaim launch
{"level":"INFO","time":"2025-04-01T16:58:57.282Z","logger":"controller","message":"disrupting via drift replace, terminating 1 nodes (2 pods) ip-10-0-24-100.us-west-2.compute.internal/c4.large/spot and replacing with node from types c5.large, c4.large, m6a.large, r4.large, m6i.large and 40 other(s)","commit":"490ef94","controller":"disruption","command-id":"3a295ac9-2a0d-4ddf-a6cb-e8d08915cff2"}
{"level":"INFO","time":"2025-04-01T16:58:57.318Z","logger":"controller","message":"created nodeclaim","commit":"490ef94","controller":"disruption","NodePool":{"name":"default"},"NodeClaim":{"name":"default-pflq6"},"requests":{"cpu":"430m","memory":"632Mi","pods":"6"},"instance-types":"c4.2xlarge, c4.4xlarge, c4.8xlarge, c4.large, c4.xlarge and 40 other(s)"}
{"level":"INFO","time":"2025-04-01T16:59:00.052Z","logger":"controller","message":"launched nodeclaim","commit":"490ef94","controller":"nodeclaim.lifecycle","controllerGroup":"karpenter.sh","controllerKind":"NodeClaim","NodeClaim":{"name":"default-pflq6"},"namespace":"","name":"default-pflq6","reconcileID":"a314c3cc-925a-4039-a313-b10e3d762fed","provider-id":"aws:///us-west-2b/i-05f149a8dcf7d844d","instance-type":"c4.large","zone":"us-west-2b","capacity-type":"spot","allocatable":{"cpu":"1930m","ephemeral-storage":"17Gi","memory":"2878Mi","pods":"29"}}

# 노드 register > initialize
{"level":"INFO","time":"2025-04-01T17:00:10.779Z","logger":"controller","message":"registered nodeclaim","commit":"490ef94","controller":"nodeclaim.lifecycle","controllerGroup":"karpenter.sh","controllerKind":"NodeClaim","NodeClaim":{"name":"default-pflq6"},"namespace":"","name":"default-pflq6","reconcileID":"5a16a55e-9a6f-434f-b59e-c7daf0a93bf3","provider-id":"aws:///us-west-2b/i-05f149a8dcf7d844d","Node":{"name":"ip-10-0-28-136.us-west-2.compute.internal"}}
{"level":"INFO","time":"2025-04-01T17:00:33.021Z","logger":"controller","message":"initialized nodeclaim","commit":"490ef94","controller":"nodeclaim.lifecycle","controllerGroup":"karpenter.sh","controllerKind":"NodeClaim","NodeClaim":{"name":"default-pflq6"},"namespace":"","name":"default-pflq6","reconcileID":"c4e40956-a02a-4337-bd69-6b9be1d72d5f","provider-id":"aws:///us-west-2b/i-05f149a8dcf7d844d","Node":{"name":"ip-10-0-28-136.us-west-2.compute.internal"},"allocatable":{"cpu":"1930m","ephemeral-storage":"18242267924","hugepages-1Gi":"0","hugepages-2Mi":"0","memory":"3119300Ki","pods":"29"}}
{"level":"INFO","time":"2025-04-01T17:00:42.921Z","logger":"controller","message":"command succeeded","commit":"490ef94","controller":"disruption.queue","command-id":"3a295ac9-2a0d-4ddf-a6cb-e8d08915cff2"}

# 노드 삭제
{"level":"INFO","time":"2025-04-01T17:00:42.963Z","logger":"controller","message":"tainted node","commit":"490ef94","controller":"node.termination","controllerGroup":"","controllerKind":"Node","Node":{"name":"ip-10-0-24-100.us-west-2.compute.internal"},"namespace":"","name":"ip-10-0-24-100.us-west-2.compute.internal","reconcileID":"73867503-c843-4373-b03e-a3406d6f60b3"}
{"level":"INFO","time":"2025-04-01T17:00:45.441Z","logger":"controller","message":"deleted node","commit":"490ef94","controller":"node.termination","controllerGroup":"","controllerKind":"Node","Node":{"name":"ip-10-0-24-100.us-west-2.compute.internal"},"namespace":"","name":"ip-10-0-24-100.us-west-2.compute.internal","reconcileID":"03122384-44e7-4a0b-b28b-372ec6e10f1b"}
{"level":"INFO","time":"2025-04-01T17:00:45.808Z","logger":"controller","message":"deleted nodeclaim","commit":"490ef94","controller":"nodeclaim.termination","controllerGroup":"karpenter.sh","controllerKind":"NodeClaim","NodeClaim":{"name":"default-6css4"},"namespace":"","name":"default-6css4","reconcileID":"85396b01-d267-4a9a-b995-fcabaaf5e423","Node":{"name":"ip-10-0-24-100.us-west-2.compute.internal"},"provider-id":"aws:///us-west-2b/i-0cc37fd17692cedac"}

 

Karpenter의 Drift 방식으로 업그레이드가 완료되었습니다.

 

3.5. Self-managed 노드 업그레이드

Self-managed 노드 업그레이드는 사용자가 직접 AMI를 업데이트 해야하며, 이후 변경된 AMI ID를 업데이트 하는 방식으로 업그레이드를 수행합니다.

base.tf의 에서 self-managed 노드 그룹의 ami_id를 변경하고, terraform apply -auto-approve를 통해서 적용합니다.

self_managed_node_groups = {
  self-managed-group = {
    instance_type = "m5.large"

...

    # Additional configurations
    ami_id           = "ami-086414611b43bb691" # Replaced the latest AMI ID for EKS 1.26
    subnet_ids       = module.vpc.private_subnets
    .
    .
    .
    launch_template_use_name_prefix = true
  }
}

 

이후 노드가 변경된 것을 확인할 수 있습니다. 다만 terraform apply 이 종료된 것 처럼 보이지만, 실제 노드 그룹이 재생성 되는 시간은 조금 더 걸리는 것으로 확인됩니다. 관리형 노드 그룹은 terraform apply가 종료되는 시점과 업그레이드가 일치하지만, Self-managed 노드 업그레이드는 terraform apply가 종료되는 시점과 다르다는 점에 차이가 있습니다.

 

 

3.6. Fargate 노드 업그레이드

Fargate는 가상 머신 그룹을 직접 프로비저닝하거나 관리할 필요가 없습니다. 그러하므로, 업그레이드를 하려면 단순히 파드를 재시작해 Fargate 컨트롤러가 최신 쿠버네티스 버전으로 업데이트를 하도록 예약합니다.

아래와 같이 진행 합니다.

# 최초 상태
ec2-user:~/environment:$ kubectl get pods -n assets -o wide
NAME                      READY   STATUS    RESTARTS   AGE     IP            NODE                                                NOMINATED NODE   READINESS GATES
assets-7ccc84cb4d-2p284   1/1     Running   0          2d11h   10.0.37.152   fargate-ip-10-0-37-152.us-west-2.compute.internal   <none>           <none>
ec2-user:~/environment:$ kubectl get node $(kubectl get pods -n assets -o jsonpath='{.items[0].spec.nodeName}') -o wide
NAME                                                STATUS   ROLES    AGE     VERSION                INTERNAL-IP   EXTERNAL-IP   OS-IMAGE         KERNEL-VERSION                  CONTAINER-RUNTIME
fargate-ip-10-0-37-152.us-west-2.compute.internal   Ready    <none>   2d11h   v1.25.16-eks-2d5f260   10.0.37.152   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25

# 디플로이먼트 재시작
ec2-user:~/environment:$ kubectl rollout restart deployment assets -n assets
deployment.apps/assets restarted

 

신규 파드가 Running 상태가 되면 노드 또한 1.26.15로 변경된 것을 알 수 있습니다.

ec2-user:~/environment:$ kubectl get pods -n assets -o wide
NAME                      READY   STATUS    RESTARTS   AGE   IP           NODE                                               NOMINATED NODE   READINESS GATES
assets-66c4799cfc-4s7s6   1/1     Running   0          78s   10.0.28.67   fargate-ip-10-0-28-67.us-west-2.compute.internal   <none>           <none>
ec2-user:~/environment:$ kubectl get node $(kubectl get pods -n assets -o jsonpath='{.items[0].spec.nodeName}') -o wide
NAME                                               STATUS   ROLES    AGE   VERSION                INTERNAL-IP   EXTERNAL-IP   OS-IMAGE         KERNEL-VERSION                  CONTAINER-RUNTIME
fargate-ip-10-0-28-67.us-west-2.compute.internal   Ready    <none>   33s   v1.26.15-eks-2d5f260   10.0.28.67    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25

 

 

여기까지 In-place 클러스터 업그레이드를 살펴봤습니다.

모든 노드들이 1.26.15 버전으로 업그레이드가 완료되었습니다.

ec2-user:~/environment:$ kubectl get no 
NAME                                               STATUS   ROLES    AGE    VERSION
fargate-ip-10-0-28-67.us-west-2.compute.internal   Ready    <none>   10m    v1.26.15-eks-2d5f260
ip-10-0-28-136.us-west-2.compute.internal          Ready    <none>   27m    v1.26.15-eks-59bf375
ip-10-0-28-191.us-west-2.compute.internal          Ready    <none>   96m    v1.26.15-eks-59bf375
ip-10-0-3-227.us-west-2.compute.internal           Ready    <none>   60m    v1.26.15-eks-59bf375
ip-10-0-31-15.us-west-2.compute.internal           Ready    <none>   7m9s   v1.26.15-eks-59bf375
ip-10-0-35-1.us-west-2.compute.internal            Ready    <none>   12m    v1.26.15-eks-59bf375
ip-10-0-46-150.us-west-2.compute.internal          Ready    <none>   95m    v1.26.15-eks-59bf375

 

 

4. Blue/Green 클러스터 업그레이드

Blue/Green 클러스터 업그레이드는 앞서 살펴본 관리형 노드 그룹의 Blue/Green 업그레이드와 동일합니다. 신규 Green 클러스터를 생성하고, 이후 트래픽을 라우팅 하는 방식으로 업그레이드를 완료할 수 있습니다.

 

결국 In-place와의 차이점은 Blue/Green 클러스터는 별개의 클러스터이기 때문에 한번에 원하는 버전으로 업그레이드를 할 수 있다는 장점이 있습니다. 또한 기존 클러스터를 유지하여 간단하게 Rollback을 가능하게 합니다. 다만 동시에 2개 클러스터를 구성하게 되어 추가 비용이 발생할 수 있다는 점과 신규 클러스터로 Stateful 워크로드의 이전과 트래픽 라우팅의 복잡성이 존재합니다.

 

실습 방식 자체는 어렵지 않기 때문에 워크샵의 대략적인 개요만 설명드리겠습니다.

 

1) Green 클러스터 생성: Terraform 코드로 생성하고 배포 합니다. 사전에 적합한 쿠버네티스 버전과 대응하는 애드온을 업데이트 해야합니다.

2) Stateless 워크로드 마이그레이션: 상태가 없는 애플리케이션은 신규 클러스터에 배포합니다. 다만 업그레이드 버전에 deprecated API 등이 없는지 확인 후 미리 변경해야 합니다.

3) Stateful 워크로드 마이그레이션: Sateful 워크로드는 데이터 동기화 이슈가 있습니다. 이 때문에 사전에 스토리지 동기화나 데이터 동기화를 통해서 신규 클러스터에서 동일한 상태를 가지도록 해야 합니다. 이 부분은 간단하지 않기 때문에 많은 고민이 필요해 보입니다.

4) 트래픽 전환: 신규 클러스터 구성이 완료되면 트래픽을 Green으로 라우팅 합니다.

 

 

그럼 이상으로 EKS Upgrade 에 대한 포스트를 마무리 하겠습니다.

'EKS' 카테고리의 다른 글

[7] EKS Fargate  (0) 2025.03.23
[6] EKS의 Security - EKS 인증/인가와 Pod IAM 권한 할당  (0) 2025.03.16
[5-2] EKS의 오토스케일링 Part2  (0) 2025.03.07
[5-1] EKS의 오토스케일링 Part1  (0) 2025.03.07
[4] EKS의 모니터링과 로깅  (0) 2025.03.01

이번 포스트에서는 EKS의 Fargate에 대해서 살펴보겠습니다.

 

EKS Fargate는 EKS의 노드 그룹을 사용하지 않고 컨테이너를 서버리스 컴퓨팅 엔진에 실행하는 방식입니다.

먼저 EKS Fargate를 살펴보고, 이와 유사한 AKS의 Virtual Nodes를 통해 각 Managed Kubernetes Service에서 노드를 사용하지 않고 컨테이너를 실행하기 위한 구현 방식을 살펴보고, 실습을 통해 확인해보습니다.

 

목차

  1. EKS의 Fargate
  2. AKS의 Virtual Nodes

 

1. EKS Fargate

일반적으로 EKS에서는 노드 그룹을 생성하여 워커 노드를 사용할 수 있습니다. EKS의 컴퓨팅을 제공하는 옵션 중 노드인 EC2 인스턴스를 활용하지 않는 방식으로 EKS Fargate가 있습니다.

 

먼저 AWS Fargate를 이해하기 위해서 Amazon ECS를 먼저 살펴보겠습니다.

AWS에서 컨테이너를 실행하는 방식 중 하나로 Amazon ECS(Elastic Container Service)라는 완전 관리형 컨테이너 오케스트레이션 서비스를 제공하고 있습니다. 사용자는 Amazon ECS를 통해서 컨테이너화된 애플리케이션을 쉽게 배포하고 관리할 수 있습니다.

 

Amazon ECS는 아래와 같이 세 가지 계층을 가지고 있는데, 이 중 ECS가 실행되는 인프라를 의미하는 Capacity options에 AWS Fargate가 있다는 것을 알 수 있습니다.

출처: https://docs.aws.amazon.com/ko_kr/AmazonECS/latest/developerguide/Welcome.html

 

ECS의 용량 옵션에서 EC2를 선택하면 실제 EC2 인스턴스를 통해 컨테이너가 실행됩니다. 반면 Fargate는 서버리스 종량제 컴퓨팅 엔진을 의미합니다. 즉 가상 머신 자체를 배포하지 않는 형태이기 때문에 경량이라는 장점이 있습니다.

 

EKS의 Fargate도 동일합니다. EKS는 노드 그룹을 통해서 EC2를 통해 사용자 워커 노드를 제공하는데, 서버리스 컴퓨팅 엔진인 Fargate를 활용할 수 있습니다.

아래와 같이 EKS의 파드가 실행되는 Data Plane을 위한 개별 옵션입니다.

출처: https://www.eksworkshop.com/docs/fundamentals/fargate/

 

Fargate와 같은 컴퓨팅 옵션은 보통 지속적으로 실행하지 않아도 되는 유형이면서, stateless 한 애플리케이션에 적합합니다. 특정 Job을 수행하고 종료하는 워크로드 혹은 빠른 배포가 필요하고 필요없는 경우 종료가 가능한 유형의 워크로드라면 서버리스 컴퓨팅 엔진을 활용하는 Fargate를 고려할 수 있습니다.

 

실습을 통해서 EKS Fargate를 더 살펴보겠습니다.

 

해당 실습은 Amazone EKS Blueprints for Terraform의 예제를 통해서 진행하겠습니다.

참고: https://aws-ia.github.io/terraform-aws-eks-blueprints/

# 테라폼 코드 가져오기
git clone https://github.com/aws-ia/terraform-aws-eks-blueprints
cd terraform-aws-eks-blueprints/patterns/fargate-serverless

# 테라폼 초기화
terraform init

# 테라폼 Plan 확인
terraform plan

# 테라폼 배포
# 배포 : EKS, Add-ons, fargate profile - 13분 소요
terraform apply -auto-approve


# 배포 완료 후 확인
terraform state list
module.eks.data.aws_caller_identity.current
...

terraform output
...

 

생성된 리소스를 살펴보면 fargate 형태의 노드가 4대 확인되며, 또한 파드가 각 노드에 실행 중인 것을 알 수 있습니다.

이때 파드 IP와 노드 IP가 같은 것을 알 수 있는데, EKS fargate에서는 각 파드를 위해서 하나의 fargate노드가 실행되는 구조라는 것을 알 수 있습니다.

# kubeconfig 획득
aws eks --region us-west-2 update-kubeconfig --name fargate-serverless

# 노드, 파드 정보 확인
kubectl get no -o wide
NAME                                                STATUS   ROLES    AGE   VERSION               INTERNAL-IP   EXTERNAL-IP   OS-IMAGE         KERNEL-VERSION                  CONTAINER-RUNTIME
fargate-ip-10-0-1-239.us-west-2.compute.internal    Ready    <none>   48m   v1.30.8-eks-2d5f260   10.0.1.239    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25
fargate-ip-10-0-18-94.us-west-2.compute.internal    Ready    <none>   48m   v1.30.8-eks-2d5f260   10.0.18.94    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25
fargate-ip-10-0-20-74.us-west-2.compute.internal    Ready    <none>   48m   v1.30.8-eks-2d5f260   10.0.20.74    <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25
fargate-ip-10-0-35-232.us-west-2.compute.internal   Ready    <none>   48m   v1.30.8-eks-2d5f260   10.0.35.232   <none>        Amazon Linux 2   5.10.234-225.910.amzn2.x86_64   containerd://1.7.25

kubectl get pod -A -o wide
NAMESPACE     NAME                                           READY   STATUS    RESTARTS   AGE   IP            NODE
kube-system   aws-load-balancer-controller-c946d85dd-2n65t   1/1     Running   0          48m   10.0.35.232   fargate-ip-10-0-35-232.us-west-2.compute.internal   <none>           <none>
kube-system   aws-load-balancer-controller-c946d85dd-2t662   1/1     Running   0          48m   10.0.18.94    fargate-ip-10-0-18-94.us-west-2.compute.internal    <none>           <none>
kube-system   coredns-69fd949db7-95njt                       1/1     Running   0          49m   10.0.20.74    fargate-ip-10-0-20-74.us-west-2.compute.internal    <none>           <none>
kube-system   coredns-69fd949db7-b5jpf                       1/1     Running   0          49m   10.0.1.239    fargate-ip-10-0-1-239.us-west-2.compute.internal    <none>           <none>

 

노드 정보를 살펴보면 comput-type에 대해서 Label과 Taint가 적용된 것을 알 수 있습니다.

kubectl describe node | grep -A 3 Labels
Labels:             beta.kubernetes.io/arch=amd64
                    beta.kubernetes.io/os=linux
                    eks.amazonaws.com/compute-type=fargate
...
kubectl describe node | grep Taints
Taints:             eks.amazonaws.com/compute-type=fargate:NoSchedule
...

 

EKS에서 Fargate를 사용하기 위해서 Fargate Profile을 생성해야 합니다. 이 프로파일은 Fargate를 사용할 리소스의 네임스페이스와 Label을 사전에 지정(selectors)합니다. 또한 파드가 배포되는 서브넷과 IAM Role에 대한 정보도 Fargate Profile에 포함됩니다.

출처: https://aws.amazon.com/ko/blogs/containers/use-cloudformation-to-automate-management-of-the-fargate-profile-in-amazon-eks/

 

실습의 terraform 코드에서는 아래와 같이 fargate profile을 지정한 것을 알 수 있습니다.


  fargate_profiles = {
    app_wildcard = {
      selectors = [
        { namespace = "app-*" }
      ]
    }
    kube_system = {
      name = "kube-system"
      selectors = [
        { namespace = "kube-system" }
      ]
    }
  }

 

웹 콘솔에서는 아래와 같이 확인할 수 있습니다.

 

kube-system 이라는 프로파일로 인해서 실제로 kube-system 또한 관리형 노드 그룹을 사용하지 않고 모두 fargate 형태로 실행되었습니다.

 

API 서버에 Fargate에 해당하는 파드가 요청되면 Admission controller에 의해 Mutating Webhook으로 Fargate로 스케줄링되도록 정보가 변경됩니다.

 

이 과정을 세부적으로 살펴보면 아래와 같이 파드가 요청되면 Mutating Webhook에 의해서 Fargate Profile에 대한 정보와 schedulerName이 Fargate-scheduler로 지정됩니다. 이 정보를 바탕으로 Fargate Scheduler는 Fagate 환경에 파드가 스케줄링하고 파드가 실행됩니다.

출처: https://aws.amazon.com/ko/blogs/containers/the-role-of-aws-fargate-in-the-container-world/

 

이를 coredns 파드를 통해서 살펴보면 아래와 같이 fargate-profile과 또한 schedulerName이 지정된 것을 확인할 수 있습니다.

kubectl get po -n kube-system   coredns-69fd949db7-95njt -oyaml |grep fargate
    eks.amazonaws.com/fargate-profile: kube-system
  nodeName: fargate-ip-10-0-20-74.us-west-2.compute.internal
  schedulerName: fargate-scheduler

 

이렇게 요청된 파드의 정보가 Fargate Profile의 Selector에서 지정한 정보와 일치하는 지를 바탕으로 스케줄링을 수행하기 때문에 Fargate로 스케줄링된 리소스는 일반 노드에는 배포되지 않습니다.

일반 노드에 실행되는 워크로드와 Fargate에 실행되는 워크로드는 스케줄링에 있어 배타적인 관계입니다. 예를 들어, 노드가 부족한 경우라도 파드가 Fargate로 Burst해서 실행할 수 있는 구조가 아닙니다.

 

Fargate 자체는 사용자가 생성한 노드 리소스가 아니기 때문에 EC2 인스턴스에서는 인스턴스가 확인되지 않습니다.

이때 Network Interface는 확인이 가능합니다. 다만 아래의 정보와 같이 Network Interface이 Owner와 Instance의 Owner가 다르다는 것을 알 수 있습니다.

 

 

EKS Fargate가 사용자 VPC와 연계되는 방식은 아래와 같은 형태로 구성됩니다.

출처: https://www.kiranjthomas.com/posts/fargate-under-the-hood/

 

1) Fargate를 위한 EC2 인스턴스가 별도의 Fargate VPC에서 실행됩니다.

2) 이 인스턴스의 Primary Network Interface는 Fargate VPC에 위치하여, Container Runtime, Fargate Agent, Guest Kernel& OS를 위한 네트워크 트래픽을 처리합니다.

3) 이 인스턴스의 Secondary Network Interface가 사용자 VPC에 연결되어 컨테이너간 통신과 Image Pulling과 같은 네트워크 트래픽을 처리합니다.

 

위의 그림과 설명에서는 Fargate가 EC2로 표현되어 있지만 이는 Lightweight VM으로 알려진 Firecracker를 사용하고 있습니다.

 

EKS의 Fargate는 EC2 인스턴스를 유지하지 않아도 되기 때문에 비용 효과적이라고 생각할 수 있지만, 일반적으로 Fargate는 동일한 용량의 EC2에 비해서는 비용이 더 비싸게 책정됩니다. 이는 실행되는 파드를 위해서 노드에서 실행되는 kube-proxy, containerd, kubelet 컴포넌트가 배포되어 일부 추가적인 리소스를 사용하기 때문입니다.

 

이를 아래 장표에서 보시면 256MB 정도가 추가되는 것을 확인할 수 있습니다.

출처: https://www.youtube.com/watch?v=N0uLK5syctU

 

추가로 이러한 리소스는 Fargate 리소스 타입에 맞춰 반올림되어 구성되기 때문에, 실제 파드 spec의 request 용량보다 큰 사이즈의 Fargate 리소스가 사용되는 점도 아실 필요가 있습니다.

 

그러하므로 EKS의 Fargate 옵션은 비용 측면보다는 서버리스 워크로드에 적합한지 여부를 바탕으로 판단할 필요가 있습니다.

 

또한 EKS Fargate에는 다수 고려사항이 있으므로, 제약사항을 문서를 통해 사전에 확인하시기 바랍니다.

https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/fargate.html#fargate-considerations

 

아래와 같이 리소스를 정리하고 실습을 마무리 하겠습니다.

terraform destroy -auto-approve

 

 

2. AKS Virtual Nodes

AKS에서는 노드를 사용하지 않고 Virtual Nodes를 사용하여 파드를 실행할 수 있습니다.

 

Azure에서는 ACI(Azure Container Instance)라는 서버리스 컨테이너 서비스를 가지고 있습니다(이는 AWS의 ECS와 같은 서비스 입니다). AKS에서 Virtual Nodes를 통해 파드를 실행하면 파드는 ACI의 형태로 실행된다고 볼 수 있습니다.

참고: https://learn.microsoft.com/ko-kr/azure/container-instances/container-instances-overview

 

AKS에서 Virtual Nodes를 사용하면 실제로 노드를 확인 했을 때 Virtual Nodes가 추가되는 형태로 보이는데, 이는 Virtual Kubelet이라는 오픈 소스를 기반으로 동작합니다.

 

Virtual Kubelet은 kubelet과 같이 동작하면서 쿠버네티스가 다른 API와 연계되도록 동작합니다. 이 방식을 통해서 다른 ACI, AWS Fargate 등과 같은 서비스를 통해서 노드를 사용하는 것 처럼 할 수 있습니다.

 

아래 그림은 Virtual Kubelet의 동작 방식으로, Virtual Kublet은 kubelet과 같이 자신을 노드로 등록하여, 실제로 파드가 Virtual Node에 스케줄링될 수도록 API를 구현하고 있습니다.

diagram

출처: https://github.com/virtual-kubelet/virtual-kubelet?tab=readme-ov-file

 

AKS에서 Virtual Nodes를 사용하게 되면 Virtual Nodes에 스케줄링이 되고, virtual kubelet이 ACI와 연계하여 파드를 실행하는 방식으로 동작하게 됩니다.

 

AKS에서는 addon 형태로 Virtual Nodes를 지원합니다.

아래 실습 문서를 바탕으로 진행하면서 AKS Virtual Nodes에 대해서 살펴보겠습니다.

https://docs.azure.cn/en-us/aks/virtual-nodes-cli

# 변수 선언
PREFIX=aks-vn
RG=${PREFIX}-rg
AKSNAME=${PREFIX}
LOC=koreacentral
VNET=aks-vnet
AKSSUBNET=aks-subnet
VNSUBNET=vn-subnet

# 리소스 그룹 생성
az group create --name $RG --location $LOC -o none

az network vnet create --resource-group $RG --name $VNET --address-prefixes 10.0.0.0/8 --subnet-name $AKSSUBNET --subnet-prefix 10.240.0.0/16 -o none
az network vnet subnet create --resource-group $RG --vnet-name $VNET --name $VNSUBNET --address-prefixes 10.241.0.0/16 -o none

SUBNET_ID=$(az network vnet subnet show --resource-group $RG --vnet-name $VNET --name $AKSSUBNET --query id -o tsv)

# AKS 클러스터 설치
az aks create --resource-group $RG --name $AKSNAME --node-count 2 --network-plugin azure --vnet-subnet-id $SUBNET_ID --generate-ssh-keys

# 노드 정보 확인
az aks get-credentials --resource-group $RG --name $AKSNAME
kubectl get nodes
NAME                                STATUS   ROLES    AGE    VERSION
aks-nodepool1-14565790-vmss000000   Ready    <none>   100s   v1.30.9
aks-nodepool1-14565790-vmss000001   Ready    <none>   100s   v1.30.9

 

AKS를 생성하면 기본 노드 2대가 확인됩니다. EKS는 addon 컴포넌트들도 Fargate로 실행될수 있는 반면, AKS는 기본적인 시스템 컴포넌트는 여전히 일반 노드에서 실행이 필요합니다.

 

이제 Virtual Nodes addon을 활성화하고 다시 노드를 살펴보면 virtual node에 해당하는 노드가 확인됩니다.

# Virtual Nodes addon 활성화
az aks enable-addons --resource-group $RG --name $AKSNAME --addons virtual-node --subnet-name $VNSUBNET

# 노드 정보 확인
kubectl get nodes
NAME                                STATUS   ROLES    AGE     VERSION
aks-nodepool1-14565790-vmss000000   Ready    <none>   14m     v1.30.9
aks-nodepool1-14565790-vmss000001   Ready    <none>   14m     v1.30.9
virtual-node-aci-linux              Ready    agent    2m51s   v1.25.0-vk-azure-aci-1.6.2

 

실행 중인 파드를 살펴보면 aci-connector-linux라는 파드가 실행되는 것을 알 수 있는데, virtual kubelet의 역할을 수행하며 AKS 클러스터와 ACI의 Management API 간의 가교 역할을 수행합니다.

 

아래 명령으로 살펴보면 aci-connector-linux 와 노드의 IP가 10.240.0.32으로 동일한 것을 알 수 있습니다.

kubectl get po -A -owide
NAMESPACE     NAME                                   READY   STATUS    RESTARTS   AGE     IP            NODE                                NOMINATED NODE   READINESS GATES
kube-system   aci-connector-linux-79d9bf8946-7hv8s   1/1     Running   0          17m     10.240.0.32   aks-nodepool1-14565790-vmss000001   <none>           <none>
..

kubectl get no -A -owide
NAME                                STATUS   ROLES    AGE   VERSION                      INTERNAL-IP   EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION      CONTAINER-RUNTIME
..
virtual-node-aci-linux              Ready    agent    15m   v1.25.0-vk-azure-aci-1.6.2   10.240.0.32   <none>        <unknown>            <unknown>           <unknown>

 

또한 포탈에서 확인해보면 AKS 노드를 위한 서브넷과 다르게, Virtual Node를 위해 생성된 서브넷은 실제로 ACI에서 배포를 진행하게 되므로 Azure Container Instance에 위임된 상태임을 알 수 있습니다.

 

EKS에서는 Fargate Profile을 생성하고, 특정 파드가 이 프로파일에 적용 가능하면 Fargate Scheduler에 의해서 Fargate로 배포가 되는 형태였습니다.

 

반면 Virtual Nodes에는 기본적으로 아래와 같은 Taint가 적용되어 있고, 기본적인 Taint, Toleration 방식을 통해서 일반 노드나 혹은 Virtual Nodes로 배포되도록 할 수 있습니다. 이는 일반적인 스케줄링 기법과 다르지 않습니다.

$ kubectl describe no virtual-node-aci-linux |grep -A 1 -B 1 Taint
CreationTimestamp:  Sat, 22 Mar 2025 15:57:33 +0000
Taints:             virtual-kubelet.io/provider=azure:NoSchedule
Unschedulable:      false

 

그러하므로 Virtual Nodes에 실행되는 워크로드는 Toleration이 필요합니다. 만약 파드의 스케줄링을 Virtual nodes로 강제하지 않으면 일반 노드에서도 실행될 수 있다는 것을 알 수 있습니다.

 

아래로 샘플 애플리케이션을 배포해서 실제로 어떻게 배포되는지 살펴보겠습니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: aci-helloworld
spec:
  replicas: 4
  selector:
    matchLabels:
      app: aci-helloworld
  template:
    metadata:
      labels:
        app: aci-helloworld
    spec:
      containers:
      - name: aci-helloworld
        image: mcr.microsoft.com/azuredocs/aci-helloworld
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: 200m
      tolerations:
      - key: virtual-kubelet.io/provider
        operator: Exists
      - key: azure.com/aci
        effect: NoSchedule
      affinity:
        nodeAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 1
              preference:
                matchExpressions:
                  - key: type
                    operator: NotIn
                    values:
                      - virtual-kubelet

 

파드의 Toleration과 Afffinity를 살펴볼 필요가 있습니다. 먼저 Virtual Nodes의 Taint에 대한 toleration이 지정되어 있습니다.

      tolerations:
      - key: virtual-kubelet.io/provider
        operator: Exists
      - key: azure.com/aci
        effect: NoSchedule

 

이 경우에는 파드가 바로 Virtual Nodes로 배포될 수 있으므로, 아래와 같이 nodeAffinity를 임의로 지정했습니다.

      affinity:
        nodeAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 1
              preference:
                matchExpressions:
                  - key: type
                    operator: NotIn
                    values:
                      - virtual-kubelet

 

이렇게 배포하면 nodeAffinity에 따라 virtual-kubelet이 아닌 노드에 먼저 스케줄링이 되고, 배포되지 못한 나머지 파드가 virtual node에 배포된 것을 확인할 수 있습니다.

kubectl get po -owide
NAME                              READY   STATUS    RESTARTS   AGE   IP            NODE                                NOMINATED NODE   READINESS GATES
aci-helloworld-86c987d849-9pw8r   1/1     Running   0          52s   10.240.0.55   aks-nodepool1-14565790-vmss000000   <none>           <none>
aci-helloworld-86c987d849-hp5nv   1/1     Running   0          53s   10.240.0.8    aks-nodepool1-14565790-vmss000001   <none>           <none>
aci-helloworld-86c987d849-rh9tx   1/1     Running   0          52s   10.241.0.4    virtual-node-aci-linux              <none>           <none>
aci-helloworld-86c987d849-v8kdx   1/1     Running   0          52s   10.240.0.18   aks-nodepool1-14565790-vmss000001   <none>           <none>

 

즉, 해당 파드는 toleration이 지정되어 있기 때문에 virtual node에도 배포가 가능하므로, unschedulable 파드가 virtual node로 배포가 됩니다. Cluster Autoscaler를 사용하지 않고도 Virtual Nodes를 통해 확장성을 가질 수 있습니다.

 

이때 aci-connector-linux 파드 로그를 살펴보면 실제로 ACI에서 container group을 생성하는 로그를 확인할 수 있습니다. 마지막에 컨테이너가 Started 된 로그를 ACI를 통해 전달받은 것을 확인할 수 있습니다.

time="2025-03-22T16:05:41Z" level=info msg="creating container group with name: default-aci-helloworld-6d49f9cfbc-h76bc" addedViaRedirty=false azure.region=koreacentral azure.resourceGroup=MC_aks-vn-rg_aks-vn_koreacentral delayedViaRateLimit=5ms key=default/aci-helloworld-6d49f9cfbc-h76bc method=CreateContainerGroup name=aci-helloworld-6d49f9cfbc-h76bc namespace=default originallyAdded="2025-03-22 16:05:41.362846244 +0000 UTC m=+488.605163852" phase=Pending plannedForWork="2025-03-22 16:05:41.367846244 +0000 UTC m=+488.610163852" pod=aci-helloworld-6d49f9cfbc-h76bc queue=syncPodsFromKubernetes reason= requeues=0 uid=d6a836b6-6b7d-4b57-90db-a5c109d17d6a workerId=49
...
time="2025-03-22T16:05:43Z" level=warning msg="cannot fetch aci events for pod aci-helloworld-6d49f9cfbc-h76bc in namespace default" error="cg is not found" method=PodsTracker.processPodUpdates
time="2025-03-22T16:05:43Z" level=info msg="Created pod in provider" addedViaRedirty=false delayedViaRateLimit=5ms key=default/aci-helloworld-6d49f9cfbc-h76bc method=createOrUpdatePod name=aci-helloworld-6d49f9cfbc-h76bc namespace=default originallyAdded="2025-03-22 16:05:41.362846244 +0000 UTC m=+488.605163852" phase=Pending plannedForWork="2025-03-22 16:05:41.367846244 +0000 UTC m=+488.610163852" pod=aci-helloworld-6d49f9cfbc-h76bc queue=syncPodsFromKubernetes reason= requeues=0 uid=d6a836b6-6b7d-4b57-90db-a5c109d17d6a workerId=49
time="2025-03-22T16:05:43Z" level=info msg="Event(v1.ObjectReference{Kind:\"Pod\", Namespace:\"default\", Name:\"aci-helloworld-6d49f9cfbc-h76bc\", UID:\"d6a836b6-6b7d-4b57-90db-a5c109d17d6a\", APIVersion:\"v1\", ResourceVersion:\"5821\", FieldPath:\"\"}): type: 'Normal' reason: 'ProviderCreateSuccess' Create pod in provider successfully"
E0322 16:05:43.818182       1 event.go:346] "Server rejected event (will not retry!)" err="events is forbidden: User \"system:serviceaccount:kube-system:aci-connector-linux\" cannot create resource \"events\" in API group \"\" in the namespace \"default\"" event="&Event{ObjectMeta:{aci-helloworld-6d49f9cfbc-h76bc.182f2b9f418838d9  default    0 0001-01-01 00:00:00 +0000 UTC <nil> <nil> map[] map[] [] [] []},InvolvedObject:ObjectReference{Kind:Pod,Namespace:default,Name:aci-helloworld-6d49f9cfbc-h76bc,UID:d6a836b6-6b7d-4b57-90db-a5c109d17d6a,APIVersion:v1,ResourceVersion:5821,FieldPath:,},Reason:ProviderCreateSuccess,Message:Create pod in provider successfully,Source:EventSource{Component:virtual-node-aci-linux/pod-controller,Host:,},FirstTimestamp:2025-03-22 16:05:43.814912217 +0000 UTC m=+491.057229925,LastTimestamp:2025-03-22 16:05:43.814912217 +0000 UTC m=+491.057229925,Count:1,Type:Normal,EventTime:0001-01-01 00:00:00 +0000 UTC,Series:nil,Action:,Related:nil,ReportingController:virtual-node-aci-linux/pod-controller,ReportingInstance:,}"
...
time="2025-03-22T16:06:50Z" level=error msg="failed to retrieve pod aci-helloworld-6d49f9cfbc-h76bc status from provider" error="container aci-helloworld properties CurrentState StartTime cannot be nil" method=PodsTracker.processPodUpdates
time="2025-03-22T16:06:55Z" level=info msg="Event(v1.ObjectReference{Kind:\"Pod\", Namespace:\"default\", Name:\"aci-helloworld-6d49f9cfbc-h76bc\", UID:\"d6a836b6-6b7d-4b57-90db-a5c109d17d6a\", APIVersion:\"v1\", ResourceVersion:\"5821\", FieldPath:\"spec.containers{aci-helloworld}\"}): type: 'Normal' reason: 'Pulling' pulling image \"mcr.microsoft.com/azuredocs/aci-helloworld@sha256:b9cec4d6b50c6bf25e3f7f93bdc1628e5dca972cf132d38ed8f5bc955bb179c3\""
time="2025-03-22T16:06:55Z" level=info msg="Event(v1.ObjectReference{Kind:\"Pod\", Namespace:\"default\", Name:\"aci-helloworld-6d49f9cfbc-h76bc\", UID:\"d6a836b6-6b7d-4b57-90db-a5c109d17d6a\", APIVersion:\"v1\", ResourceVersion:\"5821\", FieldPath:\"spec.containers{aci-helloworld}\"}): type: 'Normal' reason: 'Pulled' Successfully pulled image \"mcr.microsoft.com/azuredocs/aci-helloworld@sha256:b9cec4d6b50c6bf25e3f7f93bdc1628e5dca972cf132d38ed8f5bc955bb179c3\""
time="2025-03-22T16:06:55Z" level=info msg="Event(v1.ObjectReference{Kind:\"Pod\", Namespace:\"default\", Name:\"aci-helloworld-6d49f9cfbc-h76bc\", UID:\"d6a836b6-6b7d-4b57-90db-a5c109d17d6a\", APIVersion:\"v1\", ResourceVersion:\"5821\", FieldPath:\"spec.containers{aci-helloworld}\"}): type: 'Normal' reason: 'Started' Started container"
...

 

앞서 살펴본바와 같이 EKS의 Fargate에 실행되는 워크로드는 일반 노드에 배포되지 않는 배타적인 성격의 스케줄링이 된다면, AKS의 Virtual Nodes에 실행은 일반 노드에 대한 보완적인 관계가 됩니다. 즉 일반 노드에 배포되고, 그 이상의 리소스가 필요할 때 Cluster Autoscaler가 없어도 Virtual Nodes를 활용하는 시나리오를 사용할 수 있습니다.

 

물론 특별한 요구사항이 있는 경우에는 Virtual Nodes에만 배포되도록 아래와 같이 NodeSelector와 같은 스케줄링 기법을 사용하실 수 있습니다. 혹은 tolerance를 사용하시는 경우에도 Virtual Nodes에 먼저 스케줄링 되게 됩니다.

...
      nodeSelector:
        kubernetes.io/role: agent
        beta.kubernetes.io/os: linux
        type: virtual-kubelet
...

 

EKS는 Fargate 파드와 노드가 1:1로 맵핑되는 반면, AKS의 Virtual Nodes는 해당 노드에 실행되는 파드가 많아져도 대응하는 노드는 1대입니다.

아래와 같이 디플로이먼트를 6개로 스케일링하고 Virtual Nodes에 여러 개의 파드가 배포되도록 유도합니다. 노드 정보를 확인해보면 virtual node는 한대만 있는 것을 알 수 있습니다.

kubectl scale deployment aci-helloworld --replicas 6
deployment.apps/aci-helloworld scaled

kubectl get po -owide
NAME                              READY   STATUS    RESTARTS   AGE     IP            NODE                                NOMINATED NODE   READINESS GATES
aci-helloworld-86c987d849-9dbdx   1/1     Running   0          79s     10.240.0.42   aks-nodepool1-14565790-vmss000000   <none>           <none>
aci-helloworld-86c987d849-9pw8r   1/1     Running   0          7m49s   10.240.0.55   aks-nodepool1-14565790-vmss000000   <none>           <none>
aci-helloworld-86c987d849-fchwx   1/1     Running   0          79s     10.241.0.5    virtual-node-aci-linux              <none>           <none>
aci-helloworld-86c987d849-rh9tx   1/1     Running   0          7m49s   10.241.0.4    virtual-node-aci-linux              <none>           <none>
..

kubectl get no
NAME                                STATUS   ROLES    AGE   VERSION
aks-nodepool1-14565790-vmss000000   Ready    <none>   41m   v1.30.9
aks-nodepool1-14565790-vmss000001   Ready    <none>   41m   v1.30.9
virtual-node-aci-linux              Ready    agent    29m   v1.25.0-vk-azure-aci-1.6.2

 

 

마지막으로 포탈에서 확인하면 AKS의 인프라스트럭처 리소스 그룹에 Virtual Nodes에 해당하는 파드들이 ACI의 형태로 실행되고 있는 것을 알 수 있습니다.

 

Virtual Node의 파드는 Azure 입장에서는 ACI의 형태로 실행되기 때문에 포탈에서 접근해서 ACI의 UI를 통해 로그 확인/콘솔 접속 등을 사용할 수 있습니다.

 

AKS의 Virtual Nodes 또한 몇 가지 제약사항을 가지고 있습니다. 이는 ACI의 제약사항을 상속받은 것일 수 있으며, daemonset이나 initContainer와 같은 사용이 불가한 점도 있습니다.

 

AKS Virtual Nodes에 대해서 문서의 제약사항을 살펴보시기 부탁드립니다.

https://learn.microsoft.com/en-us/azure/aks/virtual-nodes#limitations

 

리소스를 정리하고 실습을 마무리 하겠습니다.

az group delete --name $RG

 

 

마무리

EKS와 AKS에서 노드를 사용하지 않고 파드를 실행할 수 있는 방식을 살펴보았습니다.

 

EKS Fargate는 Admission controller를 통하여 Fargate scheduler를 통해 스케줄링을 하는 방식이었다면, AKS는 virtual kubelet을 통해서 Virtual Nodes를 등록하고 해당 노드의 Taint를 통해서 스케줄링을 유도하는 방식을 사용할 수 있었습니다.

 

그럼 이번 포스트를 마무리 하겠습니다.

'EKS' 카테고리의 다른 글

[8] EKS Upgrade  (0) 2025.04.02
[6] EKS의 Security - EKS 인증/인가와 Pod IAM 권한 할당  (0) 2025.03.16
[5-2] EKS의 오토스케일링 Part2  (0) 2025.03.07
[5-1] EKS의 오토스케일링 Part1  (0) 2025.03.07
[4] EKS의 모니터링과 로깅  (0) 2025.03.01

이번 포스트에서는 EKS의 보안(Security)에 대해서 알아 보겠습니다.

 

물론 쿠버네티스의 보안에는 이미지 보안, 노드 보안과 같은 영역도 있지만, 여기서는 쿠버네티스의 인증(Authentication)/인가(Authorization)가 EKS에 적용된 방식과, 워크로드(파드)의 AWS의 리소스에 대한 보안 접근이라는 두 가지 주제를 살펴보겠습니다.

 

먼저 EKS의 인증/인가의 흐름을 kubeconfig를 바탕으로 이해해보고, 두번째로 워크로드(파드)에 AWS의 리소스에 접근 권한을 부여하기 위해 파드에 IAM을 할당하는 방식에 대해서 살펴보겠습니다.

 

목차

  1. Kubernetes의 인증/인가
  2. EKS의 인증/인가
  3. AKS의 인증/인가
  4. Kubernetes의 파드 권한
  5. EKS의 파드 권한 할당
  6. AKS의 파드 권한 할당

 

1. Kubernetes의 인증/인가

쿠버네티스에서는 API를 접근을 통제하기 위해서 아래와 같은 방식이 사용됩니다.

사용자나 파드(Service Account)는 Authentication(인증) -> Authorization(인가) -> Admission Control을 단계를 지나서 비로소 쿠버네티스 API에 접근할 수 있습니다.

출처: https://kubernetes.io/docs/concepts/security/controlling-access/

 

다만 쿠버네티스 자체는 직접적으로 사용자를 저장해 인증하는 방식을 구현하지 않고 있기 때문에 다른 인증 시스템에 위임을 하여 사용자에 대한 인증을 진행할 수 있습니다.

그 이후 인가 단계에서는 인증된 주체가 쿠버네티스 리소스에 대한 적절한 접근 권한을 가진 여부를 체크하게 됩니다. 마지막으로 Admission Control에서 요청 자체에 대한 Validation이나 Mutation과 같은 추가적인 절차를 진행할수 있도록 설계되어 있습니다.

그리고 그림에서 살펴보듯이 각 단계는 퍼즐 조각처럼 여러 형태의 인증, 인가, Admission Control을 선택적으로 추가할 수 있도록 되어 있습니다.

 

이후 EKS의 인증/인가 절에서는 AWS의 인증/엑세스 관리를 담당하는 IAM을 통해서 쿠버네티스의 인증/인가를 진행하는 과정을 설명하겠습니다. 즉, AWS에서 유효한 주체(사용자)가 어떻게 쿠버네티스의 인증/인가를 거쳐 쿠버네티스를 이용할 수 있는가의 관점입니다.

 

 

2. EKS의 인증/인가

사용자의 EKS의 인증/인가 체계를 이해하기 위해서 아래 그림을 바탕으로 kubectl 명령의 실행 흐름을 따라가 보겠습니다.

출처: https://www.youtube.com/watch?v=bksogA-WXv8&t=600s

 

1) kubectl get node 명령을 수행

2) kubeconfig에 정의된 aws eks get-token 명령으로 AWS STS reigional endpoint로 Amazon EKS 클러스터에 대한 인증 토큰 요청

3) aws eks get-token의 응답으로 Token 값 수신(base64로 디코딩하면 STS(Secure Token Service)로 GetCallerIdentity를 호출하는 Pre-Signed URL 값이 들어가 있음)

<< 이 단계까지는 EKS API Endpoint로 인증 요청 전 단계 >>

4) kubectl는 Pre-Signed URL을 bearer Token으로 EKS API Cluster Endpoint로 요청

5) API 서버는 aws-iam-authenticator server(Webhook Token Authentication)로 Token Review 요청

6) aws-iam-authenticator server에서 sts GetCallerIdentity를 호출

7) AWS IAM은 토큰이 유효한지 확인 후 인증 완료하고, IAM User나 Role에 대한 ARN을 반환

8) IAM의 User/Role을 쿠버네티스의 그룹으로 맵핑한 aws-auth(ConfigMap)을 통해 쿠버네티스의 보안 주체를 확인

9) aws-iam-authenticator server(Webhook Token Authentication)에서는 TokenReview라는 데이터 타입으로 useruame과 쿠버네티스 group을 반환

<< 이 단계까지가 인증의 단계 >>

10) 이 정보를 바탕으로 Kubernetes RBAC 기반 인가 진행

<< 이 단계까지가 인가의 단계 >>

11) 인가된 경우 kubectl get node에 대한 결과 반환

 

이와 같이 kubectl를 수행하면 IAM을 통해 사용자를 인증하고, 쿠버네티스 RBAC에 따라 인가를 하게 됩니다.

 

이를 다시 요약하여 아래와 같은 4단계로 나눠보겠습니다.

 

1) kubectl 요청을 수행하면 AWS 인증 정보를 통하여 EKS 클러스터에 대한 인증 토큰 요청

2) Webhook Token Authentication을 따라 IAM을 통한 인증 진행

3) 인증 완료 후 정보로 반환된 arn을 바탕으로 쿠버네티스 그룹과의 맵핑을 확인하는데, 이 절차는 아래와 같이 두가지 방식이 있습니다.

  • aws-auth(ConfigMap) 방식 (deprecated 될 예정)
  • EKS API 방식

4) 인증된 IAM 정보를 바탕으로 쿠버네티스 RBAC을 통해 인가 진행

 

 

참고로 위의 설명에서 aws-auth(컨피그 맵)에 IAM Role/User의 arn과 쿠버네티스의 권한 그룹과 맵핑 정보를 담고 있습니다. 사용자는 eksctl create iamidentitymapping를 통해 IAM 사용자와 클러스터 그룹을 맵핑하고, 이것이 컨피그 맵에 반영됩니다.

 

다만 이 방식은 컨피그 맵이 쿠버네티스에 노출되므로, 잘못 수정하는 경우 클러스터에 이슈가 발생할 수 있는 등 여러가지 문제가 있어 최근 EKS API 방식을 도입하였습니다.

 

EKS API는 컨피그 맵이 없어지고 EKS API를 통해서 Access Entry에 IAM Role/User와 Access Policy를 맵핑하여 관리하도록 합니다. IAM을 통한 인증 완료 후 반환된 ARN 정보를 EKS API의 Access Entry 맵핑을 확인하고, 이후 쿠버네티스 RBAC 인가를 받도록 절차가 변경됩니다.

출처: https://aws.amazon.com/ko/blogs/containers/a-deep-dive-into-simplified-amazon-eks-access-management-controls/

 

웹 콘솔을 접근해 EKS에서 Access 탭에서 Authentication mode를 확인할 수 있습니다. 기본 생성된 EKS 클러스터는 EKS API 및 ConfigMap이 선택되어 있습니다. 이 옵션에서 EKS API와 ConfigMap이 중복 설정되는 경우는 EKS API가 우선적용됩니다.

 

해당 페이지의 Manage access를 통해서 아래와 같이 변경 가능한 인터페이스가 있습니다.

 

이제 실제 EKS 환경에서 동작을 확인해보겠습니다.

 

클러스터 엑세스: ConfigMap

해당 옵션의 설정 방식을 살펴보기 위해서 아래와 같이 testuser를 만들고, EKS 클러스터에 접근하기 위한 권한을 할당하는 방식을 알아보겠습니다.

# testuser 사용자 생성
aws iam create-user --user-name testuser

# 사용자에게 프로그래밍 방식 액세스 권한 부여
aws iam create-access-key --user-name testuser
{
    "AccessKey": {
        "UserName": "testuser",
        "AccessKeyId": "AKIA5ILF2##",
        "Status": "Active",
        "SecretAccessKey": "TxhhwsU8##",
        "CreateDate": "2023-05-23T07:40:09+00:00"
    }
}
# testuser 사용자에 정책을 추가
aws iam attach-user-policy --policy-arn arn:aws:iam::aws:policy/AdministratorAccess --user-name testuser

# 아래 실습은 kubectl을 신규로 세팅하기 위해 기존 aws configure가 되지 않은 VM에서 진행합니다.
# testuser 자격증명 설정
aws configure
AWS Access Key ID [None]: ...
AWS Secret Access Key [None]: ....
Default region name [None]: ap-northeast-2

# get-caller-identity 확인
aws sts get-caller-identity --query Arn
"arn:aws:iam::911283464785:user/testuser"

# testuser에 대한 kubeconfig를 획득합니다.
CLUSTER_NAME=myeks
aws eks update-kubeconfig --name $CLUSTER_NAME --user-alias testuser

# kubectl 시도 
kubectl get node
E0315 22:46:29.480897    1795 memcache.go:265] "Unhandled Error" err="couldn't get current server API group list: the server has asked for the client to provide credentials"
E0315 22:46:30.514466    1795 memcache.go:265] "Unhandled Error" err="couldn't get current server API group list: the server has asked for the client to provide credentials"
E0315 22:46:31.629986    1795 memcache.go:265] "Unhandled Error" err="couldn't get current server API group list: the server has asked for the client to provide credentials"
E0315 22:46:32.658748    1795 memcache.go:265] "Unhandled Error" err="couldn't get current server API group list: the server has asked for the client to provide credentials"
E0315 22:46:33.649009    1795 memcache.go:265] "Unhandled Error" err="couldn't get current server API group list: the server has asked for the client to provide credentials"
error: You must be logged in to the server (the server has asked for the client to provide credentials)

# kubeconfig 확인
cat ~/.kube/config
apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: ~
    server: ~
contexts:
- context:
    cluster: arn:aws:eks:ap-northeast-2:xx:cluster/myeks
    user: testuser
  name: testuser
current-context: testuser
kind: Config
preferences: {}
users:
- name: testuser
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      args:
      - --region
      - ap-northeast-2
      - eks
      - get-token
      - --cluster-name
      - myeks
      - --output
      - json
      command: aws

kubectl get cm -n kube-system aws-auth -o yaml

apiVersion: v1
data:
  mapRoles: |
    - groups:
      - system:bootstrappers
      - system:nodes
      rolearn: arn:aws:iam::xx:role/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-e7CGUnBoQC96
      username: system:node:{{EC2PrivateDNSName}}
kind: ConfigMap
metadata:
  creationTimestamp: "2025-03-15T13:10:23Z"
  name: aws-auth
  namespace: kube-system
  resourceVersion: "2028"
  uid: 13151df2-4cd2-4fc2-92dc-2b0289a1be55

 

testuser도 AdministratorAccess 권한을 가지고 있지만 실제로 EKS의 API Server에 인증되는 권한은 없습니다. (단, 컨피그 맵에는 정보가 없지만 EKS를 생성한 admin 계정은 EKS API의 Access Entry에 등록되어 권한이 있음)

 

아래 iamidentitymapping 를 생성하면 aws-auth(컨피그 맵)이 업데이트 됩니다.

# Creates a mapping from IAM role or user to Kubernetes user and groups
eksctl get iamidentitymapping --cluster $CLUSTER_NAME
ARN                                                                                     USERNAME                                GROUPS                          ACCOUNT
arn:aws:iam::xx:role/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-e7CGUnBoQC96 system:node:{{EC2PrivateDNSName}}       system:bootstrappers,system:nodes

# IAM Identity Mapping 생성
ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
eksctl create iamidentitymapping --cluster $CLUSTER_NAME --username testuser --group system:masters --arn arn:aws:iam::$ACCOUNT_ID:user/testuser

# 확인
eksctl get iamidentitymapping --cluster $CLUSTER_NAME
ARN                                                                                     USERNAME                                GROUPS                          ACCOUNT
arn:aws:iam::xx:role/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-e7CGUnBoQC96 system:node:{{EC2PrivateDNSName}}       system:bootstrappers,system:nodes
arn:aws:iam::xx:user/testuser                                                 testuser                                system:masters

kubectl get cm -n kube-system aws-auth -o yaml
...
  mapUsers: |
    - groups:
      - system:masters
      userarn: arn:aws:iam::xx:user/testuser
      username: testuser
...

 

다만 IAM Identity Mapping 생성 후에 즉각적으로 kubectl 이 가능해지는 것은 아니며, 반영에 일부 시간이 걸릴 수 있습니다.

 

이후에 실행해보면 비로소 EKS에서 kubectl이 성공합니다.

# 시도
kubectl get node 
NAME                                               STATUS   ROLES    AGE   VERSION
ip-192-168-1-41.ap-northeast-2.compute.internal    Ready    <none>   64m   v1.31.5-eks-5d632ec
ip-192-168-2-79.ap-northeast-2.compute.internal    Ready    <none>   65m   v1.31.5-eks-5d632ec
ip-192-168-3-202.ap-northeast-2.compute.internal   Ready    <none>   65m   v1.31.5-eks-5d632ec

 

실습을 마무리하고, 다음 실습을 위해서 iamidentitymapping을 삭제하겠습니다.

# testuser IAM 맵핑 삭제
eksctl delete iamidentitymapping --cluster $CLUSTER_NAME --arn  arn:aws:iam::$ACCOUNT_ID:user/testuser

# Get IAM identity mapping(s)
eksctl get iamidentitymapping --cluster $CLUSTER_NAME
kubectl get cm -n kube-system aws-auth -o yaml

 

 

클러스터 엑세스: EKS API

웹 콘솔의 EKS>Access>IAM access entries 를 보면 현재 할당된 권한을 확인할 수 있습니다.

현재 EKS API and ConfigMap으로 해당 클러스터를 생성한 관리자 계정은 이미 AmazoneEKSClusterAdminPolicy를 할당 받은 것으로 확인 됩니다.

먼저 아래 명령으로 EKS API 엑세스 모드로 변경합니다. 옵션을 변경하는 경우 다시 기존 옵션으로 원복은 불가한점 유의가 필요합니다.

# EKS API 액세스모드로 변경
aws eks update-cluster-config --name $CLUSTER_NAME --access-config authenticationMode=API

 

웹 콘솔에서도 변경된 것으로 확인 됩니다.

 

참고로 아래 문서를 살펴보시면 EKS를 위해 생성된 Access Policy와 어떤 권한이 할당되었는지를 확인하실 수 있으며, 현재 제공되는 Policy는 아래와 같습니다. CLI에서는 aws eks list-access-policies 를 통해서 확인 가능 합니다.

https://docs.aws.amazon.com/eks/latest/userguide/access-policy-permissions.html

 

현재 생성된 Access Entry를 확인 할 수 있습니다.

# 현재 생성된 Access Entry 확인
aws eks list-access-entries --cluster-name $CLUSTER_NAME | jq
{
  "accessEntries": [
    "arn:aws:iam::xx:role/aws-service-role/eks.amazonaws.com/AWSServiceRoleForAmazonEKS",
    "arn:aws:iam::xx:role/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-e7CGUnBoQC96",
    "arn:aws:iam::xx:user/eksadmin"
  ]
}

# admin 계정의 Associated Access Policy 확인 -> AmazonEKSClusterAdminPolicy
export ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
aws eks list-associated-access-policies --cluster-name $CLUSTER_NAME --principal-arn arn:aws:iam::$ACCOUNT_ID:user/admin | jq # Linux
{
    "associatedAccessPolicies": [
        {
            "policyArn": "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy",
            "accessScope": {
                "type": "cluster",
                "namespaces": []
            },
            "associatedAt": "2025-03-15T21:56:26.361000+09:00",
            "modifiedAt": "2025-03-15T21:56:26.361000+09:00"
        }
    ],
    "clusterName": "myeks",
    "principalArn": "arn:aws:iam:xx:user/eksadmin"
}

 

앞서 생성한 testuser에 대해서 Access Entry를 생성하고 Associated Access Policy를 연결합니다.

# testuser 의 access entry 생성
aws eks create-access-entry --cluster-name $CLUSTER_NAME --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser
aws eks list-access-entries --cluster-name $CLUSTER_NAME | jq -r .accessEntries[]

# testuser에 AmazonEKSClusterAdminPolicy 연동
aws eks associate-access-policy --cluster-name $CLUSTER_NAME --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser \
  --policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy --access-scope type=cluster

#  Associated Access Policy 확인
aws eks list-associated-access-policies --cluster-name $CLUSTER_NAME --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser
{
    "associatedAccessPolicies": [
        {
            "policyArn": "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy",
            "accessScope": {
                "type": "cluster",
                "namespaces": []
            },
            "associatedAt": "2025-03-15T23:30:17.290000+09:00",
            "modifiedAt": "2025-03-15T23:30:17.290000+09:00"
        }
    ],
    "clusterName": "myeks",
    "principalArn": "arn:aws:iam::xx:user/testuser"
}

 

기존 testuser에서 EKS API를 통해서도 정상적으로 kubectl 수행이 가능합니다.

# kubectl 시도
kubectl get node 
NAME                                               STATUS   ROLES    AGE   VERSION
ip-192-168-1-41.ap-northeast-2.compute.internal    Ready    <none>   79m   v1.31.5-eks-5d632ec
ip-192-168-2-79.ap-northeast-2.compute.internal    Ready    <none>   79m   v1.31.5-eks-5d632ec
ip-192-168-3-202.ap-northeast-2.compute.internal   Ready    <none>   79m   v1.31.5-eks-5d632ec

# 현재는 AmazonEKSClusterAdminPolicy 이기 때문에 해당 작업이 가능함
kubectl auth can-i delete pods --all-namespaces
yes

# 컨피그 맵에 값이 반영되지 않는 것을 알 수 있습니다.
kubectl get cm -n kube-system aws-auth -o yaml
...
  mapUsers: |
    []
...

 

한편 Access Entry 자체를 쿠버네티스 그룹과도 맵핑해줄 수 있습니다. 먼저 앞서 생성한 Access Entry를 제거하고, 쿠버네티스 그룹과 맵핑을 해보겠습니다.

# 기존 testuser access entry 제거
aws eks delete-access-entry --cluster-name $CLUSTER_NAME --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser
aws eks list-access-entries --cluster-name $CLUSTER_NAME | jq -r .accessEntries[]

# 확인
(testuser:N/A) [root@operator-host-2 ~]# kubectl get no
error: You must be logged in to the server (Unauthorized)

# Cluster Role 생성
cat <<EoF> ~/pod-viewer-role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: pod-viewer-role
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["list", "get", "watch"]
EoF

kubectl apply -f ~/pod-viewer-role.yaml

# Cluster Rolebinding 생성
kubectl create clusterrolebinding viewer-role-binding --clusterrole=pod-viewer-role --group=pod-viewer


# Access Entry 생성 (--kubernetes-group 옵션 추가)
aws eks create-access-entry --cluster-name $CLUSTER_NAME --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser --kubernetes-group pod-viewer

# acess policy 자체에서는 정보가 보이지 않는다.
aws eks list-associated-access-policies --cluster-name $CLUSTER_NAME --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser

{
    "associatedAccessPolicies": [],
    "clusterName": "myeks",
    "principalArn": "arn:aws:iam::xx:user/testuser"
}

aws eks describe-access-entry --cluster-name $CLUSTER_NAME --principal-arn arn:aws:iam::$ACCOUNT_ID:user/testuser | jq
{
  "accessEntry": {
    "clusterName": "myeks",
    "principalArn": "arn:aws:iam::xx:user/testuser",
    "kubernetesGroups": [
      "pod-viewer"
    ],
    "accessEntryArn": "arn:aws:eks:ap-northeast-2:xx:access-entry/myeks/user/xx/testuser/4ccacd1e-2a2e-fd71-93e2-94ead13e95e3",
    "createdAt": "2025-03-15T23:36:39.977000+09:00",
    "modifiedAt": "2025-03-15T23:36:39.977000+09:00",
    "tags": {},
    "username": "arn:aws:iam::xx:user/testuser",
    "type": "STANDARD"
  }
}

 

아래와 같이 정보를 확인해봅니다.

# kubectl 시도 (node 조회는 불가, po는 조회 가능)
(testuser:N/A) [root@operator-host-2 ~]# kubectl get no
Error from server (Forbidden): nodes is forbidden: User "arn:aws:iam::xx:user/testuser" cannot list resource "nodes" in API group "" at the cluster scope
(testuser:N/A) [root@operator-host-2 ~]# kubectl get po
No resources found in default namespace.

# can-i 로 확인
kubectl auth can-i get pods --all-namespaces
yes
kubectl auth can-i delete pods --all-namespaces
no

 

EKS의 인증/인가에 대한 실습을 마무리하겠습니다.

 

 

3. AKS의 인증/인가

Azure에서는 Microsoft Entra ID(이전 명칭: Azure AD(Azure Active Directory))라는 ID 및 엑세스 관리 서비스를 가지고 있습니다.

AKS의 인증 또한 Entra ID를 이용할 수 있으며 Azure RBAC을 함께 사용하여 다양한 옵션을 제공하고 있습니다.

애저 포탈에서 AKS의 Settings>security configuration를 확인해보면 AKS에서 선택 가능한 인증/인가 방식을 확인할 수 있습니다.

 

사실 이부분에 대해서 제대로 이 분류를 설명하고 있는 문서가 없고, 아래의 공식 문서 또한 각 용어들을 산별적으로 설명하고 있어 이해하기가 쉽지 않습니다.

https://learn.microsoft.com/en-us/azure/aks/concepts-identity

것을 설명하는 데 있어서도 어려운 부분이라 단계별로 설명을 이어나가 보겠습니다.

 

사전 지식1

이때 Microsoft Entra ID는 Azure와 M365를 포괄하는 ID 인증 관리 체계로 이해할 수 있으며,

 

Azure RBAC은 Azure 수준에 대한 권한 부여 방식이라고 간단히 이해하고 넘어가겠습니다. Azure RBAC을 이용하는 역할 할당은 각 수준별(관리그룹, 구독, 리소스 그룹, 리소스) 엑세스 제어(IAM) 메뉴에서 가능합니다.

Azure Portal의 액세스 제어(C:\Users\montauk\Desktop\STD\5. Seminar\202502_AEWS(EKS)\EKS과제6주차_20250310.assets\sub-role-assignments.png) 페이지 스크린샷.

출처: https://learn.microsoft.com/ko-kr/azure/role-based-access-control/rbac-and-directory-admin-roles

 

Azure 관점에서 각 Azure 리소스의 엑세스 제어(IAM)에 Microsoft Entra ID의 주체(사용자, 애플리케이션)와 Role을 할당하는 것으로 간단히 생각하실 수 있습니다.

 

사전 지식2

AWS와 Azure의 IAM 부분에서 용어나 관점의 차이가 있습니다.

AWS의 Role은 사용자와 같은 주체의 의미입니다. Azure에서 이러한 주체는 Service Principal(SP)이나 Managed Identity(MI)라고 합니다.

Azure의 Role은 권한(Action)들의 집합을 의미합니다. AWS에서는 이 개념을 Policy라고 합니다.

 

AWS의 IAM에서는 사용자나 Role에 Policy를 할당합니다. AWS의 IAM은 주체 관점의 RBAC(주체에 리소스+권한을 할당)을 구현하고 있습니다.

Azure의 RBAC에서는 대상(리소스)에 사용자나 주체(SP,MI)를 Role을 맵핑합니다. Azure는 리소스 관점의 RBAC(리소스에 사용자+권한을 할당)을 구현하고 있습니다.

 

예를 들어, 특정 testuser에게 가상 머신의 관리자 권한을 준다고할 때 AWS와 Azure의 방식은 아래와 같습니다.

  • AWS는 신규 Policy에 가상머신을 선택하고 관리자 권한을 부여하고, testuser에게 이 Policy를 할당합니다.
  • Azure는 가상머신에서 testuser와 관리자 Role을 맵핑하여 권한을 할당합니다.

 

사전 지식3

AKS를 위한 Azure RBAC에서는 AKS 리소스를 위한 RoleKubernetes를 위한 Role이 구분되어 있습니다.

AKS 리소스를 위한 Role이라는 것은 Azure 리소스 차원에서 AKS에 대한 CRUD(클러스터 설정 변경, 노드 풀 스케일링 등)에 대한 권한입니다. 또한 az aks get-credentials를 통한 kubeconfig를 획득하기 위한 권한도 별도로 있습니다.

Kubernetes를 위한 Role은 쿠버네티스 내부의 리소스에 대한 CRUD(Deployment 생성, confimgMap 조회 등)을 의미하며, Microsoft Entra ID authentication with Azure RBAC에서 Azure RBAC을 통해서 Kubernetes에 대한 권한을 부여할 수 있다는 의미입니다.

 

사전 지식4

그 다음은 Local accounts라는 개념으로, 활성화 된 경우 admin에 해당하는 local account가 기본적으로 생성되어 있습니다. 이는 Microsoft Entra ID를 사용하는 경우에도 존재할 수 있으며, az aks get-credentials--admin 플래그를 사용하는 경우 kubeconfig admin credentials을 획득할 수 있습니다. 이를 통해 관리자가 Entra ID인증 없이 쿠버네티스를 접근할 수 있으나, 이는 보안에 취약할 수 있어 local accounts를 비활성화 할 수 있습니다.

참고: https://learn.microsoft.com/en-us/azure/aks/manage-local-accounts-managed-azure-ad

 

사전 지식을 바탕으로 아래에 대해서 설명을 이어 나가겠습니다.

 

Local accounts with Kubernetes RBAC

Microsoft Entra ID와 인증을 연동하지 않은 모드입니다.

이 구성에서 Azure Kubernetes Service Cluster User Role을 부여 받은 사용자나 그룹은 az aks get-credentials을 통해 kubeconfig를 획득할 수 있습니다. 이 Kubeconfig는 Kubernetes의 admin 권한입니다. 이후 Kubernetes RBAC을 통해서 인가를 구성할 수 있습니다. 이 구성에서는 user credentials이 admin 권한을 가지기 때문에 --admin 플래그와 차이가 없습니다.

 

이 방식은 Microsoft Entra ID를 통한 쿠버네티스 인증이 없습니다. 단순히 kubeconfig 를 획득할 수 있는 Role을 부여하거나 부여하지 않는 방식으로 사용자를 구분할 수 있지만, 획득한 kubeconfig는 모두 동일하게 쿠버네티스에 대한 admin 권한을 가집니다.

 

Microsoft Entra ID authentication with Kubernetes RBAC

이 구성은 Microsoft Entra ID로 인증을 연동하고, 또한 Entra ID의 사용자나 그룹을 Kubernetes RBAC의 주체로 사용할 수 있습니다.

아래는 Microsoft Entra ID authentication with Kubernetes RBAC를 선택한 옵션으로 Kubernetes의 admin에 대한 ClusterRoleBinding을 지정할 Entra ID의 그룹을 지정할 수 있습니다. 해당 지정된 그룹의 사용자는 쿠버네티스의 admin 에 해당하는 권한을 할당 받습니다.

 

이 때 사용자 시나리오는 관리자 그룹에서 Kubernetes의 role/binding을 관리하고, 나머지 사용자나 그룹에 Kubernetes의 RBAC을 부여하여 사용하는 방식입니다.

 

Azure Kubernetes Service Cluster Admin Role을 가지는 사용자는 Kubernetes에 대한 admin 권한을 가진 kubeconfig를 획득할 수 있습니다. 이 때문에 local account를 비활성화 하는 옵션이 아래에 표시되어 있습니다.

 

이 구성에서도 Azure Kubernetes Service Cluster User Role을 부여 받은 사용자나 그룹만 az aks get-credentials을 통해 kubeconfig를 획득할 수 있습니다.

 

이후 획득한 kubeconfig는 Microsoft Entra ID의 인증과 연동하도록 구성되어 있으며, 또한 Kubernetes RBAC 모드에서는 RoleBinding에서 Azure Entra ID의 사용자(UPN, User Principal Name)나 그룹(Object ID)을 지정할 수 있습니다.

kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: dev-user-access
  namespace: dev
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: dev-user-full-access
subjects:
- kind: Group # 일반 유저인 경우 kind: User
  namespace: dev
  name: groupObjectId # 일반 유저인 경우 user principal name (UPN) 입력

 

이 방식은 EKS의 인증/인가 방식에서 ConfigMap을 사용하는 방식(IAM의 User를 쿠버네티스의 그룹과 맵핑하는 방식)과 유사하다고 생각됩니다. 한편, EKS API의 access entry에서도 IAM의 User와 쿠버네티스 그룹을 맵핑해줄 수도 있습니다.

 

Microsoft Entra ID authentication with Azure RBAC

이 구성은 Microsoft Entra ID로 인증을 연동하고, 또한 Azure RBAC을 통하여 쿠버네티스 인가를 사용할 수 있습니다.

이 구성에서도 Azure Kubernetes Service Cluster User Role을 부여 받은 사용자나 그룹만 az aks get-credentials을 통해 kubeconfig를 획득할 수 있습니다.

이후 획득한 kubeconfig는 Microsoft Entra ID의 인증과 연동하도록 구성되어 있으며, 또한 Azure RBAC을 통해서 쿠버네티스의 인가를 구성할 수 있습니다.

 

먼저 아래와 같은 Built-in Role을 사용하거나 별도의 Custom Role을 생성할 수도 있습니다.

https://learn.microsoft.com/en-us/azure/aks/manage-azure-rbac?tabs=azure-cli#aks-built-in-roles

 

또한 Azure RBAC을 통해서 해당 Role을 사용자/그룹에 할당할 수 있습니다. 이때 --scope을 통해서 특정 네임스페이스에 할당할 수 있는 점에서 완전히 Azure RBAC을 통해 쿠버네티스 수준의 관리까지 지원을 하고 있는 것을 알 수 있습니다.

# AKS에 권한 할당
az role assignment create --role "Azure Kubernetes Service RBAC Admin" --assignee <AAD-ENTITY-ID> --scope $AKS_ID

# 특정 Namespace에 권한 할당
az role assignment create --role "Azure Kubernetes Service RBAC Reader" --assignee <AAD-ENTITY-ID> --scope $AKS_ID/namespaces/<namespace-name>

 

참고로, Azure Kubernetes Service Cluster Admin Role을 가지는 사용자는 Kubernetes에 대한 admin 권한을 가진 kubeconfig를 획득할 수 있습니다. 이 때문에 local account를 비활성화할 수 있습니다.

 

Microsoft Entra ID authentication with Azure RBAC의 접근 방식은 AKS라는 리소스에 권한을 할당하는 방식만으로 쿠버네티스 권한 관리까지 수행할 수 있기 때문에, Azure 수준에서 AKS에 할당된 모든 권한(리소스와 쿠버네티스 권한)를 확인할 수 있다는 장점이 있습니다.

 

이 방식은 EKS의 인증/인가 방식에서 EKS API를 사용하는 방식(AWS IAM의 User를 정의된 Policy와 맵핑하는 방식)과 유사한 것 같습니다. 다만 EKS API의 access entry에는 쿠버네티스 그룹을 맵핑해줄 수도 있습니다.

 

AKS의 인증/인가 요약

요약하면, AKS에서는 특정 사용자/그룹에 한해 kubeconfig를 획득하는 권한을 할당해야 합니다.

Microsoft Entra ID와 통합하지 않은 경우에는 admin 권한의 kubeconfig를 사용하는 방식으로 사용할 수 있습니다.

Kubernetes의 인증을 Microsoft Entra ID와 통합할 수 있으며, 또한 인가 방식에서 Kubernetes RBAC과 Azure RBAC를 선택할 수 있습니다.

마지막으로 Microsoft Entra ID와 통합을 한 경우 local account를 비활성화 할 수 있습니다.

 

EKS와의 차이점을 보면, kubeconfig를 획득하기 위한 권한을 별도로 가지고 있다는 점과 Microsoft Entra ID를 인증을 하지 않는 local account 방식을 제공하는 부분이 있습니다. 또한 Azure RBAC을 통해 인가를 처리해줄 수 있는 부분이 있습니다.

 

 

4. Kubernetes의 파드 권한

쿠버네티스에서 파드에 ServiceAccount를 부여하고, ServiceAccount라는 주체를 RBAC으로 구성하면 쿠버네티스의 리소스에 대한 권한을 할당 받습니다. 예를 들어, 배포를 담당하는 파드가 있고, 해당 파드 할당된 ServiceAccount에 deployments에 대한 CRUD를 허용하면, 파드에서 deployment 배포가 가능하게 됩니다.

 

다만 파드가 쿠버네티스의 리소스가 아닌 클라우드 자원 자체에 접근한다는 것은 다른 이야기입니다. 예를 들어, 파드가 AWS의 S3를 조회하거나 파일을 업로드하는 것입니다. 즉, 쿠버네티스에서 인증/인가할 수 있는 범위를 넘어서, 클라우드에서 제공하는 Identity 및 Access 관리 솔루션을 통해서 인증/인가를 받아야 합니다.

 

앞서 살펴본 EKS의 인증/인가 절에서는 AWS에서 유효한 사용자가 어떻게 쿠버네티스의 인증/인가를 이용할 수 있는가의 관점이라면, 지금 다루는 주제는 쿠버네티스에서 유효한 주체가 어떻게 AWS의 인증/인가를 이용할 수 있는가에 대한 문제입니다.

 

EKS에서는 IRSA(IAM Roles for Service Accounts)와 Pod Identity라는 방식을 제공하고 있고, 이를 다음 절 EKS의 파드 권한 할당에서 자세히 살펴보겠습니다.

 

 

5. EKS의 파드 권한 할당

아무런 권한을 부여하지 않은 파드는 노드의 권한을 가지게 됩니다. AWS에서는 인스턴스에 부여된 IAM Role을 가지게 됩니다.

아래와 같이 확인을 해볼 수 있습니다.

# awscli 파드 생성
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: awscli-pod
spec:
  replicas: 2
  selector:
    matchLabels:
      app: awscli-pod
  template:
    metadata:
      labels:
        app: awscli-pod
    spec:
      containers:
      - name: awscli-pod
        image: amazon/aws-cli
        command: ["tail"]
        args: ["-f", "/dev/null"]
      terminationGracePeriodSeconds: 0
EOF

# 파드 생성 확인
kubectl get pod -owide

# 파드 이름 변수 지정
APODNAME1=$(kubectl get pod -l app=awscli-pod -o jsonpath="{.items[0].metadata.name}")
APODNAME2=$(kubectl get pod -l app=awscli-pod -o jsonpath="{.items[1].metadata.name}")
echo $APODNAME1, $APODNAME2

# awscli 파드에서 EC2 InstanceProfile(IAM Role)의 ARN 정보가 확인됨
kubectl exec -it $APODNAME1 -- aws sts get-caller-identity --query Arn
"arn:aws:sts::xx:assumed-role/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-e7CGUnBoQC96/i-0e6fd9f697b0a4c2f"
kubectl exec -it $APODNAME2 -- aws sts get-caller-identity --query Arn
"arn:aws:sts::xx:assumed-role/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-e7CGUnBoQC96/i-04ed117980b8faf7f"

# 해당 IAM Role에 권한이 없기 때문에 실패함
kubectl exec -it $APODNAME1 -- aws s3 ls
An error occurred (AccessDenied) when calling the ListBuckets operation: User: arn:aws:sts::xx:assumed-role/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-e7CGUnBoQC96/i-0e6fd9f697b0a4c2f is not authorized to perform: s3:ListAllMyBuckets because no identity-based policy allows the s3:ListAllMyBuckets action
command terminated with exit code 254

 

그렇다면 인스턴스에 부여된 IAM Role을 통하여 권한을 할당하면 된다고 생각할 수 있지만, 이런 방식에서는 노드에 실행된 모든 파드에서 동일한 권한이 부여되기 때문에 최소 권한에 위배됩니다.

 

참고로 EKS를 생성하는 ClusterConfig에 관리 노드 그룹에 대해 아래와 같이 IAM을 지정할 수 있습니다.

...
managedNodeGroups:
- amiFamily: AmazonLinux2023
  desiredCapacity: 3
  iam:
    withAddonPolicies:
      autoScaler: true
      certManager: true
      externalDNS: true
  instanceType: t3.medium
...

웹 콘솔에서는 아래와 같이 인스턴스에 할당된 IAM을 따라가보면 아래와 같이 확인할 수 있습니다.

 

이러한 이유로 권한이 필요한 파드에 IAM Role을 부여하는 방식이 필요합니다. 파드 권한 할당을 위해 EKS에서 제공하는 IRSA와 Pod Identity를 확인해보겠습니다.

 

IRSA(IAM Roles for Service Accounts)

IRSA는 권한이 부여된 IAM Role을 SerivceAccount에 할당하고, 파드가 ServiceAccount를 사용하여 AWS의 인증을 통해 AWS 리소스를 접근하는 방식입니다. 이를 위해서 OIDC Issuer가 JWT를 발급해주고, 또한 IAM과 신뢰관계를 통해서 발급 여부를 확인해줍니다.

 

IRSA는 아래와 같은 절차를 통해서 이뤄집니다.

출처: https://github.com/awskrug/security-group/blob/main/files/AWSKRUG_2024_02_EKS_ROLE_MANAGEMENT.pdf

 

실습을 통해 살펴보겠습니다.

# Create an iamserviceaccount - AWS IAM role bound to a Kubernetes service account
eksctl create iamserviceaccount \
  --name my-sa \
  --namespace default \
  --cluster $CLUSTER_NAME \
  --approve \
  --attach-policy-arn $(aws iam list-policies --query 'Policies[?PolicyName==`AmazonS3ReadOnlyAccess`].Arn' --output text)

2025-03-15 23:58:25 [ℹ]  1 existing iamserviceaccount(s) (kube-system/aws-load-balancer-controller) will be excluded
2025-03-15 23:58:25 [ℹ]  1 iamserviceaccount (default/my-sa) was included (based on the include/exclude rules)
2025-03-15 23:58:25 [!]  serviceaccounts that exist in Kubernetes will be excluded, use --override-existing-serviceaccounts to override
2025-03-15 23:58:25 [ℹ]  1 task: {
    2 sequential sub-tasks: {
        create IAM role for serviceaccount "default/my-sa",
        create serviceaccount "default/my-sa",
    } }2025-03-15 23:58:25 [ℹ]  building iamserviceaccount stack "eksctl-myeks-addon-iamserviceaccount-default-my-sa"
2025-03-15 23:58:25 [ℹ]  deploying stack "eksctl-myeks-addon-iamserviceaccount-default-my-sa"
2025-03-15 23:58:25 [ℹ]  waiting for CloudFormation stack "eksctl-myeks-addon-iamserviceaccount-default-my-sa"
2025-03-15 23:58:56 [ℹ]  waiting for CloudFormation stack "eksctl-myeks-addon-iamserviceaccount-default-my-sa"
2025-03-15 23:58:56 [ℹ]  created serviceaccount "default/my-sa"

# SA 확인
kubectl get sa
NAME      SECRETS   AGE
default   0         122m
my-sa     0         4m37s

kubectl describe sa my-sa
Name:                my-sa
Namespace:           default
Labels:              app.kubernetes.io/managed-by=eksctl
Annotations:         eks.amazonaws.com/role-arn: arn:aws:iam::xx:role/eksctl-myeks-addon-iamserviceaccount-default--Role1-MYPji4gGE3x2
Image pull secrets:  <none>
Mountable secrets:   <none>
Tokens:              <none>
Events:              <none>

 

쿠버네티스에 새로운 SerivceAccount가 생성되었고, 어노테이션으로 ARN이 지정된 것을 알 수 있습니다.

 

또한 위 명령을 수행하면 CloudFormation이 실행되고 IAM Role이 생성됩니다. CloudFormation을 확인해서 리소스를 보면 IAM Role이 생성된 것을 확인할 수 있습니다.

생성된 ServiceAccount를 통해 IRSA를 사용하는 파드를 생성 합니다.

# 파드 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: eks-iam-test3
spec:
  serviceAccountName: my-sa
  containers:
    - name: my-aws-cli
      image: amazon/aws-cli:latest
      command: ['sleep', '36000']
  restartPolicy: Never
  terminationGracePeriodSeconds: 0
EOF

# 파드에서 aws cli 사용 확인
NAMESPACE       NAME                            ROLE ARN
default         my-sa                           arn:aws:iam::xx:role/eksctl-myeks-addon-iamserviceaccount-default--Role1-MYPji4gGE3x2
kube-system     aws-load-balancer-controller    arn:aws:iam::xx:role/eksctl-myeks-addon-iamserviceaccount-kube-sys-Role1-oyUqvqXumqCT

kubectl exec -it eks-iam-test3 -- aws sts get-caller-identity --query Arn
"arn:aws:sts::xx:assumed-role/eksctl-myeks-addon-iamserviceaccount-default--Role1-MYPji4gGE3x2/botocore-session-1742051277"

# 할당된 Policy에 의해 가능한 작업 (에러 발생하지 않음)
kubectl exec -it eks-iam-test3 -- aws s3 ls

# 할당된 Policy에 의해 불가한 작업
kubectl exec -it eks-iam-test3 -- aws ec2 describe-instances --region ap-northeast-2
An error occurred (UnauthorizedOperation) when calling the DescribeInstances operation: You are not authorized to perform this operation. User: arn:aws:sts::xx:assumed-role/eksctl-myeks-addon-iamserviceaccount-default--Role1-MYPji4gGE3x2/botocore-session-1742051277 is not authorized to perform: ec2:DescribeInstances because no identity-based policy allows the ec2:DescribeInstances action
command terminated with exit code 254

 

파드 스펙에는 ServiceAccount 이름만 지정하지만, Admission Controller의 Mutation Webhook에 의해서 필요한 정보가 추가로 등록된 것을 확인할 수 있습니다.

# 해당 SA를 파드가 사용 시 mutatingwebhook으로 Env,Volume 추가함: AWS IAM 역할을 Pod에 자동으로 주입
kubectl get mutatingwebhookconfigurations pod-identity-webhook -o yaml
...
webhooks:
- admissionReviewVersions:
  - v1beta1
  clientConfig:
    caBundle: xxx
    url: https://127.0.0.1:23443/mutate
  failurePolicy: Ignore
  matchPolicy: Equivalent
  name: iam-for-pods.amazonaws.com
  namespaceSelector: {}
  objectSelector:
    matchExpressions:
    - key: eks.amazonaws.com/skip-pod-identity-webhook
      operator: DoesNotExist
  reinvocationPolicy: IfNeeded
  rules:
  - apiGroups:
    - ""
    apiVersions:
    - v1
    operations:
    - CREATE
    resources:
    - pods
    scope: '*'
  sideEffects: None
  timeoutSeconds: 10
...

# Pod Identity Webhook은 mutating webhook을 통해 아래 Env 내용과 1개의 볼륨을 추가함
kubectl get pod eks-iam-test3
kubectl get pod eks-iam-test3 -o yaml
...
    env:
    - name: AWS_STS_REGIONAL_ENDPOINTS
      value: regional
    - name: AWS_DEFAULT_REGION
      value: ap-northeast-2
    - name: AWS_REGION
      value: ap-northeast-2
    - name: AWS_ROLE_ARN
      value: arn:aws:iam::xx:role/eksctl-myeks-addon-iamserviceaccount-default--Role1-MYPji4gGE3x2
    - name: AWS_WEB_IDENTITY_TOKEN_FILE
      value: /var/run/secrets/eks.amazonaws.com/serviceaccount/token
...
    volumeMounts: 
    - mountPath: /var/run/secrets/eks.amazonaws.com/serviceaccount
      name: aws-iam-token
      readOnly: true
...
  volumes:
  - name: aws-iam-token
    projected:
      defaultMode: 420
      sources:
      - serviceAccountToken:
          audience: sts.amazonaws.com
          expirationSeconds: 86400
          path: token
...

 

마운트된 토큰을 확인해보겠습니다.

# 토큰 확인
kubectl exec -it eks-iam-test3 -- cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token ; echo

 

JWT로 디코드 해보면 아래와 같은 정보를 확인할 수 있습니다.

{
  "aud": [
    "sts.amazonaws.com"
  ],
  "exp": 1742137620,
  "iat": 1742051220,
  "iss": "https://oidc.eks.ap-northeast-2.amazonaws.com/id/EF882B...",
  "jti": "585eab74-0dd1-4047-a8d5-2181c3db9c13",
  "kubernetes.io": {
    "namespace": "default",
    "node": {
      "name": "ip-192-168-1-41.ap-northeast-2.compute.internal",
      "uid": "fb4be118-8152-452a-a0aa-eaff394022e2"
    },
    "pod": {
      "name": "eks-iam-test3",
      "uid": "784ef7a7-0acd-4394-bcd7-c4f71f5c101f"
    },
    "serviceaccount": {
      "name": "my-sa",
      "uid": "bd180c18-c38d-4974-8f34-8d9b2f7b37c8"
    }
  },
  "nbf": 1742051220,
  "sub": "system:serviceaccount:default:my-sa"
}

 

JWT 토큰의 iss를 확인해보면, 웹 콘솔의 EKS를 확인해보면 OpenID Connect provider URL와 일치하는 것을 알 수 있습니다.

 

실습을 마무리하고 IRSA 관련 리소스는 삭제하겠습니다.

# 실습 확인 후 파드 삭제 및 IRSA 제거
kubectl delete deply awscli-pod
kubectl delete pod eks-iam-test3
eksctl delete iamserviceaccount --cluster $CLUSTER_NAME --name my-sa --namespace default

# 확인
eksctl get iamserviceaccount --cluster $CLUSTER_NAME
kubectl get sa

 

이러한 IRSA는 관리 복잡성과, ServiceAccount를 세부적으로 지정하지 않는 경우 보안에 취약한 점 등으로 현재는Classic으로 여겨지고 이후 Pod Identity가 도입되었습니다.

 

Pod Identity

IRSA는 2019년에 도입되었다면, Pod Identity는 2023년 비교적 최근에 도입된 방식으로 보안과 사용성 측면에서 개선된 사항이 많습니다.

 

Pod Identity는 EKS Pod Identity Agent를 통해서 credentials을 발급 받고, EKS Auth API를 통해서 인증을 처리 받습니다. 아래의 처리과정을 참고 부탁드립니다.

출처: https://aws.amazon.com/ko/blogs/containers/amazon-eks-pod-identity-a-new-way-for-applications-on-eks-to-obtain-iam-credentials/

 

아래와 같이 실습을 진행하겠습니다.

Pod Identity는 애드온으로 설치가 가능합니다.

# Pod Identity 버전 확인
ADDON=eks-pod-identity-agent
aws eks describe-addon-versions \
    --addon-name $ADDON \
    --kubernetes-version 1.31 \
    --query "addons[].addonVersions[].[addonVersion, compatibilities[].defaultVersion]" \
    --output text
v1.3.5-eksbuild.2
False
v1.3.4-eksbuild.1
True
v1.3.2-eksbuild.2
False
v1.3.0-eksbuild.1
False
v1.2.0-eksbuild.1
False
v1.1.0-eksbuild.1
False
v1.0.0-eksbuild.1
False

# 설치
eksctl create addon --cluster $CLUSTER_NAME --name eks-pod-identity-agent --version 1.3.5

# 확인
eksctl get addon --cluster $CLUSTER_NAME

NAME                    VERSION                 STATUS          ISSUES  IAMROLE                                                                                 UPDATE AVAILABLE CONFIGURATION VALUES            POD IDENTITY ASSOCIATION ROLES
aws-ebs-csi-driver      v1.40.1-eksbuild.1      ACTIVE          0       arn:aws:iam::xx:role/eksctl-myeks-addon-aws-ebs-csi-driver-Role1-15a6w33Xm4wR
coredns                 v1.11.4-eksbuild.2      ACTIVE          0
eks-pod-identity-agent  v1.3.5-eksbuild.2       CREATING        0
kube-proxy              v1.31.3-eksbuild.2      ACTIVE          0
metrics-server          v0.7.2-eksbuild.2       ACTIVE          0
vpc-cni                 v1.19.3-eksbuild.1      ACTIVE          0       arn:aws:iam::xx:role/eksctl-myeks-addon-vpc-cni-Role1-RS9uYpCia7T9            enableNetworkPolicy: "true"

# 데몬 셋으로 설치됨
kubectl -n kube-system get daemonset eks-pod-identity-agent

NAME                     DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
eks-pod-identity-agent   3         3         3       3            3           <none>          33s

 

아래와 같이 Pod Identity Association을 생성합니다.

# Pod Identity Association을 생성
eksctl create podidentityassociation \
--cluster $CLUSTER_NAME \
--namespace default \
--service-account-name s3-sa \
--role-name s3-eks-pod-identity-role \
--permission-policy-arns arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess \
--region ap-northeast-2

2025-03-16 00:22:39 [ℹ]  1 task: {
    2 sequential sub-tasks: {
        create IAM role for pod identity association for service account "default/s3-sa",
        create pod identity association for service account "default/s3-sa",
    } }2025-03-16 00:22:39 [ℹ]  deploying stack "eksctl-myeks-podidentityrole-default-s3-sa"
2025-03-16 00:22:40 [ℹ]  waiting for CloudFormation stack "eksctl-myeks-podidentityrole-default-s3-sa"
2025-03-16 00:23:10 [ℹ]  waiting for CloudFormation stack "eksctl-myeks-podidentityrole-default-s3-sa"
2025-03-16 00:23:11 [ℹ]  created pod identity association for service account "s3-sa" in namespace "default"
2025-03-16 00:23:11 [ℹ]  all tasks were completed successfully

# 확인
kubectl get sa

NAME      SECRETS   AGE
default   0         142m

eksctl get podidentityassociation --cluster $CLUSTER_NAME
ASSOCIATION ARN                                                                                 NAMESPACE       SERVICE ACCOUNT NAME    IAM ROLE ARN            OWNER ARN
arn:aws:eks:ap-northeast-2:xx:podidentityassociation/myeks/a-8zp14caxh5ask7ed0        default         s3-sa                   arn:aws:iam::xx:role/s3-eks-pod-identity-role

aws eks list-pod-identity-associations --cluster-name $CLUSTER_NAME | jq
{
  "associations": [
    {
      "clusterName": "myeks",
      "namespace": "default",
      "serviceAccount": "s3-sa",
      "associationArn": "arn:aws:eks:ap-northeast-2:xx:podidentityassociation/myeks/a-8zp14caxh5ask7ed0",
      "associationId": "a-8zp14caxh5ask7ed0"
    }
  ]
}

 

eksctl create podidentityassociation 또한 CloudFormation을 실행하도록 동작하며, ServiceAccount 는 별도로 생성되지 않습니다.

 

웹 콘솔을 확인해보면 Pod Association에서 정보가 확인 가능합니다. IRSA는 웹 콘솔에는 노출되지 않습니다.

 

Pod Identity를 사용하는 파드도 연관된 ServiceAccount 이름을 지정하는 것으로 사용할 수 있습니다.

# 서비스어카운트, 파드 생성
kubectl create sa s3-sa

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: eks-pod-identity
spec:
  serviceAccountName: s3-sa
  containers:
    - name: my-aws-cli
      image: amazon/aws-cli:latest
      command: ['sleep', '36000']
  restartPolicy: Never
  terminationGracePeriodSeconds: 0
EOF

# 파드 정보 확인
kubectl get pod eks-pod-identity -o yaml 
...
    env:
    - name: AWS_STS_REGIONAL_ENDPOINTS
      value: regional
    - name: AWS_DEFAULT_REGION
      value: ap-northeast-2
    - name: AWS_REGION
      value: ap-northeast-2
    - name: AWS_CONTAINER_CREDENTIALS_FULL_URI
      value: http://169.254.170.23/v1/credentials
    - name: AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE
      value: /var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token
...
    - mountPath: /var/run/secrets/pods.eks.amazonaws.com/serviceaccount
      name: eks-pod-identity-token
      readOnly: true
...
  volumes:
  - name: eks-pod-identity-token
    projected:
      defaultMode: 420
      sources:
      - serviceAccountToken:
          audience: pods.eks.amazonaws.com
          expirationSeconds: 86400
          path: eks-pod-identity-token
...

# Pod Identity로 정보 확인
kubectl exec -it eks-pod-identity -- aws sts get-caller-identity --query Arn
"arn:aws:sts::xx:assumed-role/s3-eks-pod-identity-role/eks-myeks-eks-pod-id-0382fb7d-1b2b-45c2-84bb-d5b123292589"

# 에러 발생하지 않음
kubectl exec -it eks-pod-identity -- aws s3 ls

# 에러 발생
kubectl exec -it eks-pod-identity -- aws ec2 describe-instances --region ap-northeast-2
An error occurred (UnauthorizedOperation) when calling the DescribeInstances operation: You are not authorized to perform this operation. User: arn:aws:sts::xx:assumed-role/s3-eks-pod-identity-role/eks-myeks-eks-pod-id-0382fb7d-1b2b-45c2-84bb-d5b123292589 is not authorized to perform: ec2:DescribeInstances because no identity-based policy allows the ec2:DescribeInstances action
command terminated with exit code 254

 

마찬가지로 토큰을 확인해보겠습니다.

# 토큰 정보 확인
kubectl exec -it eks-pod-identity -- cat /var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token; echo

 

aud(Audience) 가 STS에서 pods.eks.amazonaws.com 으로 다른 것을 알 수 있습니다.

{
  "aud": [
    "pods.eks.amazonaws.com"
  ],
  "exp": 1742138781,
  "iat": 1742052381,
  "iss": "https://oidc.eks.ap-northeast-2.amazonaws.com/id/EF882B...",
  "jti": "50985ae4-392b-4caa-ae3c-c4de13f58e3c",
  "kubernetes.io": {
    "namespace": "default",
    "node": {
      "name": "ip-192-168-2-79.ap-northeast-2.compute.internal",
      "uid": "9e28f27b-c643-4c76-9e33-3658ca4014ed"
    },
    "pod": {
      "name": "eks-pod-identity",
      "uid": "276bf7da-851f-4eec-87d5-07488e972f2a"
    },
    "serviceaccount": {
      "name": "s3-sa",
      "uid": "7438f7de-ec70-435f-8d34-de7a239a955e"
    }
  },
  "nbf": 1742052381,
  "sub": "system:serviceaccount:default:s3-sa"
}

 

실습을 마치고 리소스를 삭제하겠습니다.

eksctl delete podidentityassociation --cluster $CLUSTER_NAME --namespace default --service-account-name s3-sa
kubectl delete pod eks-pod-identity
kubectl delete sa s3-sa

 

 

6. AKS의 파드 권한 할당

AKS에서는 파드에 Azure의 리소스에 접근하는 권한을 할당하는 방식으로 Workload Identity를 사용할 수 있습니다.

이 방식은 Azure에서 Managed Identity라는 주체를 생성하고, Azure RBAC을 통해서 권한 관리를 하며, Managed Identity를 ServiceAccount에서 사용하는 방식으로 진행됩니다.

 

또한 AKS에서 workload Identity은 --enable-oidc-issuer--enable-workload-identity 옵션을 통해 활성화 할 수 있습니다.

az aks create --resource-group "${RESOURCE_GROUP}" --name "${CLUSTER_NAME}" --enable-oidc-issuer --enable-workload-identity --generate-ssh-keys

 

AKS의 Workload Identity의 동작 과정은 아래의 문서를 살펴보실 수 있습니다.

https://learn.microsoft.com/en-us/azure/aks/workload-identity-overview?tabs=dotnet

 

실제 클러스터에 구성하는 방식은 아래에서 설명하고 있습니다.

https://learn.microsoft.com/en-us/azure/aks/workload-identity-deploy-cluster

 

절차를 요약하면 아래와 같습니다.

1) 클러스터에 Workload Identity와 OIDC issuer 활성화

2) Managed Identity 생성

3) 쿠버네티스 ServiceAccount 생성 (Managed Identity의 Client ID 입력)

4) Federated Identity Credentials 생성

  • Managed Identity와 OIDC Issuer, 그리고 주체(ServiceAccount)를 연결
export FEDERATED_IDENTITY_CREDENTIAL_NAME="myFedIdentity$RANDOM_ID"
az identity federated-credential create --name ${FEDERATED_IDENTITY_CREDENTIAL_NAME} --identity-name "${USER_ASSIGNED_IDENTITY_NAME}" --resource-group "${RESOURCE_GROUP}" --issuer "${AKS_OIDC_ISSUER}" --subject system:serviceaccount:"${SERVICE_ACCOUNT_NAMESPACE}":"${SERVICE_ACCOUNT_NAME}" --audience api://AzureADTokenExchange

5) Managed Identity에 대한 Azure 리소스 권한 할당 (생략)

6) 애플리케이션 생성 (azure.workload.identity/use: "true" label 및 ServiceAccount 입력)

kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
    name: sample-workload-identity-key-vault
    namespace: ${SERVICE_ACCOUNT_NAMESPACE}
    labels:
        azure.workload.identity/use: "true"
spec:
    serviceAccountName: ${SERVICE_ACCOUNT_NAME}
    containers:
      - image: ghcr.io/azure/azure-workload-identity/msal-go
        name: oidc
        env:
          - name: KEYVAULT_URL
            value: ${KEYVAULT_URL}
          - name: SECRET_NAME
            value: ${KEYVAULT_SECRET_NAME}
    nodeSelector:
        kubernetes.io/os: linux
EOF

 

이러한 과정은 eksctl 을 사용하는 EKS에 비해서 다소 복잡하게 느껴지기는 합니다. 한편으로는 EKS는 신규 API를 추가하여 기능을 간단하게 제공하고, AKS는 기존 Azure의 주체를 활용하는 방식으로 기존 Azure API를 통한 처리를 하는 것으로 이해됩니다.

 

 

마무리

해당 포스트에서 EKS의 인증/인가와 파드에 IAM 권한을 할당하는 방식을 살펴봤습니다.

이 과정에서 EKS가 AWS의 IAM이라는 ID 및 엑세스 관리 서비스와 연계되는 방식을 살펴봤습니다.

 

이러한 과정은 서로 다른 시스템 간의 인증이 연동되는 방식으로 이해할 수 있으며, AWS에서 유효한 주체가 어떻게 쿠버네티스의 인증/인가를 이용할 수 있는가와 쿠버네티스에서 유효한 주체가 어떻게 AWS의 인증/인가를 이용할 수 있는가에 대한 답변이 되었으면 좋겠습니다.

 

간단히 요약하면 AWS의 사용자는 API 서버의 Token Webhook Authentication으로 IAM을 통해서 인증을 진행하고, 확인된 ARN 정보와 쿠버네티스의 그룹(혹은 Policy)의 맵핑을 확인하여 쿠버네티스 RBAC을 통해 인가가 이뤄줬습니다.

쿠버네티스의 유효한 주체는 OIDC나 혹은 Pod Identity Agent를 통해서 AWS IAM과 연계 및 인증을 통해서 유효한 토큰을 획득하고 AWS의 리소스를 접근할 수 있었습니다.

 

다음 포스트에서는 EKS에서 Fargate, Hybrid node를 사용하는 방식을 살펴보겠습니다.

'EKS' 카테고리의 다른 글

[8] EKS Upgrade  (0) 2025.04.02
[7] EKS Fargate  (0) 2025.03.23
[5-2] EKS의 오토스케일링 Part2  (0) 2025.03.07
[5-1] EKS의 오토스케일링 Part1  (0) 2025.03.07
[4] EKS의 모니터링과 로깅  (0) 2025.03.01

본 포스트에서는 기본적인 쿠버네티스 환경의 스케일링 기술을 살펴보겠습니다. 이후 EKS의 오토스케일링 옵션을 살펴보고, 각 옵션을 실습을 통해 살펴도록 하겠습니다. 마지막으로 AKS의 오토스케일링 옵션을 EKS와 비교해 보겠습니다.

 

이번 포스트에서는 EKS의 오토스케일링(Autoscaling) Part2로 지난 포스트에 이어서 Cluster Autoscaler 부터 이어나가겠습니다.

 

목차

EKS의 오토스케일링 Part1 (https://a-person.tistory.com/38)

  1. 쿠버네티스 환경의 스케일링
  2. EKS의 오토스케일링 개요
  3. 실습 환경 생성
  4. HPA(Horizontal Pod Autoscaler)
  5. KEDA(Kubernetes Event-driven Autoscaler)
  6. VPA(Vertical Pod Autoscaler)

EKS의 오토스케일링 Part2

  1. CA(Cluster Autoscaler)
  2. Karpenter
  3. AKS의 오토스케일링
  4. 오토스케일링에 대한 주의사항

 

1. CA(Cluster Autoscaler)

노드를 스케일링하는 CA(Cluster Autoscaler)를 살펴보겠습니다.

많은 사람들이 클라우드 환경에서 컴퓨팅 자원을 기반으로 한 오토스케일링에 대한 이해를 하고 있기 때문에, 가상 머신 세트(예를 들어, ASG, VMSS 등)의 CPU/Memory와 같은 리소스 사용률이 CA를 동작시키는 것으로 오해하는 경우가 많습니다.

 

하지만 쿠버네티스의 CA는 아래와 같은 상황에서 동작합니다.

https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/FAQ.md#what-is-cluster-autoscaler

Cluster Autoscaler increases the size of the cluster when:

  • there are pods that failed to schedule on any of the current nodes due to insufficient resources.
  • adding a node similar to the nodes currently present in the cluster would help.

Cluster Autoscaler decreases the size of the cluster when some nodes are consistently unneeded for a significant amount of time. A node is unneeded when it has low utilization and all of its important pods can be moved elsewhere.

 

즉, 현재 노드의 리소스가 부족하여 파드가 스케줄링이 될 수 없는 상황에서 노드의 수를 증가시키게 됩니다. 그러하므로 Pending 파드, 정확하게는 unschedurable 파드가 발생한 상황에서 노드 수가 증가하는 개념입니다.

 

아래는 EKS에서 HPA와 CA를 설명하고 있습니다.

출처: https://www.youtube.com/watch?v=jLuVZX6WQsw

 

EKS의 CA는 노드의 아래 두가지 태그가 등록되어 있는 노드들에 대해서 동작합니다. 아래와 같이 사전 정보를 확인하실 수 있습니다.

# EKS 노드에 이미 아래 tag가 들어가 있음
# k8s.io/cluster-autoscaler/enabled : true
# k8s.io/cluster-autoscaler/myeks : owned
aws ec2 describe-instances  --filters Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node --query "Reservations[*].Instances[*].Tags[*]" --output yaml
...
- Key: k8s.io/cluster-autoscaler/myeks
      Value: owned
- Key: k8s.io/cluster-autoscaler/enabled
      Value: 'true'
...

 

CA가 동작할 수 있도록 ASG의 MaxSize를 6개로 사전에 수정합니다.

# 현재 autoscaling(ASG) 정보 확인
aws autoscaling describe-auto-scaling-groups \
    --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='myeks']].[AutoScalingGroupName, MinSize, MaxSize,DesiredCapacity]" \
    --output table
-----------------------------------------------------------------
|                   DescribeAutoScalingGroups                   |
+------------------------------------------------+----+----+----+
|  eks-ng1-70cab5c8-890d-c414-cc6d-c0d2eac06322  |  3 |  3 |  3 |
+------------------------------------------------+----+----+----+

# MaxSize 6개로 수정
export ASG_NAME=$(aws autoscaling describe-auto-scaling-groups --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='myeks']].AutoScalingGroupName" --output text)
aws autoscaling update-auto-scaling-group --auto-scaling-group-name ${ASG_NAME} --min-size 3 --desired-capacity 3 --max-size 6

# 확인
aws autoscaling describe-auto-scaling-groups --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='myeks']].[AutoScalingGroupName, MinSize, MaxSize,DesiredCapacity]" --output table
-----------------------------------------------------------------
|                   DescribeAutoScalingGroups                   |
+------------------------------------------------+----+----+----+
|  eks-ng1-70cab5c8-890d-c414-cc6d-c0d2eac06322  |  3 |  6 |  3 |
+------------------------------------------------+----+----+----+

 

이제 클러스터에 CA를 설치 하겠습니다.

# 배포 : Deploy the Cluster Autoscaler (CAS)
curl -s -O https://raw.githubusercontent.com/kubernetes/autoscaler/master/cluster-autoscaler/cloudprovider/aws/examples/cluster-autoscaler-autodiscover.yaml
...
            - ./cluster-autoscaler
            - --v=4
            - --stderrthreshold=info
            - --cloud-provider=aws
            - --skip-nodes-with-local-storage=false # 로컬 스토리지를 가진 노드를 autoscaler가 scale down할지 결정, false(가능!)
            - --expander=least-waste # 노드를 확장할 때 어떤 노드 그룹을 선택할지를 결정, least-waste는 리소스 낭비를 최소화하는 방식으로 새로운 노드를 선택.
            - --node-group-auto-discovery=asg:tag=k8s.io/cluster-autoscaler/enabled,k8s.io/cluster-autoscaler/<YOUR CLUSTER NAME>
...

sed -i -e "s|<YOUR CLUSTER NAME>|$CLUSTER_NAME|g" cluster-autoscaler-autodiscover.yaml
kubectl apply -f cluster-autoscaler-autodiscover.yaml

 

cluster-autoscaler 파드(디플로이먼트)가 노드에 실행되는 것을 확인할 수 있습니다.

# 확인
kubectl get pod -n kube-system | grep cluster-autoscaler
cluster-autoscaler-6df6d76b9f-ss5gd           1/1     Running   0          11s

# node-group-auto-discovery에서 활용되는 asg:tag를 확인할 수 있습니다.
kubectl describe deployments.apps -n kube-system cluster-autoscaler | grep node-group-auto-discovery
      --node-group-auto-discovery=asg:tag=k8s.io/cluster-autoscaler/enabled,k8s.io/cluster-autoscaler/myeks

# (옵션) cluster-autoscaler 파드가 동작하는 워커 노드가 퇴출(evict) 되지 않게 설정
kubectl -n kube-system annotate deployment.apps/cluster-autoscaler cluster-autoscaler.kubernetes.io/safe-to-evict="false"

 

아래 예제를 통해서 CA의 동작을 확인합니다.

# 노드 모니터링 
while true; do date; kubectl get node; echo "------------------------------" ; sleep 5; done

# Deploy a Sample App
# We will deploy an sample nginx application as a ReplicaSet of 1 Pod
cat << EOF > nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-to-scaleout
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        service: nginx
        app: nginx
    spec:
      containers:
      - image: nginx
        name: nginx-to-scaleout
        resources:
          limits:
            cpu: 500m
            memory: 512Mi
          requests:
            cpu: 500m
            memory: 512Mi
EOF
kubectl apply -f nginx.yaml
kubectl get deployment/nginx-to-scaleout

# Scale our ReplicaSet
# Let’s scale out the replicaset to 15
kubectl scale --replicas=15 deployment/nginx-to-scaleout && date

deployment.apps/nginx-to-scaleout scaled
Thu Mar  6 23:48:09 KST 2025

# 확인
kubectl get po |grep Pending
nginx-to-scaleout-7cfb655fb5-4vtb9   0/1     Pending   0          20s
nginx-to-scaleout-7cfb655fb5-6z6lk   0/1     Pending   0          20s
nginx-to-scaleout-7cfb655fb5-9g7s6   0/1     Pending   0          20s
nginx-to-scaleout-7cfb655fb5-ckph6   0/1     Pending   0          20s
nginx-to-scaleout-7cfb655fb5-lqbhc   0/1     Pending   0          20s
nginx-to-scaleout-7cfb655fb5-vk5bb   0/1     Pending   0          20s
nginx-to-scaleout-7cfb655fb5-vwnv7   0/1     Pending   0          20s

# 노드 자동 증가 확인
kubectl get nodes
aws autoscaling describe-auto-scaling-groups \
    --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='myeks']].[AutoScalingGroupName, MinSize, MaxSize,DesiredCapacity]" \
    --output table
-----------------------------------------------------------------
|                   DescribeAutoScalingGroups                   |
+------------------------------------------------+----+----+----+
|  eks-ng1-70cab5c8-890d-c414-cc6d-c0d2eac06322  |  3 |  6 |  6 |
+------------------------------------------------+----+----+----+

# [운영서버 EC2] 최근 1시간 Fleet API 호출 확인 - Link
# https://ap-northeast-2.console.aws.amazon.com/cloudtrailv2/home?region=ap-northeast-2#/events?EventName=CreateFleet
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=CreateFleet \
  --start-time "$(date -d '1 hour ago' --utc +%Y-%m-%dT%H:%M:%SZ)" \
  --end-time "$(date --utc +%Y-%m-%dT%H:%M:%SZ)"

{
    "Events": [
        {
            "EventId": "d16d3ea9-58ef-4d1e-8776-6172f2ea0d4a",
            "EventName": "CreateFleet",
            "ReadOnly": "false",
            "EventTime": "2025-03-06T23:48:25+09:00",
            "EventSource": "ec2.amazonaws.com",
            "Username": "AutoScaling",
            "Resources": [],
         ...

# (참고) Event name : UpdateAutoScalingGroup
# https://ap-northeast-2.console.aws.amazon.com/cloudtrailv2/home?region=ap-northeast-2#/events?EventName=UpdateAutoScalingGroup

 

EKS에서 Pending Pod가 발생한 이후 노드 생성 시점을 시간을 확인해보고, 비슷한 테스트를 AKS에서 진행한 경우 Pending Pod와 노드 생성 시점을 비교해봤습니다.

먼저 EKS는 t3.medium(2 vCPU, 4GB)을 사용했고, AKS에서도 Burstable에 해당하는 Standard_B2s(2 vCPU, 4GB)를 사용했습니다.

EKS CA 테스트

# 애플리케이션 추가
kubectl scale --replicas=15 deployment/nginx-to-scaleout && date
deployment.apps/nginx-to-scaleout scaled
Thu Mar  6 23:48:09 KST 2025

# 노드 생성 전
------------------------------
Thu Mar  6 23:49:03 KST 2025
NAME                                               STATUS   ROLES    AGE    VERSION
ip-192-168-1-87.ap-northeast-2.compute.internal    Ready    <none>   100m   v1.31.5-eks-5d632ec
ip-192-168-2-195.ap-northeast-2.compute.internal   Ready    <none>   100m   v1.31.5-eks-5d632ec
ip-192-168-3-136.ap-northeast-2.compute.internal   Ready    <none>   100m   v1.31.5-eks-5d632ec
...
# 노드 추가 -> 대략 1:10초 걸림
------------------------------
Thu Mar  6 23:49:17 KST 2025
NAME                                               STATUS     ROLES    AGE    VERSION
ip-192-168-1-67.ap-northeast-2.compute.internal    NotReady   <none>   5s     v1.31.5-eks-5d632ec
ip-192-168-1-87.ap-northeast-2.compute.internal    Ready      <none>   100m   v1.31.5-eks-5d632ec
ip-192-168-2-195.ap-northeast-2.compute.internal   Ready      <none>   100m   v1.31.5-eks-5d632ec
ip-192-168-2-246.ap-northeast-2.compute.internal   NotReady   <none>   10s    v1.31.5-eks-5d632ec
ip-192-168-3-136.ap-northeast-2.compute.internal   Ready      <none>   100m   v1.31.5-eks-5d632ec
ip-192-168-3-229.ap-northeast-2.compute.internal   NotReady   <none>   4s     v1.31.5-eks-5d632ec
...
# 전체 Ready -> 대략 1:30초 걸림
------------------------------
Thu Mar  6 23:49:32 KST 2025
NAME                                               STATUS   ROLES    AGE    VERSION
ip-192-168-1-67.ap-northeast-2.compute.internal    Ready    <none>   19s    v1.31.5-eks-5d632ec
ip-192-168-1-87.ap-northeast-2.compute.internal    Ready    <none>   100m   v1.31.5-eks-5d632ec
ip-192-168-2-195.ap-northeast-2.compute.internal   Ready    <none>   100m   v1.31.5-eks-5d632ec
ip-192-168-2-246.ap-northeast-2.compute.internal   Ready    <none>   24s    v1.31.5-eks-5d632ec
ip-192-168-3-136.ap-northeast-2.compute.internal   Ready    <none>   100m   v1.31.5-eks-5d632ec
ip-192-168-3-229.ap-northeast-2.compute.internal   Ready    <none>   18s    v1.31.5-eks-5d632ec

 

AKS CA 테스트

AKS에서도 동일하게 3~6으로 autoscaling 설정을 하였습니다.

 

결과와 시간을 볼 때는 유의미한 차이가 있는 것 같지는 않습니다. EKS와 AKS 모두 1분 30초 정도에 노드들이 추가 된 것으로 확인됩니다. 물론 이 테스트는 대략적인 시간을 확인한 것이므로 참고만 부탁드립니다.

# 애플리케이션 추가
$ kubectl scale --replicas=15 deployment/nginx-to-scaleout && date
deployment.apps/nginx-to-scaleout scaled
Thu Mar  6 15:08:17 UTC 2025

# 노드 생성 전
------------------------------
Thu Mar  6 15:09:41 UTC 2025
aks-userpool-13024277-vmss000000    Ready    <none>   9m56s   v1.31.4
aks-userpool-13024277-vmss000001    Ready    <none>   9m51s   v1.31.4
aks-userpool-13024277-vmss000002    Ready    <none>   9m57s   v1.31.4
# 노드 추가 -> 대략 1:30초 걸림
------------------------------
Thu Mar  6 15:09:46 UTC 2025
aks-userpool-13024277-vmss000000    Ready      <none>   10m     v1.31.4
aks-userpool-13024277-vmss000001    Ready      <none>   9m57s   v1.31.4
aks-userpool-13024277-vmss000002    Ready      <none>   10m     v1.31.4
aks-userpool-13024277-vmss000003    NotReady   <none>   1s      v1.31.4
aks-userpool-13024277-vmss000004    Ready      <none>   2s      v1.31.4
aks-userpool-13024277-vmss000005    Ready      <none>   1s      v1.31.4
# 전체 Ready -> 대략 1:35초 걸림
------------------------------
Thu Mar  6 15:09:52 UTC 2025
aks-userpool-13024277-vmss000000    Ready    <none>   10m   v1.31.4
aks-userpool-13024277-vmss000001    Ready    <none>   10m   v1.31.4
aks-userpool-13024277-vmss000002    Ready    <none>   10m   v1.31.4
aks-userpool-13024277-vmss000003    Ready    <none>   6s    v1.31.4
aks-userpool-13024277-vmss000004    Ready    <none>   7s    v1.31.4
aks-userpool-13024277-vmss000005    Ready    <none>   6s    v1.31.4
------------------------------

 

다음 실습을 위해서 리소스를 모두 삭제하겠습니다.

# 위 실습 중 디플로이먼트 삭제 후 10분 후 노드 갯수 축소되는 것을 확인 후 아래 삭제를 해보자! >> 만약 바로 아래 CA 삭제 시 워커 노드는 4개 상태가 되어서 수동으로 2대 변경 하자!
kubectl delete -f nginx.yaml

# size 수정 
aws autoscaling update-auto-scaling-group --auto-scaling-group-name ${ASG_NAME} --min-size 3 --desired-capacity 3 --max-size 3
aws autoscaling describe-auto-scaling-groups --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='myeks']].[AutoScalingGroupName, MinSize, MaxSize,DesiredCapacity]" --output table

# Cluster Autoscaler 삭제
kubectl delete -f cluster-autoscaler-autodiscover.yaml

 

Karpenter에서는 공식 가이드를 참고하여 신규 클러스터를 사용하므로, 해당 실습을 마무리하면 아래와 같이 생성된 실습 환경도 삭제하겠습니다.

# eksctl delete cluster --name $CLUSTER_NAME && aws cloudformation delete-stack --stack-name $CLUSTER_NAME
nohup sh -c "eksctl delete cluster --name $CLUSTER_NAME && aws cloudformation delete-stack --stack-name $CLUSTER_NAME" > /root/delete.log 2>&1 &

# (옵션) 삭제 과정 확인
tail -f /root/delete.log

 

추가로 CA에 관련하여 AWS의 Workshop 문서를 참고하실 수 있습니다.

https://catalog.us-east-1.prod.workshops.aws/workshops/9c0aa9ab-90a9-44a6-abe1-8dff360ae428/ko-KR/100-scaling/200-cluster-scaling

 

 

2. Karpenter

이전까지 CA에 대해서 살펴보고 동작 과정을 실습해 보았습니다. CA는 CSP에서 제공하는 가상머신 세트(ex. ASG, VMSS)를 통해 노드를 스케일링하는 옵션입니다.

 

다만 CA는 사용자의 노드 그룹을 기준으로 스케일링을 하기 때문에 아래와 같은 한계점을 가지고 있습니다.

먼저 요구 조건별 많은 노드 그룹이 생성된 경우 복잡해지는 점과 파드의 용량(request) 관점이 아닌 노드 관점의 스케일링이 발생한다는 점입니다. 또한 CA는 내부적으로 Auto Scaling Group을 통해 EC2 인스턴스를 컨트롤 하기 때문에 일부 지연이 예상됩니다.

출처: https://www.youtube.com/watch?v=jLuVZX6WQsw

 

이러한 CA의 복잡성과 지연을 극복하기 위해 Karpenter가 도입되었습니다. AWS에서 개발한 Karpenter는 현재 오픈소스로 전환하여 타 CSP에서도 사용 가능합니다.

 

Karpenter는 고성능의 지능형 쿠버네티스 스케일링 도구입니다. Karpenter는 CA와 다르게 Pending pods의 용량을 바탕으로 적합한 노드 사이즈를 선택합니다.

출처: https://www.youtube.com/watch?v=yMOaOlPvrgY&t=717s

 

또한 EC2 Fleet API로 인스턴스 생성을 요청하고, Watch API를 통해서 Pending Pod를 감시합니다.

출처: https://www.youtube.com/watch?v=jLuVZX6WQsw

 

요약하면, CA와 Karpenter에는 아래와 같은 차이점이 있습니다.

  • CA는 10초에 한번씩 Pending(unschedulable) pod 이벤트를 체크하는 반면, Karpenter는 Watch를 통해서 즉시 감지할 수 있습니다.
  • CA는 CA -> ASG -> EC2 Fleet API로 ASG라는 단계를 추가로 거치게 되는데 비해, Karpenter가 ASG에 의존하지 않고 즉시 EC2 Fleet API에 호출하여 속도가 빠른 점이 있습니다. (여러 노드 그룹에 Pending Pod가 발생한다면 CA는 이를 순차 처리하기 때문에 더 늦어 질 수 있습니다)
  • CA는 Pending pods의 용량에 비례해서 증가하기 보다는 노드 그룹에 지정된 용량으로 노드가 증가합니다. 이 때문에 right size로 노드가 생성된다고 보기 어렵습니다. 반면 Karpenter의 경우 Pending pods를 batch로 판단할 수 있고, 이들의 용량에 적합한 인스턴스 사이즈를 결정합니다.

 

아래와 같이 Karpenter의 동작 과정 이해할 수 있습니다. 요약하면 감지(watch) -> 평가 -> Fleet 요청으로 이뤄집니다.

출처: https://www.youtube.com/watch?v=jLuVZX6WQsw

 

 

실습을 진행하기 위해서 신규 EKS 클러스터를 생성하겠습니다.

# 변수 설정
export KARPENTER_NAMESPACE="kube-system"
export KARPENTER_VERSION="1.2.1"
export K8S_VERSION="1.32"
export AWS_PARTITION="aws" 
export CLUSTER_NAME="karpenter-demo" # ${USER}-karpenter-demo
export AWS_DEFAULT_REGION="ap-northeast-2"
export AWS_ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"
export TEMPOUT="$(mktemp)"
export ALIAS_VERSION="$(aws ssm get-parameter --name "/aws/service/eks/optimized-ami/${K8S_VERSION}/amazon-linux-2023/x86_64/standard/recommended/image_id" --query Parameter.Value | xargs aws ec2 describe-images --query 'Images[0].Name' --image-ids | sed -r 's/^.*(v[[:digit:]]+).*$/\1/')"

# 확인
echo "${KARPENTER_NAMESPACE}" "${KARPENTER_VERSION}" "${K8S_VERSION}" "${CLUSTER_NAME}" "${AWS_DEFAULT_REGION}" "${AWS_ACCOUNT_ID}" "${TEMPOUT}" "${ALIAS_VERSION}"

# CloudFormation 스택으로 IAM Policy/Role, SQS, Event/Rule 생성 : 3분 정도 소요
## IAM Policy : KarpenterControllerPolicy-gasida-karpenter-demo
## IAM Role : KarpenterNodeRole-gasida-karpenter-demo
curl -fsSL https://raw.githubusercontent.com/aws/karpenter-provider-aws/v"${KARPENTER_VERSION}"/website/content/en/preview/getting-started/getting-started-with-karpenter/cloudformation.yaml  > "${TEMPOUT}" \
&& aws cloudformation deploy \
  --stack-name "Karpenter-${CLUSTER_NAME}" \
  --template-file "${TEMPOUT}" \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides "ClusterName=${CLUSTER_NAME}"


# 클러스터 생성 : EKS 클러스터 생성 15분 정도 소요
eksctl create cluster -f - <<EOF
---
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: ${CLUSTER_NAME}
  region: ${AWS_DEFAULT_REGION}
  version: "${K8S_VERSION}"
  tags:
    karpenter.sh/discovery: ${CLUSTER_NAME}

iam:
  withOIDC: true
  podIdentityAssociations:
  - namespace: "${KARPENTER_NAMESPACE}"
    serviceAccountName: karpenter
    roleName: ${CLUSTER_NAME}-karpenter
    permissionPolicyARNs:
    - arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:policy/KarpenterControllerPolicy-${CLUSTER_NAME}

iamIdentityMappings:
- arn: "arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:role/KarpenterNodeRole-${CLUSTER_NAME}"
  username: system:node:{{EC2PrivateDNSName}}
  groups:
  - system:bootstrappers
  - system:nodes
  ## If you intend to run Windows workloads, the kube-proxy group should be specified.
  # For more information, see https://github.com/aws/karpenter/issues/5099.
  # - eks:kube-proxy-windows

managedNodeGroups:
- instanceType: m5.large
  amiFamily: AmazonLinux2023
  name: ${CLUSTER_NAME}-ng
  desiredCapacity: 2
  minSize: 1
  maxSize: 10
  iam:
    withAddonPolicies:
      externalDNS: true

addons:
- name: eks-pod-identity-agent
EOF


# eks 배포 확인
eksctl get cluster
NAME            REGION          EKSCTL CREATED
karpenter-demo  ap-northeast-2  True

eksctl get nodegroup --cluster $CLUSTER_NAME
CLUSTER         NODEGROUP               STATUS  CREATED                 MIN SIZE        MAX SIZE        DESIRED CAPACITY        INSTANCE TYPE   IMAGE ID                ASG NAME                                                   TYPE
karpenter-demo  karpenter-demo-ng       ACTIVE  2025-03-06T15:38:46Z    1               10              2                       m5.large        AL2023_x86_64_STANDARD  eks-karpenter-demo-ng-96cab60d-f4b7-28dd-a83d-8366de887a29 managed


# k8s 확인
kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone
NAME                                                STATUS   ROLES    AGE     VERSION               INSTANCE-TYPE   CAPACITYTYPE   ZONE
ip-192-168-33-227.ap-northeast-2.compute.internal   Ready    <none>   5m25s   v1.32.1-eks-5d632ec   m5.large        ON_DEMAND      ap-northeast-2a
ip-192-168-91-227.ap-northeast-2.compute.internal   Ready    <none>   5m25s   v1.32.1-eks-5d632ec   m5.large        ON_DEMAND      ap-northeast-2b

kubectl get po -A
NAMESPACE     NAME                              READY   STATUS    RESTARTS   AGE
kube-system   aws-node-9nppw                    2/2     Running   0          5m30s
kube-system   aws-node-x9ffn                    2/2     Running   0          5m30s
kube-system   coredns-844d8f59bb-j9jf9          1/1     Running   0          9m33s
kube-system   coredns-844d8f59bb-pqgpf          1/1     Running   0          9m33s
kube-system   eks-pod-identity-agent-bnshb      1/1     Running   0          5m30s
kube-system   eks-pod-identity-agent-f49wd      1/1     Running   0          5m30s
kube-system   kube-proxy-qqtss                  1/1     Running   0          5m29s
kube-system   kube-proxy-vk86h                  1/1     Running   0          5m30s
kube-system   metrics-server-74b6cb4f8f-dg8qk   1/1     Running   0          9m35s
kube-system   metrics-server-74b6cb4f8f-rkrhr   1/1     Running   0          9m35s

 

실습 과정에서 노드 생성을 확인하기 위해서 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 service.main.type=LoadBalancer --set env.TZ="Asia/Seoul" --namespace kube-system

# 접속
echo -e "http://$(kubectl get svc -n kube-system kube-ops-view -o jsonpath="{.status.loadBalancer.ingress[0].hostname}"):8080/#scale=1.5"

 

이제 Karpenter를 설치해 보겠습니다.

# Logout of helm registry to perform an unauthenticated pull against the public ECR
helm registry logout public.ecr.aws

# Karpenter 설치를 위한 변수 설정 및 확인
export CLUSTER_ENDPOINT="$(aws eks describe-cluster --name "${CLUSTER_NAME}" --query "cluster.endpoint" --output text)"
export KARPENTER_IAM_ROLE_ARN="arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:role/${CLUSTER_NAME}-karpenter"
echo "${CLUSTER_ENDPOINT} ${KARPENTER_IAM_ROLE_ARN}"

# karpenter 설치
helm upgrade --install karpenter oci://public.ecr.aws/karpenter/karpenter --version "${KARPENTER_VERSION}" --namespace "${KARPENTER_NAMESPACE}" --create-namespace \
  --set "settings.clusterName=${CLUSTER_NAME}" \
  --set "settings.interruptionQueue=${CLUSTER_NAME}" \
  --set controller.resources.requests.cpu=1 \
  --set controller.resources.requests.memory=1Gi \
  --set controller.resources.limits.cpu=1 \
  --set controller.resources.limits.memory=1Gi \
  --wait

# 확인
helm list -n kube-system
NAME            NAMESPACE       REVISION        UPDATED                                 STATUS          CHART                   APP VERSION
karpenter       kube-system     1               2025-03-07 00:48:49.238176978 +0900 KST deployed        karpenter-1.2.1         1.2.1
kube-ops-view   kube-system     1               2025-03-07 00:47:14.936078967 +0900 KST deployed        kube-ops-view-1.2.2     20.4.0

kubectl get pod -n $KARPENTER_NAMESPACE |grep karpenter
karpenter-5bdb74ddd6-kx7bq        1/1     Running   0          113s
karpenter-5bdb74ddd6-qpzvh        1/1     Running   0          113s

kubectl get crd | grep karpenter
ec2nodeclasses.karpenter.k8s.aws             2025-03-06T15:48:48Z
nodeclaims.karpenter.sh                      2025-03-06T15:48:48Z
nodepools.karpenter.sh                       2025-03-06T15:48:48Z

 

Nodepool과 EC2NodeClass를 생성합니다.

# 변수 확인
echo $ALIAS_VERSION
v20250228

# NodePool, EC2NodeClass 생성
cat <<EOF | envsubst | kubectl apply -f -
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: default
spec:
  template:
    spec:
      requirements:
        - key: kubernetes.io/arch
          operator: In
          values: ["amd64"]
        - key: kubernetes.io/os
          operator: In
          values: ["linux"]
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["on-demand"]
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ["c", "m", "r"]
        - key: karpenter.k8s.aws/instance-generation
          operator: Gt
          values: ["2"]
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
      expireAfter: 720h # 30 * 24h = 720h
  limits:
    cpu: 1000
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 1m
---
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: default
spec:
  role: "KarpenterNodeRole-${CLUSTER_NAME}" # replace with your cluster name
  amiSelectorTerms:
    - alias: "al2023@${ALIAS_VERSION}" # ex) al2023@latest
  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
EOF

# 확인 (nodeclaim은 없음)
kubectl get nodepool,ec2nodeclass,nodeclaims
NAME                            NODECLASS   NODES   READY   AGE
nodepool.karpenter.sh/default   default     0       True    12s

NAME                                     READY   AGE
ec2nodeclass.karpenter.k8s.aws/default   True    12s

 

여기서 NodePool과 NodeClass는 아래와 같은 의미를 가지고 있습니다.

  • NodePool: 노드 그룹의 구성과 동작 정의(노드의 선택 기준/바운더리 정의). 예를 들어, 인스턴스 유형, 용량 유형, 워커노드의 Spec에 대한 요구사항, 스케일링 정책, 노드 수명 주기 관리 -> 어떤 노드가 필요한 지 정의
  • NodeClass: EC2 인스턴스의 구체적인 설정. 예를 들어, 노드 이미지, 서브넷, 보안 그룹 설정, IAM 역할, 태그 -> 노드를 AWS에서 어떻게 생성할지 정의

 

이때 Karpenter는 NodeClaim라는 오브젝트를 통해 노드를 생성하고 관리합니다. Karpenter는 NodePool과 NodeClass를 모니터링하고, 새로운 파드의 요구사항이 기존 노드의 리소스나 조건과 맞지 않을 때, NodeClaim 생성하여 적절한 사양의 새로운 노드를 프로비저닝합니다. 결국 쿠버네티스에서 각 노드는 고유한 NodeClaim과 1:1로 맵핑됩니다.

이러한 절차를 아래 그림과 같이 확인할 수 있습니다. 

출처: https://karpenter.sh/docs/concepts/nodeclaims/

 

노드의 생성단계는 아래와 같이 진행됩니다.

참고: https://repost.aws/ko/articles/ARLmKuAa3FT9yMjdpq9krOTg/karpenter-%EB%A1%9C%EA%B7%B8%EB%A5%BC-%ED%86%B5%ED%95%B4-eks-worker-node%EC%9D%98-lifecycle-event%EB%A5%BC-%EC%B6%94%EC%B6%9C%ED%95%98%EA%B3%A0-%ED%95%9C%EB%88%88%EC%97%90-%ED%8C%8C%EC%95%85%ED%95%98%EB%8A%94-%EB%B0%A9%EC%95%88

  1. Create NodeClaim: Karpenter는 배포(Provisioning) 혹은 중단(Disrupting) 요구에 따라 새로운 NodeClaim을 생성합니다.
  2. Launch NodeClaim: AWS에 새로운 EC2 Instance를 생성하기 위해 CreateFleet API를 호출합니다.
  3. Register NodeClaim: EC2 Instance가 생성되고 Cluster에 등록된 Node를 NodeClaim과 연결합니다.
  4. Initialize NodeClaim: Node가 Ready 상태가 될 때까지 기다립니다.

 

Karpenter는 모든 단계별 작업이 완료된 후 작업의 세부 내용을 시스템 로그에 기록하며, 아래는 해당 로그의 예시입니다.

## Create NodeClaim
{"level":"INFO","time":"2024-12-31T09:50:28.720Z","logger":"controller","message":"created nodeclaim","commit":"0a85efb","controller":"provisioner","namespace":"","name":"","reconcileID":"63c2695c-4c54-4a9b-9b64-1804d9ddbb82","NodePool":{"name":"default"},"NodeClaim":{"name":"default-abcde"},"requests":{"cpu":"1516m","memory":"1187Mi","pods":"17"},"instance-types":"c4.large, c4.xlarge, c5.large, c5.xlarge, c5a.2xlarge and 55 other(s)"}

 

이제 테스트를 위해서 샘플 애플리케이션을 배포하겠습니다.

# pause 파드 1개에 CPU 1개 최소 보장 할당할 수 있게 디플로이먼트 배포 (현재는 replicas:0)
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: inflate
spec:
  replicas: 0
  selector:
    matchLabels:
      app: inflate
  template:
    metadata:
      labels:
        app: inflate
    spec:
      terminationGracePeriodSeconds: 0
      securityContext:
        runAsUser: 1000
        runAsGroup: 3000
        fsGroup: 2000
      containers:
      - name: inflate
        image: public.ecr.aws/eks-distro/kubernetes/pause:3.7
        resources:
          requests:
            cpu: 1
        securityContext:
          allowPrivilegeEscalation: false
EOF

# Scale up
kubectl scale deployment inflate --replicas 5; date
deployment.apps/inflate scaled
Fri Mar  7 00:56:59 KST 2025

 

Karpenter에 의해서 노드가 생성되는 과정을 추가로 확인해보겠습니다.

# karpenter 파드의 로그 확인
kubectl logs -f -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller
...
{"level":"INFO","time":"2025-03-06T15:57:00.326Z","logger":"controller","message":"found provisionable pod(s)","commit":"058c665","controller":"provisioner","namespace":"","name":"","reconcileID":"529ce301-0064-436f-9275-6020da23c7b5","Pods":"default/inflate-5c5f75666d-gbgst, default/inflate-5c5f75666d-p6zt9, default/inflate-5c5f75666d-85csz, default/inflate-5c5f75666d-fjkhh, default/inflate-5c5f75666d-pncp9","duration":"74.997844ms"}
# 파드에 적합한 nodeclaim을 위한 계산에 들어감
{"level":"INFO","time":"2025-03-06T15:57:00.326Z","logger":"controller","message":"computed new nodeclaim(s) to fit pod(s)","commit":"058c665","controller":"provisioner","namespace":"","name":"","reconcileID":"529ce301-0064-436f-9275-6020da23c7b5","nodeclaims":1,"pods":5}
# nodeclaim을 생성
{"level":"INFO","time":"2025-03-06T15:57:00.344Z","logger":"controller","message":"created nodeclaim","commit":"058c665","controller":"provisioner","namespace":"","name":"","reconcileID":"529ce301-0064-436f-9275-6020da23c7b5","NodePool":{"name":"default"},"NodeClaim":{"name":"default-n4xc5"},"requests":{"cpu":"5150m","pods":"8"},"instance-types":"c4.2xlarge, c4.4xlarge, c5.2xlarge, c5.4xlarge, c5a.2xlarge and 55 other(s)"}
# nodeclaim을 Lauch
{"level":"INFO","time":"2025-03-06T15:57:02.422Z","logger":"controller","message":"launched nodeclaim","commit":"058c665","controller":"nodeclaim.lifecycle","controllerGroup":"karpenter.sh","controllerKind":"NodeClaim","NodeClaim":{"name":"default-n4xc5"},"namespace":"","name":"default-n4xc5","reconcileID":"d273db5c-0284-4fd1-9246-6d68fcb0c06b","provider-id":"aws:///ap-northeast-2a/i-0068e4889e1e71961","instance-type":"c5a.2xlarge","zone":"ap-northeast-2a","capacity-type":"on-demand","allocatable":{"cpu":"7910m","ephemeral-storage":"17Gi","memory":"14162Mi","pods":"58","vpc.amazonaws.com/pod-eni":"38"}}
# nodeclaim을 Register
{"level":"INFO","time":"2025-03-06T15:57:21.500Z","logger":"controller","message":"registered nodeclaim","commit":"058c665","controller":"nodeclaim.lifecycle","controllerGroup":"karpenter.sh","controllerKind":"NodeClaim","NodeClaim":{"name":"default-n4xc5"},"namespace":"","name":"default-n4xc5","reconcileID":"e49f377e-7e6c-4969-8231-b3b2657bd624","provider-id":"aws:///ap-northeast-2a/i-0068e4889e1e71961","Node":{"name":"ip-192-168-149-58.ap-northeast-2.compute.internal"}}
# nodeclaim을 initilized
{"level":"INFO","time":"2025-03-06T15:57:31.030Z","logger":"controller","message":"initialized nodeclaim","commit":"058c665","controller":"nodeclaim.lifecycle","controllerGroup":"karpenter.sh","controllerKind":"NodeClaim","NodeClaim":{"name":"default-n4xc5"},"namespace":"","name":"default-n4xc5","reconcileID":"e3481aac-a971-49ab-b670-bd8c788faff7","provider-id":"aws:///ap-northeast-2a/i-0068e4889e1e71961","Node":{"name":"ip-192-168-149-58.ap-northeast-2.compute.internal"},"allocatable":{"cpu":"7910m","ephemeral-storage":"18181869946","hugepages-1Gi":"0","hugepages-2Mi":"0","memory":"15140112Ki","pods":"58"}}
..

# json으로 확인 가능
kubectl logs -f -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller | jq '.'

kubectl logs -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller | grep 'launched nodeclaim' | jq '.'
{
  "level": "INFO",
  "time": "2025-03-06T15:57:02.422Z",
  "logger": "controller",
  "message": "launched nodeclaim",
  "commit": "058c665",
  "controller": "nodeclaim.lifecycle",
  "controllerGroup": "karpenter.sh",
  "controllerKind": "NodeClaim",
  "NodeClaim": {
    "name": "default-n4xc5"
  },
  "namespace": "",
  "name": "default-n4xc5",
  "reconcileID": "d273db5c-0284-4fd1-9246-6d68fcb0c06b",
  "provider-id": "aws:///ap-northeast-2a/i-0068e4889e1e71961",
  "instance-type": "c5a.2xlarge",
  "zone": "ap-northeast-2a",
  "capacity-type": "on-demand",
  "allocatable": {
    "cpu": "7910m",
    "ephemeral-storage": "17Gi",
    "memory": "14162Mi",
    "pods": "58",
    "vpc.amazonaws.com/pod-eni": "38"
  }
}

# 노드 모니터링 
kubectl scale deployment inflate --replicas 5; date
deployment.apps/inflate scaled
Fri Mar  7 00:56:59 KST 2025

while true; do date; kubectl get node; echo "------------------------------" ; sleep 5; done
...
# 노드 생성 전
------------------------------
Fri Mar  7 00:57:16 KST 2025
NAME                                                STATUS   ROLES    AGE   VERSION
ip-192-168-33-227.ap-northeast-2.compute.internal   Ready    <none>   17m   v1.32.1-eks-5d632ec
ip-192-168-91-227.ap-northeast-2.compute.internal   Ready    <none>   17m   v1.32.1-eks-5d632ec
------------------------------
# 노드 추가: 24초
Fri Mar  7 00:57:23 KST 2025
NAME                                                STATUS     ROLES    AGE   VERSION
ip-192-168-149-58.ap-northeast-2.compute.internal   NotReady   <none>   4s    v1.32.1-eks-5d632ec
ip-192-168-33-227.ap-northeast-2.compute.internal   Ready      <none>   17m   v1.32.1-eks-5d632ec
ip-192-168-91-227.ap-northeast-2.compute.internal   Ready      <none>   17m   v1.32.1-eks-5d632ec
------------------------------
# 노드 Ready: 31초
Fri Mar  7 00:57:30 KST 2025
NAME                                                STATUS   ROLES    AGE   VERSION
ip-192-168-149-58.ap-northeast-2.compute.internal   Ready    <none>   11s   v1.32.1-eks-5d632ec
ip-192-168-33-227.ap-northeast-2.compute.internal   Ready    <none>   17m   v1.32.1-eks-5d632ec
ip-192-168-91-227.ap-northeast-2.compute.internal   Ready    <none>   17m   v1.32.1-eks-5d632ec


# nodeClaim이 생성된다.
kubectl get nodeclaims -w
NAME            TYPE   CAPACITY   ZONE   NODE   READY   AGE
default-n4xc5                                           0s
default-n4xc5                                           0s
default-n4xc5                                   Unknown   0s
default-n4xc5   c5a.2xlarge   on-demand   ap-northeast-2a          Unknown   2s
default-n4xc5   c5a.2xlarge   on-demand   ap-northeast-2a          Unknown   2s
default-n4xc5   c5a.2xlarge   on-demand   ap-northeast-2a   ip-192-168-149-58.ap-northeast-2.compute.internal   Unknown   21s
default-n4xc5   c5a.2xlarge   on-demand   ap-northeast-2a   ip-192-168-149-58.ap-northeast-2.compute.internal   Unknown   22s
default-n4xc5   c5a.2xlarge   on-demand   ap-northeast-2a   ip-192-168-149-58.ap-northeast-2.compute.internal   Unknown   30s
default-n4xc5   c5a.2xlarge   on-demand   ap-northeast-2a   ip-192-168-149-58.ap-northeast-2.compute.internal   True      31s
default-n4xc5   c5a.2xlarge   on-demand   ap-northeast-2a   ip-192-168-149-58.ap-northeast-2.compute.internal   True      36s


# nodeClaim 확인
kubectl describe nodeclaims
Name:         default-n4xc5
Namespace:
Labels:       karpenter.k8s.aws/ec2nodeclass=default
              karpenter.k8s.aws/instance-category=c
              karpenter.k8s.aws/instance-cpu=8
              karpenter.k8s.aws/instance-cpu-manufacturer=amd
              karpenter.k8s.aws/instance-cpu-sustained-clock-speed-mhz=3300
              karpenter.k8s.aws/instance-ebs-bandwidth=3170
              karpenter.k8s.aws/instance-encryption-in-transit-supported=true
              karpenter.k8s.aws/instance-family=c5a
              karpenter.k8s.aws/instance-generation=5
              karpenter.k8s.aws/instance-hypervisor=nitro
              karpenter.k8s.aws/instance-memory=16384
              karpenter.k8s.aws/instance-network-bandwidth=2500
              karpenter.k8s.aws/instance-size=2xlarge
              karpenter.sh/capacity-type=on-demand
              karpenter.sh/nodepool=default
              kubernetes.io/arch=amd64
              kubernetes.io/os=linux
              node.kubernetes.io/instance-type=c5a.2xlarge
              topology.k8s.aws/zone-id=apne2-az1
              topology.kubernetes.io/region=ap-northeast-2
              topology.kubernetes.io/zone=ap-northeast-2a
Annotations:  compatibility.karpenter.k8s.aws/cluster-name-tagged: true
              karpenter.k8s.aws/ec2nodeclass-hash: 15535182697325354914
              karpenter.k8s.aws/ec2nodeclass-hash-version: v4
              karpenter.k8s.aws/tagged: true
              karpenter.sh/nodepool-hash: 6821555240594823858
              karpenter.sh/nodepool-hash-version: v3
API Version:  karpenter.sh/v1
Kind:         NodeClaim
Metadata:
  Creation Timestamp:  2025-03-06T15:57:00Z
  Finalizers:
    karpenter.sh/termination
  Generate Name:  default-
  Generation:     1
  Owner References:
    API Version:           karpenter.sh/v1
    Block Owner Deletion:  true
    Kind:                  NodePool
    Name:                  default
    UID:                   9342267c-6f75-488c-b067-9005999e31ef
  Resource Version:        5525
  UID:                     3bd4c5ab-c393-4b28-bb46-96531f0d1fc8
Spec:
  Expire After:  720h
  Node Class Ref:
    Group:  karpenter.k8s.aws
    Kind:   EC2NodeClass
    Name:   default
  Requirements:
    Key:       karpenter.sh/nodepool
    Operator:  In
    Values:
      default
    Key:       node.kubernetes.io/instance-type
    Operator:  In
    Values:
      c4.2xlarge
      c4.4xlarge
      c5.2xlarge
      c5.4xlarge
      c5a.2xlarge
      c5a.4xlarge
      c5a.8xlarge
      c5d.2xlarge
      c5d.4xlarge
      c5n.2xlarge
      c5n.4xlarge
      c6i.2xlarge
      c6i.4xlarge
      c6id.2xlarge
      c6id.4xlarge
      c6in.2xlarge
      c6in.4xlarge
      c7i-flex.2xlarge
      c7i-flex.4xlarge
      c7i.2xlarge
      c7i.4xlarge
      m4.2xlarge
      m4.4xlarge
      m5.2xlarge
      m5.4xlarge
      m5a.2xlarge
      m5a.4xlarge
      m5ad.2xlarge
      m5ad.4xlarge
      m5d.2xlarge
      m5d.4xlarge
      m5zn.2xlarge
      m5zn.3xlarge
      m6i.2xlarge
      m6i.4xlarge
      m6id.2xlarge
      m6id.4xlarge
      m7i-flex.2xlarge
      m7i-flex.4xlarge
      m7i.2xlarge
      m7i.4xlarge
      r3.2xlarge
      r4.2xlarge
      r4.4xlarge
      r5.2xlarge
      r5.4xlarge
      r5a.2xlarge
      r5a.4xlarge
      r5ad.2xlarge
      r5ad.4xlarge
      r5b.2xlarge
      r5d.2xlarge
      r5d.4xlarge
      r5dn.2xlarge
      r5n.2xlarge
      r6i.2xlarge
      r6i.4xlarge
      r6id.2xlarge
      r7i.2xlarge
      r7i.4xlarge
    Key:       kubernetes.io/os
    Operator:  In
    Values:
      linux
    Key:       karpenter.sh/capacity-type
    Operator:  In
    Values:
      on-demand
    Key:       karpenter.k8s.aws/instance-category
    Operator:  In
    Values:
      c
      m
      r
    Key:       karpenter.k8s.aws/instance-generation
    Operator:  Gt
    Values:
      2
    Key:       kubernetes.io/arch
    Operator:  In
    Values:
      amd64
    Key:       karpenter.k8s.aws/ec2nodeclass
    Operator:  In
    Values:
      default
  Resources:
    Requests:
      Cpu:   5150m
      Pods:  8
Status:
  Allocatable:
    Cpu:                        7910m
    Ephemeral - Storage:        17Gi
    Memory:                     14162Mi
    Pods:                       58
    vpc.amazonaws.com/pod-eni:  38
  Capacity:
    Cpu:                        8
    Ephemeral - Storage:        20Gi
    Memory:                     15155Mi
    Pods:                       58
    vpc.amazonaws.com/pod-eni:  38
  Conditions:
    Last Transition Time:  2025-03-06T15:57:02Z
    Message:
    Observed Generation:   1
    Reason:                Launched
    Status:                True
    Type:                  Launched
    Last Transition Time:  2025-03-06T15:57:21Z
    Message:
    Observed Generation:   1
    Reason:                Registered
    Status:                True
    Type:                  Registered
    Last Transition Time:  2025-03-06T15:57:31Z
    Message:
    Observed Generation:   1
    Reason:                Initialized
    Status:                True
    Type:                  Initialized
    Last Transition Time:  2025-03-06T15:58:36Z
    Message:
    Observed Generation:   1
    Reason:                Consolidatable
    Status:                True
    Type:                  Consolidatable
    Last Transition Time:  2025-03-06T15:57:31Z
    Message:
    Observed Generation:   1
    Reason:                Ready
    Status:                True
    Type:                  Ready
  Image ID:                ami-089f1bf55c5291efd
  Last Pod Event Time:     2025-03-06T15:57:36Z
  Node Name:               ip-192-168-149-58.ap-northeast-2.compute.internal
  Provider ID:             aws:///ap-northeast-2a/i-0068e4889e1e71961
Events:
  Type    Reason             Age    From       Message
  ----    ------             ----   ----       -------
  Normal  Launched           4m35s  karpenter  Status condition transitioned, Type: Launched, Status: Unknown -> True, Reason: Launched
  Normal  DisruptionBlocked  4m31s  karpenter  Nodeclaim does not have an associated node
  Normal  Registered         4m16s  karpenter  Status condition transitioned, Type: Registered, Status: Unknown -> True, Reason: Registered
  Normal  Initialized        4m6s   karpenter  Status condition transitioned, Type: Initialized, Status: Unknown -> True, Reason: Initialized
  Normal  Ready              4m6s   karpenter  Status condition transitioned, Type: Ready, Status: Unknown -> True, Reason: Ready
  Normal  Unconsolidatable   3m     karpenter  Can't replace with a cheaper node

 

Karpenter는 노드 용량 추적을 위해 클러스터의 CloudProvider 머신과 CustomResources 간의 매핑을 만듭니다. 이 매핑이 일관되도록 하기 위해 Karpenter는 다음 태그 키를 활용합니다.

  • karpenter.sh/managed-by
  • karpenter.sh/nodepool
  • kubernetes.io/cluster/${CLUSTER_NAME}

 

Karpenter에 의해 등록된 노드에 추가 라벨이 등록된 것이 확인됩니다.

kubectl get node -l karpenter.sh/registered=true -o jsonpath="{.items[0].metadata.labels}" | jq '.'
...
  "karpenter.sh/initialized": "true",
  "karpenter.sh/nodepool": "default",
  "karpenter.sh/registered": "true",
...

 

생성된 노드 ip-192-168-149-58.ap-northeast-2.compute.interna는 기존 노드와 다른 c5a.2xlarge로 생성된 것을 확인할 수 있습니다.

kubectl get no
NAME                                                STATUS   ROLES    AGE   VERSION
ip-192-168-149-58.ap-northeast-2.compute.internal   Ready    <none>   11m   v1.32.1-eks-5d632ec
ip-192-168-33-227.ap-northeast-2.compute.internal   Ready    <none>   29m   v1.32.1-eks-5d632ec
ip-192-168-91-227.ap-northeast-2.compute.internal   Ready    <none>   29m   v1.32.1-eks-5d632ec

 

웹 콘솔에서 확인하였습니다.

 

Karpenter는 스케줄링이 필요한 모든 파드를 수용할 수 있는 하나의 노드를 생성하였고, 또한 CA에 비해서 더 빠른 프로비저닝 속도를 확인할 수 있었습니다.

  • Pending Pod 발생

  • 노드 생성 이후

 

Karpenter 실습을 마무리하고 리소스를 정리하겠습니다.

# Karpenter helm 삭제 
helm uninstall karpenter --namespace "${KARPENTER_NAMESPACE}"

# Karpenter IAM Role 등 생성한 CloudFormation 삭제
aws cloudformation delete-stack --stack-name "Karpenter-${CLUSTER_NAME}"

# EC2 Launch Template 삭제
aws ec2 describe-launch-templates --filters "Name=tag:karpenter.k8s.aws/cluster,Values=${CLUSTER_NAME}" |
    jq -r ".LaunchTemplates[].LaunchTemplateName" |
    xargs -I{} aws ec2 delete-launch-template --launch-template-name {}

# 클러스터 삭제
eksctl delete cluster --name "${CLUSTER_NAME}"

 

참고로 디플로이먼트를 스케일링 다운해 Karpenter에 의해 생성된 노드가 삭제된 이후 클러스터를 삭제하셔야 합니다.

바로 클러스터를 삭제하니 Karpenter에 의해 생성된 노드는 삭제되지 않고 EC2 인스턴스에 남아 있는 현상을 발견했습니다. 아무래도 Karpenter에서 생성된 노드이다보니, EKS가 직접 관리하는 리소스로 정리가 되지 않는 것으로 보입니다. 먼저 스케일링 다운으로  Karpenter에 의해 생성된 노드가 삭제된 후 클러스터 삭제를 진행을 하셔야 합니다.

 

추가로 클러스터 삭제 이후에도, CloudFormation 생성한 Karpenter IAM Role이 삭제안될 경우 AWS CloudFormation 관리 콘솔에서 직접 삭제하시기 바랍니다.

 

 

3. AKS의 오토스케일링

AKS에서도 EKS의 오토스케일링 옵션에 대응하는 솔루션을 제공하고 있습니다.

앞서 살펴본바와 같이 EKS의 오토스케일링 옵션은 사용자가 직접 해당 컴포넌트를 설치하는 방식으로 제공되고 있습니다.

 

AKS에서는 오토스케일링 옵션을 애드온 혹은 기능으로 제공하고 있기 때문에 클러스터 생성 시점에 필요한 옵션을 사용하면 해당 기능을 사용할 수 있습니다(혹은 설치된 클러스터에 기능을 활성화할 수 있음).

 

앞서 살펴본 바와 같이 HPA는 쿠버네티스 환경에서 기본으로 제공되기 때문에 어떤 환경에 있는 쿠버네티스에서도 사용이 가능합니다. 그 외 AKS에서 제공하는 나머지 오토스케일링 기능에 대해 아래와 같습니다.

 

KEDA: add-on으로 제공

https://learn.microsoft.com/en-us/azure/aks/keda-about

클러스터 옵션의 --enable-keda 옵션을 통해서 활성화 할 수 있습니다.

az aks create --resource-group myResourceGroup --name myAKSCluster --enable-keda --generate-ssh-keys

 

VPA: --enable-vpa 옵션

https://learn.microsoft.com/en-us/azure/aks/use-vertical-pod-autoscaler

클러스터 옵션의 --enable-vpa 옵션을 통해서 VPA 기능을 활성화 할 수 있습니다.

az aks create --resource-group myResourceGroup --name myAKSCluster --enable-keda --generate-ssh-keys

 

Cluster Autoscaler: --enable-cluster-autoscaler 옵션

https://learn.microsoft.com/en-us/azure/aks/cluster-autoscaler?tabs=azure-cli

클러스터 옵션의 --enable-cluster-autoscaler 으로 활성화할 수 있으며, --min-count--max-count으로 최소/최대 값을 지정할 수 있습니다.

az aks create --resource-group myResourceGroup --name myAKSCluster --node-count 1 --vm-set-type VirtualMachineScaleSets --load-balancer-sku standard --enable-cluster-autoscaler --min-count 1 --max-count 3 --generate-ssh-keys

 

추가로 CA의 scan interval, expander와 같은 옵션을 cluster autoscaler profile로 정의할 수 있습니다.

아래 문서를 통해 지원 가능한 옵션을 살펴보실 수 있습니다.

https://learn.microsoft.com/en-us/azure/aks/cluster-autoscaler?tabs=azure-cli#use-the-cluster-autoscaler-profile

 

Karpenter: NAP(Node Autoprovisioning)으로 활성화

https://learn.microsoft.com/en-us/azure/aks/node-autoprovision?tabs=azure-cli

클러스터 옵션의 --node-provisioning-mode Auto 사용하여 Node Autoprovisioning 을 활성화 할 수 있습니다. NAP는 2025년 03월 기준 Preview 상태입니다.

az aks create --name $CLUSTER_NAME --resource-group $RESOURCE_GROUP_NAME --node-provisioning-mode Auto --network-plugin azure --network-plugin-mode overlay --network-dataplane cilium --generate-ssh-keys

 

 

4. 오토스케일링에 대한 주의사항

이러한 오토스케일링을 CSP에서 사용할 때는 아래와 같은 일반적인 주의사항이 있습니다.

 

  • 파드 스케일링에 사용되는 VPA, HPA를 동시에 사용하는 것은 권장되지 않습니다.
  • VPA로 신규로 생성된 파드는 사용 가능한 리소스를 초과할 수 있고 파드를 Pending 상태로 만들 수 있습니다. 이 때문에 VPA는 CA와 함께 사용해야 할 수 있습니다. (혹은 VPA를 off 모드로 사용하고 적합한 사이징을 위해서 사용하실 수도 있습니다)
  • CA와 Karpenter를 동시에 사용하지 말아야 합니다.
  • CA를 가상 머신 스케일링 메커니즘(예를 들어, CPU 사용량에 따른 가상머신 스케일링을 설정)과 동시에 설정하지 말아야합니다. 이는 의도치 않은 결과를 만들어 낼 수 있습니다.
  • 노드 스케일링 옵션에서 Scale down은 의도치 않은 파드의 eviction을 발생시킬 수 있으므로, 필요한 경우 파드 내 annotation으로 evict를 하지 않도록 설정하거나(cluster-autoscaler.kubernetes.io/safe-to-evict="false"), 혹은 PDB(Pod Distruption Budget)으로 안정적인 eviction을 유도할 수 있습니다.
  • 빈번한 Scale up/down이 발생하는 경우 오히려 애플리케이션의 안전성이 무너질 수 있으므로 모니터링을 통해 리소스 사용을 안정화 할 필요가 있습니다. 혹은 Scale up/down에 조정 시간을 주는 옵션을 검토해야 합니다.
  • 노드 스케일링으로 API 요청이 빈번하게 발생하는 경우 API throttling이 발생할 수 있고, 이 경우 요청이 정상 처리 되지 않을 수 있는 점도 유의하실 필요가 있습니다.

 

마무리

금번 포스트에서는 EKS의 오토스케일링 옵션을 살펴보고 AKS와 비교해 보았습니다.

 

EKS의 특성이 기본 구성이 최소화된 점과, 한편으로 사용자에게 자율성을 주는 것으로도 이해할 수 있습니다. 오토스케일링 또한 사용자가 직접 컴포넌트를 구성해야 하는데, 이러한 과정에서 사용자가 설치를 제대로 하지 못하거나 정확한 기능을 이해하지 못하는 경우 오토스케일링이 제대로 동작하지 않을 수 있습니다. 또한 해당 컴포넌트의 업그레이드도 사용자의 몫입니다.

 

반면 관련 컴포넌트를 사용자 데이터 플레인에 위치 시키므로 해당 컴포넌트의 동작을 이해하고, 이슈를 직접 트러블 슈팅할 수 있습니다. 또한 오픈소스 컴포넌트를 그대로 사용하기 때문에 다양한 옵션을 활용할 수 있습니다. 이러한 측면에서 EKS의 환경은 가볍지만 상당 부분을 고객이 직접 구성하므로 고급 사용자에게 적합하지 않은가라는 생각도 들기는 합니다.

 

AKS는 오토스케일링 옵션을 Managed Service의 일부로 제공합니다. 클러스터에서 VPA, CAS, KEDA를 활성화 하는 옵션을 제공하고 있으며, 최근 Karpenter를 NAP(Node Auto Provisioning)라는 이름으로 Preview로 제공하고 있습니다.

이로써 해당 기능에 대한 개념을 이해하는 일반 사용자 또한 쉽게 애드온으로 기능을 사용할 수 있으며, 애드온으로 제공된다는 것은 해당 컴포넌트의 라이프사이클을 AKS에서 직접 관리해주기 때문에 관리 편의성이 높습니다.

 

다만 해당 구성에서 제공되는 옵션 또한 검증된 부분만 제공하기 때문에 오픈 소스의 모든 옵션을 제공하지 않을 수 있으므로 Limitation을 확인하셔야 합니다. 또한 컴포넌트들이 컨트롤 플레인 영역에 배치되어 직접 트러블 슈팅을 하는데 제한이 있을 수 있습니다. 한편 Managed Service로 기능이 제공되기 때문에 옵션 추가 등에서 오픈 소스의 기능을 빠르게 따라가지 못하는 점도 아쉬운 점으로 남을 수 있습니다.

 

다른 측면으로 한가지를 언급 드릴 부분은, EKS를 기능적으로 지원하는 컴포넌트들은 상당한 부분이 커스터마이즈(옵션 변경, 삭제 등)가 가능합니다. 반대로 AKS의 시스템 컴포넌트는 addon Manager에 의해서 관리되어, 이러한 컴포넌트 혹은 Configmap을 살펴보면 addonmanager.kubernetes.io/mode=Reconcile 로 레이블이 지정되어 있습니다. 이는 addon manager에 의해 정기적으로 조정(reconcile)되는 리소스이기 때문에 사용자가 임의로 변경해도 다시 원복 됩니다. 즉, AKS는 허용된 방식으로만 시스템 컴포넌트를 제어할 수 있습니다. 일반적으로 매니지드 영역에 대한 수정은 권장하지 않고 있습니다.

 

[Note]
Addon manager에 대해서 아래의 문서를 참고 부탁드립니다.
https://github.com/kubernetes/kubernetes/blob/master/cluster/addons/addon-manager/README.md#addon-manager

 

 

그럼 이번 포스트를 마무리 하도록 하겠습니다. 

다음 포스트에서는 EKS의 보안에 대해서 학습한 내용을 작성해 보겠습니다.

'EKS' 카테고리의 다른 글

[7] EKS Fargate  (0) 2025.03.23
[6] EKS의 Security - EKS 인증/인가와 Pod IAM 권한 할당  (0) 2025.03.16
[5-1] EKS의 오토스케일링 Part1  (0) 2025.03.07
[4] EKS의 모니터링과 로깅  (0) 2025.03.01
[3-2] EKS 노드 그룹  (0) 2025.02.23

이번 포스트에서는 EKS의 오토스케일링(Autoscaling) 옵션을 살펴보겠습니다.

 

기본적인 쿠버네티스 환경의 스케일링 옵션을 전반적으로 살펴보겠습니다. 이후 EKS의 오토스케일링 옵션을 살펴보고, 이를 실습을 통해 확인 해보도록 하겠습니다. 마지막으로 AKS의 오토스케일링 옵션을 EKS와 비교해 보겠습니다.

 

글을 작성하는 과정에서 분량이 너무 길어져, Part1에서는 HPA, KEDA, VPA까지의 내용을 다루고 Part2에서 Cluster Autoscaler 부터 이어서 설명하도록 하겠습니다.

 

목차

EKS의 오토스케일링 Part1

  1. 쿠버네티스 환경의 스케일링
  2. EKS의 오토스케일링 개요
  3. 실습 환경 생성
  4. HPA(Horizontal Pod Autoscaler)
  5. KEDA(Kubernetes Event-driven Autoscaler)
  6. VPA(Vertical Pod Autoscaler)

EKS의 오토스케일링 Part2 (https://a-person.tistory.com/39)

  1. CA(Cluster Autoscaler)
  2. Karpenter
  3. AKS의 오토스케일링
  4. 오토스케일링에 대한 주의사항

 

1. 쿠버네티스 환경의 스케일링

쿠버네티스 패턴(책만, 2020)에서는 쿠버네티스 환경의 애플리케이션의 스케일링 레벨을 아래와 같이 설명하고 있습니다.

출처: 빌긴 이브리암/롤란트 후스, 안승규/서한배, 책만, 2020, 272.

 

먼저 애플리케이션 튜닝이 필요합니다. 쿠버네티스 환경이라고 할지라도 애플리케이션 자체가 최대한의 성능을 사용하도록 동작해야 합니다. 애플리케이션에 할당된 리소스를 증가시키거나 복제본을 증가시키는 것은 부차적인 일입니다.

 

두번째는 수직 파드 오토스케일러(VPA, Vertical Pod Autoscaler)입니다. 파드의 리소스가 부족할 때 설정된 리소스(CPU/MEM) 자체를 증가시키는 방식입니다. 쿠버네티스 환경에서는 request와 limit을 지정할 수 있고, 이 값이 변경됩니다.

 

세번째는 수평 파드 오토스케일러(HPA, Horizontal Pod Autoscaler)입니다. 파드의 리소스가 임계치 이상 사용되면, 동일한 파드의 복제본을 증가시키는 방식입니다.

 

네번째는 VPA나 HPA로 애플리케이션이 용량이나 갯수가 증가했을 때, 노드의 할당 가능한 자원(Allocatable resource)을 모두 소진하면 파드가 스케줄링 불가능(Unschedurable Pod)한 상황이 발생할 수 있습니다. 이때는 노드 자체를 증가시켜야 합니다. 이것을 클러스터 오토스케일러(CA, Cluster Autoscaler)에서 지원합니다.

 

쿠버네티스 환경은 애플리케이션의 스케일링을 하기 위해서 위와 같은 기법을 사용할 수 있으며, 이후 EKS에서는 이들을 어떤식으로 활용할 수 있는지 살펴보겠습니다.

 

일반적으로 애플리케이션 튜닝은 쿠버네티스에 국한되지 않은 별개의 영역이므로 설명에 제외하도록 하겠습니다.

 

 

2. EKS의 오토스케일링 개요

EKS에서는 설명한 HPA, VPA, CA를 모두 지원하고 있으며, Karpenter이라는 노드 오토스케일링을 방식을 추가로 제공하고 있습니다. 또한 그림에는 없지만 KEDA를 통해서 이벤트를 통해 HPA를 확장할 수 있는 방식도 있습니다.

출처: https://www.youtube.com/watch?v=jLuVZX6WQsw

 

HPA는 쿠버네티스 환경에서 기본으로 제공되기 때문에 별도의 설치 과정 없이 hpa 오브젝트를 생성하여 사용할 수 있습니다.

 

EKS에서는 KEDA, VPA, CA, Karpenter는 helm이나 yaml을 배포하는 방식으로 사용자가 설치 및 구성해야 합니다. 애드온과 같은 방식이 아니라 오픈 소스를 배포하므로 직접 라이프사이클을 관리하며, 제공되는 모든 옵션을 활용할 수 있습니다.

 

컨트롤러에 해당하는 컴포넌트가 데이터 플레인에 직접 배포되기 때문에 동작 과정을 이해할 수 있습니다. 다만 이 또한 데이터 플레인의 리소스를 사용한다는 점과 사용자 책임의 관리가 필요한 점을 유의해야 합니다.

 

참고로 AKS의 KEDA, VPA, CA, Karpenter는 애드온이나 기능으로 제공되기 때문에 Managed의 영역으로 이해할 수 있습니다.

 

 

3. 실습 환경 생성

실습 환경은 아래와 같습니다.

 

CloudFormation을 바탕으로 실습 환경을 구성하도록 하겠습니다.

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

# 변수 지정
CLUSTER_NAME=myeks
SSHKEYNAME=<SSH 키 페이 이름>
MYACCESSKEY=<IAM Uesr 액세스 키>
MYSECRETKEY=<IAM Uesr 시크릿 키>

# CloudFormation 스택 배포
aws cloudformation deploy --template-file myeks-5week.yaml --stack-name $CLUSTER_NAME --parameter-overrides KeyName=$SSHKEYNAME SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32  MyIamUserAccessKeyID=$MYACCESSKEY MyIamUserSecretAccessKey=$MYSECRETKEY ClusterBaseName=$CLUSTER_NAME --region ap-northeast-2

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

# EC2 접속
ssh -i ~/.ssh/ekskey.pem ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)

 

CloudFormation에서 운영서버를 배포하고, 이후 EKS 까지 배포를 하게 되어 있습니다. 대략 15~20분 가량이 소요됩니다.

그리고 생성된 EKS를 확인하고 kubeconfig을 받습니다.

# 변수 지정
CLUSTER_NAME=myeks

# 클러스터 확인
eksctl get cluster

# kubeconfig 생성
aws sts get-caller-identity --query Arn
aws eks update-kubeconfig --name $CLUSTER_NAME --user-alias <위 출력된 자격증명 사용자>

# 클러스터 기본 확인
kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone
kubectl get pod -A
NAME                                               STATUS   ROLES    AGE     VERSION               INSTANCE-TYPE   CAPACITYTYPE   ZONE
ip-192-168-1-87.ap-northeast-2.compute.internal    Ready    <none>   3m12s   v1.31.5-eks-5d632ec   t3.medium       ON_DEMAND      ap-northeast-2a
ip-192-168-2-195.ap-northeast-2.compute.internal   Ready    <none>   3m9s    v1.31.5-eks-5d632ec   t3.medium       ON_DEMAND      ap-northeast-2b
ip-192-168-3-136.ap-northeast-2.compute.internal   Ready    <none>   3m8s    v1.31.5-eks-5d632ec   t3.medium       ON_DEMAND      ap-northeast-2c

kubectl get pod -A
NAMESPACE     NAME                                  READY   STATUS    RESTARTS   AGE
kube-system   aws-node-b7vsh                        2/2     Running   0          3m16s
kube-system   aws-node-fnrj8                        2/2     Running   0          3m13s
kube-system   aws-node-ltkxn                        2/2     Running   0          3m12s
kube-system   coredns-86f5954566-96j8r              1/1     Running   0          9m10s
kube-system   coredns-86f5954566-j6dvp              1/1     Running   0          9m10s
kube-system   ebs-csi-controller-549bf6879f-6h7jg   6/6     Running   0          49s
kube-system   ebs-csi-controller-549bf6879f-p2gml   6/6     Running   0          49s
kube-system   ebs-csi-node-bm94h                    3/3     Running   0          49s
kube-system   ebs-csi-node-h5ntq                    3/3     Running   0          49s
kube-system   ebs-csi-node-n7s2s                    3/3     Running   0          49s
kube-system   kube-proxy-brf4v                      1/1     Running   0          3m16s
kube-system   kube-proxy-x7sbw                      1/1     Running   0          3m13s
kube-system   kube-proxy-zm6ht                      1/1     Running   0          3m12s
kube-system   metrics-server-6bf5998d9c-bzxn6       1/1     Running   0          9m9s
kube-system   metrics-server-6bf5998d9c-zs5mg       1/1     Running   0          9m10s

 

그리고 이후 실습에 활용하기 위한 일부 컴포넌트들을 설치합니다.

# 환경 변수
CERT_ARN=$(aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text)
MyDomain=aperson.link # 각자 자신의 도메인 이름 입력
MyDnzHostedZoneId=$(aws route53 list-hosted-zones-by-name --dns-name "$MyDomain." --query "HostedZones[0].Id" --output text)

# AWS LoadBalancerController
helm repo add eks https://aws.github.io/eks-charts
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
echo $MyDomain
curl -s https://raw.githubusercontent.com/gasida/PKOS/main/aews/externaldns.yaml | MyDomain=$MyDomain MyDnzHostedZoneId=$MyDnzHostedZoneId envsubst | 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 service.main.type=ClusterIP  --set env.TZ="Asia/Seoul" --namespace kube-system

# kubeopsview 용 Ingress 설정 : group 설정으로 1대의 ALB를 여러개의 ingress 에서 공용 사용
echo $CERT_ARN
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
    alb.ingress.kubernetes.io/group.name: study
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
    alb.ingress.kubernetes.io/load-balancer-name: $CLUSTER_NAME-ingress-alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/ssl-redirect: "443"
    alb.ingress.kubernetes.io/success-codes: 200-399
    alb.ingress.kubernetes.io/target-type: ip
  labels:
    app.kubernetes.io/name: kubeopsview
  name: kubeopsview
  namespace: kube-system
spec:
  ingressClassName: alb
  rules:
  - host: kubeopsview.$MyDomain
    http:
      paths:
      - backend:
          service:
            name: kube-ops-view
            port:
              number: 8080  # name: http
        path: /
        pathType: Prefix
EOF

 

Prometheus와 Grafana도 설치를 진행합니다.

# repo 추가
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts

# 파라미터 파일 생성 : PV/PVC(AWS EBS) 삭제에 불편하여 PV/PVC 미사용 하도록 수정
cat <<EOT > monitor-values.yaml
prometheus:
  prometheusSpec:
    scrapeInterval: "15s"
    evaluationInterval: "15s"
    podMonitorSelectorNilUsesHelmValues: false
    serviceMonitorSelectorNilUsesHelmValues: false
    retention: 5d
    retentionSize: "10GiB"

  # Enable vertical pod autoscaler support for prometheus-operator
  verticalPodAutoscaler:
    enabled: true

  ingress:
    enabled: true
    ingressClassName: alb
    hosts: 
      - prometheus.$MyDomain
    paths: 
      - /*
    annotations:
      alb.ingress.kubernetes.io/scheme: internet-facing
      alb.ingress.kubernetes.io/target-type: ip
      alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
      alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
      alb.ingress.kubernetes.io/success-codes: 200-399
      alb.ingress.kubernetes.io/load-balancer-name: myeks-ingress-alb
      alb.ingress.kubernetes.io/group.name: study
      alb.ingress.kubernetes.io/ssl-redirect: '443'

grafana:
  defaultDashboardsTimezone: Asia/Seoul
  adminPassword: xxx # Grafana 패스워드
  defaultDashboardsEnabled: false

  ingress:
    enabled: true
    ingressClassName: alb
    hosts: 
      - grafana.$MyDomain
    paths: 
      - /*
    annotations:
      alb.ingress.kubernetes.io/scheme: internet-facing
      alb.ingress.kubernetes.io/target-type: ip
      alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
      alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
      alb.ingress.kubernetes.io/success-codes: 200-399
      alb.ingress.kubernetes.io/load-balancer-name: myeks-ingress-alb
      alb.ingress.kubernetes.io/group.name: study
      alb.ingress.kubernetes.io/ssl-redirect: '443'

kube-state-metrics:
  rbac:
    extraRules:
      - apiGroups: ["autoscaling.k8s.io"]
        resources: ["verticalpodautoscalers"]
        verbs: ["list", "watch"]
  customResourceState:
    enabled: true
    config:
      kind: CustomResourceStateMetrics
      spec:
        resources:
          - groupVersionKind:
              group: autoscaling.k8s.io
              kind: "VerticalPodAutoscaler"
              version: "v1"
            labelsFromPath:
              verticalpodautoscaler: [metadata, name]
              namespace: [metadata, namespace]
              target_api_version: [apiVersion]
              target_kind: [spec, targetRef, kind]
              target_name: [spec, targetRef, name]
            metrics:
              - name: "vpa_containerrecommendations_target"
                help: "VPA container recommendations for memory."
                each:
                  type: Gauge
                  gauge:
                    path: [status, recommendation, containerRecommendations]
                    valueFrom: [target, memory]
                    labelsFromPath:
                      container: [containerName]
                commonLabels:
                  resource: "memory"
                  unit: "byte"
              - name: "vpa_containerrecommendations_target"
                help: "VPA container recommendations for cpu."
                each:
                  type: Gauge
                  gauge:
                    path: [status, recommendation, containerRecommendations]
                    valueFrom: [target, cpu]
                    labelsFromPath:
                      container: [containerName]
                commonLabels:
                  resource: "cpu"
                  unit: "core"
  selfMonitor:
    enabled: true

alertmanager:
  enabled: false
defaultRules:
  create: false
kubeControllerManager:
  enabled: false
kubeEtcd:
  enabled: false
kubeScheduler:
  enabled: false
prometheus-windows-exporter:
  prometheus:
    monitor:
      enabled: false
EOT
cat monitor-values.yaml

# helm 배포
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack --version 69.3.1 \
-f monitor-values.yaml --create-namespace --namespace monitoring

# helm 확인
helm get values -n monitoring kube-prometheus-stack

# PV 사용하지 않음
kubectl get pv,pvc -A

# 프로메테우스 웹 접속
echo -e "https://prometheus.$MyDomain"

# 그라파나 웹 접속
echo -e "https://grafana.$MyDomain"

# TargetGroup binding 확인
kubectl get targetgroupbindings.elbv2.k8s.aws -A
NAMESPACE     NAME                               SERVICE-NAME                       SERVICE-PORT   TARGET-TYPE   AGE
kube-system   k8s-kubesyst-kubeopsv-b2ecfd420f   kube-ops-view                      8080           ip            2m54s
monitoring    k8s-monitori-kubeprom-40399c957e   kube-prometheus-stack-grafana      80             ip            45s
monitoring    k8s-monitori-kubeprom-826f25cbb8   kube-prometheus-stack-prometheus   9090           ip            45s

 

 

4. HPA(Horizontal Pod Autoscaler)

HPA는 지정된 워크로드의 특정 메트릭이 임계치를 초과하는 경우 복제본(Replicas)를 증가시킵니다.

출처: https://www.youtube.com/watch?v=jLuVZX6WQsw

 

HPA는 쿠버네티스 환경에서 기본으로 제공되기 때문에 어떤 환경에 있는 쿠버네티스에서도 사용이 가능합니다. 주의할 점은 HPA가 metrics-server에서 제공되는 core system metrics에 의해서 판단하게 되므로, metrics-server가 정상적이지 않으면 HPA가 동작하지 않는 점만 기억하시면 됩니다.

 

HPA는 EKS 특화된 기술이 아니기 때문에 EKS 환경의 실습은 진행하지 않으며, 다음 절에서 KEDA를 통해 HPA를 사용하는 사례를 통해 살펴보도록 하겠습니다.

 

HPA 실습

HPA에 대한 실습은 쿠버네티스 공식 문서를 참고하실 수 있습니다.

https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/

 

1) 샘플 애플리케이션

apiVersion: apps/v1
kind: Deployment
metadata:
  name: php-apache
spec:
  selector:
    matchLabels:
      run: php-apache
  template:
    metadata:
      labels:
        run: php-apache
    spec:
      containers:
      - name: php-apache
        image: registry.k8s.io/hpa-example
        ports:
        - containerPort: 80
        resources:
          limits:
            cpu: 500m
          requests:
            cpu: 200m
---
apiVersion: v1
kind: Service
metadata:
  name: php-apache
  labels:
    run: php-apache
spec:
  ports:
  - port: 80
  selector:
    run: php-apache

 

2) HPA 정의

kubectl autoscale deployment php-apache --cpu-percent=50 --min=1 --max=10

 

3) Load 발생

# Run this in a separate terminal
# so that the load generation continues and you can carry on with the rest of the steps
kubectl run -i --tty load-generator --rm --image=busybox:1.28 --restart=Never -- /bin/sh -c "while sleep 0.01; do wget -q -O- http://php-apache; done"

 

이후 kubectl get hpa php-apache --watch를 통해 메트릭의 증가와 복제본의 증가를 확인하실 수 있습니다.

 

HPA의 메트릭 확장

HPA는 Metric API를 통해서 값을 수집하는데, 이는 쿠버네티스에서 기본 제공되는 API가 아니며, 이를 노출하기 위한 metrics-server가 필요합니다. 또한 HPA는 이미 정의된 Resource Metric에 의해서 스케일링을 판단하는데 이는 metrics-server에 의해서 기본적으로 제공됩니다.

 

이러한 Metrics API를 확장하기 위해서 Custom Metric, External Metric을 사용할 수 있습니다. 보통 Prometheus를 통해서 추가 메트릭을 수집하고, Prometheus에서 수집된 메트릭을 기반으로 Prometheus Adapter이 Custom Metric API Server의 역할을 해 Custom Metric와 External Metric을 노출합니다.

 

출처: https://itnext.io/autoscaling-apps-on-kubernetes-with-the-horizontal-pod-autoscaler-798750ab7847

 

이후 살펴볼 KEDA 또한 자체적으로 Metrics API Server를 가지고 External Metircs를 노출하게 됩니다.

 

 

5. KEDA(Kubernetes Event-driven Autoscaler)

HPA는 metrics-server에 의해서 수집된 CPU, Memory와 같은 메트릭을 기반으로 스케일링을 결정합니다. 이러한 리소스 기반이 아닌 다른 메트릭을 참조하여 HPA를 동작하도 도와주는 컴포넌트가 KEDA(Kubernetes Event-driven Autoscaler)입니다.

KEDA는 다양한 Event Source로 부터 발생하는 이벤트를 기반으로 스케일링 여부를 결정할 수 있습니다.

출처: https://www.youtube.com/watch?v=jLuVZX6WQsw

 

EKS에서는 helm을 통해서 KEDA를 설치할 수 있습니다. (해당 실습에서 prometheus를 통해 모니터링을 하는 부분이 포함되어 있어, 사전 Prometheus가 설치되어 있어야 합니다)

# 설치 전 기존 metrics-server 제공 Metris API 확인
kubectl get --raw "/apis/metrics.k8s.io" | jq
{
  "kind": "APIGroup",
  "apiVersion": "v1",
  "name": "metrics.k8s.io",
  "versions": [
    {
      "groupVersion": "metrics.k8s.io/v1beta1",
      "version": "v1beta1"
    }
  ],
  "preferredVersion": {
    "groupVersion": "metrics.k8s.io/v1beta1",
    "version": "v1beta1"
  }
}

# external metrics는 없음
kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1" | jq
Error from server (NotFound): the server could not find the requested resource


# KEDA 설치
cat <<EOT > keda-values.yaml
metricsServer:
  useHostNetwork: true

prometheus:
  metricServer:
    enabled: true
    port: 9022
    portName: metrics
    path: /metrics
    serviceMonitor:
      # Enables ServiceMonitor creation for the Prometheus Operator
      enabled: true
    podMonitor:
      # Enables PodMonitor creation for the Prometheus Operator
      enabled: true
  operator:
    enabled: true
    port: 8080
    serviceMonitor:
      # Enables ServiceMonitor creation for the Prometheus Operator
      enabled: true
    podMonitor:
      # Enables PodMonitor creation for the Prometheus Operator
      enabled: true
  webhooks:
    enabled: true
    port: 8020
    serviceMonitor:
      # Enables ServiceMonitor creation for the Prometheus webhooks
      enabled: true
EOT

helm repo add kedacore https://kedacore.github.io/charts
helm repo update
helm install keda kedacore/keda --version 2.16.0 --namespace keda --create-namespace -f keda-values.yaml

# apiservice가 생성된 것을 알 수 있습니다.
kubectl get apiservice v1beta1.external.metrics.k8s.io -o yaml
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
  annotations:
    meta.helm.sh/release-name: keda
    meta.helm.sh/release-namespace: keda
  creationTimestamp: "2025-03-06T13:23:54Z"
  labels:
    app.kubernetes.io/component: operator
    app.kubernetes.io/instance: keda
    app.kubernetes.io/managed-by: Helm
    app.kubernetes.io/name: v1beta1.external.metrics.k8s.io
    app.kubernetes.io/part-of: keda-operator
    app.kubernetes.io/version: 2.16.0
    helm.sh/chart: keda-2.16.0
  name: v1beta1.external.metrics.k8s.io
  resourceVersion: "7353"
  uid: 26d3a3b1-7487-4086-84f9-1fd3105aa89d
spec:
  caBundle: <생략>
  group: external.metrics.k8s.io
  groupPriorityMinimum: 100
  service:
    name: keda-operator-metrics-apiserver
    namespace: keda
    port: 443
  version: v1beta1
  versionPriority: 100
status:
  conditions:
  - lastTransitionTime: "2025-03-06T13:24:25Z"
    message: all checks passed
    reason: Passed
    status: "True"
    type: Available

# 설치 후 KEDA Metrics Server의해 노출된 External Metrics를 확인합니다.
kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1" | jq
{
  "kind": "APIResourceList",
  "apiVersion": "v1",
  "groupVersion": "external.metrics.k8s.io/v1beta1",
  "resources": [
    {
      "name": "externalmetrics",
      "singularName": "",
      "namespaced": true,
      "kind": "ExternalMetricValueList",
      "verbs": [
        "get"
      ]
    }
  ]
}

 

KEDA를 설치한 이후 생성된 파드를 살펴보면 KEDA의 구성요소를 알 수 있는데, 각 Agent, Metrics, Admission Webhook의 역할을 합니다.

$ kubectl get pod -n keda
NAME                                                   READY   STATUS    RESTARTS     AGE
pod/keda-admission-webhooks-86cffccbf5-nq7kw           1/1     Running   0            4m11s
pod/keda-operator-6bdffdc78-zrhmg                      1/1     Running   1 (4m ago)   4m11s
pod/keda-operator-metrics-apiserver-74d844d769-2rbqk   1/1     Running   0            4m11s

 

공식 문서를 보면 각 컴포넌트에 해당하는 역할을 확인하실 수 있습니다.

https://keda.sh/docs/2.10/concepts/#how-keda-works

  1. Agent — KEDA activates and deactivates Kubernetes Deployments to scale to and from zero on no events. This is one of the primary roles of the keda-operator container that runs when you install KEDA.
  2. Metrics — KEDA acts as a Kubernetes metrics server that exposes rich event data like queue length or stream lag to the Horizontal Pod Autoscaler to drive scale out. It is up to the Deployment to consume the events directly from the source. This preserves rich event integration and enables gestures like completing or abandoning queue messages to work out of the box. The metric serving is the primary role of the keda-operator-metrics-apiserver container that runs when you install KEDA.
  3. Admission Webhooks - Automatically validate resource changes to prevent misconfiguration and enforce best practices by using an admission controller. As an example, it will prevent multiple ScaledObjects to target the same scale target.

 

그리고 생성된 CRD를 확인해보면 아래와 같은 CRD가 생성된 것을 알 수 있습니다.

$ kubectl get crd | grep keda
cloudeventsources.eventing.keda.sh           2025-03-06T13:23:51Z
clustercloudeventsources.eventing.keda.sh    2025-03-06T13:23:51Z
clustertriggerauthentications.keda.sh        2025-03-06T13:23:51Z
scaledjobs.keda.sh                           2025-03-06T13:23:53Z
scaledobjects.keda.sh                        2025-03-06T13:23:51Z
triggerauthentications.keda.sh               2025-03-06T13:23:51Z

 

이 중 ScaledObjects 이 중요한 역할을 합니다. 이는 Event Source(예를 들어, Rabbit MQ)와 쿠버네티스 리소스(예를 들어, Deployment) 간의 의도하는 맵핑(desired mapping)을 나타냅니다.

아래 실습에 사용되는 명세를 살펴보면, cron을 바탕으로 트리거triggers되며, php-apache라는 Deployment를 대상 지정scaleTargetRef하고, spec에 HPA 오브젝트에 필요한 값이나 스케일링 속성을 지정합니다.

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: php-apache-cron-scaled
spec:
  minReplicaCount: 0
  maxReplicaCount: 2
  pollingInterval: 30
  cooldownPeriod: 300
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  triggers:
  - type: cron
    metadata:
      timezone: Asia/Seoul
      start: 00,15,30,45 * * * *
      end: 05,20,35,50 * * * *
      desiredReplicas: "1"

 

참고로 scaledObject를 생성하면 HPA도 같이 생성이 됩니다. 아래에서 다시 살펴보겠습니다.

이제 샘플 애플리케이션과 ScaledObject를 생성합니다.

# keda 네임스페이스에 디플로이먼트 생성
cat << EOF > php-apache.yaml
apiVersion: apps/v1
kind: Deployment
metadata: 
  name: php-apache
spec: 
  selector: 
    matchLabels: 
      run: php-apache
  template: 
    metadata: 
      labels: 
        run: php-apache
    spec: 
      containers: 
      - name: php-apache
        image: registry.k8s.io/hpa-example
        ports: 
        - containerPort: 80
        resources: 
          limits: 
            cpu: 500m
          requests: 
            cpu: 200m
---
apiVersion: v1
kind: Service
metadata: 
  name: php-apache
  labels: 
    run: php-apache
spec: 
  ports: 
  - port: 80
  selector: 
    run: php-apache
EOF

kubectl apply -f php-apache.yaml -n keda
kubectl get pod -n keda
...
php-apache-d87b7ff46-bbp8c                         0/1     ContainerCreating   0               3s

# ScaledObject 정책 생성 : cron
cat <<EOT > keda-cron.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: php-apache-cron-scaled
spec:
  minReplicaCount: 0
  maxReplicaCount: 2  # Specifies the maximum number of replicas to scale up to (defaults to 100).
  pollingInterval: 30  # Specifies how often KEDA should check for scaling events
  cooldownPeriod: 300  # Specifies the cool-down period in seconds after a scaling event
  scaleTargetRef:  # Identifies the Kubernetes deployment or other resource that should be scaled.
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  triggers:  # Defines the specific configuration for your chosen scaler, including any required parameters or settings
  - type: cron
    metadata:
      timezone: Asia/Seoul
      start: 00,15,30,45 * * * *
      end: 05,20,35,50 * * * *
      desiredReplicas: "1"
EOT
kubectl apply -f keda-cron.yaml -n keda

# 모니터링
kubectl get ScaledObject,hpa,pod -n keda
kubectl get ScaledObject -n keda -w

# HPA 확인 -> external 유형으로 생성되어 있음
 kubectl get hpa -o jsonpath="{.items[0].spec}" -n keda | jq
{
  "maxReplicas": 2,
  "metrics": [
    {
      "external": {
        "metric": {
          "name": "s0-cron-Asia-Seoul-00,15,30,45xxxx-05,20,35,50xxxx",
          "selector": {
            "matchLabels": {
              "scaledobject.keda.sh/name": "php-apache-cron-scaled"
            }
          }
        },
        "target": {
          "averageValue": "1",
          "type": "AverageValue"
        }
      },
      "type": "External"
    }
  ],
  "minReplicas": 1,
  "scaleTargetRef": {
    "apiVersion": "apps/v1",
    "kind": "Deployment",
    "name": "php-apache"
  }
}

 

이 예시는 cron을 통해서 00, 15, 30, 45분에 desiredReplicas에 지정한 1개의 파드가 증가하고, 이후 05, 20, 35, 40에 minReplicaCount에 지정된 0개로 파드가 줄어 듭니다. (이 예제에서 maxReplicaCount는 큰 의미가 없지만 생성되는 HPA 오브젝트에서 사용되기 때문에 작성이 필요함)

 

결과를 아래와 같이 확인할 수 있습니다. 다만 테스트를 해보면 Cron으로 정의한 45분(start), 50분(end)에 정시에 트리거가 되는 것은 아닌 것으로 확인되기 때문에 정확도에 대해서는 기대치를 다소 낮춰야 할 것 같습니다.

# 이전 시점
Thu Mar  6 22:44:53 KST 2025
NAME                                          SCALETARGETKIND      SCALETARGETNAME   MIN   MAX   READY   ACTIVE   FALLBACK   PAUSED    TRIGGERS   AUTHENTICATIONS   AGE
scaledobject.keda.sh/php-apache-cron-scaled   apps/v1.Deployment   php-apache        0     2     True    False    False      Unknown                                12m

NAME                                                                  REFERENCE               TARGETS             MINPODS   MAXPODS   REPLICAS   AGE
horizontalpodautoscaler.autoscaling/keda-hpa-php-apache-cron-scaled   Deployment/php-apache   <unknown>/1 (avg)   1         2         0          12m

NAME                                                   READY   STATUS    RESTARTS      AGE
pod/keda-admission-webhooks-86cffccbf5-nq7kw           1/1     Running   0             21m
pod/keda-operator-6bdffdc78-zrhmg                      1/1     Running   1 (20m ago)   21m
pod/keda-operator-metrics-apiserver-74d844d769-2rbqk   1/1     Running   0             21m

# 45분 이후
Thu Mar  6 22:45:29 KST 2025
NAME                                          SCALETARGETKIND      SCALETARGETNAME   MIN   MAX   READY   ACTIVE   FALLBACK   PAUSED    TRIGGERS   AUTHENTICATIONS   AGE
scaledobject.keda.sh/php-apache-cron-scaled   apps/v1.Deployment   php-apache        0     2     True    True     False      Unknown                                12m

NAME                                                                  REFERENCE               TARGETS             MINPODS   MAXPODS   REPLICAS   AGE
horizontalpodautoscaler.autoscaling/keda-hpa-php-apache-cron-scaled   Deployment/php-apache   <unknown>/1 (avg)   1         2         0          12m

NAME                                                   READY   STATUS    RESTARTS      AGE
pod/keda-admission-webhooks-86cffccbf5-nq7kw           1/1     Running   0             21m
pod/keda-operator-6bdffdc78-zrhmg                      1/1     Running   1 (21m ago)   21m
pod/keda-operator-metrics-apiserver-74d844d769-2rbqk   1/1     Running   0             21m
pod/php-apache-d87b7ff46-gblgb                         1/1     Running   0             9s


# ScaledObject 의 ACTIVE 상태가 45분 시점 True로 변경됨
kubectl get ScaledObject -n keda -w
NAME                     SCALETARGETKIND      SCALETARGETNAME   MIN   MAX   READY   ACTIVE   FALLBACK   PAUSED    TRIGGERS   AUTHENTICATIONS   AGE
php-apache-cron-scaled   apps/v1.Deployment   php-apache        0     2     True    False    False      Unknown                                2m30s
php-apache-cron-scaled   apps/v1.Deployment   php-apache        0     2     True    False    False      Unknown                                7m
php-apache-cron-scaled   apps/v1.Deployment   php-apache        0     2     True    False    False      Unknown                                12m
php-apache-cron-scaled   apps/v1.Deployment   php-apache        0     2     True    True     False      Unknown                                12m
php-apache-cron-scaled   apps/v1.Deployment   php-apache        0     2     True    True     False      Unknown                                13m
...

# 40분 쯤에 ScaleTarget을 minReplicaCount로 변경 함
kubectl logs -f -n keda keda-operator-6bdffdc78-zrhmg
...
2025-03-06T13:39:52Z    INFO    scaleexecutor   Successfully set ScaleTarget replicas count to ScaledObject minReplicaCount     {"scaledobject.Name": "php-apache-cron-scaled", "scaledObject.Namespace": "keda", "scaleTarget.Name": "php-apache", "Original Replicas Count": 1, "New Replicas Count": 0}

# 45분 쯤에 keda-operator에서 ScaleTarget을 업데이트 함
kubectl logs -f -n keda keda-operator-6bdffdc78-zrhmg
...
2025-03-06T13:45:22Z    INFO    scaleexecutor   Successfully updated ScaleTarget        {"scaledobject.Name": "php-apache-cron-scaled", "scaledObject.Namespace": "keda", "scaleTarget.Name": "php-apache", "Original Replicas Count": 0, "New Replicas Count": 1}

# 54분에 ScaleTarget을 minReplicaCount로 변경 함
2025-03-06T13:54:52Z    INFO    scaleexecutor   Successfully set ScaleTarget replicas count to ScaledObject minReplicaCount     {"scaledobject.Name": "php-apache-cron-scaled", "scaledObject.Namespace": "keda", "scaleTarget.Name": "php-apache", "Original Replicas Count": 1, "New Replicas Count": 0}

 

참고로 Grafana에서 아래의 json으로 대시보드를 만들어 모니터링 할 수 있습니다.

https://github.com/kedacore/keda/blob/main/config/grafana/keda-dashboard.json

 

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

# KEDA 및 deployment 등 삭제
kubectl delete ScaledObject -n keda php-apache-cron-scaled && kubectl delete deploy php-apache -n keda && helm uninstall keda -n keda
kubectl delete namespace keda

 

 

6. VPA(Vertical Pod Autoscaler)

VPA는 대상이 되는 리소스의 과거 사용률을 기반으로 대상의 컨테이너 스펙의 request와 limits 자체를 변경하는 오토스케일러 입니다.

VPA의 동작과정을 아래에서 설명하고 있습니다. 단, 아래 설명은 Update Mode: Auto이므로 파드가 재생성되는 과정까지를 설명하고 있습니다.

출처: https://www.youtube.com/watch?v=jLuVZX6WQsw

 

그림에서 표시된 바와 같이 VPA에는 아래와 같은 주요 컴포넌트가 있습니다.

참고: https://github.com/kubernetes/autoscaler/blob/master/vertical-pod-autoscaler/docs/components.md

  • Recommender - monitors the current and past resource consumption and, based on it, provides recommended values for the containers' cpu and memory requests.
  • Updater - checks which of the managed pods have correct resources set and, if not, kills them so that they can be recreated by their controllers with the updated requests.
  • Admission Controller - sets the correct resource requests on new pods (either just created or recreated by their controller due to Updater's activity).

VPA에서 추천값을 계산하는 방식은 과거 사용률을 바탕으로 기준값을 정하고 여기에 Margin을 더하여 결정합니다. 상세한 내용은 아래 링크를 참고 부탁드립니다.

https://devocean.sk.com/blog/techBoardDetail.do?ID=164786

 

또한 VPA에는 updateMode를 설정할 수 있는데 Auto(Default), Recreate, Initial, Off 입니다.

이해하기로는 현 시점 Auto와 Recreate은 동일하게 동작을 합니다. 즉 생성되는 파드와 실행 중인 파드를 변경하고, 필요하면 현재 실행 중인 파드를 재시작 합니다. 다만 이후 in-place resource resize(Kubernetes 1.27, Alpha)가 가능해지면, Auto 모드에서는 재시작을 하지않고, 현재 파드의 리소스를 수정만하는 방식으로 사용될 수 있습니다.

Initial은 파드가 생성되는 시점에만 추천 값을 적용하는 모드이고, Off는 추천 값을 VPA 오브젝트를 통해서 확인만 가능한 모드입니다.

 

상세한 설명은 아래 문서를 참고 부탁드립니다.

https://learn.microsoft.com/en-us/azure/aks/vertical-pod-autoscaler#vpa-object-operation-modes

 

이제 VPA를 설치하도록 하겠습니다.

참고: https://github.com/kubernetes/autoscaler/blob/master/vertical-pod-autoscaler/docs/installation.md#install-command

# VPA 코드 다운로드
git clone https://github.com/kubernetes/autoscaler.git


# 만약 openssl 이 1.1.1 이하 버전인 경우, 1.1.1 이상 버전으로 업그레이드 필요함
openssl version
OpenSSL 1.0.2k-fips  26 Jan 2017

# (필요한 경우) 1.0 제거 
yum remove openssl -y

# (필요한 경우) openssl 1.1.1 이상 버전 확인 
yum install openssl11 -y

# 스크립트 파일 내에 openssl11 수정 (commit 까지 해야 수행됨)
cd autoscaler/vertical-pod-autoscaler/
sed -i 's/openssl/openssl11/g' pkg/admission-controller/gencerts.sh
git status
git config --global user.email "you@example.com"
git config --global user.name "Your Name"
git add .
git commit -m "openssl version modify"

# Deploy the Vertical Pod Autoscaler to your cluster with the following command.
watch -d kubectl get pod -n kube-system
./hack/vpa-up.sh

# (필요한 경우) openssl 관련 에러가 발생한다면 재실행!
sed -i 's/openssl/openssl11/g' pkg/admission-controller/gencerts.sh
./hack/vpa-up.sh

# 설치 후 확인
kubectl get pod -n kube-system |grep vpa
vpa-admission-controller-659c978dcd-zwn24     1/1     Running   0          106s
vpa-recommender-9bb6d98b7-gjqc7               1/1     Running   0          112s
vpa-updater-68db47986b-jqnnh                  1/1     Running   0          116s

# mutating webhook을 통해서 파드 생성 시점 설정을 변경함
kubectl get mutatingwebhookconfigurations vpa-webhook-config

NAME                 WEBHOOKS   AGE
vpa-webhook-config   1          101s

kubectl get mutatingwebhookconfigurations vpa-webhook-config -o json | jq
{
  "apiVersion": "admissionregistration.k8s.io/v1",
  "kind": "MutatingWebhookConfiguration",
  "metadata": {
    "creationTimestamp": "2025-03-06T14:14:31Z",
    "generation": 1,
    "name": "vpa-webhook-config",
    "resourceVersion": "22754",
    "uid": "03b88fcf-c1ff-4079-b33c-38c998829d50"
  },
  "webhooks": [
    {
      "admissionReviewVersions": [
        "v1"
      ],
      "clientConfig": {
        "caBundle": "<생략>",
        "service": {
          "name": "vpa-webhook",
          "namespace": "kube-system",
          "port": 443
        }
      },
      "failurePolicy": "Ignore",
      "matchPolicy": "Equivalent",
      "name": "vpa.k8s.io",
      "namespaceSelector": {
        "matchExpressions": [
          {
            "key": "kubernetes.io/metadata.name",
            "operator": "NotIn",
            "values": [
              ""
            ]
          }
        ]
      },
      "objectSelector": {},
      "reinvocationPolicy": "Never",
      "rules": [
        {
          "apiGroups": [
            ""
          ],
          "apiVersions": [
            "v1"
          ],
          "operations": [
            "CREATE"
          ],
          "resources": [
            "pods"
          ],
          "scope": "*"
        },
        {
          "apiGroups": [
            "autoscaling.k8s.io"
          ],
          "apiVersions": [
            "*"
          ],
          "operations": [
            "CREATE",
            "UPDATE"
          ],
          "resources": [
            "verticalpodautoscalers"
          ],
          "scope": "*"
        }
      ],
      "sideEffects": "None",
      "timeoutSeconds": 30
    }
  ]
}

 

설치를 마치면 VPA와 관련된 파드와 CRD를 확인하실 수 있습니다.

kubectl get crd | grep autoscaling
verticalpodautoscalercheckpoints.autoscaling.k8s.io   2025-03-06T14:13:55Z
verticalpodautoscalers.autoscaling.k8s.io             2025-03-06T14:13:55Z

 

아래와 같이 샘플 예제를 배포하여 VPA를 테스트 하겠습니다.

# 공식 예제 배포 (autoscaler/vertical-pod-autoscaler 위치에서 수행)
kubectl apply -f examples/hamster.yaml && kubectl get vpa -w
verticalpodautoscaler.autoscaling.k8s.io/hamster-vpa created
deployment.apps/hamster created
NAME          MODE   CPU   MEM   PROVIDED   AGE
hamster-vpa   Auto                          3s
hamster-vpa   Auto   511m   262144k   True       44s # VPA의 추천 값

# 파드 리소스 Requestes 확인
kubectl describe pod | grep Requests: -A2
    Requests:
      cpu:        100m
      memory:     50Mi
--
    Requests:
      cpu:        100m
      memory:     50Mi      


# VPA에 의해 기존 파드 삭제되고 신규 파드가 생성됨
kubectl get events --sort-by=".metadata.creationTimestamp" | grep VPA
19s         Normal   EvictedByVPA        pod/hamster-598b78f579-8gjfh        Pod was evicted by VPA Updater to apply resource recommendation.
19s         Normal   EvictedPod          verticalpodautoscaler/hamster-vpa   VPA Updater evicted Pod hamster-598b78f579-8gjfh to apply resource recommendation.

# 파드 리소스 Requestes 가 변경 됨
kubectl describe pod | grep Requests: -A2
    Requests:
      cpu:        100m
      memory:     50Mi
--
    Requests:
      cpu:        100m
      memory:     50Mi
--
    Requests:
      cpu:        511m
      memory:     262144k

 

실습을 마무리하고 아래와 같이 리소스를 삭제하겠습니다.

kubectl delete -f examples/hamster.yaml && ./hack/vpa-down.sh

 

 

마무리

생각보다 길이 길어져서 오토스케일링 개요, HPA, KEDA, VPA 까지만 이번 포스트에서 작성하고, CA, Karpenter, AKS의 오토스케일링, 스케일링 주의사항은 다음 포스트에서 이어서 작성하겠습니다.

'EKS' 카테고리의 다른 글

[6] EKS의 Security - EKS 인증/인가와 Pod IAM 권한 할당  (0) 2025.03.16
[5-2] EKS의 오토스케일링 Part2  (0) 2025.03.07
[4] EKS의 모니터링과 로깅  (0) 2025.03.01
[3-2] EKS 노드 그룹  (0) 2025.02.23
[3-1] EKS 스토리지 옵션  (0) 2025.02.23

이번 포스트에서는 EKS의 모니터링과 로깅에 대해서 살펴보겠습니다.

 

먼저 모니터링과 옵저버빌리티에 대한 차이와 이에 사용되는 지표인 메트릭, 로그, 트레이싱에 대한 용어를 살펴보겠습니다. 이후 Kubernetes 환경의 모니터링 관점을 설명하고, EKS에서 어떤 방식으로 클러스터 모니터링을 제공하는지를 살펴보겠습니다. 그리고 마지막으로 AKS(Azure Kubernetes Service)의 모니터링을 비교해 보겠습니다.

 

본 포스트는 CSP 사에서 제공하는 메트릭과 이벤트, 로그 수준의 모니터링 관점으로 주로 설명을 드리겠습니다.

 

목차

  1. Monitoring과 Observability
  2. Kuberntes 환경의 모니터링
  3. 실습 환경 생성
  4. EKS의 모니터링과 로깅
  5. AKS의 모니터링과 로깅
  6. 리소스 정리

 

1. Monitoring과 Observability

과거부터 흔히 사용한 모니터링(Monitoring)이라는 단어가 익숙한데 비해 최근에 옵저버빌리티(Observability)라고 표현하는 관측 가능성이라는 용어는 조금 낯설기도 합니다. 이번 절에서는 모니터링과 옵저버빌리티를 구분을 해보고자 합니다.

 

그리고 모니터링과 옵저버빌리티에서 사용되는 Metric, Log, Tracing과 같은 용어도 알아 보겠습니다.

 

Monitoring과 Observability

모니터링은 전체적인 시스템 상태를 이해하고 감시하기 위한 활동입니다. 이를 위해 정의된 기준을 기반으로 성능 지표를 수집하여 예상치 못한 문제를 조기에 감지하는 과정을 말합니다.

과거부터 모니터링은 IT 시스템을 측정하고 장애를 예방하기 위한 목적으로 널리 사용되었지만, 시스템이 점차 다양해지고 분산 환경으로 변화됨에 따라, 개별 구성 요소들의 모니터링 지표로는 전체 서비스를 이해하기는 어려운 한계를 가지고 있습니다.

다만 시스템의 복잡성이 높아진다고 해서 모니터링 자체가 불필요 해지는 것은 아니며, 문제를 Drill down 하기 위해서는 개별 시스템의 모니터링 지표와 로그에서 유용한 정보를 찾을 필요도 있습니다.

 

옵저버빌리티 혹은 관측 가능성은 클라우드 네이티브 처럼 분산된 환경의 애플리케이션에서 발생할 수 있는 문제에 대해서 각 이벤트에 대한 통찰력을 제공하기 위해서, 각 마이크로 서비스 간의 태그, 로그를 결합해 컨텍스트(Context)를 제공하는 것이 목표입니다.

마이크로 서비스 환경에서 발생한 문제는 개별 시스템 차원의 분석으로는 설명하기는 어려울 수 있고, 각 마이크로 서비스 간의 연결 과정에서 파악해야 할 수 있습니다. 이를 파악하기 위해 지표나 이벤트를 통한 접근도 중요하지만 한편으로 이를 디버깅하는 과정에 옵저버빌리티의 역할이 커지고 있는 것 같습니다.

 

Metric, Log, Tracing

메트릭(Metrics): 특정 대상에 대한 정량화 되는 값을 의미합니다. 이는 시스템의 성능과 상태를 수치로 표현한 데이터로, 예를 들어, CPU 사용량, 메모리 사용량, 요청 지연 시간, 오류율 등이 있습니다.

시스템의 전반적인 상태를 한눈에 볼 수 있고, 이상 징후를 감지하도록 도움을 줍니다. 보통은 시간에 따른 추이를 보고, 임계치를 벗어나거나 혹은 패턴을 벗어나는 이상치를 감지할 수 있습니다.

 

로그(Logs): 시스템에서 발생한 이벤트를 기록한 텍스트나 구조화된 데이터 입니다. 로그는 보통 타임스탭프, 상태 코드, 상세 로그와 같은 형태로 기록되어, 이상 상황이나 오류가 발생한 시점을 기준으로 상세한 이유를 알 수 있어, 디버깅에 유용합니다. 예를 들어, 애플리케이션 로그, 시스템 이벤트 로그와 같은 형태입니다.

 

추적(Tracing): 분산 시스템에서 요청이 어떤 구성 요소들을 통해서 이동하는지 파악하는 것을 의미합니다. 요청의 흐름을 시각화 하고, 성능 이슈나 병목 혹은 이슈가 여러 서비스에 걸쳐 발생하는 경우 이를 진단하는데 도움을 줍니다. 예를 들어, 사용자가 어떤 화면을 접근했을 때, 그 요청이 어떤 내부 서비스를 거치는지 알 수 있습니다.

 

 

2. Kuberntes 환경의 모니터링

옵저버빌리티는 모니터링과는 다른 접근이나 솔루션이 필요하기 때문에 이후 설명은 쿠버네티스 관점에서 모니터링 관점으로 글을 이어 나가겠습니다.

물론 쿠버네티스를 자체를 모니터링 한다기 보다는 쿠버네티스 환경에서 실행되는 애플리케이션의 안전성을 위해 각 레이어별 모니터링을 설명하고자 합니다.

 

이를 설명을 하기 위해서 Azure에서 제공하는 아래 그림을 기반으로 설명을 이어 가겠습니다.

출처: https://learn.microsoft.com/ko-kr/azure/aks/monitor-aks

 

먼저 Level 1 클러스터가 위치한 네트워크 수준의 모니터링이고, Level 2로 클러스터 레벨 컴포넌트인 노드(가상머신 세트)를 모니터링 해야합니다.

그리고 Level 3은 쿠버네티스 컴포넌트에 대한 모니터링으로, 컨트롤 프레인 컴포넌트인 API 서버, Cloud Controller, Kubelet과 같은 요소들을 모니터링 해야합니다.

Level 4는 쿠버네티스 관점의 오브젝트와 워크로드에 대한 모니터링이며, 예를 들어, 컨테이너에 대한 메트릭과 파드의 재시작과 같은 이벤트도 포함해야 합니다. 한편으로 쿠버네티스에서 발생한 Event도 중요합니다.

Level 5는 애플리케이션 수준의 모니터링입니다. Level 4와 다소 겹칠 수 있지만, 애플리케이션 지표를 포함해 애플리케이션의 로그를 모니터링 할 수 있습니다.

 

그리고 이를 위한 모니터링 도구와 시각화 도구가 필요함을 설명하고 있습니다.

 

여기서 클라우드의 Managed Kubernetes Service 관점으로 생각해 볼 때, 리소스에 대한 CRUD 또한 모니터링 해야할 수도 있습니다. 예를 들어, 누가 새로운 리소스를 만들고, 구성을 변경하거나 잘못된 삭제를 한 것과 같은 이벤트입니다.

 

각 CSP(Cloud Service Provider)에서는 보통 각 상품을 위한 일반적인 모니터링을 기능을 제공하고 있습니다. AWS의 CloudWatch나 Azure의 Azure Monitor가 될 수 있습니다.

그리고 로깅 서비스를 통해 로그 데이터를 적재하고 쿼리를 통해 데이터를 조회하기 위한 기능을 제공합니다. AWS의 CloudWatch Logs와 Azure Log Analytics Workspace가 이에 해당합니다.

 

이후에는 EKS에서 모니터링을 어떤 방식으로 제공하는지를 모니터링과 로깅 관점으로 살펴보겠습니다.

 

 

3. 실습 환경 생성

EKS에서 제공하는 모니터링과 로깅을 살펴보기 위해서 아래와 같은 실습환경을 구성하도록 하겠습니다.

 

Note: 본 실습 환경은 AEWS(AWS EKS Workshop Study) 3기를 진행하 과정에서 제공받았습니다.

 

아래와 같이 CloudFormation을 통해 실습 환경을 배포합니다.

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

# 변수 지정
CLUSTER_NAME=myeks
SSHKEYNAME=ekskey
MYACCESSKEY=<IAM Uesr 액세스 키>
MYSECRETKEY=<IAM Uesr 시크릿 키>
WorkerNodeInstanceType=t3.xlarge # 워커노드 인스턴스 타입 변경 가능

# CloudFormation 스택 배포
aws cloudformation deploy --template-file myeks-4week.yaml --stack-name $CLUSTER_NAME --parameter-overrides KeyName=$SSHKEYNAME SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32  MyIamUserAccessKeyID=$MYACCESSKEY MyIamUserSecretAccessKey=$MYSECRETKEY ClusterBaseName=$CLUSTER_NAME WorkerNodeInstanceType=$WorkerNodeInstanceType --region ap-northeast-2

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

# EC2 접속
ssh -i ~/.ssh/<key>.pem ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)

 

해당 CloudFormation 스택에 배포된 운영서버를 통해서 EKS 배포까지 포함되어 있습니다.

 

EKS 배포가 완료되기 위해 20분 정도를 기다리고, 이후 설치된 EKS를 확인해 보겠습니다.

# 변수 지정
CLUSTER_NAME=myeks
SSHKEYNAME=ekskey

# 클러스터 설치 확인
eksctl get cluster
NAME    REGION          EKSCTL CREATED
myeks   ap-northeast-2  True

eksctl get nodegroup --cluster $CLUSTER_NAME
CLUSTER NODEGROUP       STATUS  CREATED                 MIN SIZE        MAX SIZE        DESIRED CAPACITY        INSTANCE TYPE   IMAGE ID                ASG NAME                                   TYPE
myeks   ng1             ACTIVE  2025-02-28T15:05:57Z    3               3               3                       t3.xlarge       AL2023_x86_64_STANDARD  eks-ng1-b4caa68b-dac3-4a9c-a489-7d63a0d70934       managed

eksctl get addon --cluster $CLUSTER_NAME
2025-03-01 00:26:31 [ℹ]  Kubernetes version "1.31" in use by cluster "myeks"
2025-03-01 00:26:31 [ℹ]  getting all addons
2025-03-01 00:26:33 [ℹ]  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.40.0-eksbuild.1      ACTIVE  0       arn:aws:iam::430118812536:role/eksctl-myeks-addon-aws-ebs-csi-driver-Role1-Ks7b8mzq4vmu
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.3-eksbuild.1      ACTIVE  0       arn:aws:iam::430118812536:role/eksctl-myeks-addon-vpc-cni-Role1-He4lLHyBeE62              enableNetworkPolicy: "true"

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-1GFoeNZ7Z43o

# kubeconfig 생성
aws sts get-caller-identity --query Arn
aws eks update-kubeconfig --name myeks --user-alias <위 출력된 자격증명 사용자>

# 기본 구성 정보 확인
kubectl cluster-info
Kubernetes control plane is running at https://7984C504F1BE86380015EB205905A2C5.gr7.ap-northeast-2.eks.amazonaws.com
CoreDNS is running at https://7984C504F1BE86380015EB205905A2C5.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'.

kubectl get node
NAME                                               STATUS   ROLES    AGE   VERSION
ip-192-168-1-115.ap-northeast-2.compute.internal   Ready    <none>   21m   v1.31.5-eks-5d632ec
ip-192-168-2-178.ap-northeast-2.compute.internal   Ready    <none>   21m   v1.31.5-eks-5d632ec
ip-192-168-3-168.ap-northeast-2.compute.internal   Ready    <none>   21m   v1.31.5-eks-5d632ec


kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone
NAME                                               STATUS   ROLES    AGE   VERSION               INSTANCE-TYPE   CAPACITYTYPE   ZONE
ip-192-168-1-115.ap-northeast-2.compute.internal   Ready    <none>   21m   v1.31.5-eks-5d632ec   t3.xlarge       ON_DEMAND      ap-northeast-2a
ip-192-168-2-178.ap-northeast-2.compute.internal   Ready    <none>   21m   v1.31.5-eks-5d632ec   t3.xlarge       ON_DEMAND      ap-northeast-2b
ip-192-168-3-168.ap-northeast-2.compute.internal   Ready    <none>   21m   v1.31.5-eks-5d632ec   t3.xlarge       ON_DEMAND      ap-northeast-2c


kubectl get pod -A
NAMESPACE     NAME                                  READY   STATUS    RESTARTS   AGE
kube-system   aws-node-4jsvr                        2/2     Running   0          21m
kube-system   aws-node-bkp8n                        2/2     Running   0          21m
kube-system   aws-node-v5rhv                        2/2     Running   0          21m
kube-system   coredns-86f5954566-4j74x              1/1     Running   0          27m
kube-system   coredns-86f5954566-mcw5d              1/1     Running   0          27m
kube-system   ebs-csi-controller-549bf6879f-26wqx   6/6     Running   0          17m
kube-system   ebs-csi-controller-549bf6879f-qgqtz   6/6     Running   0          17m
kube-system   ebs-csi-node-8zr72                    3/3     Running   0          17m
kube-system   ebs-csi-node-sc6tt                    3/3     Running   0          17m
kube-system   ebs-csi-node-v48kr                    3/3     Running   0          17m
kube-system   kube-proxy-6wkjg                      1/1     Running   0          21m
kube-system   kube-proxy-v8228                      1/1     Running   0          21m
kube-system   kube-proxy-xw8hc                      1/1     Running   0          21m
kube-system   metrics-server-6bf5998d9c-2gngg       1/1     Running   0          27m
kube-system   metrics-server-6bf5998d9c-wv68w       1/1     Running   0          27m

 

이후 실습에 사용될 일부 구성 요소를 배포하겠습니다.

 

[Note]
참고로 본 실습 전에 Route 53에서 도메인을 생성했습니다. 만약 도메인 없는 경우 Loadbalancer 등으로 서비스를 변경해야 해서 실습에 제한이 있을 수 있습니다.
- Route53을 통한 도메인 구매: https://www.youtube.com/watch?v=4HBFozkJUeU

또한 해당 도메인에 대해서 AWS Certificate Manager를 통해 인증서를 발급 받아야 합니다.
- 인증서 발급: https://www.youtube.com/watch?v=mMpPlaUj-vI

 

이어서 진행하겠습니다.

# 환경 변수
MyDomain=aperson.link # 각자 자신의 도메인 이름 입력
MyDnzHostedZoneId=$(aws route53 list-hosted-zones-by-name --dns-name "$MyDomain." --query "HostedZones[0].Id" --output text)
CERT_ARN=$(aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text) #사용 리전의 인증서 ARN 확인

# 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 service.main.type=ClusterIP  --set env.TZ="Asia/Seoul" --namespace kube-system

# gp3 스토리지 클래스 생성
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
  allowAutoIOPSPerGBIncrease: 'true'
  encrypted: 'true'
  fsType: xfs # 기본값이 ext4
EOF
kubectl get sc

# ExternalDNS
curl -s https://raw.githubusercontent.com/gasida/PKOS/main/aews/externaldns.yaml | MyDomain=$MyDomain MyDnzHostedZoneId=$MyDnzHostedZoneId envsubst | kubectl apply -f -

# AWS LoadBalancerController
helm repo add eks https://aws.github.io/eks-charts
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

# kubeopsview 용 Ingress 설정 : group 설정으로 1대의 ALB를 여러개의 ingress 에서 공용 사용
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
    alb.ingress.kubernetes.io/group.name: study
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
    alb.ingress.kubernetes.io/load-balancer-name: $CLUSTER_NAME-ingress-alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/ssl-redirect: "443"
    alb.ingress.kubernetes.io/success-codes: 200-399
    alb.ingress.kubernetes.io/target-type: ip
  labels:
    app.kubernetes.io/name: kubeopsview
  name: kubeopsview
  namespace: kube-system
spec:
  ingressClassName: alb
  rules:
  - host: kubeopsview.$MyDomain
    http:
      paths:
      - backend:
          service:
            name: kube-ops-view
            port:
              number: 8080
        path: /
        pathType: Prefix
EOF

 

또한 더불어 실습에 필요한 모니터링 데이터를 누적하기 위해서 샘플 애플리케이션도 같이 배포하겠습니다.

# Bookinfo 애플리케이션 배포
kubectl apply -f https://raw.githubusercontent.com/istio/istio/refs/heads/master/samples/bookinfo/platform/kube/bookinfo.yaml

# 확인
kubectl get all,sa

# product 웹 접속 확인
kubectl exec "$(kubectl get pod -l app=ratings -o jsonpath='{.items[0].metadata.name}')" -c ratings -- curl -sS productpage:9080/productpage | grep -o "<title>.*</title>"

# 로그
kubectl log -l app=productpage -f


# Ingress 배포
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
    alb.ingress.kubernetes.io/group.name: study
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
    alb.ingress.kubernetes.io/load-balancer-name: $CLUSTER_NAME-ingress-alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/ssl-redirect: "443"
    alb.ingress.kubernetes.io/success-codes: 200-399
    alb.ingress.kubernetes.io/target-type: ip
  labels:
    app.kubernetes.io/name: bookinfo
  name: bookinfo
spec:
  ingressClassName: alb
  rules:
  - host: bookinfo.$MyDomain
    http:
      paths:
      - backend:
          service:
            name: productpage
            port:
              number: 9080
        path: /
        pathType: Prefix
EOF
kubectl get ingress

# bookinfo 접속 정보 확인 
echo -e "bookinfo URL = https://bookinfo.$MyDomain/productpage"
open "https://bookinfo.$MyDomain/productpage" # macOS

 

배포된 리소스를 확인해봅니다. external dns 를 통해 DNS가 등록되지만 전파에 시간이 걸릴 수 있습니다.

kubectl get ingress -n kube-system
NAME          CLASS   HOSTS                      ADDRESS                                                        PORTS   AGE
kubeopsview   alb     kubeopsview.aperson.link   myeks-ingress-alb-665851389.ap-northeast-2.elb.amazonaws.com   80      5m

kubectl get ingress
NAME       CLASS   HOSTS                   ADDRESS                                                        PORTS   AGE
bookinfo   alb     bookinfo.aperson.link   myeks-ingress-alb-665851389.ap-northeast-2.elb.amazonaws.com   80      7s

 

접속을 확인해봅니다.

 

샘플 애플리케이션도 정상적으로 실행 되었습니다.

 

 

로그 발생을 위해서 아래와 같이 반복 접속을 해볼 수 있습니다.

curl -s -k https://bookinfo.$MyDomain/productpage | grep -o "<title>.*</title>"
while true; do curl -s -k https://bookinfo.$MyDomain/productpage | grep -o "<title>.*</title>" ; echo "--------------" ; sleep 1; done
for i in {1..100};  do curl -s -k https://bookinfo.$MyDomain/productpage | grep -o "<title>.*</title>" ; done

 

 

4. EKS의 모니터링과 로깅

먼저 EKS의 모니터링과 로깅에 대한 설명을 아래 두가지 문서에서 살펴볼 수 있습니다.

https://docs.aws.amazon.com/prescriptive-guidance/latest/implementing-logging-monitoring-cloudwatch/amazon-eks-logging-monitoring.html

 

EKS에서는 CloudWatch Logs를 통합하여 컨트롤 플레인 로그를 확인할 수 있습니다. 또한 CloudWatch agent를 EKS 노드에 배포하여 노드와 컨테이너 로그를 수집하는 방법도 제공합니다. 이때 Fluent Bit과 Fluentd가 컨테이너 로그를 수집하여 CloudWatch Logs로 전송하도록 지원합니다.

 

CloudWatch Container Insight는 EKS 클러스터, 노드, 파드, 서비스와 같은 수준의 모니터링을 제공하는 도구입니다. 또한 Prometheus를 통해 다양한 메트릭을 수집하는 방식도 제공합니다.

EKS의 모니터링 솔루션을 아래와 같은 그림으로 확인하실 수 있습니다.

출처: https://www.youtube.com/watch?v=349ywnrrROg

 

본 포스트에서는 CloudWatch Logs와 CloudWatch Container Insight 활용해 EKS 모니터링과 로깅을 확인해보겠습니다.

 

EKS 로깅

컨트롤 프레인 로깅

먼저 컨트롤 플레인 로깅을 먼저 살펴 보겠습니다.

컨트롤 플레인 로깅에서는 API Server, Audit, Authenticator, Controller manager, Scheduler와 같은 로그 유형을 제공하고 있습니다. 이에 대한 설명은 아래 문서를 참고 하실 수 있습니다.

https://docs.aws.amazon.com/eks/latest/userguide/control-plane-logs.html

 

컨트롤 플레인 로깅이 필요한 경우를 예를 들어보면, 특정 시점 생성된 오브젝트를 추적하기 위해 Audit 로그를 보거나, 클러스터가 비정상 동작하는 경우에 대한 API 서버 로그를 확인 하는 것, AWS 수준의 리소스와 연관된 경우 Controller manager 로그를 점검하는 것과 같은 상황이 있을 수 있습니다.

 

컨트롤 플레인 로깅은 웹 콘솔에서 Observability 탭으로 이동하여 아래로 이동하면 Control plane logs가 확인됩니다. eksctl 로 설치한 클러스터에는 기본적으로 모든 옵션이 off 인 것을 알 수 있습니다.

 

컨트롤 플레인 로깅을 활성화 하고 로그를 살펴보겠습니다.

# 모든 로깅 활성화
aws eks update-cluster-config --region ap-northeast-2 --name $CLUSTER_NAME \
    --logging '{"clusterLogging":[{"types":["api","audit","authenticator","controllerManager","scheduler"],"enabled":true}]}'

 

웹 콘솔에서 확인해보면 로그가 활성화되었습니다.

 

그리고 CloudWatch를 접근해보면 새로운 Log group이 생성된 것이 확인됩니다.

 

해당 로그 그룹으로 진입하면, 아래와 같이 각 로그에 해당하는 Log stream이 생성된 것을 확인할 수 있습니다.

 

로그 스트림 중 하나를 선택하면 실제 로그를 확인하실 수 있습니다.

 

또한 CloudWatch Log Insights를 통해서 쿼리를 통해 로그를 확인할 수 있습니다.

# EC2 Instance가 NodeNotReady 상태인 로그 검색
fields @timestamp, @message
| filter @message like /NodeNotReady/
| sort @timestamp desc

# kube-apiserver-audit 로그에서 userAgent 정렬해서 결과 확인
fields userAgent, requestURI, @timestamp, @message
| filter @logStream ~= "kube-apiserver-audit"
| stats count(userAgent) as count by userAgent
| sort count desc

# kube-scheduler 로그 확인
fields @timestamp, @message
| filter @logStream ~= "kube-scheduler"
| sort @timestamp desc

# authenticator 로그 확인
fields @timestamp, @message
| filter @logStream ~= "authenticator"
| sort @timestamp desc

# kube-controller-manager 로그 확인
fields @timestamp, @message
| filter @logStream ~= "kube-controller-manager"
| sort @timestamp desc

 

이 중 kube-audit 로그를 통해 접근한 userAgent의 갯수를 확인한 예시 입니다.

 

또한 aws logs 명령으로 로그를 확인할 수도 있습니다.

# 로그 그룹 확인
aws logs describe-log-groups | jq

# 로그 tail 확인 : aws logs tail help
aws logs tail /aws/eks/$CLUSTER_NAME/cluster | more

# 신규 로그를 바로 출력
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --follow

# 필터 패턴
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --filter-pattern <필터 패턴>

# 로그 스트림이름
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --log-stream-name-prefix <로그 스트림 prefix> --follow
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --log-stream-name-prefix kube-apiserver --follow
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --log-stream-name-prefix kube-apiserver-audit --follow
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --log-stream-name-prefix kube-scheduler --follow
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --log-stream-name-prefix authenticator --follow
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --log-stream-name-prefix kube-controller-manager --follow
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --log-stream-name-prefix cloud-controller-manager --follow
kubectl scale deployment -n kube-system coredns --replicas=1
kubectl scale deployment -n kube-system coredns --replicas=2

# 시간 지정: 1초(s) 1분(m) 1시간(h) 하루(d) 한주(w)
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --since 1h30m

# 짧게 출력
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --since 1h30m --format short

 

CloudWatch Logs에서 aws logs tail 과 같은 방식으로 손 쉽게 로그를 확인할 수 있는 방식을 제공하는 점이 큰 장점으로 보였습니다.

 

실습을 종료하고, 컨트롤 플레인 로그를 비활성화 화도록 하겠습니다.

# EKS Control Plane 로깅(CloudWatch Logs) 비활성화
eksctl utils update-cluster-logging --cluster $CLUSTER_NAME --region ap-northeast-2 --disable-types all --approve

# 로그 그룹 삭제
aws logs delete-log-group --log-group-name /aws/eks/$CLUSTER_NAME/cluster

 

 

노드와 애플리케이션 로깅

EKS의 노드와 컨테이너 모니터링을 위해서 CloudWatch agent와 Fluent Bit을 사용합니다. 두 파드는 데몬 셋으로 구성되어 아래와 같은 형태로 구성됩니다.

출처: https://aws.amazon.com/ko/blogs/containers/fluent-bit-integration-in-cloudwatch-container-insights-for-eks/

 

이들은 CloudWatch Observability라는 Addon으로 제공되므로, 아래와 같이 설치를 진행합니다.

# IRSA 설정
eksctl create iamserviceaccount \
  --name cloudwatch-agent \
  --namespace amazon-cloudwatch --cluster $CLUSTER_NAME \
  --role-name $CLUSTER_NAME-cloudwatch-agent-role \
  --attach-policy-arn arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy \
  --role-only \
  --approve

# addon 배포 (사전에 환경 변수가 정의된 EC2 인스턴스에서 실행)
aws eks create-addon --addon-name amazon-cloudwatch-observability --cluster-name $CLUSTER_NAME --service-account-role-arn arn:aws:iam::$ACCOUNT_ID:role/$CLUSTER_NAME-cloudwatch-agent-role

# addon 확인
aws eks list-addons --cluster-name myeks --output table
---------------------------------------
|             ListAddons              |
+-------------------------------------+
||              addons               ||
|+-----------------------------------+|
||  amazon-cloudwatch-observability  ||
||  aws-ebs-csi-driver               ||
||  coredns                          ||
||  kube-proxy                       ||
||  metrics-server                   ||
||  vpc-cni                          ||
|+-----------------------------------+|

# 설치 확인
kubectl get crd | grep -i cloudwatch

amazoncloudwatchagents.cloudwatch.aws.amazon.com   2025-02-28T16:27:24Z
dcgmexporters.cloudwatch.aws.amazon.com            2025-02-28T16:27:24Z
instrumentations.cloudwatch.aws.amazon.com         2025-02-28T16:27:25Z
neuronmonitors.cloudwatch.aws.amazon.com           2025-02-28T16:27:25Z

kubectl get all -n amazon-cloudwatch

NAME                                                                  READY   STATUS    RESTARTS   AGE
pod/amazon-cloudwatch-observability-controller-manager-6f76854w9rvx   1/1     Running   0          69s
pod/cloudwatch-agent-dcfqq                                            1/1     Running   0          64s
pod/cloudwatch-agent-jcvk5                                            1/1     Running   0          65s
pod/cloudwatch-agent-r8tcw                                            1/1     Running   0          64s
pod/fluent-bit-6zbmk                                                  1/1     Running   0          69s
pod/fluent-bit-j9hl8                                                  1/1     Running   0          69s
pod/fluent-bit-zrw4v                                                  1/1     Running   0          69s

..


# cloudwatch-agent 설정 확인
kubectl describe cm cloudwatch-agent -n amazon-cloudwatch
kubectl get cm cloudwatch-agent -n amazon-cloudwatch -o jsonpath="{.data.cwagentconfig\.json}" | jq
{
  "agent": {
    "region": "ap-northeast-2"
  },
  "logs": {
    "metrics_collected": {
      "application_signals": {
        "hosted_in": "myeks"
      },
      "kubernetes": {
        "cluster_name": "myeks",
        "enhanced_container_insights": true
      }
    }
  },
  "traces": {
    "traces_collected": {
      "application_signals": {}
    }
  }
}

#Fluent bit 파드 수집하는 방법 : Volumes에 HostPath를 통해서 Node Log, Container Log에 접근함
kubectl describe -n amazon-cloudwatch ds cloudwatch-agent
...
  Volumes:
   ...
   rootfs:
    Type:          HostPath (bare host directory volume)
    Path:          /
    HostPathType:  


# Fluent Bit 로그 INPUT/FILTER/OUTPUT 설정 확인
## 설정 부분 구성 : application-log.conf , dataplane-log.conf , fluent-bit.conf , host-log.conf , parsers.conf
kubectl describe cm fluent-bit-config -n amazon-cloudwatch
...
application-log.conf:
----
[INPUT]
    Name                tail
    Tag                 application.*
    Exclude_Path        /var/log/containers/cloudwatch-agent*, /var/log/containers/fluent-bit*, /var/log/containers/aws-node*, /var/log/containers/kube-proxy*
    Path                /var/log/containers/*.log
    multiline.parser    docker, cri
    DB                  /var/fluent-bit/state/flb_container.db
    Mem_Buf_Limit       50MB
    Skip_Long_Lines     On
    Refresh_Interval    10
    Rotate_Wait         30
    storage.type        filesystem
    Read_from_Head      ${READ_FROM_HEAD}
...

[FILTER]
    Name                kubernetes
    Match               application.*
    Kube_URL            https://kubernetes.default.svc:443
    Kube_Tag_Prefix     application.var.log.containers.
    Merge_Log           On
    Merge_Log_Key       log_processed
    K8S-Logging.Parser  On
    K8S-Logging.Exclude Off
    Labels              Off
    Annotations         Off
    Use_Kubelet         On
    Kubelet_Port        10250
    Buffer_Size         0

[OUTPUT]
    Name                cloudwatch_logs
    Match               application.*
    region              ${AWS_REGION}
    log_group_name      /aws/containerinsights/${CLUSTER_NAME}/application
    log_stream_prefix   ${HOST_NAME}-
    auto_create_group   true
    extra_user_agent    container-insights
...

 

Addon을 통해 생성되는 로그 그룹과 대응하는 로그는 아래와 같습니다.

출처: https://docs.aws.amazon.com/prescriptive-guidance/latest/implementing-logging-monitoring-cloudwatch/kubernetes-eks-logging.html#eks-node-application-logging

 

CloudWatch Logs에 보면 아래와 같은 로그 그룹이 생성된 것을 알 수 있습니다.

 

다만 의아한 부분은 분명 configmap에 log_group_name에 /host가 있는데 이것은 생성되지 않았고, /performance라는 로그 그룹이 추가 되어 있습니다.

kubectl describe cm fluent-bit-config -n amazon-cloudwatch |grep log_group_name
  log_group_name      /aws/containerinsights/${CLUSTER_NAME}/application
  log_group_name      /aws/containerinsights/${CLUSTER_NAME}/dataplane
  log_group_name      /aws/containerinsights/${CLUSTER_NAME}/host

 

fluent-bit 에서도 log group 생성이 실패한 것으로 보입니다.

kubectl logs -f -n amazon-cloudwatch fluent-bit-zrw4v
AWS for Fluent Bit Container Image Version 2.32.5
Fluent Bit v1.9.10
* Copyright (C) 2015-2022 The Fluent Bit Authors
* Fluent Bit is a CNCF sub-project under the umbrella of Fluentd
* https://fluentbit.io

[2025/02/28 16:27:34] [error] [filter:kubernetes:kubernetes.1] [kubernetes] no upstream connections available to cloudwatch-agent.amazon-cloudwatch:4311
[2025/02/28 16:27:39] [error] [output:cloudwatch_logs:cloudwatch_logs.0] CreateLogGroup API responded with error='OperationAbortedException', message='A conflicting operation is currently in progress against this resource. Please try again.'
[2025/02/28 16:27:39] [error] [output:cloudwatch_logs:cloudwatch_logs.0] Failed to create log group
[2025/02/28 16:27:39] [error] [output:cloudwatch_logs:cloudwatch_logs.0] Failed to send events

 

버그인거 같지만 Addon에서 관리되는 영역이라 확인이 어려운 것 같습니다.

https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_CreateLogGroup.html

  • OperationAbortedException
  • Multiple concurrent requests to update the same resource were in conflict.HTTP Status Code: 400

 

/performance 로그 그룹은 container Insight를 위한 성능 데이터를 CloudWatch Agent가 쌓고 있는 것으로 보입니다.

kubectl logs -f -n amazon-cloudwatch   cloudwatch-agent-dcfqq |grep log_group_name
        log_group_name: /aws/application-signals/data
        log_group_name: /aws/containerinsights/{ClusterName}/performance

 

 

EKS 메트릭 기반 모니터링

앞서 설치한 CloudWatch Observability 애드온에 으해서 Container Insight에 해당하는 메트릭도 수집됩니다.

웹 콘솔에서 내용을 살펴 보겠습니다.

CloudWatch → Insights → Container Insights 으로 접근할 수 있습니다.

 

우측 상단의 View performance dashboard를 눌러면 여러가지 뷰로 다양한 메트릭과 그래프를 확인 가능합니다.

 

생성된 특정 리소스(네임스페이스와 Workload로 선택)를 선택한 경우 해당 각종 메트릭을 확인할 수 있습니다.

 

전반적으로 Container Insight를 통해 확인하는 정보들이 체계적으로 분류되어 있고, 각 항목에서 시각화가 잘되어 있는 점이 인상 깊습니다. 그리고 각 View에 대한 response time도 빨랐습니다.

 

실습을 마무리하고 애드온과 생성된 로그 그룹을 삭제하도록 하겠습니다.

aws eks delete-addon --cluster-name $CLUSTER_NAME --addon-name amazon-cloudwatch-observability

aws logs delete-log-group --log-group-name /aws/containerinsights/$CLUSTER_NAME/application
aws logs delete-log-group --log-group-name /aws/containerinsights/$CLUSTER_NAME/dataplane
aws logs delete-log-group --log-group-name /aws/containerinsights/$CLUSTER_NAME/host
aws logs delete-log-group --log-group-name /aws/containerinsights/$CLUSTER_NAME/performance

 

 

EKS 리소스 이벤트 확인

EKS는 AWS CloudTrail과 통합되어 있습니다. CloudTrail는 리소스에 일어난 활동을 기록하는 서비스입니다. CloudTrail은 EKS에 일어난 모든 API 요청을 이벤트로 기록하고 있습니다. 여기에는 웹 콘솔이나 코드를 통한 Amazon EKS API operation을 모두 포함하고 있습니다.

 

Trails를 생성했다면 이러한 로그를 Amazon S3 bucket을 통해 장기 보관할 수 있으며, 특별한 설정을 하지 않아도 CloudTrail의 Event histry를 통해서 리소스에 발생한 활동을 확인할 수 있습니다.

 

보통 이런 이벤트를 확인하는 경우는 특정 리소스의 변경이나 이벤트가 EKS에 의한 것인지 아니면 사용자에 의한 것인지 확인이 필요한 경우 등이 있습니다.

 

아래와 같이 확인이 가능합니다.

 

 

해당 항목을 누르면 UserIdentity, sourceIPAddress, userAgent와 같은 상세한 내용을 확인할 수 있습니다.

AWS CloudTrail은 리소스의 변경 사항 뿐 아니라, Read를 발생시킨 요청에 대해서도 기록하고 있는 점이 인상 깊습니다.

 

EKS의 CloudTail에 대해서 아래 문서를 확인하실 수 있습니다.

https://docs.aws.amazon.com/eks/latest/userguide/logging-using-cloudtrail.html

 

여기서 EKS의 모니터링과 로깅을 마무리 하겠습니다. EKS에서도 다양한 메트릭을 제공과 시각화를 위해서 Amazon Managed Prometheus 와 Amazon Managed Grafana 서비스를 제공하고 있습니다.

 

 

5. AKS의 모니터링과 로깅

Azure의 모니터링과 로깅 솔루션으로 Azure Monitor와 Log Analytics Workspace가 있습니다.

각 AWS의 CloudWatch와 CloudWatch Logs와 대응합니다. Azure Monitor에서는 사전에 제공하는 뷰나 신규 블레이드를 생성하여 데이터를 확인할 수 있으며, Log Analytics Workspace는 테이블 형태로 데이터를 수집하므로 KQL(Kusto Query Language)를 통해서 쿼리를 수행할 수 있습니다.

 

또한 AKS 환경에 전문화된 메트릭/로깅을 제공하기 위해 Container Insight를 제공하고 있습니다.

전반적인 AKS 모니터링 옵션을 아래 문서에서 설명하고 있습니다.

 

https://learn.microsoft.com/en-us/azure/aks/monitor-aks?tabs=cilium

 

이 중 EKS에서 살펴본 순서대로 로깅과 메트릭 부분을 살펴보겠습니다.

 

AKS 로깅

먼저 컨트롤 플레인 로그는 위 테이블의 Resource logs 에서 설명하고 있습니다. Azure에서는 각 상품별로 진단 설정(Diagnostics setting)을 할 수 있는데, AKS에서는 진단 설정을 통해서 컨트롤 플레인 로그를 선택적으로 수집할 수 있습니다.

 

진단 설정에서 제공되는 항목은 아래와 같습니다. EKS와 다르게 CA나 CSI controller에 해당하는 파드들이 컨트롤 플레인에 구성되므로 해당 컴포넌트에 대한 로그도 진단 설정에서 선택할 수 있습니다.

 

다음으로 노드와 애플리케이션 모니터링을 위해서 Container Insight를 설정할 수 있습니다. Container Insight의 로그는 Log Analytics Workspace에 저장되어, 비용 측면에 아래와 같이 사전에 정의된 세트를 지정할 수 있습니다.

 

이후 수집 설정을 수정을 눌러보면 어떤 로그/메트릭 유형이 수집되는지 확인할 수 있습니다.

 

수집을 원하는 항목을 선택할 수 있으며, 성능관련 지표나, 컨테이너 로그, 그리고 각 오브젝트의 상태나 쿠버네티스 Event와 같은 정보를 수집할 수 있는 것을 알 수 있습니다.

이러한 항목은 Log Analytics Workspace에 개별 테이블로 저장되며, 클러스터의 Monitoring>Logs를 통해서 접근하거나 혹은 Log Analytics Workspace로 직접 접근해 쿼리를 사용할 수 있습니다.

 

아래 샘플 쿼리를 참고 부탁드립니다.

https://docs.azure.cn/en-us/azure-monitor/reference/queries/containerlog

 

Container Insight의 모니터링에도 Performance, Metrics를 볼 수 있지만 최근에는 Prometheus Metric으로 전환되는 방향성을 가진 것 같기도 합니다.

 

 

AKS 메트릭 기반 모니터링

다음으로 메트릭을 살펴보겠습니다.

 

Azure는 플랫폼 메트릭으로 리소스 별로 기본 제공되는 메트릭을 무료로 제공합니다. 보통 고급 모니터링 기능을 활성화 하지 않은 상태에서도 AKS>Monitoring>Metrics에서 일부 값들을 확인하실 수 있습니다.

 

예를 들어, 노드 상태, 파드 상태나 노드 리소스 메트릭 등이 선택가능하며, AKS도 최근 컨트롤 플레인 메트릭을 Preview로 제공하고 있습니다.

 

플랫폼 메트릭에 대한 전체 메트릭 설명은 아래를 참고하실 수 있습니다.

https://learn.microsoft.com/en-us/azure/aks/monitor-aks-reference#metrics

 

최근 Container Insight에서 Prometheus 메트릭과 로깅을 활성화 한 경우 AKS Monitor Experience이 크게 개선되었으며 현재 Preview 상태입니다.

https://techcommunity.microsoft.com/blog/azureobservabilityblog/public-preview-the-new-aks-monitoring-experience/4297181

 

AKS 리소스 이벤트 확인

마지막으로 Activity Log에서 해당 리로스에 대한 이벤트를 확인할 수 있습니다.

 

EKS와 마찬가지로 AKS에서도 Managed Prometheus와 Managed Grafana를 통해서 모니터링을 통합할 수 있는 기능이 제공됩니다.

 

참고로 기본 AKS>Monitoring>Insight로 접근하던 Container Insight가 AKS의 Monitor로 변경이 되었습니다. 아래 화면은 Monitor Settings으로, Container Logs 설정 외에도 Managed PrometheusManaged Grafana를 선택 할 수 있습니다.

 

다만 EKS와 비교해 보면 CloudWatch Container Insight가 조금 더 완성도 있는 구성과 시각화를 보여주는 것 같습니다.

 

 

6. 리소스 정리

실습에 사용된 환경을 아래와 같이 정리하도록 하겠습니다.

nohup sh -c "eksctl delete cluster --name $CLUSTER_NAME && aws cloudformation delete-stack --stack-name $CLUSTER_NAME" > /root/delete.log 2>&1 &

 

CloudWatch Logs가 비용이 많이 드는 것으로 알려져 있어 모든 로그 그룹이 삭제되었는지 꼭 확인하시기 바랍니다.

# 로그 그룹 삭제 : 컨트롤 플레인
aws logs delete-log-group --log-group-name /aws/eks/$CLUSTER_NAME/cluster

# 로그 그룹 삭제 : 데이터 플레인
aws logs delete-log-group --log-group-name /aws/containerinsights/$CLUSTER_NAME/application
aws logs delete-log-group --log-group-name /aws/containerinsights/$CLUSTER_NAME/dataplane
aws logs delete-log-group --log-group-name /aws/containerinsights/$CLUSTER_NAME/host
aws logs delete-log-group --log-group-name /aws/containerinsights/$CLUSTER_NAME/performance

 

마지막으로 EC2에서 혹시나 남아 있는 볼륨(Prometheus용 PV 등)들이 있다면 확인 후 모두 삭제해야 하시기 바랍니다.

 

 

마무리

해당 포스트를 통해서 Kubernetes 환경의 모니터링에 대해서 살펴보고, 이러한 모니터링을 EKS에서 어떻게 활성화 하고 지표를 살펴볼 수 있는지 확인했습니다. 또한 AKS의 모니터링 제공 수준과 살펴보고 서로 비교해봤습니다.

 

이 과정은 대체적으로 CSP에서 제공하는 옵션을 위주로 설명을 하였습니다. 반면 일부 사용자는 Prometheus나 Grafana와 같은 오픈소스로 직접 모니터링을 구성하기도 합니다. 그리고 모니터링을 전문으로 하는 SaaS 서비스를 사용할 수도 있습니다. CSP의 모니터링 솔루션을 사용할 것인지 혹은 오픈소스나 다른 형태의 모니터링을 사용할지는 사용자에게 달려있습니다.

 

살펴보기로는 CSP에서는 옵저버빌리티 수준으로 모니터링을 고도화 해가는 방향성을 가지고 있는 것을 확인할 수 있습니다. 다만 CSP 모니터링 솔루션이 비용 효율적인지에 대한 의문과, 또한 블레이드나 혹은 알림과 같은 기능에 커스터마이즈에 한계가 있기도 합니다. 그리고 멀티 클라우드 환경이라면 각 사별로 서로 다른 모니터링 스택을 관리해야하는 문제도 있습니다.

 

그러한 측면에서 오픈소스 모니터링 솔루션을 사용할 수 있습니다. 다만 각 클러스터별로 Prometheus Stack을 위한 컴포넌트가 배포되는 중복성이나 비용이 발생하는 측면과, 또한 모니터링 솔루션을 자체를 관리해야 하는 부가적인 업무도 부담이 되기는 합니다. 다만 시각화나 모니터링 항목의 커스터마이즈가 가능한 점, 다양한 환경에 동일한 모니터링 스택을 배포하며 통일된 환경을 구성할 수 있는 장점이 있습니다.

 

모니터링을 전문으로 하는 SaaS 서비스를 사용하는 옵션도 있습니다. 대체적으로 이러한 솔루션은 우수하지만 데이터를 전송하는 비용 및 보안적인 우려가 있을 수도 있고, 솔루션 자체의 비용도 부담이 될 수 있습니다.

 

어떤 모니터링을 솔루션을 사용하는 것에는 장/단점이 있기 때문에 이는 사용자의 선택이나 기술적 판단이 필요할 수 있습니다.

 

해당 포스트는 여기서 마무리 하도록 하겠습니다.

'EKS' 카테고리의 다른 글

[5-2] EKS의 오토스케일링 Part2  (0) 2025.03.07
[5-1] EKS의 오토스케일링 Part1  (0) 2025.03.07
[3-2] EKS 노드 그룹  (0) 2025.02.23
[3-1] EKS 스토리지 옵션  (0) 2025.02.23
[2-2] EKS Networking Part2 - LoadBalancer와 Ingress  (0) 2025.02.16

Kubernetes의 노드를 EKS에서는 노드 그룹으로 제공하고 있습니다. 본 포스트에서는 EKS의 노드그룹 유형을 살펴보고, 세부적으로는 관리형 노드 그룹(Managed node groups)에서 생성 가능한 노드 유형을 살펴보도록 하겠습니다.

 

 

목차

  1. CSP에서 제공하는 Kubernetes 노드 리소스
  2. EKS 노드 그룹 유형
  3. EKS 노드 그룹 AL2 -> AL2023
  4. 다양한 노드 그룹 사용해 보기
  5. 리소스 정리

 

 

1. CSP에서 제공하는 Kubernetes 노드 리소스

Kubernetes의 노드는 워크로드를 실행하기 위한 리소스입니다. 이는 가상 머신이 될 수도 물리 머신이 될 수도 있습니다. 그리고 노드는 파드를 실행하기 위한 컴포넌트를 가지는데 파드를 실행하기 위한 Kubelet, 컨테이너 런타임, 그리고 Service 를 구현하는 kube-proxy 입니다.

출처: https://learn.microsoft.com/ko-kr/azure/architecture/aws-professional/eks-to-aks/node-pools

 

 

이때 CSP(Cloud Service Provider)에서 제공하는 Managed Kubernetes Service에서는 컨트롤 플레인은 내부적으로 관리하므로, 사용자는 보통 데이터 플레인 혹은 워커 노드라고 불리는 노드들을 관리하게 됩니다.

 

보통 CSP에는 Stateless한 애플리케이션의 스케일 인/아웃을 지원하는 목적으로 동일한 목적을 가지는 가상 머신을 이미지로 만들고 이를 바탕으로 가상 머신의 스케일링을 지원하는 서비스를 제공합니다. Kubernetes의 노드는 상태를 가질 필요가 없기 때문에 이러한 가상 머신의 세트를 기반으로 노드를 제공합니다. 

 

그래서 EKS에서는 ASG(Auto Scaling Group)를 바탕으로 노드 그룹(Node Group)이란 이름으로 Kubernetes 노드를 오토 스케일링 하도록 제공하고 있으며, AKS에서는 VMSS(Virtual Machine Scale Set)을 바탕으로 한 노드 풀(Node Pool)로 동일한 기능을 제공하고 있습니다.

 

그래서 Managed Kubernetes Service에서 Kubernetes의 노드를 늘이거나 줄이는 행위, 여기서 더 나아가 ClusterAutoscaler의 동작은 이러한 제반 가상 머신 세트의 API를 호출하는 방식으로 구현됩니다.

 

 

2. EKS 노드 그룹 유형

문서의 카테고리를 바탕으로 확인해보면 EKS에서 제공하는 노드 그룹에는 아래와 같은 유형이 있습니다.

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

  • 관리형 노드 그룹(Managed node grups)
  • 자체 관리형 노드그룹(Self-managed nodes)
  • AWS Fargate
  • 하이브리드 노드(Hybrid nodes)

 

또한 Pre-built optimized AMI라는 카테고리가 있는데, 작성자가 이해하기로는 노드 그룹의 유형이라기 보다는 AWS에서 미리 Amazon Linux, Windows, Bottlerocket, Ubuntu와 같은 OS를 기반으로 최적화된 AMI를 선택할 수 있다는 의미로 이해했습니다.

 

그리고 관리형 노드 그룹의 용량 유형(Capacity type)은 온디맨드(OnDemand)와 스팟(Spot)으로 나뉩니다. 지정하지 않은 경우 온디맨드가 기본이며, Spot은 큰폭의 할인을 제공하는 여분 인스턴스를 사용하는 의미로, 필요한 경우 AWS에 의해서 리소스를 빼앗길 수 있습니다.

 

지금까지 살펴본 EKS와 AKS에는 몇 가지 부분에서 사상이 다른 점을 느낄 수 있는데, 먼저 EKS는 가볍하다는 느낌이 있습니다. 앞서 살펴본 바와 같이 노드 구성 요소가 최소화 되어 있고 CNI나 네트워크/스토리지 구현체도 최소화되어 있습니다.

 

두번째 차이는 EKS는 Managed Service의 일부를 customize 하도록 제공하고 있다는 부분입니다. 노드를 제공하는 방식에서도 AKS와 크게 다른데 AKS는 Fully Managed Service를 지향하기 때문에 관리형 노드 풀만 제공하며, 또한 노드의 설정을 Customize하는 데 있어도 인터페이스를 제공(Custom Node Configuration for AKS node pools, https://learn.microsoft.com/en-us/azure/aks/custom-node-configuration?tabs=linux-node-pools)하거나 그게 아니면 엄격하게 제한하는 점에 차이가 있습니다.

 

노드 customization에 대한 관점의 차이를 살펴보면 관리형 노드 그룹에서도 preBootstrapCommands 과 같은 방식으로 노드에 추가로 필요한 명령을 전달하는 형태로 customize를 가능 하도록 합니다.

 

아래는 amazon linux 2023 노드 그룹에서 dnf(패키지 관리자)로 추가 패키지를 설치하는 예시입니다.

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

 

그 외에도 eksctl 의 Config File Schema(https://eksctl.io/usage/schema/)를 살펴보면 kubelet custom config을 제공하는 kubeletExtraConfig 와 같은 필드도 있으며, 아래와 같이 Launch Template을 통해서 User Data를 전달하면서 가능한 변경사항을 살펴보실 수 있습니다.

출처: https://www.youtube.com/watch?v=lHYiew91iHY

 

내용을 살펴보면 userdata를 base64로 변경하여 LaunchTempateData.UserData로 전달하는 것으로 이해됩니다.

출처: https://youtu.be/lHYiew91iHY?t=821

 

마지막으로 이를 lauch template으로 생성해 새로운 노드 그룹을 생성합니다.

 

결국엔 특별한 목적이나 요구 사항이 있는 경우에는 Custom AMI 를 통해서 자체 관리형 노드 그룹을 사용할 수 있다는 결론에 다다릅니다. AKS는 자체 이미지를 통한 노드 풀 사용이 불가합니다.

 

또 한가지 차이로 EKS의 노드 그룹은 여러가서 인스턴스 타입(아래 --instance-types 옵션)을 지정할 수 있는 점이 있습니다.

aws eks create-nodegroup \
  --cluster-name $CLUSTER_NAME \
  --nodegroup-name managed-spot \
  --subnets $PubSubnet1 $PubSubnet2 $PubSubnet3 \
  --node-role $NODEROLEARN \
  --instance-types c5.large c5d.large c5a.large \
  --capacity-type SPOT \
  --scaling-config minSize=2,maxSize=3,desiredSize=2 \
  --disk-size 20

 

최근 AKS에서는 Virtual Machine 노드 풀이라는 새로운 유형의 노드 풀을 Preview로 발표했으며, 이 가상 머신 노드풀은 여러가지 유형의 VMSize를 가질 수 있습니다.

https://learn.microsoft.com/en-us/azure/aks/virtual-machines-node-pools

 

노드와 관련해 AKS에만 있는 개념은 시스템 노드 풀과 유저 노드 풀의 분리입니다. 이는 사용자 워크로드로 인해 시스템 컴포넌트(coredns, metrics-server 등)에 영향을 미치지 않도록 분리하려는 의도로 만들어진 구분입니다. dedicated system node pool은 taint로 구성될 수 있으며, 상세한 내용은 아래 문서를 참고 하시기 바랍니다.

https://learn.microsoft.com/en-us/azure/aks/use-system-pools?tabs=azure-cli

 

살펴보기로는 EKS는 사용자에 상당 부분의 자율성을 제공하는 것으로도 이해되고, 반대로 AKS는 최대한 Managed 영역으로 서비스를 제공하려는 입장(상품에서 검증된 인터페이스만 제공)을 가진 것으로도 보입니다.

 

몇 가지 근본적인 차이 외에는 제공하는 노드 형태는 EKS와 AKS와 유사합니다. EKS의 노드 그룹과 AKS의 노드풀에 대해서 아래 문서를 참고하실 수 있습니다.

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

 

 

3. EKS 노드 그룹 AL2 -> AL2023

앞선 실습을 진행한 상황이라면 Amazon Linux 2023을 기반으로한 노드풀이 생성되어 있을 것입니다.

만약 앞선 실습 환경 배포를 하지 않았다면 [3-1] EKS의 스토리지 옵션(https://a-person.tistory.com/33)을 확인 부탁드립니다.

Amazon Linux2 는 /etc/eks/bootstrap.sh 를 사용해 노드 초기화 프로세스를 가지고 있었다면, Amazon Linux 2023는 선언형 방식으로 YAML 구성 스키마를 사용하는 nodeadm을 통해서 노드 세팅을 하도록 변경하였습니다.

[root@ip-192-168-1-6 /]# cat  /etc/eks/bootstrap.sh
#!/usr/bin/env bash

echo >&2 '
!!!!!!!!!!
!!!!!!!!!! ERROR: bootstrap.sh has been removed from AL2023-based EKS AMIs.
!!!!!!!!!!
!!!!!!!!!! EKS nodes are now initialized by nodeadm.
!!!!!!!!!!
!!!!!!!!!! To migrate your user data, see:
!!!!!!!!!!
!!!!!!!!!!     https://awslabs.github.io/amazon-eks-ami/nodeadm/
!!!!!!!!!!
'

exit 1

 

예를 들어, Max Pod 개수의 변경을 한 경우 /etc/kubernetes/kubelet/config.json.d/00-nodeadm.conf 을 생성하여 기본 설정 파일인 /etc/kubernetes/kubelet/config.json을 overwrite하는 방식을 사용 합니다.

[root@ip-192-168-1-6 /]# cat /etc/kubernetes/kubelet/config.json | grep maxPods
    "maxPods": 17,
[root@ip-192-168-1-6 /]# cat /etc/kubernetes/kubelet/config.json.d/00-nodeadm.conf | grep maxPods
    "maxPods": 100

 

또한 cgroupv1cgroupv2로 변경되는 것과 같은 주요 변경이 있으므로, 상세한 내용은 아래를 참고 부탁드립니다.

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

 

 

4. 다양한 노드 사용해 보기

EKS에서 제공하는 다양한 노드 그룹을 생성해보겠습니다.

 

Graviton Instance 노드 그룹

Graviton Instance는 Amazon에서 제공하는 ARM 인스턴스 입니다. 아래와 같이 테스트 할 수 있습니다.

# 노드의 아키텍처를 확인
kubectl get nodes -L kubernetes.io/arch

NAME                                               STATUS   ROLES    AGE    VERSION               ARCH
ip-192-168-1-6.ap-northeast-2.compute.internal     Ready    <none>   156m   v1.31.5-eks-5d632ec   amd64
ip-192-168-2-172.ap-northeast-2.compute.internal   Ready    <none>   157m   v1.31.5-eks-5d632ec   amd64
ip-192-168-3-246.ap-northeast-2.compute.internal   Ready    <none>   156m   v1.31.5-eks-5d632ec   amd64

# 신규 노드 그룹 생성 (eksctl create nodegroup --help)
eksctl create nodegroup -c $CLUSTER_NAME -r ap-northeast-2 --subnet-ids "$PubSubnet1","$PubSubnet2","$PubSubnet3" \
  -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

# 확인 (arm64)
kubectl get nodes -L kubernetes.io/arch
NAME                                               STATUS   ROLES    AGE     VERSION               ARCH
ip-192-168-1-6.ap-northeast-2.compute.internal     Ready    <none>   161m    v1.31.5-eks-5d632ec   amd64
ip-192-168-2-172.ap-northeast-2.compute.internal   Ready    <none>   161m    v1.31.5-eks-5d632ec   amd64
ip-192-168-3-188.ap-northeast-2.compute.internal   Ready    <none>   2m12s   v1.31.5-eks-5d632ec   arm64
ip-192-168-3-246.ap-northeast-2.compute.internal   Ready    <none>   161m    v1.31.5-eks-5d632ec   amd64

 kubectl get nodes --label-columns eks.amazonaws.com/nodegroup,kubernetes.io/arch,eks.amazonaws.com/capacityType
NAME                                               STATUS   ROLES    AGE     VERSION               NODEGROUP   ARCH    CAPACITYTYPE
ip-192-168-1-6.ap-northeast-2.compute.internal     Ready    <none>   162m    v1.31.5-eks-5d632ec   ng1         amd64   ON_DEMAND
ip-192-168-2-172.ap-northeast-2.compute.internal   Ready    <none>   162m    v1.31.5-eks-5d632ec   ng1         amd64   ON_DEMAND
ip-192-168-3-188.ap-northeast-2.compute.internal   Ready    <none>   2m53s   v1.31.5-eks-5d632ec   ng3         arm64   ON_DEMAND
ip-192-168-3-246.ap-northeast-2.compute.internal   Ready    <none>   162m    v1.31.5-eks-5d632ec   ng1         amd64   ON_DEMAND

 

다만 이러한 환경은 multi-platform을 가진 노드를 가지기 때문에 파드들의 스케줄링에 주의가 필요합니다. 잘못된 스케줄링을 방지하기 위해서 taint를 사용할 수 있습니다.

# taint 정보 확인
aws eks describe-nodegroup --cluster-name $CLUSTER_NAME --nodegroup-name ng3 | jq .nodegroup.taints

# taints 적용 (바로 적용되지 않음)
aws eks update-nodegroup-config --cluster-name $CLUSTER_NAME --nodegroup-name ng3 --taints "addOrUpdateTaints=[{key=arm64, value=true, effect=NO_EXECUTE}]"

# 확인
kubectl describe nodes --selector family=graviton | grep Taints
Taints:             arm64=true:NoExecute

aws eks describe-nodegroup --cluster-name $CLUSTER_NAME --nodegroup-name ng3 | jq .nodegroup.taints
[
  {
    "key": "arm64",
    "value": "true",
    "effect": "NO_EXECUTE"
  }
]

 

또한 워크로드 자체도 ARM은 CPU 아키텍처가 AMD64와 상이하기 때문에 실행되는 이미지 빌드 시점에 다른 아키텍처로 빌드가 이루어져야 합니다. Multi(Cross)-Platform build 에 대해서는 docker buildx와 같은 문서를 참고 부탁드립니다.

https://docs.docker.com/build/building/multi-platform/

 

busybox는 다양한 Plaform을 지원하고 있어, 샘플 예제는 busybox를 통해 진행 하였습니다.

# 파드 생성
cat <<EOF | kubectl apply -f -
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: arm64
      operator: Exists
  nodeSelector:
    family: graviton
EOF

# 파드가 배포된 노드 정보 확인
kubectl get pod -owide
NAME             READY   STATUS    RESTARTS   AGE   IP             NODE                                               NOMINATED NODE   READINESS GATES
busybox          1/1     Running   0          12s   192.168.3.97   ip-192-168-3-188.ap-northeast-2.compute.internal   <none>           <none>

kubectl exec -it busybox -- arch
aarch64

# 삭제
kubectl delete pod busybox

 

실습을 종료하고 해당 노드 그룹을 삭제하도록 하겠습니다.

eksctl delete nodegroup -c $CLUSTER_NAME -n ng3

 

 

Spot 노드 그룹

앞서 노드 그룹에 용량 유형(Capacity Type)을 지정할 수 있다고 했습니다. 아래에서는 Spot 노드 그룹을 추가로 생성해 보겠습니다.

# 노드의 Capacity Type 확인
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-6.ap-northeast-2.compute.internal     Ready    <none>   170m   v1.31.5-eks-5d632ec   ON_DEMAND
ip-192-168-2-172.ap-northeast-2.compute.internal   Ready    <none>   170m   v1.31.5-eks-5d632ec   ON_DEMAND
ip-192-168-3-246.ap-northeast-2.compute.internal   Ready    <none>   170m   v1.31.5-eks-5d632ec   ON_DEMAND

# 노드 그룹 생성
NODEROLEARN=$(aws iam list-roles --query "Roles[?contains(RoleName, 'nodegroup-ng1')].Arn" --output text)
echo $NODEROLEARN

aws eks create-nodegroup \
  --cluster-name $CLUSTER_NAME \
  --nodegroup-name managed-spot \
  --subnets $PubSubnet1 $PubSubnet2 $PubSubnet3 \
  --node-role $NODEROLEARN \
  --instance-types c5.large c5d.large c5a.large \
  --capacity-type SPOT \
  --scaling-config minSize=2,maxSize=3,desiredSize=2 \
  --disk-size 20


# 명령이 바로 프롬프트가 떨어지므로, spot 노드그룹이 완전 Ready가 될때까지 대기하도록 합니다.
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
kubectl get nodes -L eks.amazonaws.com/capacityType,eks.amazonaws.com/nodegroup
NAME                                               STATUS   ROLES    AGE    VERSION               CAPACITYTYPE   NODEGROUP
ip-192-168-1-167.ap-northeast-2.compute.internal   Ready    <none>   68s    v1.31.5-eks-5d632ec   SPOT           managed-spot
ip-192-168-1-6.ap-northeast-2.compute.internal     Ready    <none>   173m   v1.31.5-eks-5d632ec   ON_DEMAND      ng1
ip-192-168-2-15.ap-northeast-2.compute.internal    Ready    <none>   68s    v1.31.5-eks-5d632ec   SPOT           managed-spot
ip-192-168-2-172.ap-northeast-2.compute.internal   Ready    <none>   173m   v1.31.5-eks-5d632ec   ON_DEMAND      ng1
ip-192-168-3-246.ap-northeast-2.compute.internal   Ready    <none>   173m   v1.31.5-eks-5d632ec   ON_DEMAND      ng1

 

2대의 Spot 노드가 추가되었습니다. 노드풀에 실행한 파드를 생성하겠습니다.

cat <<EOF | kubectl apply -f -
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
EOF

# 파드가 배포된 노드 정보 확인
kubectl get pod -owide
NAME             READY   STATUS    RESTARTS   AGE   IP             NODE                                              NOMINATED NODE   READINESS GATES
busybox          1/1     Running   0          8s    192.168.2.98   ip-192-168-2-15.ap-northeast-2.compute.internal   <none>           <none>

# 삭제
kubectl delete pod busybox

 

파드가 Spot 노드 중 하나에 배포된 것을 알 수 있습니다. 다만 Spot 노드는 Preempt 될 수 있기 때문에 일시적이거나 혹은 distruption에 문제가 없는 워크로드를 실행하는 것을 권장합니다.

Spot 노드 그룹 또한 taint를 주는 방법으로 일반 워크로드가 실행되지 않도록 유도하는 것이 알맞습니다.

 

AKS에서도 spot 노드 풀을 생성할 수 있습니다.

https://learn.microsoft.com/en-us/azure/aks/spot-node-pool

 

AKS에서는 spot 노드 풀에 기본적으로 kubernetes.azure.com/scalesetpriority=spot:NoSchedule taint를 적용합니다. 이는 고객의 워크로드가 의도치 않게 스케줄링 되는 이슈를 방지하고자 하는 목적으로, Spot 노드풀에 실행하고자 하는 워크로드에는 toleration이 필요한 점을 유의해야 합니다.

spec:
  containers:
  - name: spot-example
  tolerations:
  - key: "kubernetes.azure.com/scalesetpriority"
    operator: "Equal"
    value: "spot"
    effect: "NoSchedule"
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: "kubernetes.azure.com/scalesetpriority"
            operator: In
            values:
            - "spot"
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 1
        preference:
          matchExpressions:
          - key: another-node-label-key
            operator: In
            values:
            - another-node-label-value

 

마지막으로 실습에 사용한 노드풀을 삭제합니다.

 eksctl delete nodegroup -c $CLUSTER_NAME -n managed-spot

 

 

5. 리소스 정리

실습을 모두 마쳤다면, 아래와 같이 EKS를 삭제하고, 삭제를 확인한 뒤 CloudFormation으로 생성한 실습 환경을 삭제합니다.

# EKS 삭제
eksctl delete cluster --name $CLUSTER_NAME

# 실습 환경 삭제
aws cloudformation delete-stack --stack-name myeks

 

 

마무리

EKS의 스토리지 옵션과 노드 그룹에 대해서 확인해보았습니다.

 

Azure 환경을 주로 사용하기 때문에, EKS에 생성된 리소스를 바탕으로 AKS와 일부 비교를 해볼 수 있었습니다.

생각보다 시간이 너무 오래걸리기도 했고, 또 어떤 부분은 아직 EKS의 개념이 완벽하지 않은 부분도 있어서, 이후 학습을 더 진행해보고 내용을 보강하도록 하겠습니다.

 

다음 시간에는 EKS의 Observability에 대해서 학습해보고 내용을 정리하도록 하겠습니다.

이번 포스트에서는 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)에서 삭제하도록 하겠습니다.

 

Amazon EKS의 네트워킹에서 Kubernetes의 네트워크와 인그레스 트래픽을 AWS에서 어떻게 구현했는지 살펴보겠습니다.

지난 포스트에서는 EKS Networking Part 1으로 VPC CNI 개요와 노드 환경 구성을 살펴보고, 파드 통신 과정을 살펴봤습니다. 이번 포스트는 EKS Networking Part2로 LoadBalancer 유형의 서비스의 구성과 Ingress에 대해서 확인해 보겠습니다.

그리고 이들의 구현이 AKS(Azure Kubernetes Service)에서는 어떻게 다른지 설명을 해보겠습니다.

 

 

목차

  1. 실습 환경
  2. Kubernetes의 네트워크 개요
  3. AWS의 LoadBalancer 유형
  4. EKS의 LoadBalancer 구현
  5. EKS의 LoadBalancer 실습
  6. AKS의 LoadBalancer 구현
  7. EKS의 Ingress 구현 및 실습
  8. AKS의 Ingress 구현
  9. 리소스 정리

 

 

1 . 실습 환경

실습 환경

EKS Networking Part1에서 사용한 EKS를 그대로 사용하겠습니다. 아래와 같은 구성입니다.

 

혹시나 동일한 테스트를 진행하고자 한다면, 지난 포스트 EKS Networking Part1(https://a-person.tistory.com/30)의 3. 실습 환경 배포 절을 참고 부탁드립니다.

 

 

클러스터 관련 정보는 아래와 같습니다. EKS 1.31.5 버전입니다.

$ kubectl cluster-info
Kubernetes control plane is running at https://04559E327E31C75E5F6A835B3E0B6AED.gr7.ap-northeast-2.eks.amazonaws.com
CoreDNS is running at https://04559E327E31C75E5F6A835B3E0B6AED.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
NAME    REGION          EKSCTL CREATED
myeks   ap-northeast-2  True

$kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone
NAME                                               STATUS   ROLES    AGE    VERSION               INSTANCE-TYPE   CAPACITYTYPE   ZONE
ip-192-168-1-228.ap-northeast-2.compute.internal   Ready    <none>   116m   v1.31.5-eks-5d632ec   t3.medium       ON_DEMAND      ap-northeast-2a
ip-192-168-2-91.ap-northeast-2.compute.internal    Ready    <none>   116m   v1.31.5-eks-5d632ec   t3.medium       ON_DEMAND      ap-northeast-2b
ip-192-168-3-155.ap-northeast-2.compute.internal   Ready    <none>   116m   v1.31.5-eks-5d632ec   t3.medium       ON_DEMAND      ap-northeast-2c

 

 

2. Kubernetes의 네트워크 개요

Kubernetes의 네트워크에 관련된 주요 컴포넌트와 오브젝트로 CNI, Service, Ingress 등이 있는데, 이들은 각각 어떤 역할을 하는 걸까요? 이러한 요소들은 Kubernetes의 네트워크를 구성해주기도 하고, 한편으로는 워크로드의 서비스를 노출해주는 역할을 합니다.

 

먼저 CNI는 컨테이너 네트워킹을 가능하도록 구성해주는 역할을 합니다. 이를 통해 파드는 통신 가능한 상태가 됩니다.

파드에 실행된 워크로드를 사용자가 접근하기 위해서는 Service가 필요합니다. Service에는 Cluster IP, Nodeport, LoadBalancer 유형이 있습니다. 간단히 클러스터 내의 다른 Micro 서비스에 대한 호출은 Cluster IP를 사용하고, 외부 통신은 Nodeport나 LoadBalancer 유형의 Service로 가능합니다.

 

다만 LoadBalancer 유형의 서비스는 클러스터 외부에 IP를 노출해야 하기 때문에 다른 구현체(컨트롤러)를 통해서 Loadbalancing을 구현하고, 엔드포인트 IP를 클러스터의 LoadBalancer 유형의 서비스에 노출 해야합니다. 그래서 보통은 CSP에서 Cloud Controller를 통해 구현을 하거나, On-Premise에서는 LoxiLB 와 같은 솔루션을 사용할 수 있습니다.

또한 노드 수준에서는 iptables, IPVS, eBPF 로 Service 와 접점(Endpoint)를 구성해야 하는데, kube-proxy 를 통해 iptables를 사용하는 방법이 일반적입니다.

이때 Service는 L4 수준의 통신을 가능하도록 하므로, L7 통신을 가능하게 하기 위해 Ingress 리소스를 사용할 수 있습니다.

 

본 포스트에서는 기본적인 Cluster IP, Nodeport에 대한 설명은 제외하고, EKS에서 LoadBalancer 유형의 서비스와 Ingress 서비스를 어떤 식으로 구현했는지를 살펴보겠습니다.

 

 

3. AWS의 LoadBalancer 유형

EKS의 Loadbalancer와 Ingress는 각 다른 유형의 Loadbalancer 로 구현되어 있습니다. EKS 실습에 앞서 AWS의 Loadbalancer 서비스 유형을 간단히 살펴보겠습니다.

참고: https://docs.aws.amazon.com/ko_kr/elasticloadbalancing/latest/userguide/how-elastic-load-balancing-works.html

 

Elastic Load Balancing은 다음 유형의 로드 밸런서를 지원합니다.

  • Application Load Balancers (ALB)
  • Network Load Balancer (NLB)
  • Gateway Load Balancer (GWLB)
  • Classic Load Balancer(CLB)

CLB는 가장 오래된 ELB의 형태로, L4/L7 계층의 로드밸런싱이 가능하지만, 하나의 주소에 하나의 대상 그룹 밖에 지정을 할 수 없는 것과 같은 몇가지 제한사항이 있습니다. CLB는 Legacy로 분류되어 많이 사용하지 않습니다.

 

ALB는 L7계층의 로드밸런싱이 가능합니다. L7의 로드밸런싱이 가능하기 때문에 HTTP 헤더 정보를 바탕으로 라우팅을 하거나 Path Based 라우팅도 가능합니다.

 

NLB는 L4 계층이 로드밸런서 입니다. 고성능 부하 분산을 하기에 적합하며, EIP로 공인 IP를 고정할 수 있는 장점이 있습니다. 노출된 IP를 통해 DNS를 제공해줘 접근도 가능합니다.

 

GWLB는 L3 계층에서 작동합니다. GWLB를 통해서 방화벽, 침입 탐지, 패킷 검사와 같은 가상 어플라인언스를 배포 확장하는데 활요될 수 있습니다. GWLB는 앞서 설명한 LoadBalancer와는 역할이 다소 다른 것 같습니다.

 

EKS에서는 NLB 혹은 ALB를 사용하여 워크로드의 인바운드 서비스를 가능하도록 합니다.

 

 

4. EKS의 LoadBalancer 구현

EKS에서 LoadBalancer 유형의 서비스가 생성되면 AWS의 Load Balancer가 프로비저닝 됩니다.

어떤 방식으로 구현되었지를 살펴보기 위해서 아래 문서를 바탕으로 정리했습니다.

https://docs.aws.amazon.com/eks/latest/best-practices/load-balancing.html

 

EKS에서는 로드밸런서를 프로비저닝을 하는 두 가지 방법이 있습니다.

  • AWS Cloud Provider Load balancer Controller (레거시)
  • AWS Load Balancer Controller (권장)

기본적으로 Kubernetes의 LoadBalancer 유형의 서비스는 kube-controller-manager 혹은 cloud-controller-manager(in-tree controller)의 Cloud Provider 컴포넌트에 내장된 Service Controller 의해서 조정됩니다.

이때 AWS Cloud Provider Load balancer Controller는 레거시로, 현재는 critical bug fix만 가능합니다. AWS Cloud Provider Load balancer Controller에서는 CLB가 기본적으로 생성되고, 알맞은 annotation을 통해 NLB를 생성할 수 있습니다.

 

AWS 에서는 확장된 기능을 제공하기 위해서 AWS Load Balancer Controller를 EKS에 설치하는 방식을 제공하며, 이 경우 LoadBalancerClass에서 service.beta.kubernetes.io/aws-load-balancer-type Annotation으로 Load Balancer Controller로 명시적으로 오프로드를 해야합니다.

 

Note: 확인해보면 lodbalancerClass라는 리소스는 없는데, 실제 어떤 의미인지 정확하지 않습니다. 이후 실습에서 사용하는 LoadBalancer 유형의 서비스에서 loadBalancerClass: service.k8s.aws/nlb 로 지정하기는 합니다.

 

이제 Load Balancer 대상 유형에 따른 차이를 살펴보겠습니다.

EKS의 LoadBalancer 유형의 서비스에 service.beta.kubernetes.io/aws-load-balancer-nlb-target-type Annotation을 통해 대상 유형을 instance 혹은 ip 유형을 지정할 수 있습니다.

 

먼저 instance 로 지정한 경우 아래와 같은 구성으로 연결됩니다.

출처: https://docs.aws.amazon.com/eks/latest/best-practices/load-balancing.html

 

이 구성에서는 NodePort 를 통해서 대상이 등록되는 것을 알 수 있습니다. 이로 인해서 패킷은 노드로 전달되고 이후 iptables를 따라서 파드로 패킷이 전달되는 추가 hop이 발생합니다.

 

이 때문에 대상 유형을 ip로 지정하는 것이 권장되며, 이때 아래와 같이 구성됩니다.

 

출처: https://docs.aws.amazon.com/eks/latest/best-practices/load-balancing.html

 

이 방식은 Load Balancer의 대상이 바로 파드 IP로 등록됨에 따라, 네트워크 경로가 단순화 되고 iptables의 오버헤드가 제거되는 장점을 가지게 됩니다.

 

 

5. EKS의 LoadBalancer 실습

먼저 아무런 설정이 없이 LoadBalancer 서비스를 생성해 보겠습니다.

# 터미널1 (모니터링)
watch -d 'kubectl get pod,svc'

# 수퍼마리오 디플로이먼트 배포
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mario
  labels:
    app: mario
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mario
  template:
    metadata:
      labels:
        app: mario
    spec:
      containers:
      - name: mario
        image: pengbai/docker-supermario
---
apiVersion: v1
kind: Service
metadata:
   name: mario
spec:
  selector:
    app: mario
  ports:
  - port: 80
    protocol: TCP
    targetPort: 8080
  type: LoadBalancer
EOF


# 배포 확인 : CLB 배포 확인
kubectl get deploy,svc,ep mario

# 마리오 게임 접속 : CLB 주소로 웹 접속
kubectl get svc mario -o jsonpath={.status.loadBalancer.ingress[0].hostname} | awk '{ print "Maria URL = http://"$1 }'

 

배포가 되고 서비스에 EXTERNAL-IP가 할당되었습니다.

NAME                         READY   STATUS    RESTARTS   AGE
pod/mario-6d8c76fd8d-kqmft   1/1     Running   0          22s

NAME                 TYPE           CLUSTER-IP     EXTERNAL-IP                                                                    PORT(S)        AGE
service/kubernetes   ClusterIP      10.100.0.1     <none>                                                                         443/TCP        50m
service/mario        LoadBalancer   10.100.40.60   ac12f1c2b92b84516b339ffeea5afd12-1085404426.ap-northeast-2.elb.amazonaws.com   80:31207/TCP   22s

 

이 상황에서 웹 콘솔에서 배포된 리소스를 확인해보겠습니다.

 

Service 오브젝트에 아무런 annotation이 없는 상황으로 CLB가 배포된 것을 확인할 수 있습니다.

 

 

AWS Load Balancer Controller 배포

이제 AWS Load Balancer Controller를 배포해 보겠습니다. Helm(https://helm.sh/docs/intro/install/)을 사용합니다.

주요 컴포넌트가 addon이 아닌 형태로 제공되면 이후에 업그레이드나 관리에 어려움이 있을텐데 이 컴포넌트는 왜 addon이 아닐지 의문이 듭니다. (Helm은 Life Cycle Management를 할 수 없음)

# 설치 전 CRD 확인
$ kubectl get crd
NAME                                         CREATED AT
cninodes.vpcresources.k8s.aws                2025-02-15T14:39:16Z
eniconfigs.crd.k8s.amazonaws.com             2025-02-15T14:41:52Z
policyendpoints.networking.k8s.aws           2025-02-15T14:39:16Z
securitygrouppolicies.vpcresources.k8s.aws   2025-02-15T14:39:16Z

$ kubectl get ingressclass
No resources found

# 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

 

배포가 완료되면 2개의 CRD와 ingress class가 생성된 것을 확인할 수 있습니다.

## 설치 확인
$ kubectl get crd
NAME                                         CREATED AT
cninodes.vpcresources.k8s.aws                2025-02-15T14:39:16Z
eniconfigs.crd.k8s.amazonaws.com             2025-02-15T14:41:52Z
ingressclassparams.elbv2.k8s.aws             2025-02-15T15:35:37Z
policyendpoints.networking.k8s.aws           2025-02-15T14:39:16Z
securitygrouppolicies.vpcresources.k8s.aws   2025-02-15T14:39:16Z
targetgroupbindings.elbv2.k8s.aws            2025-02-15T15:35:37Z

$ kubectl get ingressclass
NAME   CONTROLLER            PARAMETERS   AGE
alb    ingress.k8s.aws/alb   <none>       104s

# 생성된 CRD
$ kubectl explain ingressclassparams.elbv2.k8s.aws
GROUP:      elbv2.k8s.aws
KIND:       IngressClassParams
VERSION:    v1beta1

DESCRIPTION:
    IngressClassParams is the Schema for the IngressClassParams API

FIELDS:
  apiVersion    <string>
    APIVersion defines the versioned schema of this representation of an object.
    Servers should convert recognized schemas to the latest internal value, and
    may reject unrecognized values. More info:
    https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources

  kind  <string>
    Kind is a string value representing the REST resource this object
    represents. Servers may infer this from the endpoint the client submits
    requests to. Cannot be updated. In CamelCase. More info:
    https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds

  metadata      <ObjectMeta>
    Standard object's metadata. More info:
    https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata

  spec  <Object>
    IngressClassParamsSpec defines the desired state of IngressClassParams


$ kubectl explain targetgroupbindings.elbv2.k8s.aws
GROUP:      elbv2.k8s.aws
KIND:       TargetGroupBinding
VERSION:    v1beta1

DESCRIPTION:
    TargetGroupBinding is the Schema for the TargetGroupBinding API

FIELDS:
  apiVersion    <string>
    APIVersion defines the versioned schema of this representation of an object.
    Servers should convert recognized schemas to the latest internal value, and
    may reject unrecognized values. More info:
    https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources

  kind  <string>
    Kind is a string value representing the REST resource this object
    represents. Servers may infer this from the endpoint the client submits
    requests to. Cannot be updated. In CamelCase. More info:
    https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds

  metadata      <ObjectMeta>
    Standard object's metadata. More info:
    https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata

  spec  <Object>
    TargetGroupBindingSpec defines the desired state of TargetGroupBinding

  status        <Object>
    TargetGroupBindingStatus defines the observed state of TargetGroupBinding

 

실제 생성된 디플로이먼트도 확인해봅니다.

$ kubectl get deployment -n kube-system aws-load-balancer-controller
NAME                           READY   UP-TO-DATE   AVAILABLE   AGE
aws-load-balancer-controller   2/2     2            2           5m59s
$ kubectl describe deploy -n kube-system aws-load-balancer-controller
Name:                   aws-load-balancer-controller
Namespace:              kube-system
CreationTimestamp:      Sun, 16 Feb 2025 00:35:40 +0900
Labels:                 app.kubernetes.io/instance=aws-load-balancer-controller
                        app.kubernetes.io/managed-by=Helm
                        app.kubernetes.io/name=aws-load-balancer-controller
                        app.kubernetes.io/version=v2.11.0
                        helm.sh/chart=aws-load-balancer-controller-1.11.0
Annotations:            deployment.kubernetes.io/revision: 1
                        meta.helm.sh/release-name: aws-load-balancer-controller
                        meta.helm.sh/release-namespace: kube-system
Selector:               app.kubernetes.io/instance=aws-load-balancer-controller,app.kubernetes.io/name=aws-load-balancer-controller
Replicas:               2 desired | 2 updated | 2 total | 2 available | 0 unavailable
StrategyType:           RollingUpdate
MinReadySeconds:        0
RollingUpdateStrategy:  25% max unavailable, 25% max surge
Pod Template:
  Labels:           app.kubernetes.io/instance=aws-load-balancer-controller
                    app.kubernetes.io/name=aws-load-balancer-controller
  Annotations:      prometheus.io/port: 8080
                    prometheus.io/scrape: true
  Service Account:  aws-load-balancer-controller
  Containers:
   aws-load-balancer-controller:
    Image:       public.ecr.aws/eks/aws-load-balancer-controller:v2.11.0
    Ports:       9443/TCP, 8080/TCP
    Host Ports:  0/TCP, 0/TCP
    Args:
      --cluster-name=myeks
      --ingress-class=alb
    Liveness:     http-get http://:61779/healthz delay=30s timeout=10s period=10s #success=1 #failure=2
    Readiness:    http-get http://:61779/readyz delay=10s timeout=10s period=10s #success=1 #failure=2
    Environment:  <none>
    Mounts:
      /tmp/k8s-webhook-server/serving-certs from cert (ro)
  Volumes:
   cert:
    Type:               Secret (a volume populated by a Secret)
    SecretName:         aws-load-balancer-tls
    Optional:           false
  Priority Class Name:  system-cluster-critical
  Node-Selectors:       <none>
  Tolerations:          <none>
Conditions:
  Type           Status  Reason
  ----           ------  ------
  Available      True    MinimumReplicasAvailable
  Progressing    True    NewReplicaSetAvailable
OldReplicaSets:  <none>
NewReplicaSet:   aws-load-balancer-controller-554fbd9d (2/2 replicas created)
Events:
  Type    Reason             Age    From                   Message
  ----    ------             ----   ----                   -------
  Normal  ScalingReplicaSet  8m35s  deployment-controller  Scaled up replica set aws-load-balancer-controller-554fbd9d to 2

$ 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
Name:         aws-load-balancer-controller-rolebinding
Labels:       app.kubernetes.io/instance=aws-load-balancer-controller
              app.kubernetes.io/managed-by=Helm
              app.kubernetes.io/name=aws-load-balancer-controller
              app.kubernetes.io/version=v2.11.0
              helm.sh/chart=aws-load-balancer-controller-1.11.0
Annotations:  meta.helm.sh/release-name: aws-load-balancer-controller
              meta.helm.sh/release-namespace: kube-system
Role:
  Kind:  ClusterRole
  Name:  aws-load-balancer-controller-role
Subjects:
  Kind            Name                          Namespace
  ----            ----                          ---------
  ServiceAccount  aws-load-balancer-controller  kube-system

$ kubectl describe clusterroles.rbac.authorization.k8s.io aws-load-balancer-controller-role
...
PolicyRule:
  Resources                                     Non-Resource URLs  Resource Names  Verbs
  ---------                                     -----------------  --------------  -----
  targetgroupbindings.elbv2.k8s.aws             []                 []              [create delete get list patch update watch]
  events                                        []                 []              [create patch]
  ingresses                                     []                 []              [get list patch update watch]
  services                                      []                 []              [get list patch update watch]
  ingresses.extensions                          []                 []              [get list patch update watch]
  services.extensions                           []                 []              [get list patch update watch]
  ingresses.networking.k8s.io                   []                 []              [get list patch update watch]
  services.networking.k8s.io                    []                 []              [get list patch update watch]
  endpoints                                     []                 []              [get list watch]
  namespaces                                    []                 []              [get list watch]
  nodes                                         []                 []              [get list watch]
  pods                                          []                 []              [get list watch]
  endpointslices.discovery.k8s.io               []                 []              [get list watch]
  ingressclassparams.elbv2.k8s.aws              []                 []              [get list watch]
  ingressclasses.networking.k8s.io              []                 []              [get list watch]
  ingresses/status                              []                 []              [update patch]
  pods/status                                   []                 []              [update patch]
  services/status                               []                 []              [update patch]
  targetgroupbindings/status                    []                 []              [update patch]
  ingresses.elbv2.k8s.aws/status                []                 []              [update patch]
  pods.elbv2.k8s.aws/status                     []                 []              [update patch]
  services.elbv2.k8s.aws/status                 []                 []              [update patch]
  targetgroupbindings.elbv2.k8s.aws/status      []                 []              [update patch]
  ingresses.extensions/status                   []                 []              [update patch]
  pods.extensions/status                        []                 []              [update patch]
  services.extensions/status                    []                 []              [update patch]
  targetgroupbindings.extensions/status         []                 []              [update patch]
  ingresses.networking.k8s.io/status            []                 []              [update patch]
  pods.networking.k8s.io/status                 []                 []              [update patch]
  services.networking.k8s.io/status             []                 []              [update patch]
  targetgroupbindings.networking.k8s.io/status  []                 []              [update patch]

 

해당 디플로이먼트는 aws-load-balancer-controller 라는 ServiceAccount를 사용하는데, Service/Ingerss 및 관련된 구성요소들에 대한 권한을 가진 것을 알 수 있습니다.

 

 

대상 유형별 리소스 배포 실습

AWS Load Balancer Controller 를 배포하면 Service 나 Ingress의 매니페스트에 추가된 Annotation이 다릅니다.

 

Note: The configuration of the provisioned load balancer is controlled by annotations that are added to the manifest for the Service or Ingress object and are different when using the AWS Load Balancer Controller than they are when using the AWS cloud provider load balancer controller.
https://docs.aws.amazon.com/eks/latest/best-practices/load-balancing.html

 

 

대상 유형(Target Type)의 차이를 확인하기 위해서 각각 instance 타입과 ip 타입으로 Loadbalancer 서비스를 생성해서 확인해보겠습니다.

# instance type을 위한 디플로이먼트 & 서비스 생성
cat << EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deploy-echo1
spec:
  replicas: 2
  selector:
    matchLabels:
      app: deploy-websrv1
  template:
    metadata:
      labels:
        app: deploy-websrv1
    spec:
      terminationGracePeriodSeconds: 0
      containers:
      - name: aews-websrv
        image: k8s.gcr.io/echoserver:1.5
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: svc-nlb-instance-type
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: instance
    service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
    service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: "8080"
    service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
spec:
  ports:
    - port: 80
      targetPort: 8080
      protocol: TCP
  type: LoadBalancer
  loadBalancerClass: service.k8s.aws/nlb
  selector:
    app: deploy-websrv1
EOF

 

그 다음은 대상 유형을 IP로 배포해 보겠습니다.

# 디플로이먼트 & 서비스 생성
cat << EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deploy-echo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: deploy-websrv
  template:
    metadata:
      labels:
        app: deploy-websrv
    spec:
      terminationGracePeriodSeconds: 0
      containers:
      - name: aews-websrv
        image: k8s.gcr.io/echoserver:1.5
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: svc-nlb-ip-type
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
    service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
    service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: "8080"
    service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
spec:
  ports:
    - port: 80
      targetPort: 8080
      protocol: TCP
  type: LoadBalancer
  loadBalancerClass: service.k8s.aws/nlb
  selector:
    app: deploy-websrv
EOF

 

아래와 같이 CLB로 구성된 mario 서비스와 NLB로 구성된 instance type과 ip type으로 서비스가 각각 생성되었습니다.

$ kubectl get svc
NAME                    TYPE           CLUSTER-IP       EXTERNAL-IP                                                                         PORT(S)        AGE
kubernetes              ClusterIP      10.100.0.1       <none>                                                                              443/TCP        74m
mario                   LoadBalancer   10.100.40.60     ac12f1c2b92b84516b339ffeea5afd12-1085404426.ap-northeast-2.elb.amazonaws.com        80:31207/TCP   24m
svc-nlb-instance-type   LoadBalancer   10.100.73.240    k8s-default-svcnlbin-9674736b71-4aae57171be6e3b8.elb.ap-northeast-2.amazonaws.com   80:32332/TCP   14s
svc-nlb-ip-type         LoadBalancer   10.100.124.220   k8s-default-svcnlbip-d503e33dc2-2ecb99f47d0bc5bf.elb.ap-northeast-2.amazonaws.com   80:30264/TCP   47s

 

웹 콘솔에서 두 대상 유형이 다른 NLB 구성이 어떻게 차이가 나는지 확인해보겠습니다.

 

먼저 instance로 생성한 NLB의 resource map을 살펴보면 Target이 NodePort로 등록되어 있는걸 알 수 있습니다.

이상한 점은 분명 서비스 엔드포인트로 호출이 되고 NodePort도 정상인데, 대상들이 모두 Unhealthy: Health checks failed로 표시된다는 점입니다. 이 부분이 by design인지 정확하지 않습니다.

 

아래는 ip로 생성한 NLB의 resource map입니다. 이 경우에는 파드 IP가 직접 등록된 것을 확인할 수 있습니다.

 

아래로 endpoint 정보를 확인하면 동일한 것을 알 수 있습니다.

$ kubectl get svc
NAME                    TYPE           CLUSTER-IP       EXTERNAL-IP                                                                         PORT(S)        AGE
kubernetes              ClusterIP      10.100.0.1       <none>                                                                              443/TCP        92m
mario                   LoadBalancer   10.100.40.60     ac12f1c2b92b84516b339ffeea5afd12-1085404426.ap-northeast-2.elb.amazonaws.com        80:31207/TCP   42m
svc-nlb-instance-type   LoadBalancer   10.100.73.240    k8s-default-svcnlbin-9674736b71-4aae57171be6e3b8.elb.ap-northeast-2.amazonaws.com   80:32332/TCP   18m
svc-nlb-ip-type         LoadBalancer   10.100.124.220   k8s-default-svcnlbip-d503e33dc2-2ecb99f47d0bc5bf.elb.ap-northeast-2.amazonaws.com   80:30264/TCP   18m
$ kubectl get ep
NAME                    ENDPOINTS                              AGE
kubernetes              192.168.2.190:443,192.168.3.91:443     92m
mario                   192.168.1.136:8080                     42m
svc-nlb-instance-type   192.168.2.180:8080,192.168.3.56:8080   18m
svc-nlb-ip-type         192.168.1.137:8080,192.168.3.45:8080   18m

 

 

6. AKS의 LoadBalancer 구현

Azure의 LoadBalancer 유형의 서비스는 Azure LoadBalancer를 통해 구현됩니다.

AWS의 ELB들이 실제 서브넷에 위치해 파드 IP로 접근한 것과 다르게, Azure Loadbalancer는 서브넷에 위치하지 않습니다. 이러한 이유로 Azure Loadbalancer는 파드 IP로 직접 접근을 하지 못합니다.

 

Note: 파드IP가 가상 네트워크에서 유효한 IP를 가지고 있다고 하더라도 파드 IP로 다른 리소스에 접근하는 것과 다른 리소스에서 파드IP로 접근하는 것은 다릅니다. 파드 IP로 다른 리소스를 접근하는 것은 SNAT로 가능하지만 보통 반대 방향의 통신은 불가합니다.

 

실제 대상이 가상 네트워크에 있지 않으면 리소스는 파드IP로 접근할 수 없습니다.

 

이러한 이유로 AKS의 Azure Loadbalancer는 노드 IP를 통한 백엔드 구성을 합니다. 이 경우 EKS의 service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: instance 와 동일하다고 생각할 수 있지만, Azure Loadbalancer에서는 Floating IP(https://learn.microsoft.com/en-us/azure/load-balancer/load-balancer-floating-ip#floating-ip)라는 방식을 통해서 Loadbalancer에서 SNAT/DNAT이 없이 VIP로 서버로 패킷을 전달하고 iptables에서 DSR(Direct Server Return)할 수 있도록 구현되어 있습니다.

 

service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: instance와 동일한 구성도 가능한데, AKS에서 Loadbalancer 서비스에서 service.beta.kubernetes.io/azure-disable-load-balancer-floating-ip : "true"을 통해 Floating IP를 disable하면 DIP로 패킷이 전달되고, 이 때는 백엔드가 Nodeport 방식으로 구성됩니다.

참고: https://cloud-provider-azure.sigs.k8s.io/topics/loadbalancer/#loadbalancer-annotations

 

참고로 Azure의 Application Gateway는 서브넷에 위치하는 리소스 입니다. AKS의 Ingress 구현체 중 Application gateway Ingresss controller를 사용할 수 있는데, Appilcation Gateway의 백엔드 대상은 파드 IP를 직접 대상으로 등록합니다.

 

 

7. EKS의 Ingress 구현 및 실습

EKS에서 Ingress를 생성하면 ALB가 프로비저닝 됩니다. 또한 아래 문서의 사전 조건을 살펴보면 이를 위해서 AWS Load Balancer Controller 가 있어야 하는 것을 알 수 있습니다.

https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/alb-ingress.html

 

아래와 같이 실습을 진행해 보겠습니다.

# 게임 파드와 Service, Ingress 배포
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Namespace
metadata:
  name: game-2048
---
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: game-2048
  name: deployment-2048
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: app-2048
  replicas: 2
  template:
    metadata:
      labels:
        app.kubernetes.io/name: app-2048
    spec:
      containers:
      - image: public.ecr.aws/l6m2t8p7/docker-2048:latest
        imagePullPolicy: Always
        name: app-2048
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  namespace: game-2048
  name: service-2048
spec:
  ports:
    - port: 80
      targetPort: 80
      protocol: TCP
  type: NodePort
  selector:
    app.kubernetes.io/name: app-2048
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  namespace: game-2048
  name: ingress-2048
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
spec:
  ingressClassName: alb
  rules:
    - http:
        paths:
        - path: /
          pathType: Prefix
          backend:
            service:
              name: service-2048
              port:
                number: 80
EOF

 

서비스 매니페스트를 확인해보면 앞서 AWS Load Balancer Controller에서 생성된 IngressClass인 alb를 지정한 것을 알 수 있습니다.

ingress가 배포되었습니다.

$ kubectl get ing -n game-2048
NAME           CLASS   HOSTS   ADDRESS                                                                        PORTS   AGE
ingress-2048   alb     *       k8s-game2048-ingress2-70d50ce3fd-1587098903.ap-northeast-2.elb.amazonaws.com   80      3m9s

$ kubectl get svc -n game-2048
NAME           TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
service-2048   NodePort   10.100.251.246   <none>        80:30521/TCP   5m31s

$ kubectl get ep -n game-2048
NAME           ENDPOINTS                           AGE
service-2048   192.168.1.239:80,192.168.3.219:80   5m23s

 

 

다시 웹 콘솔로 로드밸런서 정보를 확인해보면 ALB가 생성되었습니다.

 

ALB의 대상그룹을 확인해보면 ALB에서 파드 IP로 직접 전달하는 것을 알 수 있습니다. Ingress 이므로 Rules가 추가되어 시각화 되어 있는 것을 알 수 있습니다.

 

 

EKS의 Ingress에서는 Ingress Group를 지정해서 하나의 Ingress를 바라보도록 해서, 서로 다른 주체가 관리하도록 하는 개념이 있습니다.

 

출처: https://aws.amazon.com/ko/blogs/tech/a-deeper-look-at-ingress-sharing-and-target-group-binding-in-aws-load-balancer-controller/

 

아래와 같이 서로 다른 Ingerss를 사용하면서 하나의 ingress group으로 묶어서 배포하는 방식을 사용할 수 있는 점이 독특한 것 같습니다.

https://github.com/aws-samples/containers-blog-maelstrom/blob/main/aws-lb-controller-blog/ingress-grouping/orange-purple-ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: orange-purple-ingress
  namespace: orange-purple-ns
  labels:
    app: color-2
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/group.name: app-color-lb
spec:
  ingressClassName: alb
  rules:
    - http:
        paths:
          - path: /orange
            pathType: Prefix
            backend:
              service:
                name: orange-service
                port:
                  number: 80                        
          - path: /purple
            pathType: Prefix
            backend:
              service:
                name: purple-service
                port:
                  number: 80

 

https://github.com/aws-samples/containers-blog-maelstrom/blob/main/aws-lb-controller-blog/ingress-grouping/blue-green-ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: blue-green-ingress
  namespace: blue-green-ns
  labels:
    app: color-1
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/group.name: app-color-lb
spec:
  ingressClassName: alb
  rules:
    - http:
        paths:
          - path: /blue
            pathType: Prefix
            backend:
              service:
                name: blue-service
                port:
                  number: 80                        
          - path: /green
            pathType: Prefix
            backend:
              service:
                name: green-service
                port:
                  number: 80

 

 

여기까지 EKS에서 살펴본 LoadBalancer 유형의 서비스와 Ingress가 사용하는 NLB, ALB 구성에 대한 전체적인 구성이 아래와 같다는 것을 알 수 있습니다.

출처: https://www.youtube.com/watch?v=E49Q3y9wsUo

 

 

8. AKS의 Ingress 구현

AKS에서 Ingress를 구현한 방식에는 크게 3가지가 있습니다. Application Gateway Ingress Controller, Application Gateway for Containers, Application Routing addon 입니다.

 

Note: 아래 문서를 보시면 Istio-based service mesh 또한 Ingress 옵션으로 이야기 하고 있지만 이는 istio Ingress API 이므로, 제외하고 설명하도록 하겠습니다.
https://learn.microsoft.com/en-us/azure/aks/concepts-network-ingress#compare-ingress-options

 

Application Gateway Ingress Controller는 AGIC 파드가 내부에서 API 서버를 모니터링 하다가 Azure Resource Manager로 설정 변경을 하는 아키텍처를 가지고 있습니다. Azure Load Balancer와 다르게 Applicaton Gateway는 서브넷에 위치하는 리소스로 파드 IP를 직접 백엔드로 구성합니다.

다만 Ingress의 다양한 설정을 하기에는 기존에 Application Gateway 라는 제품에서 가지고 있는 기능에서만 구현이 가능하다는 점에서 확장성이 부족한 솔루션으로 알려져 있습니다.

참고: https://learn.microsoft.com/en-us/azure/application-gateway/ingress-controller-overview

 

이로 인해서 비교적 최근 Application Gateway for Containers(이하 AGC) 라는 AKS를 위한 별도의 상품이 추가 되었습니다. AGC는 상대적으로 AGIC에 비해 다양한 고급 라우팅 기능이 추가 되었으며, Gateway API도 지원하고 있습니다.

참고: https://learn.microsoft.com/en-us/azure/application-gateway/for-containers/overview

 

다만 이러한 솔루션을 사용하지 않고 사용자가 직접 Nginx Ingress Controller 배포하여 사용하는 경우가 있습니다. 이를 위해서 AKS에서는 Application Routing addon으로 managed Nginx ingress를 제공하고 있습니다.

Managed Nginx ingress는 기존 Nginx Ingress Controller를 addon 형태로 제공할 뿐 구성되는 아키텍처는 동일합니다. 즉 Ingress controller가 파드로 실행되고, LoadBalancer 유형의 서비스로 엔드포인트를 구성합니다. Addon으로 제공하므로 설치 및 업그레이드와 같은 부분에서 Managed 서비스를 제공합니다. 다만 Manaed 서비스이기 때문에 오픈 소스 Nginx Ingress의 모든 설정을 지원하지 않으므로 사용하기 전에 평가가 필요합니다.

참고: https://learn.microsoft.com/en-us/azure/aks/app-routing

 

 

9. 리소스 정리

아래 절차로 생성된 EKS와 CloudFormation을 삭제합니다.

eksctl delete cluster --name $CLUSTER_NAME

# EKS 삭제 완료 후 실행
aws cloudformation delete-stack --stack-name myeks

 

 

마무리

EKS Networking을 두 포스트를 통해서 알아보고, 그 과정에서 EKS와 AKS의 차이를 살펴 봤습니다.

 

기본적으로 EKS의 VPC와 Azure의 Virtual Network에 차이가 있기 때문에 구현 방식이 달라지는 부분이 있었고, CNI를 구현하는 방식에는 유사한 듯 하면서도 서로 일부 제약사항이 있는 상황입니다.

전반적으로 EKS가 단일 CNI와 ELB 상품을 통한 Service/Ingress를 구현한 반면, AKS는 CNI나 Ingress 쪽에서 구현체가 다양한 모습을 확인할 수 있었습니다.

 

다음 포스트에서는 EKS의 노드 그룹과 스토리지 옵션을 살펴보도록 하겠습니다.

'EKS' 카테고리의 다른 글

[4] EKS의 모니터링과 로깅  (0) 2025.03.01
[3-2] EKS 노드 그룹  (0) 2025.02.23
[3-1] EKS 스토리지 옵션  (0) 2025.02.23
[2-1] EKS Networking Part1 - VPC CNI와 파드 통신  (0) 2025.02.16
[1] EKS 생성과 리소스 살펴보기  (2) 2025.02.07

이번 포스트에서는 EKS Networking이라는 주제로 이어가 보겠습니다.

내용이 길어질 것으로 보아 파트를 나눠서 작성할 예정이며, EKS Networking Part1에서는 EKS의 VPC CNI와 기본 환경을 검토해보고, 파드와 노드 통신을 확인해보겠습니다. 이후 EKS Networking Part2에서 Loadbalancer 유형의 서비스의 구성과 Ingress에 대해서 확인해 보겠습니다.

 

 

목차

  1. AWS VPC CNI 개요
  2. 파드 생성 갯수 제한
  3. 실습 환경 배포
  4. 노드 정보 확인
  5. 파드 통신 확인

 

1. AWS VPC CNI 개요

CNI란?

CNI(Container Network Interface)는 CNCF(Cloud Native Computing Foundation)의 프로젝트로 Specification과 리눅스 컨테이너의 네트워크 인터페이스를 구성하기 위한 plugin을 작성하기 위한 라이브러리로 구성됩니다.

CNI는 컨테이너의 네트워크 연결성과 컨테이너가 삭제되었을 때 할당된 리소스를 제거하는 역할에 집중합니다.

참고: https://github.com/containernetworking/cni

 

보통 Kubernetes에 어떤 CNI를 쓰느냐라고 얘기를 하면 의미가 통하기는 하지만, 실제로 calico, cilium, flannel 등은 CNI plugin이라고 할 수 있습니다.

Kubernetes 에서 CNI Plugin의 동작은 간략히 아래와 같이 이뤄집니다.

  1. Kubelet이 Container Runtime에 컨테이너 생성을 요청
  2. Container Runtime이 컨테이너의 Network Namespace를 생성
  3. Container Runtime이 CNI 설정과 환경변수를 표준 입력으로 CNI Plugin 호출
  4. CNI Plugin이 컨테이너의 네트워크 인터페이스를 구성하고, IP를 할당하고, 호스트 네트워크 간의 veth pair 를 생성
  5. CNI Plugin이 호스트 네트워크 네임스페이스와 컨테이너 네트워크 네임스페이스에 라우팅을 구성

 

관련하여 CNI plugin이 실제로 어떤 방식으로 호출되고 동작하는지에 대해서 재미난 실습이 있어 별도의 포스트를 작성하였으니 아래를 확인해보시기 바랍니다.

https://a-person.tistory.com/32

 

 

AWS VPC CNI 소개

오픈소스 CNI plugin들은 파드 네트워크를 VxLAN이나 IPIP와 같은 프로토콜을 이용해 overlay 네트워크로 구성하는 경우가 많습니다. 이것은 노드가 위치한 네트워크가 물리적으로 구성되어 있거나 하는 이유로 CNI가 직접 IP를 관리하기 어려운 이유도 있을 수 있고, 한편으로는 어떤 네트워크 환경에서든 유연하게 사용 가능하게 하기 위한 목적일 수도 있습니다.

 

반면 AWS VPC CNI는 AWS 환경에 최적화되어 VPC 네트워크와 동일한 IP 주소를 파드에 할당합니다. 또 AWS VPC CNI는 eBPF 기반의 Network Policy를 기본으로 제공하고 있습니다. (앞서 노드에 배포된 파드를 확인하면서 aws-node의 컨테이너가 각 VPC CNI, Network Policy Agent 인것을 확인했습니다)

 

AWS VPC CNI의 컴포넌트는 CNI 바이너리와 ipamd로 이뤄져 있습니다.

  • CNI binary: 파드 네트워크를 구성
  • ipamd: IP Address Management(IPAM) 데몬으로, ENI를 관리하고 사용가능한 IP 혹은 prefix를 warm-pool로 관리합니다.

앞서 실습을 통해 EKS를 생성한 이후 노드에 ENI가 2개 생성된 경우가 확인했는데 이것은 VPC CNI의 동작과도 관련이 있습니다.

 

먼저 인스턴스가 생성되면 EC2는 첫번째 ENI를 생성해 연결하고, VPC CNI가 hostNetwork 모드로 Primary IP로 실행됩니다. 노드가 프로비저닝 되면, CNI 플러그인 Primary ENI에 IP 혹은 Prefix의 slots를 가져와 IP Pool에 할당합니다. 이러한 IP Pool을 warm pool 이라고 하며 warm pool의 사이즈는 노드의 인스턴스 타입에 결정됩니다.

각 ENI는 인스턴스는 타입에 따라 제한된 slot 개수를 지원하기 때문에, 추가로 필요한 slot에 따라 CNI는 인스턴스에 추가 ENI를 할당 요청 합니다.

 

요약하면, 인스턴스 타입에 따라 IP Pool의 개수가 정해져 있고, 이때 slots 단위로 IP를 warm pool에 저장해두고, IP가 필요하면 warm pool에서 파드에 할당합니다. 인스턴스 타입에 따라 ENI가 가질 수 있는 IP 개수도 제한되기 때문에 warm pool이 증가할 때 추가 ENI가 필요할 수 있습니다.

 

아래 VPC CNI 문서의 그림을 살펴보면 flow를 확인할 수 있습니다.

새 slot이 필요할 때, Primary ENI에 여유 slot이 있으면 파드에 이 slot을 할당합니다. Primary ENI에 여유 slot이 없으면, Secondary ENI가 있는지 확인하고, 다시 Secondary ENI에서 여유 slot을 확인해 파드에 slot을 할당합니다.

Secondary ENI가 없는 경우, 인스턴스에 새로운 ENI 가용을 확인하여, 가능하다면 새로운 ENI를 할당합니다.

참고: https://docs.aws.amazon.com/eks/latest/best-practices/vpc-cni.html

 

이러한 절차에서 복잡한 느낌을 지울 수 없습니다.

 

AKS의 Azure CNI는 AWS VPC CNI와 유사하게 Virtual Network(EKS의 VPC)에서 Pod IP를 가져와 사용합니다. 이때 하나의 NIC에서 할당 가능한 IP개수의 제한은 없으므로, Azure CNI은 단순히 정의된 max-pods 개수 만큼 secondary IP로 배정합니다.

 

물론 VPC CNI는 slots 만큼 동적 할당을 하기 때문에 불필요한 IP를 static 하게 할당받지 않는 장점이 있을 수 있습니다.

참고로 AKS는 다양한 CNI plugin을 제공하고 있습니다.

먼저 VPC CNI와 유사하게 Virtual Network의 IP를 사용하는 Azure CNI가 있고, Azure CNI에서 동적으로 파드 IP할당하는 방식인 Azure CNI with dynamic IP allocation 이 있습니다. 또한 overlay network 사용하는 kubenet, Azure CNI overlay가 있습니다.

이때, Azure CNI overlay와 Azure CNI with dynamic IP allocation의 경우 dataplane을 cilium으로 오프로딩하는 Azure CNI powered by cilium 를 사용할 수 있습니다.

참고: https://learn.microsoft.com/en-us/azure/aks/concepts-network-cni-overview#choosing-a-cni

 

 

VPC CNI의 동작 과정은 아래 그림에서 살펴보실 수 있습니다.

출처: https://docs.aws.amazon.com/eks/latest/best-practices/vpc-cni.html

  1. 파드가 스케줄링 됩니다.
  2. kubelet은 Add 요청을 VPC CNI로 보냅니다.
  3. VPC CNI는 L-IPAM에 파드 IP를 요청합니다.
  4. L-IPAM에서 파드 IP를 반환합니다.
  5. VPC CNI는 네트워크 네임스페이스를 구성합니다.
  6. VPC CNI는 파드 IP를 kubelet으로 반환합니다.
  7. kubelet은 파드 IP를 파드에 할당합니다.

 

이 절차가 사실 일반적인 CNI plugin의 동작 과정과 조금 다르게 묘사되어 있습니다. kubelet이 IP를 할당한다라.. 쉽게 설명하려고 한건지 정확하진 않네요.

이 그림에서 VPC CNI의 구성요소와 전반적인 흐름을 이해하는 정도로 넘어가는게 좋을 것 같습니다.

 

추가로 해당 문서를 보면 L-IPAM에서 iptables의 NAT rules를 업데이트 한다고 표시도 있습니다. 아마도 VPC IP에 대한 정보를 알고 있으므로, 이를 바탕으로 VPC를 제외한 네트워크에 대해 SNAT 정책을 업데이트 하는 것으로 이해됩니다.

 

 

2. 파드 생성 갯수 제한

AWS는 인스턴스 타입별로 최대 ENI 개수와 ENI별 할당가능한 최대 IP 개수가 정해져 있습니다.

아래와 같은 명령으로 t3 시리즈에 해당하는 값을 확인할 수 있습니다.

$ aws ec2 describe-instance-types --filters Name=instance-type,Values=t3.\* \
 --query "InstanceTypes[].{Type: InstanceType, MaxENI: NetworkInfo.MaximumNetworkInterfaces, IPv4addr: NetworkInfo.Ipv4AddressesPerInterface}" \
 --output table
 --------------------------------------
|        DescribeInstanceTypes       |
+----------+----------+--------------+
| IPv4addr | MaxENI   |    Type      |
+----------+----------+--------------+
|  15      |  4       |  t3.2xlarge  |
|  6       |  3       |  t3.medium   |
|  12      |  3       |  t3.large    |
|  15      |  4       |  t3.xlarge   |
|  2       |  2       |  t3.nano     |
|  2       |  2       |  t3.micro    |
|  4       |  3       |  t3.small    |
+----------+----------+--------------+

 

예를 들어, t3.medium은 최대 3개의 ENI를 연결할 수 있고, ENI별 6개의 IP가 할당 가능 합니다.

총 3*6=18개 IP가 사용가능합니다만, 각 ENI에 할당되는 IP는 파드에서 사용할 수 없게 됩니다. 또한 VPC CNI와 kube-proxy가 hostNetwork를 사용하므로 이들은 IP가 필요 없습니다.

 

결론적으로, EKS에서 최대 파드 개수는 인스턴스 타입에 따라 아래와 같은 공식으로 계산됩니다.

(Number of network interfaces for the instance type * (the number of IP addresses per network interface - 1)) + 2

즉, t3.medium은 3*(6-1)+2= 17개 파드가 실행될 수 있습니다.

 

이는 인스턴스 유형마다 다를 수 있지만 최대 파드 개수에는 limit이 있습니다. vCPU 30개 미만의 EC2 인스턴스 유형에서는 최대 파드 개수가 110개로 제한되고, vCPU 30이상의 EC2 인스턴스 유형에서는 250개로 제한을 권고 합니다.

VPC CNI 절에서 설명한 ENI와 파드 생성 갯수 제한에 대한 테스트는 이후 노드 정보를 확인하는 과정에서 다시 살펴보겠습니다.

 

 

참고로 AKS는 노드에 생성 가능한 파드 갯수의 제한은 인터페이스와 관련이 없이 250으로 지정되어 있으며 또한 --max-pods 옵션으로 정의할 수 있습니다. 다만 --max-pods 옵션을 지정하지 않은 경우 CNI별로 일부 차이가 있으므로 이는 문서를 확인하시기 바랍니다.

https://learn.microsoft.com/en-us/azure/aks/quotas-skus-regions#service-quotas-and-limits

 

 

EKS에서는 인스턴스 타입과 ENI 제약으로 인해 최대 파드 개수가 제한되는 독특한 문제를 가지고 있습니다. 이를 해소하기 위해 ENI에 특정 IP prefix를 할당하는 Prefix Delegation(https://www.eksworkshop.com/docs/networking/vpc-cni/prefix/)와 같은 방식을 제공하고 있습니다.

 

 

AWS VPC CNI의 컨셉을 살펴보면 Azure CNI와 비슷한 문제점이 있고, 또한 해결을 위한 유사한 접근 방법을 가지고 있습니다.

첫번째 문제는 다수의 파드/노드가 생성되는 경우 VPC 자체의 IP가 고갈되는 문제입니다.

근본적으로 더 큰 VPC를 가진 클러스터를 생성해야 합니다. 또한 EKS와 AKS는 동일하게 VPC, Virtual Network에 Address Space를 추가하는 방식을 제공하고 있습니다.

추가로 AKS에서는 이러한 제약사항을 극복하기 위해서 overlay network를 사용하는 CNI(kubenet, Azure CNI overlay)를 선택할 수 있다는 점에서 차이점이 있습니다.

 

두번째 문제는 Pre-IP allocation으로 이미 할당은 되었지만 미사용되는 IP로 인한 낭비입니다. 이런 warm pool이 다수의 노드에 존재한다면 IP가 상당히 낭비될 수 있습니다.

AWS VPC CNI에서는 Warm Pool 에 관련된 설정을 변경하는 방식으로 할당되는 IP를 최소화 할 수 있습니다. 이때 WARM_IP_TARGET, MINIMUM_IP_TARGET 과 같은 값을 조정할 수 있습니다.

AKS에서는 Azure CNI with Dynamic Pod IP Allocation 방식을 통해 Pod가 사용하는 서브넷을 지정할 수 있습니다. 이 방식은 Azure CNI와 다르게, batch 사이즈 만큼의 IP를 할당하고 지정된 퍼센티지 이상 사용하는 경우 batch 사이즈 만큼을 추가로 할당하는 방식을 사용합니다. 이 방식은 EKS의 VPC CNI의 worm pool 방식과 Custom Networking(https://www.eksworkshop.com/docs/networking/vpc-cni/custom-networking/)을 섞은 것 같기도 합니다.

 

 

3. 실습 환경 배포

먼저 아래를 따라 기본적인 실습 환경을 구성합니다.

아래 CloudFormation을 배포하면서 Key를 인자로 전달하기 때문에 사전에 key pair가 있는지 확인하시고, 없으면 생성을 해야 합니다. (EC2>Key pairs>Create key pair>.pem 형식으로 생성)

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

# 배포
# aws cloudformation deploy --template-file myeks-1week.yaml --stack-name mykops --parameter-overrides KeyName=<My SSH Keyname> SgIngressSshCidr=<My Home Public IP Address>/32 --region <리전>
예시) aws cloudformation deploy --template-file ./myeks-2week.yaml \
     --stack-name myeks --parameter-overrides KeyName=ekskey SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32 --region ap-northeast-2

# CloudFormation 스택 배포 완료 후 운영서버 EC2 IP 확인
ec2ip=$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[*].OutputValue' --output text)

# 운영서버 EC2 에 SSH 접속
ssh -i <ssh 키파일> ec2-user@$ec2ip

 

CloudFormation으로 배포되는 실습 환경은 아래와 같습니다.

 

myeks-vpc와 operator-vpc를 각각 만들고 VPC Peering으로 연결합니다.

operator-vpc에는 CloudFormation으로 미리 operator-host 라는 서버를 배포했습니다.

myeks-vpc에는 각 가용 영역에 PublicSubnet과 PrivateSubnet을 구성했고, PublicSubnet에 EKS를 배포할 예정입니다.

 

이제 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

# 출력된 내용 참고해 아래 yaml에서 vpc/subnet id, ssh key 경로 수정
eksctl create cluster --name $CLUSTER_NAME --region=ap-northeast-2 --nodegroup-name=ng1 --node-type=t3.medium --nodes 3 --node-volume-size=30 --vpc-public-subnets "$PubSubnet1","$PubSubnet2","$PubSubnet3" --version 1.31 --with-oidc --external-dns-access --full-ecr-access --alb-ingress-access --node-ami-family AmazonLinux2023 --ssh-access --dry-run > myeks.yaml

 

이후 생성된 myeks.yaml 에서 # 각자 환경 정보로 수정로 커맨트된 열을 확인하고 정보가 다른 경우 수정합니다.

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

kubernetesNetworkConfig:
  ipFamily: IPv4

iam:
  vpcResourceControllerPolicy: true
  withOIDC: true

accessConfig:
  authenticationMode: API_AND_CONFIG_MAP

vpc:
  autoAllocateIPv6: false
  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: vpc-0ab40d2acbda845d8  # 각자 환경 정보로 수정
  manageSharedNodeSecurityGroupRules: true # if you want to manage the rules of the shared node security group
  nat:
    gateway: Disable
  subnets:
    public:
      ap-northeast-2a:
        az: ap-northeast-2a
        cidr: 192.168.1.0/24
        id: subnet-014dc12ab7042f604  # 각자 환경 정보로 수정
      ap-northeast-2b:
        az: ap-northeast-2b
        cidr: 192.168.2.0/24
        id: subnet-01ba554d3b16a15a7  # 각자 환경 정보로 수정
      ap-northeast-2c:
        az: ap-northeast-2c
        cidr: 192.168.3.0/24
        id: subnet-0868f7093cbb17c34  # 각자 환경 정보로 수정

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

privateCluster:
  enabled: false
  skipEndpointCreation: false

managedNodeGroups:
- amiFamily: AmazonLinux2023
  desiredCapacity: 3
  disableIMDSv1: true
  disablePodIMDS: false
  iam:
    withAddonPolicies:
      albIngress: false # Disable ALB Ingress Controller
      appMesh: false
      appMeshPreview: false
      autoScaler: false
      awsLoadBalancerController: true # Enable AWS Load Balancer Controller
      certManager: true # Enable cert-manager
      cloudWatch: false
      ebs: false
      efs: false
      externalDNS: true # Enable ExternalDNS
      fsx: false
      imageBuilder: true
      xRay: false
  instanceSelector: {}
  instanceType: t3.medium
  preBootstrapCommands:
    # install additional packages
    - "dnf install nvme-cli links tree tcpdump sysstat ipvsadm ipset bind-utils htop -y"
    # disable hyperthreading
    - "for n in $(cat /sys/devices/system/cpu/cpu*/topology/thread_siblings_list | cut -s -d, -f2- | tr ',' '\n' | sort -un); do echo 0 > /sys/devices/system/cpu/cpu${n}/online; done"
  labels:
    alpha.eksctl.io/cluster-name: myeks
    alpha.eksctl.io/nodegroup-name: ng1
  maxSize: 3
  minSize: 3
  name: ng1
  privateNetworking: false
  releaseVersion: ""
  securityGroups:
    withLocal: null
    withShared: null
  ssh:
    allow: true
    publicKeyName: mykeyname  # 각자 환경 정보로 수정
  tags:
    alpha.eksctl.io/nodegroup-name: ng1
    alpha.eksctl.io/nodegroup-type: managed
  volumeIOPS: 3000
  volumeSize: 30
  volumeThroughput: 125
  volumeType: gp3

 

 

마지막으로 생성된 myeks.yaml을 바탕으로 EKS를 배포합니다.

# kubeconfig 파일 경로 위치 지정 : 
export KUBECONFIG=$HOME/.kube/config

# 배포
eksctl create cluster -f myeks.yaml --verbose 4

 

생성이 완료되면 클러스터 정보를 확인합니다.

# 클러스터 확인
$ kubectl cluster-info
Kubernetes control plane is running at https://F141CFF9E7E8776AF6826A7D1341FBEA.yl4.ap-northeast-2.eks.amazonaws.com
CoreDNS is running at https://F141CFF9E7E8776AF6826A7D1341FBEA.yl4.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
NAME    REGION          EKSCTL CREATED
myeks   ap-northeast-2  True

# 노드 확인
$ kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone
NAME                                               STATUS   ROLES    AGE     VERSION               INSTANCE-TYPE   CAPACITYTYPE   ZONE
ip-192-168-1-203.ap-northeast-2.compute.internal   Ready    <none>   2m37s   v1.31.4-eks-aeac579   t3.medium       ON_DEMAND      ap-northeast-2a
ip-192-168-2-77.ap-northeast-2.compute.internal    Ready    <none>   2m35s   v1.31.4-eks-aeac579   t3.medium       ON_DEMAND      ap-northeast-2b
ip-192-168-3-141.ap-northeast-2.compute.internal   Ready    <none>   2m35s   v1.31.4-eks-aeac579   t3.medium       ON_DEMAND      ap-northeast-2c

# 생성 노드 확인
$ aws ec2 describe-instances --query "Reservations[*].Instances[*].{InstanceID:InstanceId, PublicIPAdd:PublicIpAddress, PrivateIPAdd:PrivateIpAddress, InstanceName:Tags[?Key=='Name']|[0].Value, Status:State.Name}" --filters Name=instance-state-name,Values=running --output table
-----------------------------------------------------------------------------------------
|                                   DescribeInstances                                   |
+----------------------+-----------------+----------------+-----------------+-----------+
|      InstanceID      |  InstanceName   | PrivateIPAdd   |   PublicIPAdd   |  Status   |
+----------------------+-----------------+----------------+-----------------+-----------+
|  i-095eb2faccd652e06 |  myeks-ng1-Node |  192.168.3.141 |  43.201.9.71    |  running  |
|  i-035d1900144a7ba5f |  operator-host  |  172.20.1.100  |  3.34.186.167   |  running  |
|  i-0b35dccaffe41de41 |  myeks-ng1-Node |  192.168.1.203 |  43.202.40.202  |  running  |
|  i-0543c6996984a0290 |  myeks-ng1-Node |  192.168.2.77  |  15.164.237.196 |  running  |
+----------------------+-----------------+----------------+-----------------+-----------+

# 관리형 노드 그룹 확인
$ eksctl get nodegroup --cluster $CLUSTER_NAME
CLUSTER NODEGROUP       STATUS  CREATED                 MIN SIZE        MAX SIZE        DESIRED CAPACITY        INSTANCE TYPE   IMAGE ID                ASG NAMETYPE
myeks   ng1             ACTIVE  2025-02-14T13:28:22Z    3               3               3                       t3.medium       AL2023_x86_64_STANDARD  eks-ng1-7cca8252-ac30-3af4-6c5e-a3b2d91981c3     managed

 

AWS는 서브넷이 각 가용 영역에 배치되는 리소스입니다. Azure에서는 서브넷을 생성해도 이는 가용 영역을 걸쳐서 생성되는 리전 리소스입니다. 이러한 이유로 EKS는 생성 시 가용 영역별로 서브넷을 제공하면 노드들은 자동으로 가용 영역에 배치됩니다.

 

AKS는 서브넷을 지정해도 가용 영역의 구분이 없기 때문에 생성 시점에 가용 영역 여부를 별도로 지정해야 합니다. 아래는 포탈에서 AKS를 생성하며 가용 영역을 지정하는 화면입니다.

 

아래로 AKS의 availability zone 문서를 참고하실 수 있습니다.

https://learn.microsoft.com/en-us/azure/aks/availability-zones-overview#zone-spanning

 

 

파드 정보도 확인해봅니다.

# 파드 확인
$ kubectl get pod -A
NAMESPACE     NAME                              READY   STATUS    RESTARTS   AGE
kube-system   aws-node-5skww                    2/2     Running   0          13m
kube-system   aws-node-qmmn8                    2/2     Running   0          13m
kube-system   aws-node-x5rxg                    2/2     Running   0          13m
kube-system   coredns-9b5bc9468-8prkc           1/1     Running   0          18m
kube-system   coredns-9b5bc9468-qp9mj           1/1     Running   0          18m
kube-system   kube-proxy-d7l2t                  1/1     Running   0          13m
kube-system   kube-proxy-fp99v                  1/1     Running   0          13m
kube-system   kube-proxy-sxq5k                  1/1     Running   0          13m
kube-system   metrics-server-86bbfd75bb-smjpb   1/1     Running   0          18m
kube-system   metrics-server-86bbfd75bb-xm8ds   1/1     Running   0          18m

$ 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>          18m
kube-system   kube-proxy   3         3         3       3            3           <none>          18m

# eks addon 확인
$ eksctl get addon --cluster $CLUSTER_NAME
2025-02-14 22:43:26 [ℹ]  Kubernetes version "1.31" in use by cluster "myeks"
2025-02-14 22:43:26 [ℹ]  getting all addons
2025-02-14 22:43:27 [ℹ]  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
coredns         v1.11.3-eksbuild.1      ACTIVE  0                                                                                       v1.11.4-eksbuild.2,v1.11.4-eksbuild.1,v1.11.3-eksbuild.2
kube-proxy      v1.31.2-eksbuild.3      ACTIVE  0                                                                                       v1.31.3-eksbuild.2
metrics-server  v0.7.2-eksbuild.1       ACTIVE  0
vpc-cni         v1.19.0-eksbuild.1      ACTIVE  0       arn:aws:iam::430118812536:role/eksctl-myeks-addon-vpc-cni-Role1-fdAOfLzN8tNl    v1.19.2-eksbuild.5,v1.19.2-eksbuild.1

 

앞서 설명드린 바와 같이 EKS의 노드에는 aws-node 가 daemonset 으로 실행되고, VPC CNI와 Network Policy Agent로 멀티 컨테이너로 구성되어 있습니다.

 

Azure CNI에서는 azure-vnet과 azure-vnet-ipam가 binary로 구현되어 있고 별도의 파드가 생성되지 않습니다. 다만 azure CNI overlay과 같은 유형의 CNI에서는 IPAM으로 azure-cns 데몬셋이 사용됩니다.

 

 

한편 EKS의 VPC CNI는 L-IPAM 에서 NAT rules를 iptables로 반영합니다.

AKS의 Azure CNI도 Virtual Network 이 외의 네트워크에 대해서 NAT rules을 생성하는데, ip-masq-agent를 daemonset으로 사용하는 차이가 있습니다.

# Virtual Network에 대해서 nonMasqeuradeCIDRs 로 정의되어 있음
$ kubectl get cm azure-ip-masq-agent-config-reconciled -n kube-system -oyaml
apiVersion: v1
data:
  ip-masq-agent-reconciled: |-
    nonMasqueradeCIDRs:
      - 10.244.0.0/16
    masqLinkLocal: true
kind: ConfigMap
...
  labels:
    addonmanager.kubernetes.io/mode: Reconcile
    component: ip-masq-agent
    kubernetes.io/cluster-service: "true"
  name: azure-ip-masq-agent-config-reconciled
  namespace: kube-system

# 노드의 iptables 정보
root@aks-nodepool1-76251328-vmss000000:/# iptables -t nat -S |grep ip-masq-agent
-A POSTROUTING -m comment --comment "\"ip-masq-agent: ensure nat POSTROUTING directs all non-LOCAL destination traffic to our custom IP-MASQ-AGENT chain\"" -m addrtype ! --dst-type LOCAL -j IP-MASQ-AGENT
-A IP-MASQ-AGENT -d 10.244.0.0/16 -m comment --comment "ip-masq-agent: local traffic is not subject to MASQUERADE" -j RETURN
-A IP-MASQ-AGENT -m comment --comment "ip-masq-agent: outbound traffic is subject to MASQUERADE (must be last in chain)" -j MASQUERADE

 

 

4. 노드 정보 확인

앞서 살펴본 VPC CNI의 특성을 확인하기 위해서 노드 정보를 살펴보겠습니다.

 

먼저 웹 콘솔에서 노드를 살펴봅니다. t3.medium으로 생성되었습니다.

 

Networking 탭을 살펴보면 2개의 ENI에 해당하는 Private IP가 2개 확인되며, Secondary IP가 10개 확인됩니다.

 

아래를 살펴보면 2개의 ENI가 연결되었음을 알 수 있습니다.

 

노드에 할당된 IP를 확인해보면 hostNetwork를 제외하고 3개의 IP가 secondary IP에서 사용되고 있습니다.

$ kubectl get po -A -owide |grep ip-192-168-3-141.ap-northeast-2.compute.internal
kube-system   aws-node-x5rxg                    2/2     Running   0          70m   192.168.3.141   ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
kube-system   coredns-9b5bc9468-8prkc           1/1     Running   0          75m   192.168.3.45    ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
kube-system   coredns-9b5bc9468-qp9mj           1/1     Running   0          75m   192.168.3.48    ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
kube-system   kube-proxy-sxq5k                  1/1     Running   0          70m   192.168.3.141   ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
kube-system   metrics-server-86bbfd75bb-smjpb   1/1     Running   0          75m   192.168.3.69    ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>

 

물론 노드에는 2개의 ENI에 해당하는 ens5, ens6의 IP만 확인됩니다.

[root@ip-192-168-3-141 /]# ip -br -c a
lo               UNKNOWN        127.0.0.1/8 ::1/128
ens5             UP             192.168.3.141/24 metric 1024 fe80::8ef:19ff:fe5b:1dd3/64
eni9a92535a0a9@if3 UP             fe80::b428:33ff:feaa:6633/64
enib9bf070cdb3@if3 UP             fe80::b458:f9ff:fe9a:1a0b/64
eni54524c4f9d9@if3 UP             fe80::e8a1:acff:fe6f:6a57/64
enid0ded64c01f@if3 UP             fe80::9445:9ff:feaf:ad50/64
ens6             UP             192.168.3.172/24 fe80::832:3eff:fe60:eec5/64
eni98b6af8f1d6@if3 UP             fe80::54a1:b9ff:fe80:c915/64

 

앞서 EKS 노드에서 최대 파드 개수는 인스턴스 타입에 따라 아래와 같은 공식으로 계산되고, t3.medium에서는 3*(6-1)+2= 17개 파드가 실행될 수 있다고 알아봤습니다.

 

이 내용을 테스트를 위해서 테스트 디플로이먼트를 배포합니다.

$ cat <<EOF | kubectl apply -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

 

아래와 같이 replicas를 늘여서 실제 변경되는 부분을 확인해 보겠습니다.

# 파드 확인 
$ kubectl get pod -o wide
NAME                            READY   STATUS    RESTARTS   AGE     IP              NODE                                               NOMINATED NODE   READINESS GATES
netshoot-pod-744bd84b46-g7hzp   1/1     Running   0          6m18s   192.168.3.223   ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
netshoot-pod-744bd84b46-llqjz   1/1     Running   0          6m18s   192.168.1.220   ip-192-168-1-203.ap-northeast-2.compute.internal   <none>           <none>
netshoot-pod-744bd84b46-rwblp   1/1     Running   0          6m18s   192.168.2.140   ip-192-168-2-77.ap-northeast-2.compute.internal    <none>           <none>
nsenter-vg4pv8                  1/1     Running   0          7m1s    192.168.3.141   ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>

# 파드 증가 테스트 >> 파드 정상 생성 확인, 워커 노드에서 eth, eni 갯수 확인
kubectl scale deployment netshoot-pod --replicas=8

# 노드에 접속 후 확인 (사전)
[root@ip-192-168-3-141 /]# ip -br -c a
lo               UNKNOWN        127.0.0.1/8 ::1/128
ens5             UP             192.168.3.141/24 metric 1024 fe80::8ef:19ff:fe5b:1dd3/64
eni9a92535a0a9@if3 UP             fe80::b428:33ff:feaa:6633/64
enib9bf070cdb3@if3 UP             fe80::b458:f9ff:fe9a:1a0b/64
eni54524c4f9d9@if3 UP             fe80::e8a1:acff:fe6f:6a57/64
enid0ded64c01f@if3 UP             fe80::9445:9ff:feaf:ad50/64
ens6             UP             192.168.3.172/24 fe80::832:3eff:fe60:eec5/64
eni98b6af8f1d6@if3 UP             fe80::54a1:b9ff:fe80:c915/64

# 노드에 접속 후 확인 (사후)
[root@ip-192-168-3-141 /]# ip -br -c a
lo               UNKNOWN        127.0.0.1/8 ::1/128
ens5             UP             192.168.3.141/24 metric 1024 fe80::8ef:19ff:fe5b:1dd3/64
eni9a92535a0a9@if3 UP             fe80::b428:33ff:feaa:6633/64
enib9bf070cdb3@if3 UP             fe80::b458:f9ff:fe9a:1a0b/64
eni54524c4f9d9@if3 UP             fe80::e8a1:acff:fe6f:6a57/64
enid0ded64c01f@if3 UP             fe80::9445:9ff:feaf:ad50/64
ens6             UP             192.168.3.172/24 fe80::832:3eff:fe60:eec5/64
eni98b6af8f1d6@if3 UP             fe80::54a1:b9ff:fe80:c915/64
eni9c40e9afed0@if3 UP             fe80::58ea:7dff:fe10:23f/64
ens7             UP             192.168.3.104/24 fe80::883:eff:fe48:b1bf/64

 

확인해보면 파드 개수가 늘어나자 ens7이 신규로 생성된 것을 알 수 있습니다.

웹 콘솔에서도 Secondary IP가 15개로 증가한 것을 확인할 수 있습니다.

 

이때 Primary ENI가 아닌 ENI는 통신에 관여를 할지 한번 확인해보겠습니다.

kubectl get po -A -owide |grep ip-192-168-3-141.ap-northeast-2.compute.internal
default       netshoot-pod-744bd84b46-7ggdq     1/1     Running   0          7m10s   192.168.3.83    ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
default       netshoot-pod-744bd84b46-g7hzp     1/1     Running   0          15m     192.168.3.223   ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
default       nsenter-vg4pv8                    1/1     Running   0          15m     192.168.3.141   ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
kube-system   aws-node-x5rxg                    2/2     Running   0          88m     192.168.3.141   ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
kube-system   coredns-9b5bc9468-8prkc           1/1     Running   0          92m     192.168.3.45    ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
kube-system   coredns-9b5bc9468-qp9mj           1/1     Running   0          92m     192.168.3.48    ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
kube-system   kube-proxy-sxq5k                  1/1     Running   0          88m     192.168.3.141   ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
kube-system   metrics-server-86bbfd75bb-smjpb   1/1     Running   0          93m     192.168.3.69    ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
kube-system   metrics-server-86bbfd75bb-xm8ds   1/1     Running   0          93m     192.168.3.56    ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>

 

ENI 정보를 확인해보면 192.168.3.83만 두번째 ENI에 할당된 IP로 확인됩니다.

 

해당 파드에서 ping을 하고, tcpdump를 확인해봐도 Primary ENI에서만 패킷이 확인되는 것을 알 수 있습니다.

(정정) 이후 파드 통신 절에서 파드 통신 테스트를 한번 더 해보니 이건 ENI가 통신 관여를 하지 않는 건 아니고, 외부 통신은 Primary ENI에서 내부 통신은 해당 IP를 가진 Secondary ENI에서 통신이 이뤄졌습니다.

 

이번에는 replicas를 50개까지 증가시켜보겠습니다.

$ kubectl scale deployment netshoot-pod --replicas=50
deployment.apps/netshoot-pod scaled

$ kubectl get po |grep -v Running
NAME                            READY   STATUS    RESTARTS   AGE
netshoot-pod-744bd84b46-69r4x   0/1     Pending   0          23s
netshoot-pod-744bd84b46-bhprl   0/1     Pending   0          23s
netshoot-pod-744bd84b46-g6pp4   0/1     Pending   0          24s
netshoot-pod-744bd84b46-gs4dt   0/1     Pending   0          24s
netshoot-pod-744bd84b46-jpxbc   0/1     Pending   0          24s
netshoot-pod-744bd84b46-spdw6   0/1     Pending   0          24s
netshoot-pod-744bd84b46-v56h5   0/1     Pending   0          23s
netshoot-pod-744bd84b46-vhlzn   0/1     Pending   0          24s
netshoot-pod-744bd84b46-vngbr   0/1     Pending   0          23s
netshoot-pod-744bd84b46-wf58t   0/1     Pending   0          24s
$ kubectl get po -A |wc
     62     372    5026
$ kubectl get po |grep -v Running|wc
     11      55     725

 

t3.medium 에서는 17개의 파드가 실행 가능하고, 노드가 3대이므로 총 51개의 파드가 실행 가능합니다. 결국 11개의 파드는 unschedulable 한 상태로 빠집니다.

 

 

5. 파드 통신 확인

EKS의 VPC CNI는 파드에 VPC의 IP를 할당하는 방식으로 NAT 통신이 없이 파드간 통신이 이뤄집니다.

 

클러스터 내 파드 간 통신 확인

현재 파드는 3개로 각 노드에 배포되어 있습니다. 이때, 파드간 통신을 확인해보고, 노드에서 tcpdump로 확인합니다.

$ kubectl get po -A -owide
NAMESPACE     NAME                              READY   STATUS    RESTARTS   AGE    IP              NODE                                               NOMINATED NODE   READINESS GATES
default       netshoot-pod-744bd84b46-8mrkj     1/1     Running   0          15s    192.168.3.31    ip-192-168-3-141.ap-northeast-2.compute.internal   <none>           <none>
default       netshoot-pod-744bd84b46-lgkxf     1/1     Running   0          15s    192.168.2.140   ip-192-168-2-77.ap-northeast-2.compute.internal    <none>           <none>
default       netshoot-pod-744bd84b46-qzq29     1/1     Running   0          15s    192.168.1.126   ip-192-168-1-203.ap-northeast-2.compute.internal   <none>           <none>
..

# 파드 이름 변수 지정
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}')

# 파드 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}')

echo $PODIP1 $PODIP2 $PODIP3
192.168.3.31 192.168.2.140 192.168.1.126

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

# 워커 노드 EC2 : TCPDUMP 확인
## For Pod to external (outside VPC) traffic, we will program iptables to SNAT using Primary IP address on the Primary ENI.
[root@ip-192-168-3-141 /]# sudo tcpdump -i any -nn icmp
tcpdump: data link type LINUX_SLL2
dropped privs to tcpdump
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
15:31:58.525323 eni89548af8467 In  IP 192.168.3.31 > 192.168.2.140: ICMP echo request, id 7, seq 1, length 64
15:31:58.525366 ens6  Out IP 192.168.3.31 > 192.168.2.140: ICMP echo request, id 7, seq 1, length 64
15:31:58.528035 ens6  In  IP 192.168.2.140 > 192.168.3.31: ICMP echo reply, id 7, seq 1, length 64
15:31:58.528089 eni89548af8467 Out IP 192.168.2.140 > 192.168.3.31: ICMP echo reply, id 7, seq 1, length 64
...
15:35:38.173442 eni89548af8467 In  IP 192.168.3.31 > 8.8.8.8: ICMP echo request, id 13, seq 1, length 64
15:35:38.173494 ens5  Out IP 192.168.3.141 > 8.8.8.8: ICMP echo request, id 24172, seq 1, length 64
15:35:38.202228 ens5  In  IP 8.8.8.8 > 192.168.3.141: ICMP echo reply, id 24172, seq 1, length 64
15:35:38.202403 eni89548af8467 Out IP 8.8.8.8 > 192.168.3.31: ICMP echo reply, id 13, seq 1, length 64
..

 

파드 간 통신에서 출발지는 컨테이너의 veth 인터페이스이며, 이후 ens6를 통해 Pod IP에서 Pod IP로 직접 통신이 이뤄지는 것을 알 수 있습니다. 192.168.3.31은 두번째 EN에 있는 secondary IP입니다.

 

반면 파드의 외부 통신은 출발지는 컨테이너의 veth 인터페이스이며, 이후 ens5로 통신이 이뤄지는데 이 구간에서 노드 IP로의 SNAT이 이뤄지는 것을 확인할 수 있습니다. 독특하네요!

 

아래 CNI 제안 문서에서 제공하는 그림을 보시면 이해할 수 있습니다.

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

 

그렇다면 3번째 ENI의 secondary IP의 파드는 ens7을 사용할까요? 그렇습니다.

 

통신을 해보면 ens7로 나갑니다.

[root@ip-192-168-3-141 /]# ip -br -c a
lo               UNKNOWN        127.0.0.1/8 ::1/128
ens5             UP             192.168.3.141/24 metric 1024 fe80::8ef:19ff:fe5b:1dd3/64
eni9a92535a0a9@if3 UP             fe80::b428:33ff:feaa:6633/64
enib9bf070cdb3@if3 UP             fe80::b458:f9ff:fe9a:1a0b/64
eni54524c4f9d9@if3 UP             fe80::e8a1:acff:fe6f:6a57/64
enid0ded64c01f@if3 UP             fe80::9445:9ff:feaf:ad50/64
ens6             UP             192.168.3.172/24 fe80::832:3eff:fe60:eec5/64
eni89548af8467@if3 UP             fe80::c8:88ff:fea3:5679/64
enidefacbb986c@if3 UP             fe80::2825:89ff:fec5:d261/64
eni56e598ef751@if3 UP             fe80::6ce6:79ff:fe8e:7f2c/64
eni7e0515a0713@if3 UP             fe80::40f2:24ff:fedb:164a/64
eniea2b33504a4@if3 UP             fe80::6492:f6ff:fe5d:371a/64
eni4dfe955a07f@if3 UP             fe80::c435:82ff:feae:b50b/64
ens7             UP             192.168.3.123/24 fe80::880:71ff:feec:d51b/64
[root@ip-192-168-3-141 /]# sudo tcpdump -i any -nn icmp
tcpdump: data link type LINUX_SLL2
dropped privs to tcpdump
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
16:40:48.254174 eni9e794cfd960 In  IP 192.168.3.189 > 192.168.2.140: ICMP echo request, id 7, seq 1, length 64
16:40:48.254222 ens7  Out IP 192.168.3.189 > 192.168.2.140: ICMP echo request, id 7, seq 1, length 64
16:40:48.257570 ens7  In  IP 192.168.2.140 > 192.168.3.189: ICMP echo reply, id 7, seq 1, length 64
16:40:48.257619 eni9e794cfd960 Out IP 192.168.2.140 > 192.168.3.189: ICMP echo reply, id 7, seq 1, length 64

 

 

파드의 외부 통신 확인

두번째 테스트는 VPC Peering된 네트워크에 있는 서버와의 통신이 어떻게 이뤄지는지 테스트 해보겠습니다.

 

 

해당 서버의 IP는 172.20.1.100 입니다.

[root@operator-host ~]# ip -br -c a
lo               UNKNOWN        127.0.0.1/8 ::1/128
eth0             UP             172.20.1.100/24 fe80::3f:d8ff:fe93:8b1f/64
docker0          DOWN           172.17.0.1/16

 

EKS에서는 아래와 같은 SNAT rules에 따라 VPC Cidr(192.168.0.0/16)이 아닌 경우 SNAT을 수행합니다.

[root@ip-192-168-3-141 /]#  iptables -t nat -S | grep 'A AWS-SNAT-CHAIN'
-A AWS-SNAT-CHAIN-0 -d 192.168.0.0/16 -m comment --comment "AWS SNAT CHAIN" -j RETURN
-A AWS-SNAT-CHAIN-0 ! -o vlan+ -m comment --comment "AWS, SNAT" -m addrtype ! --dst-type LOCAL -j SNAT --to-source 192.168.3.141 --random-fully

 

이때 Pod1에서 서버로 Ping을 테스트 해보겠습니다.

# kubectl exec -it $PODNAME1 -- ping -c 1 172.20.1.100
PING 172.20.1.100 (172.20.1.100) 56(84) bytes of data.
64 bytes from 172.20.1.100: icmp_seq=1 ttl=254 time=1.73 ms

--- 172.20.1.100 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 1.732/1.732/1.732/0.000 ms

 

이 구간에서는 SNAT가 수행되는 것을 확인할 수 있습니다.

 

다만 Pod -> 서버로는 통신이 되지만 서버 -> pod로는 통신이 되지 않습니다.

 

 

만약 서버가 있는 네트워크가 DX/VPN/TGW로 연결된 확장된 네트워크라고 가정을 해보고, 해당 VPC Cidr에 대해서도 NAT rules를 제외하는방법을 알아 보겠습니다.

 

아래와 같이 aws-node에 AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS를 환경 변수로 전달하는 방법을 사용할 수 있습니다. 이 경우 aws-node 파드들이 재시작 하는 것을 확인할 수 있습니다.

$ kubectl set env daemonset aws-node -n kube-system AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS=172.20.0.0/16
daemonset.apps/aws-node env updated

# 파드 롤링 업데이트 수행
$ kubectl get po -A -w
NAMESPACE     NAME                              READY   STATUS            RESTARTS   AGE
default       netshoot-pod-744bd84b46-8mrkj     1/1     Running           0          36m
default       netshoot-pod-744bd84b46-lgkxf     1/1     Running           0          36m
default       netshoot-pod-744bd84b46-qzq29     1/1     Running           0          36m
default       nsenter-vg4pv8                    1/1     Running           0          83m
kube-system   aws-node-hg2mg                    0/2     PodInitializing   0          1s
kube-system   aws-node-nlqk9                    2/2     Running           0          5s
kube-system   aws-node-x5rxg                    2/2     Running           0          156m
kube-system   coredns-9b5bc9468-8prkc           1/1     Running           0          160m
kube-system   coredns-9b5bc9468-qp9mj           1/1     Running           0          160m
kube-system   kube-proxy-d7l2t                  1/1     Running           0          156m
kube-system   kube-proxy-fp99v                  1/1     Running           0          156m
kube-system   kube-proxy-sxq5k                  1/1     Running           0          156m
kube-system   metrics-server-86bbfd75bb-smjpb   1/1     Running           0          160m
kube-system   metrics-server-86bbfd75bb-xm8ds   1/1     Running           0          160m
kube-system   aws-node-hg2mg                    1/2     Running           0          2s
kube-system   aws-node-hg2mg                    2/2     Running           0          3s
kube-system   aws-node-x5rxg                    2/2     Terminating       0          156m
kube-system   aws-node-x5rxg                    0/2     Completed         0          156m
kube-system   aws-node-5qxln                    0/2     Pending           0          1s
kube-system   aws-node-5qxln                    0/2     Pending           0          1s


이제 노드에서도 확인해보면 172.20.0.0/16 에 대해서 AWS SNAT CHAIN EXCLUSION으로 추가된 것을 알 수 있습니다.

[root@ip-192-168-3-141 /]# iptables -t nat -S | grep 'A AWS-SNAT-CHAIN'
-A AWS-SNAT-CHAIN-0 -d 172.20.0.0/16 -m comment --comment "AWS SNAT CHAIN EXCLUSION" -j RETURN
-A AWS-SNAT-CHAIN-0 -d 192.168.0.0/16 -m comment --comment "AWS SNAT CHAIN" -j RETURN
-A AWS-SNAT-CHAIN-0 ! -o vlan+ -m comment --comment "AWS, SNAT" -m addrtype ! --dst-type LOCAL -j SNAT --to-source 192.168.3.141 --random-fully

 

다시 통신 테스트를 진행해보면 SNAT이 없이 Pod -> 서버 통신이 가능한 것으로 확인됩니다.

 

 

다만 kubectl set env daemonset aws-node -n kube-system으로 적용하는 방법은 영구적으로 적용되지 않기 때문에 아래와 같이 Addon 에 Configuration values를 추가해 줄 수 있습니다.

{
    "env": {
        "AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS" : "172.20.0.0/16"
    }
}

 

 

웹 콘솔에서 EKS에서 클러스터를 선택하고, Add-ons탭에서 Amazon VPC CNI를 진입하고 Edit을 선택합니다.

Add-on configuration schema를 통해서 내용을 하고, json 구조에 맞춰 작성이 필요합니다.

 

적용을 하면 aws-node 파드들이 자동으로 재배포 됩니다.

kubectl get po -A -w
NAMESPACE     NAME                              READY   STATUS    RESTARTS   AGE
kube-system   aws-node-b7qj7                    2/2     Running   0          40s
kube-system   aws-node-j77kr                    2/2     Running   0          36s
kube-system   aws-node-qtchj                    2/2     Running   0          44s

 

확인해보면 AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS 이 적용되어 있는 것을 확인할 수 있습니다.

kubectl get ds aws-node -n kube-system -o json | jq '.spec.template.spec.containers[0].env'
[
  {
    "name": "ADDITIONAL_ENI_TAGS",
    "value": "{}"
  },
..
  {
    "name": "AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS",
    "value": "172.20.0.0/16"
  },
..

 

 

AKS에서 사용하는 ip-masq-agent의 기본 설정은 azure-ip-masq-agent-config-reconciled라는 configmap에 정보를 확인할 수 있습니다.

# Virtual Network에 대해서 nonMasqeuradeCIDRs 로 정의되어 있음
$ kubectl get cm azure-ip-masq-agent-config-reconciled -n kube-system -oyaml
apiVersion: v1
data:
  ip-masq-agent-reconciled: |-
    nonMasqueradeCIDRs:
      - 10.244.0.0/16
    masqLinkLocal: true
kind: ConfigMap
...
  labels:
    addonmanager.kubernetes.io/mode: Reconcile
    component: ip-masq-agent
    kubernetes.io/cluster-service: "true"
  name: azure-ip-masq-agent-config-reconciled
  namespace: kube-system

# 노드의 iptables 정보
root@aks-nodepool1-76251328-vmss000000:/# iptables -t nat -S |grep ip-masq-agent
-A POSTROUTING -m comment --comment "\"ip-masq-agent: ensure nat POSTROUTING directs all non-LOCAL destination traffic to our custom IP-MASQ-AGENT chain\"" -m addrtype ! --dst-type LOCAL -j IP-MASQ-AGENT
-A IP-MASQ-AGENT -d 10.244.0.0/16 -m comment --comment "ip-masq-agent: local traffic is not subject to MASQUERADE" -j RETURN
-A IP-MASQ-AGENT -m comment --comment "ip-masq-agent: outbound traffic is subject to MASQUERADE (must be last in chain)" -j MASQUERADE

 

 

다만 이 configmap은 addonmanager.kubernetes.io/mode: Reconcile으로 지정되어 addonManager에 의해서 관리되므로, 사용자가 임의로 변경해도 다시 Reconcile 됩니다.

 

아래와 같은 절차로 configmap을 생성하는 경우 iptables가 변경되는 것을 확인하실 수 있습니다.

$ cat config-custom.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: azure-ip-masq-agent-config
  namespace: kube-system
  labels:
    component: ip-masq-agent
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: EnsureExists
data:
  ip-masq-agent: |-
    nonMasqueradeCIDRs:
      - 172.31.1.0/24
    masqLinkLocal: false
    masqLinkLocalIPv6: true

$ kubectl apply -f config-custom.yaml
configmap/azure-ip-masq-agent-config created

# 노드의 iptables 정보 (172.31.1.0/24가 추가됨)
root@aks-nodepool1-76251328-vmss000000:/# iptables -t nat -S |grep ip-masq-agent
-A POSTROUTING -m comment --comment "\"ip-masq-agent: ensure nat POSTROUTING directs all non-LOCAL destination traffic to our custom IP-MASQ-AGENT chain\"" -m addrtype ! --dst-type LOCAL -j IP-MASQ-AGENT
-A IP-MASQ-AGENT -d 172.31.1.0/24 -m comment --comment "ip-masq-agent: local traffic is not subject to MASQUERADE" -j RETURN
-A IP-MASQ-AGENT -d 10.244.0.0/16 -m comment --comment "ip-masq-agent: local traffic is not subject to MASQUERADE" -j RETURN
-A IP-MASQ-AGENT -m comment --comment "ip-masq-agent: outbound traffic is subject to MASQUERADE (must be last in chain)" -j MASQUERADE

 

 

Azure의 공식 문서에서 가이드가 확인하기 어렵지만, 아래 깃헙 리파지터를 확인해보시면 daemonset과 동일한 네임스페이스에 configmap을 생성하면 되는 것을 알 수 있습니다.

https://github.com/Azure/ip-masq-agent-v2

 

 

마무리

EKS Networking이라는 주제로 CNI의 의미를 살펴보고 AWS VPC CNI의 특성과 EKS에서 인스턴스 타입에 따른 파드 생성 제한에 대해서 확인해 보았습니다.

이후 EKS 클러스터를 생성해보고, 앞서 확인한 정보를 바탕으로 노드와 파드 정보를 확인하는 실습을 다뤄봤습니다.

한편으로 EKS는 단일 VPC CNI만 제공하고 있는 반면 AKS에서는 다양한 CNI를 제공하는 차이점이 있었습니다.

 

다음 포스트에는 EKS Networking 두번째 주제로, Loadbalancer 유형의 서비스와 Ingress를 EKS에서 어떻게 구현하는 지 살펴보겠습니다.

'EKS' 카테고리의 다른 글

[4] EKS의 모니터링과 로깅  (0) 2025.03.01
[3-2] EKS 노드 그룹  (0) 2025.02.23
[3-1] EKS 스토리지 옵션  (0) 2025.02.23
[2-2] EKS Networking Part2 - LoadBalancer와 Ingress  (0) 2025.02.16
[1] EKS 생성과 리소스 살펴보기  (2) 2025.02.07

+ Recent posts