목차
- Karpenter 비용 최적화가 어려운 이유
- Karpenter 노드 스케일링을 줄이는 핵심 개념
- 실전 1: requests부터 먼저 다이어트하기
- 실전 2: NodePool 제약으로 큰 인스턴스 남발 막기
- 실전 3: Consolidation과 만료 정책으로 잔여 노드 줄이기
- 실전 4: Spot과 On-Demand를 역할별로 분리하기
- ⚠️ 주의사항: 실제로 많이 막히는 포인트
- 1. requests는 줄였는데 HPA가 갑자기 민감해진 경우
- 2. PDB가 너무 보수적인 경우
- 3. anti-affinity를 습관처럼 강제한 경우
- 4. DaemonSet 리소스를 잊은 경우
- 검증: Karpenter 비용 최적화가 잘 되었는지 확인하는 법
- 정리: 제가 권장하는 적용 순서
[k8s] Karpenter 비용 최적화: 불필요한 노드 스케일링 방지 전략
Karpenter 비용 최적화는 쿠버네티스 비용 절감에서 생각보다 훨씬 큰 비중을 차지하는데요. 클러스터를 한동안 운영해보면 CPU나 메모리를 거의 안 쓰는데도 노드가 계속 살아 있고, 짧은 스파이크 때문에 큰 인스턴스가 붙었다가 한참 뒤에야 내려가는 상황을 꼭 한 번쯤 겪게 되더라고요. 저도 홈랩과 실서비스 환경에서 Karpenter를 붙인 뒤 처음엔 “오토스케일링이면 자동으로 다 좋아지겠지”라고 생각했었는데, 실제로 써보니까 설정 하나 잘못 두면 Karpenter 노드 스케일링이 오히려 비용을 키우는 방향으로 움직이더라고요.
특히 요청값(requests) 과대 설정, 파드 배치 밀도 부족, 비어 있지 않은 애매한 노드, 지나치게 넓은 인스턴스 선택 범위가 겹치면 노드가 쉽게 늘어나는데요. 여기서 중요한 포인트는 Karpenter가 문제의 원인이 아니라 현재 스케줄링 조건에 굉장히 충실하게 반응하는 엔진이라는 거더라고요. 쉽게 말해 입력이 거칠면 결과도 거칠게 나옵니다. 이번 글에서는 제가 직접 삽질하면서 정리한 Karpenter 비용 최적화 방법을, 현장에서 바로 적용할 수 있는 기준 위주로 풀어보겠습니다.

