EKS에서 워크로드를 운영하려면 두 가지를 결정해야 한다. 어디서 실행할 것인가(컴퓨팅)와 얼마나 실행할 것인가(오토스케일링). 3주차에서는 관리형 노드 그룹으로 컴퓨팅 옵션을 살펴보고 HPA부터 Karpenter까지 여러 스케일링 방법을 실습한다.
0. 실습 환경 구성
Terraform으로 EKS 클러스터를 배포한다. 이번 실습 환경의 변경점은 k8s 1.35 버전 사용과 워커노드 Private Subnet 배치, SSM 접속 지원이다. add-on으로 metrics-server와 external-dns가 포함되어 있다.
# 코드 다운로드
git clone https://github.com/gasida/aews.git
cd aews/3w
# IAM Policy 파일 작성
curl -o aws_lb_controller_policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/refs/heads/main/docs/install/iam_policy.json
# 배포 (약 12분 소요)
terraform init
terraform plan
nohup sh -c "terraform apply -auto-approve" > create.log 2>&1 &
tail -f create.log
# 배포 완료 후 kubeconfig 설정
$(terraform output -raw configure_kubectl)
kubectl config rename-context $(cat ~/.kube/config | grep current-context | awk '{print $2}') myeks
배포가 완료되면 add-on을 확인한다.
aws eks list-addons --cluster-name myeks | jq
# coredns, external-dns, kube-proxy, metrics-server, vpc-cni
external-dns는 policy=upsert-only로 설정되어 있어 레코드 생성/수정만 하고 삭제는 수동으로 해야 한다. policy=sync로 바꾸면 자동 삭제도 가능하다.
SSM으로 노드 접속하기
워커 노드가 Private Subnet에 있기 때문에 SSH 대신 SSM Session Manager로 접속한다.
# SSM 대상 인스턴스 확인
aws ssm describe-instance-information \
--query "InstanceInformationList[*].{InstanceId:InstanceId, Status:PingStatus}" \
--output text
# 접속
aws ssm start-session --target i-0a6db521a84f6ea1f
SSM은 AWS 공인 IP 대역을 통해 연결되며 별도의 인바운드 포트 오픈이 필요 없다. IAM 사용자의 세션 활동 로깅도 가능해서 감사(audit) 측면에서도 유리하다.
AWS Load Balancer Controller 설치
LBC 설치 시 이슈가 있었다. VPC를 찾지 못한다는 오류가 발생했는데 노드의 IMDS에서 VPC ID를 가져와야 하는 상황이었다. Terraform 코드에서 hop limit을 2로 설정해 두었기 때문에 파드에서 IMDS 접근이 가능하다.
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 --version 3.1.0 --set clusterName=myeks
eks-node-viewer 설치
노드의 할당 가능 용량과 요청 리소스를 시각적으로 보여주는 도구다. "실제 파드 리소스 사용량이 아니라 request 값을 표시한다"는 점을 기억해야 한다. init container는 포함하지 않는다.
brew tap aws/tap
brew install eks-node-viewer
# CPU와 메모리를 함께 표시
eks-node-viewer --resources cpu,memory
# 노드 나이와 AZ 정보까지 표시
eks-node-viewer --resources cpu,memory --extra-labels eks-node-viewer/node-age,topology.kubernetes.io/zone
kube-ops-view + kube-prometheus-stack
클러스터 시각화를 위해 kube-ops-view를 배포하고 모니터링을 위해 kube-prometheus-stack을 설치한다. 같은 ALB를 공유하기 위해 group.name: study 설정을 사용한다.
# 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
# kube-prometheus-stack
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack \
--version 80.13.3 -f monitor-values.yaml --create-namespace --namespace monitoring
EKS 컨트롤 플레인 메트릭(apiserver, scheduler, controller manager)도 additionalScrapeConfigs로 수집하도록 구성했다. 설치 후 prometheus.hackjap.link와 grafana.hackjap.link로 접속할 수 있다.
1. 관리형 노드 그룹
EKS 관리형 노드 그룹(Managed Node Group)은 온디맨드, Graviton(ARM), Spot 인스턴스로 나뉜다.
1.1 온디맨드 노드 그룹
기본 생성되는 myeks-ng-1은 온디맨드 인스턴스다.
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-19-163.ap-northeast-2.compute.internal Ready <none> 40m v1.35.2-eks-f69f56f myeks-ng-1 amd64 ON_DEMAND
ip-192-168-22-130.ap-northeast-2.compute.internal Ready <none> 40m v1.35.2-eks-f69f56f myeks-ng-1 amd64 ON_DEMAND
amd64 아키텍처에 ON_DEMAND 타입으로 2대가 실행 중이다.
1.2 Graviton(ARM) 노드 그룹
AWS Graviton은 64-bit ARM 프로세서 코어 기반의 AWS 커스텀 반도체다. 기존 인텔 x86 대비 "20~40% 향상된 가격대비 성능"을 제공한다. Terraform 코드에서 ami_type을 AL2023_ARM_64_STANDARD로 지정하고 t4g.medium 인스턴스를 사용한다.
멀티 아키텍처 환경에서 가장 주의할 점은 이미지 호환성이다. ARM 노드에 amd64 전용 이미지를 배포하면 실행되지 않는다. Taint와 Toleration으로 제어한다.
# node label
labels = {
tier = "secondary"
}
# node taint - Toleration 없는 파드는 스케줄링되지 않고, 기존 파드도 Evict됨
taints = {
frontend = {
key = "cpuarch"
value = "arm64"
effect = "NO_EXECUTE"
}
}
NO_EXECUTE는 가장 강한 Taint 효과다. 스케줄링을 막을 뿐 아니라 이미 실행 중인 파드도 Toleration이 없으면 퇴출한다.
노드 추가 후 SSM으로 접속하면 arch 명령어로 aarch64가 출력되는 것을 확인할 수 있다.
NAME STATUS ROLES AGE NODEGROUP ARCH CAPACITYTYPE
ip-192-168-14-105.ap-northeast-2.compute.internal Ready <none> 113s myeks-ng-2 arm64 ON_DEMAND
ip-192-168-19-163.ap-northeast-2.compute.internal Ready <none> 46m myeks-ng-1 amd64 ON_DEMAND
ip-192-168-22-130.ap-northeast-2.compute.internal Ready <none> 46m myeks-ng-1 amd64 ON_DEMAND
ARM 노드에 파드를 배포할 때는 nodeSelector로 아키텍처를 지정하고 Toleration을 추가해야 한다. nginx:alpine처럼 멀티 아키텍처를 지원하는 이미지를 사용하면 된다.
1.3 Spot 인스턴스 노드 그룹
Spot 인스턴스는 AWS EC2 여유 용량을 경매 방식으로 사용한다. 온디맨드 대비 최대 90%까지 저렴하지만 언제든 회수될 수 있다. 상태 비저장(Stateless) 워크로드에 적합하다. Pod를 우아하게 종료할 수 있고 Spot이 중단되면 다른 노드에서 대체 Pod를 스케줄링할 수 있기 때문이다.
Spot 노드 그룹 설정에서 "여러 인스턴스 타입을 지정하는 것"이 가장 중요하다.
third = {
name = "${var.ClusterBaseName}-ng-3"
ami_type = "AL2023_x86_64_STANDARD"
capacity_type = "SPOT"
instance_types = ["c5a.large", "c6a.large", "t3a.large", "t3a.medium"]
desired_size = 1
max_size = 1
min_size = 1
}
인스턴스 타입 하나만 지정하면 해당 타입의 Spot 용량이 없을 때 노드를 확보하지 못한다. 여러 타입으로 가용 풀을 넓혀야 안정적이다. nodeSelector로 Spot 노드에 파드를 배치할 수 있다.
kubectl get nodes --label-columns eks.amazonaws.com/capacityType
NAME STATUS ROLES AGE CAPACITYTYPE
ip-192-168-14-187.ap-northeast-2.compute.internal Ready <none> 106s SPOT
ip-192-168-19-163.ap-northeast-2.compute.internal Ready <none> 61m ON_DEMAND
ip-192-168-22-130.ap-northeast-2.compute.internal Ready <none> 61m ON_DEMAND
1.4 IMDS 보안 이슈
노드에서 동작하는 파드는 EC2의 IMDS(Instance Metadata Service)에 접근할 수 있다. 이게 왜 문제가 되는지 확인해본다.
awscli 파드를 배포하고 별도의 IAM 자격 증명 없이 AWS API를 호출한다.
# 별도 IAM 자격 증명 없이도 동작한다
kubectl exec -it $APODNAME1 -- aws ec2 describe-instances --region ap-northeast-2 --output table --no-cli-pager
kubectl exec -it $APODNAME2 -- aws ec2 describe-vpcs --region ap-northeast-2 --output table --no-cli-pager
자격 증명이 없는데도 API 호출이 된다. 파드가 노드의 EC2 Instance Profile(IAM Role)을 그대로 사용하기 때문이다. 보안이 취약한 컨테이너가 탈취되면 IMDS로 노드의 IAM Role에 접근할 수 있다는 뜻이다.
IMDSv2에서는 토큰 기반으로 접근을 제한한다.
# IMDSv2 토큰 요청
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
# 토큰으로 IAM Role 자격증명 조회
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/myeks-ng-1
출력된 AccessKeyId, SecretAccessKey, Token은 Expiration 전까지 어디서든 사용 가능하다. 제가 생각하기에 이 부분은 IMDS 접근 자체를 Pod 단위로 차단하고 IRSA(IAM Roles for Service Accounts)나 Pod Identity를 사용하는 것을 추천드립니다. 자세한 내용은 4주차(보안)에서 다룬다.
2. 스케일링 전략 개요
스케일링을 적용하기 전에 먼저 "우리 워크로드에 어떤 수준의 스케일링이 필요한가"를 판단해야 한다. EKS 스케일링 전략 의사결정 프레임워크를 참고하면 유용하다.
| 접근법 | 핵심 전략 | E2E 스케일링 시간 | 복잡도 | 적합한 워크로드 |
|---|---|---|---|---|
| 반응형 고속화 | Karpenter + KEDA + Warm Pool | 5~45초 | 매우 높음 | 극소수 미션 크리티컬 |
| 예측형 스케일링 | CronHPA + Predictive Scaling | 사전 확장 (0초) | 낮음 | 패턴 있는 대부분의 서비스 |
| 아키텍처 복원력 | SQS/Kafka + Circuit Breaker | 스케일링 지연 허용 | 중간 | 비동기 처리 가능한 서비스 |
| 적정 기본 용량 | 기본 replica 20~30% 증설 | 불필요 (이미 충분) | 매우 낮음 | 안정적인 트래픽 |
"트래픽 급증 시 사용자 에러 방지"라는 동일한 문제를 해결하는 접근법이 4가지나 있다. 대부분의 워크로드에서는 접근법 2~4가 더 비용 효율적이다. 반응형 고속화는 극소수 미션 크리티컬 서비스에만 적용하면 된다.
스케일링 방법은 크게 다섯 가지다.
- HPA - 파드 수를 수평 확장. CPU/Memory 메트릭 기반
- VPA - 파드 리소스를 수직 확장. request/limit 최적값 자동 조정
- KEDA - 이벤트 기반 파드 오토스케일링. Cron, SQS, Kafka 등
- CA/CAS - 노드를 수평 확장. ASG 연동
- Karpenter - 노드를 수평/수직 확장. EC2 API 직접 호출
3. HPA - Horizontal Pod Autoscaler
HPA는 쿠버네티스에 내장된 오토스케일러다. Pod 메트릭을 관찰하고 목표값에 맞춰 복제본 수를 조절한다.
3.1 HPA 실습
샘플 애플리케이션을 배포한다. hpa-example 이미지는 내부적으로 100만번 덧셈을 수행하는 CPU 과부하 연산을 실행한다.
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
spec:
ports:
- port: 80
selector:
run: php-apache
HPA 정책을 생성한다. CPU 사용률이 50%를 넘으면 Pod를 늘리고 최대 10개까지 확장한다.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: php-apache
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: php-apache
minReplicas: 1
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
averageUtilization: 50
type: Utilization
증가와 감소의 대기 시간이 다르다는 점을 알아두자. 증가 시 기본 30초이고 감소 시 기본 5분이다. 감소 대기 시간이 더 긴 이유는 트래픽이 다시 급증할 경우를 대비하기 위해서다.
부하를 발생시켜 스케일링을 확인한다.
kubectl exec -it curl -- sh -c '
for i in $(seq 1 5); do
while true; do curl -s php-apache & sleep 1; done &
done
wait'

