
Karpenter를 도입하고 Spot으로 전환하면 비용은 30~70% 아낀다. 근데 이걸 유지하는 게 진짜 문제다. 우리 팀에서도 초반에 몇 번 겪었다. 어느 날 갑자기 특정 Availability Zone에서 c6i.4xlarge만 골라서 반복적으로 회수(termination) 되는데, 노드가 뜨자마자 5분도 안 돼서 다시 죽는 상황이 이어졌다.
원인은 결국 하나였다. NodePool 정의가 너무 좁게 잡혀 있었다. Karpenter가 아무리 똑똑해도 선택할 수 있는 Spot capacity pool 자체가 몇 개 없으면 대체할 수단이 없다. 최근 AWS 문서에서도 Spot-to-Spot consolidation을 사용하려면 최소 15개 이상의 인스턴스 타입을 허용해야 한다고 명시하고 있는데, 이게 그냥 권장이 아니라 사실상 필수다. 오늘은 우리가 몇 달 시행착오 끝에 정착한 NodePool 다양화 체크리스트를 정리한다.
Spot capacity pool을 최대한 넓게 벌린다
Spot pool은 "인스턴스 타입 × AZ" 조합 하나가 하나의 pool이다. c6i.large가 ap-northeast-2a, 2b, 2c에 각각 있으면 3개 pool이다. Karpenter가 선택할 수 있는 pool 수를 늘리는 게 회수율 낮추는 첫 번째 조건이다.
우리가 실제로 쓰는 NodePool 예시는 이렇게 생겼다. 특정 세대에 고정하지 않고 여러 세대와 CPU 벤더를 섞는다.
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: general-spot
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"]
- key: karpenter.k8s.aws/instance-family
operator: In
values: ["c6i", "c6a", "c7i", "c7a", "m6i", "m6a", "m7i", "m7a"]
- key: karpenter.k8s.aws/instance-size
operator: In
values: ["large", "xlarge", "2xlarge", "4xlarge"]
- key: topology.kubernetes.io/zone
operator: In
values: ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 1m
계산해 보면 instance-family 8개 × instance-size 4개 × AZ 3개 = 96개 pool이다. 실제 Karpenter가 요청 시점에 매칭 가능한 건 이보다 적지만 여유는 충분히 있다.
혹시 workload가 CPU 특성 민감하다면 intel/amd 계열만 섞어 쓰는 것도 방법이지만, 검증 안 된 상태로 arm64(m7g, c7g)까지 무작정 넣지는 말자. 컨테이너 이미지가 arm64 지원 안 하면 노드가 뜨고 나서 pod가 Pending 되는 상황이 생긴다. 여기서 실제 몇 번 당했다.
Weight로 우선순위는 주되, 강제하지는 말자
우리는 on-demand fallback을 위한 NodePool을 하나 더 두는데, weight를 낮게 잡아서 spot이 정말 안 뜰 때만 사용되게 한다.
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: general-ondemand
spec:
weight: 10
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"]
# 나머지 요구사항은 spot NodePool과 동일
spot NodePool은 weight를 명시하지 않아 기본값(50)을 쓰고, on-demand는 10으로 낮춘다. Karpenter는 weight가 높은 NodePool을 먼저 시도한다. 이렇게 하면 평시엔 100% spot으로 운영되다가 spot capacity가 고갈되는 순간에만 on-demand가 살아난다.
여기서 주의할 점 하나. AWS re:Post에도 최근 올라온 질문인데, ARM64 spot에서 같은 weight의 NodePool 여러 개를 두면 AZ가 심하게 편중되는 케이스가 보고되고 있다. Karpenter의 PCO(Price Capacity Optimized) 전략이 특정 zone의 pool을 반복적으로 선택하는 것으로 추정되는데, 이게 pod topology spread constraint와 충돌하면 노드가 뜨는데 pod는 스케줄이 안 되는 상황이 온다. 이럴 땐 pod에 topologySpreadConstraints를 명확히 걸어두는 게 안전하다.
Interruption 처리는 반드시 켠다
Karpenter는 EC2 Spot Interruption Warning(2분 전 경고)을 받아서 노드를 미리 cordon하고 pod를 drain시킨다. 이걸 활성화하려면 SQS queue와 EventBridge rule을 설정해야 한다. EKS Blueprints 모듈이나 Karpenter 공식 가이드의 CloudFormation 템플릿을 쓰면 5분이면 되는 일인데, 의외로 이걸 안 켠 클러스터를 자주 본다.
# Karpenter Helm values
settings:
interruptionQueue: "karpenter-interruption-queue"
이거 없이 spot을 돌리면 pod가 unclean shutdown 되는 확률이 급격히 올라간다. graceful termination에 의존하는 워크로드(gRPC 롱-라이브드 커넥션, DB 커넥션 풀 등)라면 이건 무조건이다.
실전 팁: consolidation 주기와 PDB
consolidateAfter를 1m으로 잡아뒀는데, 처음엔 30초로 짧게 해봤다가 노드가 계속 회전하면서 오히려 pod restart가 잦아졌다. 5분 이상으로 늘리면 비용 이득이 반감된다. 우리 팀에선 1분에서 어느 정도 밸런스가 잡혔는데, 워크로드 성격에 따라 조정이 필요하다.
그리고 PodDisruptionBudget은 반드시 걸어둬야 한다. Karpenter의 consolidation은 PDB를 존중한다. 예를 들어 replicas=3인 deployment에 minAvailable: 2를 걸어두면, consolidation 중에도 최소 2개 pod는 유지된다. PDB 없이 spot 회수 + consolidation이 겹치면 순간적으로 pod 개수가 0이 되는 상황이 생길 수 있다. 아직 이걸로 큰 사고를 낸 적은 없지만, 몇 번 아찔한 순간은 있었다.
마무리
정리하면 세 가지다. instance-family를 넓게 벌리고, weight로 on-demand fallback을 만들어두고, interruption handling을 반드시 켠다. 특히 첫 번째가 제일 중요한데, 신규 인스턴스 세대가 나올 때마다 NodePool을 업데이트하는 것도 잊지 말자. c8i 세대가 GA된 이후에 우리 팀도 요구사항에 추가했다.
혹시 다른 팀에선 어떤 diversification 전략을 쓰시는지 궁금하다. 특히 GPU 노드처럼 pool이 원래 좁은 경우에 어떻게 접근하는지는 아직 답을 못 찾았다.
'IT > Kubernets' 카테고리의 다른 글
| HPA behavior의 stabilizationWindowSeconds, 이거 하나 바꿨더니 야간 알람이 사라졌다 (0) | 2026.07.08 |
|---|---|
| CoreDNS ndots:5 때문에 P99가 800ms 튀던 이야기 (0) | 2026.07.08 |
| PodDisruptionBudget 실전 설정 가이드 (0) | 2026.07.07 |
| EndpointSlice와 kube-proxy, 사실 내부적으로는 이렇게 돈다 (0) | 2026.07.07 |
| CoreDNS 때문에 새벽에 페이지 받은 이야기 (0) | 2026.07.02 |