포스팅은 CloudNet@팀 서종호(Gasida)님이 진행하시는 AWS EKS Workshop Study 내용을 참고하여 작성합니다.
안녕하세요!
오늘은 Cluster 오토스케일링 기술인
Cluster Autoscaler와 Kapenter에 대해서
알아보도록 하겠습니다.
01. Karpenter vs Cluster AutoScaler 분석
Cluster Autoscaler (CA) 는 Kubernetes 공식 SIG-Autoscaling에서 관리하는 오토스케일러입니다.
작동 방식이 명확한데, 스케줄 실패한 Pod를 감지하고, Node 추가/삭제 전에 시뮬레이션을 돌린 뒤, EC2 Auto Scaling Group의 DesiredReplicas 필드를 조정하는 방식으로 동작합니다.
쉽게 말하면 CA는 Kubernetes → AWS ASG 사이의 번역기입니다.
Kubernetes가 "노드 더 필요해"라고 하면 CA가 ASG에 "인스턴스 더 켜줘"라고 전달하는 구조입니다.
반면에 Karpenter는 이 구조를 뒤집었습니다.
Karpenter는 Pod의 스케줄링 요건(resource requests, node selector, affinity, toleration 등)을 직접 평가해서 그 요건에 맞는 노드를 직접 프로비저닝합니다.
AWS와 Kubernetes API 사이를 오가지 않아도 되고, Kubernetes 버전에 타이트하게 묶이지도 않습니다
CA의 구조적 한계와 Karpenter가 해결한 것
📌 CA는 태생적으로 ASG 레이어를 통해서만 스케일링합니다.
노드그룹 폭발 문제
팀별, 워크로드별로 다른 인스턴스 타입이 필요하면 Node Group을 계속 추가해야 합니다. Node Group이 많아질수록 Cluster Autoscaler의 성능이 저하되는데, CA는 매 스캔 인터벌마다 전체 클러스터 상태를 메모리에 올려두고 Pod마다 모든 Node Group에 대해 스케줄링 시뮬레이션을 실행하기 때문입니다
스케일링 레이턴시
CA는 비용 절감을 위해 필요할 때만 노드를 추가하는데, 노드가 실제로 Ready 상태가 되는 데 수 분이 걸릴 수 있어 Pod 스케줄링 지연이 크게 늘어납니다
Karpenter는 위 문제를 아래와 같이 해결합니다.
ASG와 MNG 없이도 하나의 유연한 NodePool로 다양한 워크로드를 커버
Pod의 요건에 딱 맞는 인스턴스를 즉시 프로비저닝
인스턴스 오케스트레이션 책임이 단일 시스템에 통합되어 더 단순하고 안정적입니다.
eCommerce EKS 기준
제가 eCommerce 레거시 인프라를 운영중이지만 추 후에 EKS 환경으로 마이그레이션 프로젝트를 담당하기 때문에
이커머스 기준으로 한번 알아보았습니다.
이커머스는 트래픽 패턴이 극단적입니다.
평일 낮 → 저녁 피크 → 심야 거의 0, 거기에 플래시 세일 같은 스파이크가 덮칩니다.
Karpenter는 수요의 급격한 변동이나 다양한 컴퓨팅 요건을 가진 워크로드에 최적입니다.
MNG와 ASG는 비교적 정적이고 일관된 워크로드에 적합하며, 두 가지를 혼합해서 사용하는 것도 가능합니다.
이커머스 레이어별 추천을 정리하자면:
레이어
추천
이유
API Gateway / 웹 프론트
Karpenter
트래픽 스파이크 즉각 대응
주문/결제 처리
CA + On-Demand
예측 가능하고 중단 불가
상품 검색/추천
Karpenter + Spot
비용 최적화, 간헐적 부하
배치 정산/리포트
Karpenter + Spot
야간 배치, 인터럽션 허용 가능
DB Proxy / Redis
고정 MNG
안정성 최우선
Karpenter 핵심 권장 사항
📌 NodePool 설계 원칙
서로 다른 팀이 클러스터를 공유하거나, OS나 인스턴스 타입 요건이 다를 때는 여러 NodePool을 만들어야 합니다.
여러 NodePool이 동시에 매칭될 수 있는 상황을 피하기 위해 NodePool은 서로 배타적이거나 가중치가 부여되도록
설계하는 것이 권장사항 입니다. (그렇지 않으면 Karpenter가 임의로 하나를 선택해 예측 불가능한 결과가 생김)
Spot을 쓸 때 인스턴스 타입을 너무 좁게 제한하면 안 됩니다. Spot 인터럽션 발생 시 대체 인스턴스를 찾지 못해 Pod가 Pending 상태로 남을 수 있습니다. Karpenter는 Price-Capacity Optimized 전략으로 가장 깊은 풀에서 인스턴스를 선택하는데, 허용된 인스턴스 타입이 다양할수록 EC2가 최적화를 더 잘할 수 있습니다.
Spot 인터럽션 처리
Karpenter는 Spot 인터럽션의 2분 사전 통보를 받으면 즉시 새 노드를 시작해서 Pod를 옮길 수 있도록 합니다. 이를 위해 '--interruption-queue' CLI 인수에 SQS 큐 이름을 설정해야 합니다. Karpenter 인터럽션 핸들링과 Node Termination Handler를 동시에 사용하는 것은 권장되지 않습니다
AMI 핀닝
@latest alias를 사용하거나 테스트되지 않은 AMI가 자동 배포되는 방식은 프로덕션 클러스터에서 워크로드 장애와 다운타임을 일으킬 수 있습니다. 프로덕션에서는 반드시 검증된 AMI 버전을 고정하고, 신규 버전은 비프로덕션에서 먼저 테스트하는 것이 강력히 권장됩니다.
amiSelectorTerms:
- alias: al2023@v20240807 # 검증된 버전 고정
비용 폭증 방지
NodePool 단위로 리소스 한도를 설정하고, 한도 초과 시 CloudWatch 로그를 통해 알람을 받을 수 있도록 구성해야 합니다. 글로벌 클러스터 단위 한도는 설정할 수 없고, NodePool별로만 적용됩니다.
spec:
limits:
cpu: 500 # 이커머스 피크 기준으로 설정
memory: 2000Gi
Karpenter 실행 시 유의사항
📌 Controller는 어디서 실행해야 될까?
1) Karpenter가 관리하는 노드 위에 Karpenter 자신을 실행하면 안됩니다.
최소 하나의 소규모 고정 Node Group을 만들거나,
Karpenter 네임스페이스에 Fargate 프로파일을 생성해서 Fargate 위에서 실행하는 것이 권장됩니다.
2) Private 클러스터라면 추가 VPC 엔드포인트가 필요합니다.
Karpenter는 IRSA를 사용하므로 STS VPC 엔드포인트가 필수
SSM 파라미터로 AMI ID를 조회하기 때문에 SSM VPC 엔드포인트도 만들어야 합니다
Price List API는 VPC 엔드포인트가 없어서 오프라인 상태가 되면 가격 데이터가 오래될 수 있지만, Karpenter 바이너리 내부에 온디맨드 가격 데이터가 내장되어 있어서 완전히 깨지지는 않습니다.
그렇지 않으면 하나의 CA가 다른 클러스터의 노드 그룹을 수정하는 사고가 발생할 수 있습니다.
📌 NodeGroup 설계
Node Group 내의 모든 노드는 동일한 스케줄링 속성(레이블, 테인트, 리소스)을 가져야 합니다.
MixedInstancePolicy를 사용할 때는 CPU, Memory, GPU 형태가 동일한 인스턴스 타입만 섞어야 하며,
첫 번째 인스턴스 타입이 스케줄링 시뮬레이션에 사용됩니다.
📌 오버프로비저닝으로 레이턴시 개선
이커머스에서 플래시 세일 같은 순간 스파이크에 대응하려면 오버프로비저닝이 효과적입니다.
오버프로비저닝은 negative priority를 가진 임시 Pod를 클러스터에 미리 배치해두는 방식으로, 실제 Pod가 들어오면 이 임시 Pod가 선점되면서 자리를 내줍니다. 그러면 임시 Pod가 다시 스케줄 불가 상태가 되어 CA가 새 노드를 추가하게 됩니다.
또한 오버프로비저닝은 preferredDuringSchedulingIgnoredDuringExecution 규칙에 의한 AZ 분산 배치의 성공률도 높여줍니다.
📌 중요 워크로드 eviction 방지
배치 작업이나 재시작 비용이 큰 Pod에는 cluster-autoscaler.kubernetes.io/safe-to-evict=false
어노테이션을 붙여서 스케일 다운 시 강제 eviction되지 않도록 해야합니다.
02. Karpenter vs Cluster AutoScaler 장단점 비교
1) Karpenter
장점
ASG 없이 EC2 직접 프로비저닝 → 빠른 스케일 아웃
단일 NodePool로 수십 가지 인스턴스 타입 혼합 가능.
Consolidation으로 빈 노드 자동 통합 → 비용 절감
Spot 인터럽션 2분 전 선제 대응 내장
Kubernetes 버전과 느슨하게 결합
단점
상대적으로 신규 프로젝트 → 엣지케이스 존재
Custom Launch Template 미지원 (v1 API 기준)
Private 클러스터에서 STS, SSM VPC 엔드포인트 추가 필요
학습 곡선 (NodePool / NodeClass 개념을 새로 익혀야함)
2) Cluster AutoScaler
장점
오랜 운영 이력 → 대규모 프로덕션 검증 완료
Kubernetes 공식 SIG 프로젝트 → 커뮤니티 방대함.
기존 ASG 인프라 그대로 재사용 가능
Expander 전략으로 세밀한 스케일 우선순위 제어
EBS 다중 AZ, GPU 가속기 등 특수 워크로드 성숙한 지원
단점
노드 그룹 수 증가 시 성능 급격히 저하
Kubernetes 버전과 1:1 매칭 필수
ASG 레이어 경유로 인한 스케일링 레이턴시
스케일 다운 후 재스케일 딜레이 기본 10분
위 내용을 토대로 항목별 비교 분석한 표 입니다.
항목
karpenter
Cluster AutoScaler
스케일아웃 속도
EC2 직접 호출 — 수십 초 빠름
ASG 경유 — 수 분 느림
인스턴스 다양성
NodePool 하나로 수백 종 혼합 유연
노드 그룹당 유사 타입만 제한적
비용 최적화
Consolidation — 빈 노드 자동 통합 적극적
스케일 다운 — 10분 딜레이 소극적
운영 성숙도
신규 프로젝트 — 엣지케이스 있음 성장중
다년간 대규모 검증 완료 안정적
쿠버네티스 버전
느슨하게 결합 자유로움
버전 1:1 매칭 필수 제약 있음
Spot 대응
인터럽션 핸들링 내장 선제적
NTH 별도 설치 필요 외부 의존
이커머스 추천
API·검색·배치 레이어
결제·주문 등 Critical 레이어
위 표를 토대로 점수를 매겨보았습니다.
03. Karpenter Consolidation 동작 원리 심층 분석
📌 Consolidation이 해결하는 문제
CA의 스케일 다운은 단순합니다. "이 노드 CPU 사용률이 낮으면 Pod를 옮기고 삭제한다." 끝
반면 Karpenter의 Consolidation은 클러스터 전체 비용을 능동적으로 재최적화하는 루프 입니다. 삭제뿐 아니라 교체(더 싼 인스턴스로 치환)까지 포함하고, 여러 노드를 동시에 묶어서 판단합니다.
Consolidation은 세 가지 상황을 감지합니다.
노드가 비어있어서 제거 가능한 경우
노드의 워크로드가 클러스터의 다른 노드들의 여유 capacity로 수용 가능한 경우
워크로드 변화로 인해 더 낮은 가격의 인스턴스로 교체 가능한 경우 입니다.
📌 WhenEmpty vs WhenEmptyOrUnderutilizaed 차이
이 두 정책은 단순히 "공격적이냐, 아니냐"의 차이가 아닙니다. Consolidation 트리거 조건 자체가 다릅니다.
consolidationPolicy는 노드가 통합 대상이 되기 위한 전제 조건을 결정합니다.
WhenEmpty 는 실행 중인 non-daemon Pod가 하나도 없는 경우에만 노드를 통합 대상으로 봅니다.
WhenEmptyOrUnderutilized는 consolidateAfter 에 설정된 시간이 지나면 비어있지 않아도 통합을 고려합니다.
실제로 이 차이가 얼마나 중요한지는 이커머스 야간 트래픽 시나리오를 보면 명확해집니다.
낮에 8개 노드로 돌아가던 클러스터가 자정 이후 트래픽이 30%로 떨어졌다고 가정하면,
WhenEmpty는 Pod가 자연적으로 빠져나가서 완전히 빈 노드가 생길 때까지 기다립니다.
반면 WhenEmptyOrUnderutilized는 "이 노드의 Pod들을 다른 노드로 옮기면 이 노드를 없앨 수 있다"
판단 자체를 능동적으로 내립니다.
spec:
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized # 기본값 (v1 기준)
consolidateAfter: 0s # 기본값 — 조건 충족 즉시 통합 시도
consolidateAfter: 0s가 기본값인 게 중요합니다.
Pod가 사라지거나 새로 스케줄된 직후부터 바로 통합 가능성을 평가한다는 뜻입니다. 이커머스처럼 트래픽이 급격히 변하는 환경에서는 이 값을 약간 올려두는 게 안정적입니다.
📌 Consolidation 내부 실행 순서
Consolidation은 세 단계를 순서대로 시도합니다.
첫째, 완전히 빈 노드를 병렬로 삭제하는 Empty Node Consolidation
둘째, 둘 이상의 노드를 병렬 삭제하면서 가격이 더 낮은 교체 노드 하나를 띄우는 Multi Node Consolidation
셋째, 단일 노드를 삭제하면서 더 낮은 가격의 교체 노드를 띄우는 Single Node Consolidation
이 순서가 중요한 이유는, Empty Node가 먼저 처리되어서 비용이 가장 확실하게 줄어드는 케이스를
우선적으로 제거하고, 그 다음에 복잡한 시뮬레이션이 필요한 케이스로 넘어갑니다.
여러 노드가 동시에 통합 대상이 되면 Karpenter는 어떤 걸 먼저 없앨지 선택해야 합니다.
Karpenter는 워크로드에 대한 전반적인 충격이 가장 적은 방향으로 통합하는데,
Pod 수가 적은 노드, 곧 만료될 노드, 낮은 우선순위 Pod가 있는 노드를 우선적으로 종료합니다.
실제 검증 : kubectl로 Consolidation 상태 관찰
Consolidation이 실제로 어떻게 동작하는지 확인하는 명령어 시퀀스입니다.
현재 노드 상태와 Karpenter 이벤트 확인:
# Karpenter가 관리하는 노드 전체 조회
kubectl get nodes -l karpenter.sh/nodepool --show-labels
# NodeClaim 상태 조회 (Karpenter의 노드 표현)
kubectl get nodeclaims -o wide
# 특정 노드에 대한 Consolidation 이벤트 확인
# Karpenter는 통합 불가 이유를 Node 이벤트로 기록함
kubectl describe node <node-name> | grep -A 5 "Events:"
실제 이벤트 출력 예시 — 이 형태가 보이면 통합이 막혀 있다는 신호입니다:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Unconsolidatable 66s karpenter pdb default/payment-pdb prevents pod evictions
Normal Unconsolidatable 33s karpenter can't replace with a lower-priced node
# NodePool의 현재 disruption 설정 확인
kubectl get nodepool default -o jsonpath='{.spec.disruption}' | jq .
# 현재 삭제 중인 노드 수 (Budget 계산에 포함됨)
kubectl get nodeclaims \
-o jsonpath='{range .items[?(@.metadata.deletionTimestamp)]}{.metadata.name}{"\n"}{end}'
WhenEmptyOrUnderutilized의 핵심 함정 — Preferred Anti-Affinity
Preferred Anti-Affinity와 Topology Spread를 사용하면 Consolidation 효율이 떨어질 수 있습니다.
Karpenter는 노드 실행 시 이 설정들을 충족시키려 하는데, 노드 churning을 줄이기 위해 Consolidation도 이 제약을 지키려 합니다. 그 결과 kube-scheduler는 Pod를 다른 노드에 배치할 수 있더라도 Karpenter는 preference 위반을 피하기 위해 노드를 통합하지 않을 수 있습니다.
# 위 현상에 대한 로그는 아래와 같이 나타납니다.
pod default/api-server-55894c5d8b-522jd has a preferred Anti-Affinity
which can prevent consolidation
이커머스에서 AZ 분산 배치를 위해 preferredDuringSchedulingIgnoredDuringExecution으로
Anti-Affinity를 걸어놓으면 야간에 Consolidation이 예상보다 덜 일어날 수 있습니다.
이 경우, required가 아닌 preferred임에도 Karpenter가 보수적으로 동작한다는 것을 알고있어야 합니다.
do-not-disrupt 어노테이션: 이커머스 결제 레이어 보호
결제·주문 처리 Pod는 트래픽이 줄어드는 야간에도 Consolidation 대상이 되어서는 안 됩니다.
karpenter.sh/do-not-disrupt: "true" 어노테이션이 붙은 Pod가 있는 노드는 Consolidation에서 제외. 단, NodeClaim에 terminationGracePeriod가 설정된 경우에는 Drift에 의한 disruption은 여전히 가능.
즉, 노드 8개에 nodes: "20%" 설정이면 동시에 최대 2개 노드만 삭제할 수 있습니다.
node-2.3.5를 한번에 날리지 않고 2개 → 1개 배치로 나눠서 진행함.
📌 Disruption Budget: 이커머스 야간 스케줄 활용
Budget에 Schedule과 Duration을 함께 설정하면 특정 시간대에만 적용되는 예산을 만들 수 있습니다.
Schedule은 cron 문법이고, Duration은 그 시작점에서 얼마 동안 적용할지를 지정합니다. (UTC 기준)
이커머스 기준 실용적인 Budget 설정:
spec:
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 5m # 급격한 트래픽 변동 시 버퍼
budgets:
# 평상시 — 전체의 20%까지 동시 통합 허용
- nodes: "20%"
# 낮 피크 시간 (UTC 00:00~12:00 = KST 09:00~21:00) — 보수적으로
- nodes: "10%"
schedule: "0 0 * * *" # 매일 UTC 00:00 (KST 09:00)
duration: 12h
reasons: ["Underutilized"]
# 야간 배포 직전 (UTC 15:00 = KST 00:00) — Underutilized 통합 잠깐 차단
- nodes: "0"
schedule: "0 15 * * *" # UTC 15:00 (KST 00:00)
duration: 10m
reasons: ["Underutilized"]
📌 requests ≠ limits일 때 Consolidation이 왜 위험한가
위에서 do-not-disrupt의 결제 서비스 deployment yaml 보시면 consolidation 정확도를 위해
requests와 limits 값을 동일하게 적용하라고 말씀드렸습니다.
Consolidation은 Pod의 resource requests를 기준으로 스케줄링 시뮬레이션을 합니다.
(limits은 보지 않습니다)
memory limit이 request보다 크게 설정된 Pod들이 같은 노드에 몰렸을 때 동시에 burst하면
OOM 상황이 발생할 수 있고, Consolidation은 이런 상황을 더 자주 만들 수 있습니다.
만약 아래와 같은 설정이 되어있다고 가정한다면?
resources:
requests:
memory: "256Mi" # Consolidation은 이것만 봄
limits:
memory: "1Gi" # 실제 사용 가능량 — 4배 차이
여러 Pod가 256Mi request로 한 노드에 모였는데, 각자 트래픽을 받으면서
실제로 500~800Mi씩 쓰면 OOM killer가 발동합니다.
이커머스에서는 비 Critical 워크로드(검색, 추천)도 requests == limits으로 맞추거나
LimitRange로 기본값을 지정해두는 게 안전합니다.
📌 검증 체크 리스트
실제 클러스터에서 Consolidation이 제대로 동작하는지 확인할 때 쓰는 커맨드 묶음입니다.
# 1. 현재 NodePool disruption 설정 확인
kubectl get nodepool -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.disruption}{"\n"}{end}' | jq -R 'split("\t") | {pool: .[0], disruption: (.[1] | fromjson)}'
# 2. Unconsolidatable 이벤트가 있는 노드 찾기
kubectl get events --all-namespaces \
--field-selector reason=Unconsolidatable \
--sort-by='.lastTimestamp' | tail -20
# 3. do-not-disrupt 어노테이션 붙은 Pod 목록
kubectl get pods --all-namespaces \
-o jsonpath='{range .items[?(@.metadata.annotations.karpenter\.sh/do-not-disrupt)]}{.metadata.namespace}{"/"}{.metadata.name}{"\n"}{end}'
# 4. 현재 삭제 진행 중인 NodeClaim (Budget 소진 여부 파악)
kubectl get nodeclaims \
-o custom-columns='NAME:.metadata.name,DELETION:.metadata.deletionTimestamp,NODE:.status.nodeName' \
| grep -v '<none>'
# 5. 각 노드 실제 resource 사용률 vs requests 비교
kubectl top nodes
kubectl describe nodes | grep -A 5 "Allocated resources"