Karpenter가 스케줄링 수요를 받아 노드를 추가하고, 한가한 노드를 통합하는 흐름을 보여주는 개요 이미지입니다.
Karpenter 비용 최적화가 어려운 이유
저도 처음엔 헷갈렸는데, 많은 분이 Cluster Autoscaler(클러스터 오토스케일러)와 Karpenter를 비슷하게만 보시더라고요. 그런데 운영 관점에서는 결이 조금 다르더라고요. Karpenter는 스케줄되지 못한 파드(Pod)를 보고 그때그때 더 적절한 노드를 만들려는 성향이 강합니다. 그래서 잘 맞추면 정말 시원하게 붙고 빠지는데, 반대로 조건이 지저분하면 필요 이상으로 세분화된 노드가 생기기 쉬워요.
쉽게 말해, 아래 네 가지가 겹치면 비용이 올라갑니다.
- 과한 requests: 애플리케이션이 실제보다 큰 CPU/메모리 요청을 잡고 있는 경우
- 낮은 bin packing: 비슷한 파드끼리 한 노드에 촘촘히 못 모이는 경우
- 느슨한 인스턴스 선택: 너무 큰 타입까지 후보에 열어둔 경우
- 애매한 축소 정책: 비어 있지 않지만 옮길 수 있는 노드가 오래 살아남는 경우
결국 핵심은 필요한 순간에는 빨리 늘리고, 불필요해진 순간에는 최대한 덜 남기기예요. 이 균형을 못 잡으면 오토스케일링 최적화가 아니라 오토 과금이 됩니다 ㅎㅎ
Karpenter 노드 스케일링을 줄이는 핵심 개념
제가 실무에서 가장 먼저 보는 건 세 가지입니다. 바로 requests, disruption(중단/통합 정책), 그리고 노드 풀의 제약조건이에요.
| 항목 | 왜 중요한가 | 비용 영향 |
|---|---|---|
| resources.requests | 스케줄러가 파드 배치 가능 여부를 판단하는 기준 | 과대 설정 시 필요 노드 수 증가 |
| NodePool 제약 | 어떤 인스턴스 계열과 용량 타입을 쓸지 결정 | 너무 넓으면 큰 노드가 쉽게 선택됨 |
| Consolidation(통합) | 덜 효율적인 노드를 더 적은 수의 노드로 재배치 | 잔여 노드 제거에 직접적 |
| DaemonSet 오버헤드 | 모든 노드에 공통으로 올라가는 리소스 사용량 | 소형 노드 전략을 무너뜨릴 수 있음 |
여기서 특히 놓치기 쉬운 게 DaemonSet(데몬셋, 모든 노드에 공통 배포되는 워크로드) 오버헤드인데요. 모니터링 에이전트, 로그 수집기, CNI(컨테이너 네트워크 인터페이스) 관련 파드가 생각보다 자리를 많이 차지하더라고요. 저도 처음엔 “왜 이렇게 작은 파드 몇 개 때문에 노드가 또 붙지?” 싶었는데, 계산해보니 공통 오버헤드가 누적돼 있더라고요.
실전 1: requests부터 먼저 다이어트하기
Karpenter 비용 최적화에서 가장 효과가 큰 건 의외로 Karpenter 설정이 아니라 워크로드 설정 정리더라고요. 실제로 써보니까 requests를 현실화하는 것만으로도 불필요한 노드 스케일링이 확 줄었거든요. HPA(Horizontal Pod Autoscaler, 수평 확장)나 VPA(Vertical Pod Autoscaler, 수직 조정)를 쓰더라도 requests가 터무니없이 크면 소용이 없더라고요.
- 상위 소비 워크로드를 찾습니다.
- 실사용량 대비 requests가 과한지 봅니다.
- 갑자기 줄이지 말고 단계적으로 낮춥니다.
- 배포 후 Pending(스케줄 불가)이나 OOMKilled를 꼭 확인합니다.
kubectl top pod -A --containers
kubectl get deploy -A -o yaml
kubectl describe pod -n your-namespace your-pod
예를 들어 웹 애플리케이션이 실제로는 CPU를 크게 쓰지 않는데도 requests를 넉넉하게 잡아두면, 스케줄러는 그 값을 진실로 믿고 더 많은 노드를 요구하게 돼요. 아래처럼 현실적인 범위로 정리하면 밀도가 확 좋아집니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-api
spec:
replicas: 3
selector:
matchLabels:
app: web-api
template:
metadata:
labels:
app: web-api
spec:
containers:
- name: web-api
image: nginx:stable
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
물론 숫자는 서비스마다 다르게 가져가야 해요. 여기서 중요한 건 정확한 절대값이 아니라, 실제 사용 패턴과 요청값의 간격을 줄이는 것입니다. 혹시 CPU는 낮은데 메모리만 높게 유지되는 워크로드가 있다면, 그 자체가 노드 분산의 원인이 될 수 있더라고요.
실전 2: NodePool 제약으로 큰 인스턴스 남발 막기
다음으로 많이 효과를 본 게 NodePool(노드풀, 노드 생성 정책 묶음) 제약이에요. Karpenter는 후보군이 넓을수록 순간적으로 커 보이는 선택을 할 수 있거든요. 특히 “아무 인스턴스나 가능”처럼 열어두면 비용 최적화보다 스케줄 성공률 쪽으로 기울 수 있더라고요. 그래서 저는 업무 성격별로 NodePool을 나눠 두는 편입니다.
아래 예시는 범용 워크로드를 위한 보수적인 제약 패턴이에요. 인스턴스 계열과 세대, 아키텍처, 용량 타입(capacity type)을 좁혀서 운영 예측 가능성을 높이는 방식이거든요.
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: general
spec:
template:
spec:
requirements:
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
- key: kubernetes.io/os
operator: In
values: ["linux"]
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m"]
- key: karpenter.k8s.aws/instance-generation
operator: Gt
values: ["3"]
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand", "spot"]
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 5m
여기서 중요한 포인트! 제약은 좁히되, 너무 빡빡하게는 두지 않는 것이 중요해요. 후보가 지나치게 적으면 스케줄 실패나 가격 변동 대응력 저하로 이어질 수 있더라고요. 제가 처음엔 계열을 너무 좁게 잡았다가 특정 시간대에 배치가 꼬여서 다시 완화했었습니다.

