본 포스트에서는 기본적인 쿠버네티스 환경의 스케일링 기술을 살펴보겠습니다. 이후 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

+ Recent posts