본 포스트에서는 기본적인 쿠버네티스 환경의 스케일링 기술을 살펴보겠습니다. 이후 EKS의 오토스케일링 옵션을 살펴보고, 각 옵션을 실습을 통해 살펴도록 하겠습니다. 마지막으로 AKS의 오토스케일링 옵션을 EKS와 비교해 보겠습니다.
이번 포스트에서는 EKS의 오토스케일링(Autoscaling) Part2로 지난 포스트에 이어서 Cluster Autoscaler 부터 이어나가겠습니다.
목차
EKS의 오토스케일링 Part1 (https://a-person.tistory.com/38)
- 쿠버네티스 환경의 스케일링
- EKS의 오토스케일링 개요
- 실습 환경 생성
- HPA(Horizontal Pod Autoscaler)
- KEDA(Kubernetes Event-driven Autoscaler)
- VPA(Vertical Pod Autoscaler)
EKS의 오토스케일링 Part2
- CA(Cluster Autoscaler)
- Karpenter
- AKS의 오토스케일링
- 오토스케일링에 대한 주의사항
1. CA(Cluster Autoscaler)
노드를 스케일링하는 CA(Cluster Autoscaler)를 살펴보겠습니다.
많은 사람들이 클라우드 환경에서 컴퓨팅 자원을 기반으로 한 오토스케일링에 대한 이해를 하고 있기 때문에, 가상 머신 세트(예를 들어, ASG, VMSS 등)의 CPU/Memory와 같은 리소스 사용률이 CA를 동작시키는 것으로 오해하는 경우가 많습니다.
하지만 쿠버네티스의 CA는 아래와 같은 상황에서 동작합니다.
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 문서를 참고하실 수 있습니다.
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/
노드의 생성단계는 아래와 같이 진행됩니다.
- Create NodeClaim: Karpenter는 배포(Provisioning) 혹은 중단(Disrupting) 요구에 따라 새로운 NodeClaim을 생성합니다.
- Launch NodeClaim: AWS에 새로운 EC2 Instance를 생성하기 위해 CreateFleet API를 호출합니다.
- Register NodeClaim: EC2 Instance가 생성되고 Cluster에 등록된 Node를 NodeClaim과 연결합니다.
- 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
로 정의할 수 있습니다.
아래 문서를 통해 지원 가능한 옵션을 살펴보실 수 있습니다.
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 |