NodePool 제약조건을 통해 인스턴스 후보군을 제어하는 구성을 시각화한 이미지입니다.
실전 3: Consolidation과 만료 정책으로 잔여 노드 줄이기
오토스케일링 최적화에서 체감 차이가 큰 기능이 Consolidation(통합)이에요. 쉽게 말해 여러 노드에 어정쩡하게 흩어진 파드를 더 적은 노드로 옮길 수 있으면 정리하는 기능이거든요. 이걸 켜두지 않으면 트래픽이 빠진 뒤에도 애매하게 남은 노드가 오래 버텨요.
제가 직접 해보니 아래 순서로 접근하는 게 안전했습니다.
- 먼저 underutilized(저활용) 통합 정책을 검토합니다.
- 바로 공격적으로 줄이지 말고, consolidateAfter 시간을 둡니다.
- PodDisruptionBudget(PDB, 파드 중단 예산) 때문에 이동이 막히는지 같이 봅니다.
- 배치 안정성이 확인되면 빈 노드 정리 시간을 조금 더 줄입니다.
kubectl get nodepool
kubectl get nodes
kubectl get pods -A -o wide
kubectl describe node <node-name>
운영하다 보면 “왜 비어 보이는 노드가 안 내려가지?” 싶은 경우가 있는데, 대부분은 다음 중 하나더라고요.
- PDB 때문에 파드 축출이 제한됨
- anti-affinity(안티 어피니티, 분산 배치 규칙)가 강해서 재배치가 어려움
- DaemonSet 오버헤드 때문에 다른 노드로 합치기 애매함
- local storage(로컬 스토리지) 의존 파드가 있음
즉, 통합 정책을 켠다고 끝이 아니라 파드가 정말 옮겨질 수 있는 상태인지를 같이 만들어야 해요. 이 부분에서 삽질 좀 했습니다 ㅎㅎ
실전 4: Spot과 On-Demand를 역할별로 분리하기
쿠버네티스 비용 절감을 노린다고 무조건 Spot(스팟, 유휴 용량 기반 저비용 인스턴스)을 늘리는 건 조금 위험해요. 비용은 줄 수 있어도 재시작 민감 워크로드가 섞이면 오히려 장애 대응 비용이 올라가더라고요. 그래서 저는 성격이 다른 워크로드를 섞지 않도록 taint/toleration(테인트/톨러레이션, 특정 노드 허용 규칙)과 NodePool 분리 전략을 권장해요.
| 워크로드 유형 | 권장 용량 타입 | 이유 |
|---|---|---|
| 배치성 잡(Job), 비동기 처리 | Spot 중심 | 중단 허용 범위가 비교적 큼 |
| 사용자 요청 직접 처리 API | On-Demand 중심 | 지속성과 예측 가능성이 중요 |
| 혼합형 워크로드 | 분리 운영 | 비용과 안정성 균형 확보 |
이렇게 나누면 Karpenter가 필요한 곳에만 공격적인 비용 절감 전략을 적용하게 되는데요. 결과적으로 비용도 줄고, 왜 특정 노드가 붙었는지도 설명이 쉬워져요.
⚠️ 주의사항: 실제로 많이 막히는 포인트
여기부터는 경험담에 가깝습니다. 저도 처음엔 “Karpenter가 똑똑하니까 알아서 정리해주겠지” 했었는데, 아래 네 가지에서 자주 막혔어요.
1. requests는 줄였는데 HPA가 갑자기 민감해진 경우
CPU requests를 낮추면 상대 사용률이 높아 보이면서 HPA가 더 빨리 반응할 수 있거든요. 즉 requests 다이어트와 HPA 목표값은 같이 봐야 합니다.
2. PDB가 너무 보수적인 경우
minAvailable이 높으면 consolidation이 생각보다 거의 안 움직여요. 서비스 특성을 해치지 않는 선에서 현실화가 필요합니다.
3. anti-affinity를 습관처럼 강제한 경우
고가용성을 위해 분산시키는 건 좋지만, 모든 워크로드에 강한 규칙을 걸면 노드가 잘 안 모여요. 꼭 필요한 곳에만 써야 합니다.
4. DaemonSet 리소스를 잊은 경우
노드를 잘게 쪼개 쓰려면 공통 오버헤드가 치명적이에요. 모니터링, 로깅, 보안 에이전트가 많을수록 소형 노드 전략은 불리해져요.
이런 문제를 점검할 때는 이벤트와 노드 상태를 같이 보는 게 좋습니다.
kubectl get events -A --sort-by=.lastTimestamp
kubectl get pdb -A
kubectl get daemonset -A
kubectl top node