kubectl describe hpa로 이벤트를 확인하면 Pod가 4개에서 5개로 순차적으로 늘어나는 것을 볼 수 있다.
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SuccessfulRescale 2m28s horizontal-pod-autoscaler New size: 4; reason: cpu resource utilization (percentage of request) above target
Normal SuccessfulRescale 2m13s horizontal-pod-autoscaler New size: 5; reason: cpu resource utilization (percentage of request) above target
3.2 커스텀 메트릭 HPA
autoscaling/v2부터는 CPU/Memory 외에 커스텀 메트릭으로도 스케일링할 수 있다. 메트릭 소스에 따라 세 가지 API로 나뉜다.
- Resource Metric API (
metrics.k8s.io) - Node나 Pod의 CPU/메모리 사용량 - Custom Metric API (
custom.metrics.k8s.io) - 클러스터 내부 사용자 정의 메트릭 - External Metric API (
external.metrics.k8s.io) - 클러스터 외부 메트릭
스케일링 기준이 되는 지표를 어디서 가져오느냐에 따라 타입이 달라진다.
- Pods 타입 - 대상 파드에서 직접 수집. 초당 패킷 수나 TPS 등
- Object 타입 - 클러스터 내 다른 리소스(Ingress, Service)에서 수집. Ingress의 초당 HTTP 요청 수 등
- External 타입 - 클러스터 외부 시스템에서 수집. AWS SQS 대기 메시지 수나 Kafka 토픽 lag 등
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50
- type: Pods
pods:
metric:
name: packets-per-second
target:
type: AverageValue
averageValue: 1k
- type: Object
object:
metric:
name: requests-per-second
describedObject:
apiVersion: networking.k8s.io/v1
kind: Ingress
name: main-route
target:
type: Value
value: 10k
메시지 큐 처리 파드의 CPU는 여유롭지만 RabbitMQ 큐에 메시지가 만 건 이상 쌓였을 때 선제적으로 파드를 늘리는 식으로 활용한다.
3.3 HPA 주의사항
HPA를 운영할 때 두 가지를 주의해야 한다.
첫째, 스파이크성 트래픽에 대응이 어렵다. 메트릭 수집부터 Pod 생성까지 시간이 걸린다. 갑자기 트래픽이 급증하면 이미 응답 지연이 발생한 뒤에야 스케일 아웃된다.
둘째, Pod 초기화 시 CPU/Memory가 일시적으로 급등하는 버스트 현상이 있다. Kubernetes Startup CPU Boost를 사용하면 InPlacePodVerticalScaling으로 시작 시에만 CPU를 높게 잡았다가 초기화 이후 원래 값으로 내릴 수 있다.
4. VPA - Vertical Pod Autoscaler
VPA는 Pod의 resources.request를 최적값으로 수정한다. HPA와 동시에 사용할 수 없다는 점이 가장 큰 제약이다.
기존에는 Pod를 재시작(기존 Pod 종료 후 새 Pod 생성)해야 했다. 최근 In-place 업데이트가 나오면서 재시작 없이 CPU를 조정할 수 있게 되었지만 Memory는 아직 불안정하다.
VPA를 설치하고 공식 예제(hamster.yaml)를 배포하면 2~3분 뒤에 Pod의 request가 자동으로 변경된다. updateMode: Off로 설정하면 추천값만 확인하고 실제 변경은 하지 않는다.
# VPA 추천값 확인
kubectl describe pod | grep Requests: -A2
Requests:
cpu: 587m
memory: 262144k
JVM 기반 애플리케이션에서 주의할 점이 있다. VPA가 memory를 4Gi로 올려도 JVM 힙 사이즈가 -Xmx2g로 고정되어 있으면 나머지 2Gi는 낭비된다. 해결 방법은 -XX:MaxRAMPercentage=75.0을 사용해서 컨테이너 limit에 비례하도록 동적 계산하는 것이다.
VPA 대안으로 KRR(Kubernetes Resource Recommender)도 있다. Prometheus 기반으로 동작하며 클러스터 외부에서 실행할 수 있고 즉시 결과를 보여준다.
brew tap robusta-dev/homebrew-krr
brew install krr
# 1달 데이터 기준으로 CPU 90 percentile, Memory 10% 버퍼
krr simple --history_duration 720 --cpu_percentile 90 --memory_buffer_percentage 10
5. KEDA - Kubernetes Event-driven Autoscaling
HPA는 리소스 메트릭 기반이다. KEDA는 여기에 이벤트 기반 스케일링을 추가한다. Airflow의 metadb에서 대기 중인 task 수를 보고 worker를 확장하거나 SQS 큐 깊이에 따라 처리 파드를 늘릴 수 있다. CPU 사용률이 올라가기 전에 미리 확장할 수 있어서 HPA보다 반응이 빠르다.
KEDA는 세 가지 컴포넌트로 구성된다.
keda-operator- Deployment를 활성화/비활성화하여 0까지 스케일 다운keda-operator-metrics-apiserver- External Metrics API로 이벤트 데이터를 HPA에 노출keda-admission-webhooks- 동일 타겟에 여러 ScaledObject가 걸리는 것을 방지
KEDA Cron 실습
KEDA를 설치하고 Cron 기반 스케일링을 테스트한다.
helm repo add kedacore https://kedacore.github.io/charts
helm install keda kedacore/keda --version 2.16.0 \
--namespace keda --create-namespace -f keda-values.yaml
ScaledObject를 생성한다.
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를 대체하며 pollingInterval(30초)마다 트리거 조건을 확인한다. cooldownPeriod(300초)는 스케일 다운까지 대기 시간이다. Kafka trigger 같은 것도 지원해서 lagThreshold를 기준으로 Consumer 파드를 스케일링할 수도 있다.
트래픽 패턴이 예측 가능한 서비스라면 Cron 트리거로 특정 시간대에 미리 파드를 확보하는 전략이 유용하다. Karpenter와 조합하면 해당 시간에 노드까지 함께 확장할 수 있다.
6. 노드 오토스케일링
Pod 스케일링만으로는 부족하다. Pod가 늘어나도 노드 자원이 부족하면 Pending 상태에 머문다. 노드 수를 자동으로 조절하는 방법을 살펴본다.
6.1 CA/CAS - Cluster Autoscaler
CA는 쿠버네티스 기본 API의 일부가 아니라 선택적 컴포넌트다. Pending 상태인 파드가 있으면 워커 노드를 스케일 아웃하고 노드가 저활용 상태이면 스케일 인한다.
AWS에서는 ASG(Auto Scaling Group)와 연동된다. Auto-Discovery 방식을 사용하면 태그 기반으로 ASG를 자동 인식한다.
# EKS 노드에 이미 아래 태그가 설정되어 있다
# k8s.io/cluster-autoscaler/enabled: true
# k8s.io/cluster-autoscaler/myeks: owned
# CA 배포
curl -s -O https://raw.githubusercontent.com/kubernetes/autoscaler/master/cluster-autoscaler/cloudprovider/aws/examples/cluster-autoscaler-autodiscover.yaml
sed -i "s/<YOUR CLUSTER NAME>/myeks/g" cluster-autoscaler-autodiscover.yaml
kubectl apply -f cluster-autoscaler-autodiscover.yaml
CA의 스케일 다운 기본 대기 시간은 10분이다. --scale-down-delay-after-add 플래그로 조정할 수 있다.
CA + ASG 조합의 한계는 분명하다.
- 리소스 관리가 분리된다. ASG와 EKS가 각각 노드를 관리하므로 EKS에서 노드를 삭제해도 ASG의 인스턴스가 남아있을 수 있다
- 노드 생성이 느리다. CA → ASG → EC2 Fleet API로 거치는 경로가 길다
- 사전 정의된 노드 그룹에 의존하므로 인스턴스 타입이 동일해야 한다. 혼합 타입이면 CPU와 메모리가 균등해야 한다
- 모범사례는 AZ당 노드 그룹을 갖는 것인데 관리할 노드 그룹이 많아진다
6.2 Karpenter
Karpenter는 CA의 한계를 해결하기 위해 등장했다. ASG를 거치지 않고 EC2 Fleet API를 직접 호출해서 노드를 생성한다.
동작 방식을 비교하면 차이가 뚜렷하다.
- CA: Pending 파드 발견 → ASG 조정 요청 → EC2 Fleet API → 노드 생성
- Karpenter: Pending 파드 발견 → Pod 스펙 평가 → EC2 Fleet API 직접 호출 → 노드 생성
ASG를 거치지 않으니 속도가 빠르다. EC2 시작 템플릿도 필요 없고 NodePool CR이 ASG를 대체한다. GitOps 방식으로 관리할 수 있다.
Karpenter v1에서 리소스 명칭이 바뀌었다.
| 이전(v1beta1) | 현재(v1) |
|---|---|
| Provisioner | NodePool |
| AWSNodeTemplate | EC2NodeClass |
| Machine | NodeClaim |
6.3 Karpenter 설치
Karpenter는 기존 myeks 클러스터가 아닌 별도의 전용 클러스터를 생성해서 실습한다.
먼저 변수를 설정하고 CloudFormation으로 필요한 AWS 리소스를 만든다.
export KARPENTER_NAMESPACE="kube-system"
export KARPENTER_VERSION="1.10.0"
export K8S_VERSION="1.34"
export CLUSTER_NAME="hackjap-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/')"
# CloudFormation으로 IAM Role, Instance Profile 등 생성
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}"
eksctl로 EKS 클러스터를 생성한다. 모든 리소스에 karpenter.sh/discovery 태그를 달아 Karpenter가 자신이 관리할 리소스를 식별하게 한다. Pod Identity addon도 함께 설치한다.
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:iam::${AWS_ACCOUNT_ID}:policy/KarpenterControllerPolicy-${CLUSTER_NAME}
iamIdentityMappings:
- arn: "arn:aws:iam::${AWS_ACCOUNT_ID}:role/KarpenterNodeRole-${CLUSTER_NAME}"
username: system:node:{{EC2PrivateDNSName}}
groups:
- system:bootstrappers
- system:nodes
managedNodeGroups:
- instanceType: m5.large
amiFamily: AmazonLinux2023
name: ${CLUSTER_NAME}-ng
desiredCapacity: 2
minSize: 1
maxSize: 10
addons:
- name: eks-pod-identity-agent
EOF
클러스터가 생성되면 Helm으로 Karpenter를 설치한다.
export CLUSTER_ENDPOINT="$(aws eks describe-cluster --name "${CLUSTER_NAME}" --query "cluster.endpoint" --output text)"
export KARPENTER_IAM_ROLE_ARN="arn:aws:iam::${AWS_ACCOUNT_ID}:role/${CLUSTER_NAME}-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
# 설치 확인
kubectl get crd | grep karpenter
6.4 NodePool과 EC2NodeClass 설정
가드레일 방식으로 인스턴스 조건만 선언하면 Karpenter가 워크로드에 맞는 최적 타입을 알아서 선택한다.
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
limits:
cpu: 1000
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 1m
---
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: default
spec:
role: "KarpenterNodeRole-${CLUSTER_NAME}"
amiSelectorTerms:
- alias: "al2023@${ALIAS_VERSION}"
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: "${CLUSTER_NAME}"
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: "${CLUSTER_NAME}"
karpenter.sh/discovery 태그로 Karpenter가 관리할 서브넷과 보안 그룹을 식별한다. 클러스터 생성 시 모든 리소스에 이 태그를 달아야 한다.
6.5 Disruption - 노드 라이프사이클 관리
Karpenter의 Disruption은 노드를 적극적으로 관리하는 메커니즘이다. 조건이 충족되면 노드에 cordon을 설정하고 Pod를 안전하게 다른 노드로 옮긴 뒤 빈 노드를 종료한다. 세 가지 유형이 있다.
Expiration(만료) - 노드가 expireAfter에 지정된 시간이 지나면 자동으로 교체된다. 기본값은 720시간(30일)이다. 오래된 노드를 주기적으로 교체해서 AMI 패치나 보안 업데이트를 자연스럽게 반영할 수 있다.
Drift(드리프트) - NodePool이나 EC2NodeClass 설정이 변경되면 기존 노드를 새 설정에 맞게 교체한다. AMI 업데이트나 보안 그룹 변경 시 자동으로 일관성을 유지한다.
Consolidation(통합) - 저활용 노드의 Pod를 다른 노드로 옮기고 빈 노드를 제거한다. Bin Packing 알고리즘으로 노드 수를 최소화한다.
consolidationPolicy 옵션은 세 가지다.
WhenEmpty- 모든 워크로드가 제거된 경우에만 종료WhenUnderutilized- 리소스 사용률이 낮고 워크로드 이동이 가능하면 종료WhenEmptyOrUnderutilized- 가장 적극적인 정책
6.6 Over-Provisioning으로 스파이크 대응
CA든 Karpenter든 노드를 새로 프로비저닝하려면 시간이 걸린다. 기존 노드에 Pod를 추가하는 것보다 느리다. 이 지연을 해결하는 방법이 Over-Provisioning이다.
우선순위가 낮은 placeholder Pod를 배포해서 여유 노드를 미리 확보해 둔다. 실제 워크로드가 확장되면 placeholder Pod가 선점(preempt)되고 그 자리에 실제 Pod가 들어간다. placeholder가 쫓겨나면 새 노드 프로비저닝이 시작되는데 이때는 이미 실제 워크로드는 기존 노드에서 돌고 있다.
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: placeholder-priority
value: -10
preemptionPolicy: Never
globalDefault: false
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: placeholder
spec:
replicas: 10
selector:
matchLabels:
pod: placeholder-pod
template:
metadata:
labels:
pod: placeholder-pod
spec:
priorityClassName: placeholder-priority
terminationGracePeriodSeconds: 0
containers:
- name: ubuntu
image: ubuntu
command: ["sleep"]
args: ["infinity"]
resources:
requests:
cpu: 200m
memory: 250Mi
placeholder Pod의 리소스 크기는 실제 워크로드의 가장 큰 Pod 크기 이상이어야 한다. 그렇지 않으면 placeholder가 선점되어도 실제 Pod가 그 자리에 들어가지 못한다.
7. 스케일링 전략 비교
| 구분 | 대상 | 방식 | 적합한 상황 |
|---|---|---|---|
| HPA | Pod | 수평 확장 | CPU/Memory 기반 Stateless 워크로드 |
| VPA | Pod | 수직 확장 | 리소스 요청값 튜닝 (HPA와 병용 불가) |
| KEDA | Pod | 이벤트 기반 수평 확장 | 메시지 큐, Cron, 외부 이벤트 |
| CA/CAS | Node | ASG 연동 | 기존 ASG 기반 환경 |
| Karpenter | Node | EC2 직접 호출 | 빠른 스케일링과 비용 최적화 |
각 도구마다 대상과 방식이 다르기 때문에 워크로드 특성과 트래픽 패턴에 맞게 조합해서 사용하는 것이 중요하다.
'AWS > EKS' 카테고리의 다른 글
| [ AEWS4 ] EKS 네트워킹 살펴보기 (0) | 2026.03.25 |
|---|---|
| [AEWS4] EKS가 만들어주는 AWS 리소스 들여다보기 (0) | 2026.03.19 |
| Amazon VPC Lattice로 간소화하는 Amazon EKS 클러스터 통신 (0) | 2025.04.27 |
| ML Infra with EKS : AI 워크로드에 대한 컨테이너 사용 (1) | 2025.04.20 |
| AWS EKS 업그레이드, In-place부터 Blue/Green 마이그레이션까지 (1) | 2025.04.02 |