불필요한 노드가 남아 있는 원인을 추적할 때 보는 지표와 병목 요소를 보여주는 이미지입니다.
검증: Karpenter 비용 최적화가 잘 되었는지 확인하는 법
설정을 바꿨으면 반드시 검증해야 해요. 저는 보통 아래 순서로 봅니다.
- 노드 수의 일중 변동 폭이 줄었는지 확인합니다.
- 트래픽 감소 후 빈 노드 정리 시간이 짧아졌는지 봅니다.
- 평균 노드 사용률이 올라갔는지 확인합니다.
- Pending 파드나 재스케줄 실패가 없는지 확인합니다.
여기서 중요한 건 비용만 보면 안 된다는 점이에요. 비용은 줄었는데 배포 시간이 늘어나거나, 특정 시간대 지연이 늘었다면 진짜 최적화가 아니거든요. 제가 보는 체크리스트는 이렇습니다.
- 노드당 파드 밀도 증가
- 불필요한 대형 인스턴스 비중 감소
- 저활용 노드 체류 시간 감소
- 서비스 안정성 유지
모니터링 시스템이 있다면 노드 생성/삭제 이벤트, CPU/메모리 요청 대비 사용량, Spot과 On-Demand 비중을 함께 보면 좋아요. 이거 진짜 편하더라고요. 원인과 결과가 한 화면에서 이어져 보이니까요.

Karpenter 비용 최적화 전후의 노드 개수와 활용률 변화를 비교하는 결과 이미지입니다.
정리: 제가 권장하는 적용 순서
마지막으로, 처음 적용하시는 분이라면 아래 순서가 가장 안전해요. 한 번에 다 건드리기보다 단계적으로 가는 게 좋습니다.
- 워크로드 requests를 현실화합니다.
- NodePool 제약으로 과도한 인스턴스 선택을 줄입니다.
- Consolidation 정책을 켜고 재배치 가능성을 점검합니다.
- Spot과 On-Demand를 워크로드별로 분리합니다.
- HPA, PDB, anti-affinity를 함께 조정합니다.
Karpenter 비용 최적화는 설정 한 줄의 마법이라기보다, 스케줄링 입력값을 정리하는 운영 습관에 가깝습니다. 저도 처음엔 Karpenter만 만지면 될 줄 알았는데, 실제로는 애플리케이션 requests와 배치 정책이 훨씬 중요했어요. 그래서 이 주제는 결국 플랫폼 팀과 애플리케이션 팀이 같이 봐야 하더라고요.
혹시 지금 클러스터에서 노드는 자꾸 늘어나는데 사용률은 낮은 상황이라면, 가장 먼저 requests와 PDB부터 점검해보세요. 체감 효과가 큽니다. 다음 글에서는 VPA와 HPA를 함께 사용할 때 어떤 순서로 튜닝하면 좋은지 다뤄볼 예정입니다. 이전 글에서 다룬 리소스 요청값 설계 글이 있다면 같이 참고하셔도 흐름이 잘 이어져요. 드디어 됐다 싶은 순간이 분명 옵니다 🎉

실무 적용 순서와 핵심 점검 항목을 한 장으로 정리한 요약 이미지입니다.
![[k8s] Karpenter 비용 최적화: 불필요한 노드 스케일링 방지 전략](https://blog.pswq.net/wp-content/uploads/2026/08/karpenter-cost-optimization-unnecessary-node-scaling-thumbnail.jpg)