Goldilocks + VPA로 Kubernetes 리소스 right-sizing해 Karpenter 비용 최대 56% 줄이기
인프라 비용이 슬금슬금 올라가는 걸 보면서 "어딘가 낭비가 있는 것 같은데" 하는 느낌, 다들 한 번쯤 받아보셨을 겁니다. 저도 예전에 EKS 클러스터를 운영하면서 CPU request를 넉넉하게 잡아두고 잊어버린 적이 있었는데, 어느 날 Karpenter 노드 목록을 보니 실제 사용률은 20%인데 m5.2xlarge가 줄줄이 떠 있더라고요. 그때 처음 제대로 파고들기 시작한 게 바로 VPA와 Goldilocks였습니다. kubectl을 쓸 줄 알고, Deployment와 HPA 개념을 아는 분이라면 무리 없이 따라오실 수 있습니다.
핵심은 간단합니다. Karpenter는 실제 사용량이 아니라 requests 값을 보고 노드 크기를 결정하기 때문에, requests를 정밀하게 조정하는 것만으로도 트래픽이 비교적 균일한 백엔드 서비스 기준으로 인프라 비용을 30~56% 줄일 수 있습니다. Goldilocks는 VPA의 분석 엔진을 안전하게 활용해 각 Deployment에 딱 맞는 requests 추천값을 대시보드로 보여주는 도구입니다. 파드를 재시작하거나 자동으로 건드리지 않으니 운영 중인 클러스터에 부담 없이 붙여볼 수 있다는 것도 큰 장점이고요.
이 글을 읽고 나면 Goldilocks + VPA의 동작 원리부터 Karpenter Consolidation과 어떻게 맞물리는지, 그리고 HPA와 충돌 없이 병용하는 패턴까지 — 프로덕션 클러스터에 당장 적용할 수 있는 단계별 설정을 손에 쥐게 됩니다.
왜 requests가 비용을 결정하는가
Karpenter가 requests를 보는 이유
Karpenter의 노드 프로비저닝 로직을 한 줄로 설명하면 이렇습니다. 스케줄링되지 않은 파드들의 resource requests를 집계해서, 그걸 수용할 수 있는 가장 비용 효율적인 인스턴스 타입을 찾아 노드를 띄웁니다.
여기서 문제가 생깁니다. 만약 실제로는 CPU 100m밖에 안 쓰는 파드가 requests: cpu: "500m"으로 선언돼 있다면, Karpenter 입장에서는 500m짜리 파드가 여럿 몰린 것처럼 보이고, 필요 이상으로 큰 노드를 선택하게 됩니다. 반대로 너무 낮게 잡으면 OOM이 발생하고요.
Bin-packing: 컨테이너를 노드에 최대한 빽빽하게 배치해 낭비 공간을 줄이는 전략. requests 값이 실제 사용량에 가까울수록 bin-packing 밀도가 높아지고 노드 수가 줄어듭니다.
VPA가 추천값을 만드는 방식
VPA는 Recommender, Admission Controller, Updater 세 가지 컴포넌트로 구성됩니다. 이 중 Recommender가 실제로 과거 리소스 사용 이력을 수집해 추천값을 계산하는 역할을 합니다.
중요한 건 알고리즘입니다. VPA Recommender는 단순 평균이 아니라 **히스토그램 기반 백분위수 추정(기본값 p90p95)**을 사용합니다. 이게 왜 중요하냐면, 상위 510% 피크 구간은 기본적으로 추천값에 반영되지 않는다는 뜻이기 때문입니다. 트래픽이 시간대별로 크게 오르내리는 서비스라면, VPA target이 피크를 반영 못 한다고 느낄 수 있는데 — 그건 설계 의도입니다. 이 점을 알고 나면 나중에 나올 "메모리 계절성" 단점도 자연스럽게 이해됩니다.
추천값은 세 가지로 나뉩니다.
| 추천값 | 의미 | 활용 팁 |
|---|---|---|
lowerBound |
스로틀링 없이 동작 가능한 최솟값 | 절대 이 아래로 내려가면 안 되는 하한선 |
target |
VPA가 권장하는 requests 값 (p90~p95 기준) | 안정적인 운영을 원한다면 이 값 기준으로 설정 |
upperBound |
파드가 필요로 할 최대 예상값 | limits 설정 참고용으로 활용 (그대로 limits에 넣으면 안 됩니다) |
저도 처음에 헷갈렸던 게 upperBound를 limits로 그대로 쓰면 된다고 생각했는데요 — upperBound는 어디까지나 참고용이고, limits는 별도로 판단해서 여유분을 두는 게 좋습니다. limits를 requests와 동일하게 줄여버리면 CPU throttling이 심화될 수 있다는 점도 주의가 필요합니다.
Goldilocks가 VPA 위에 얹히는 구조
Goldilocks는 VPA를 직접 건드리지 않습니다. 대신 네임스페이스에 레이블을 붙여두면, 해당 네임스페이스의 모든 Deployment에 대해 updateMode: "Off" 상태의 VPA 객체를 자동으로 생성합니다.
여기서 updateMode: "Off"가 왜 파드를 건드리지 않는지 궁금하실 수 있는데 — VPA의 Updater 컴포넌트가 실제로 파드를 재시작하는 주체인데, Off 모드에서는 Updater가 아예 동작하지 않습니다. Recommender는 계속 데이터를 수집하고 추천값을 계산하지만, 그 값을 파드에 적용하는 건 사람이 직접 하는 거죠.
그 추천값을 가져다가 예쁜 웹 대시보드로 보여주는 게 Goldilocks의 역할입니다. 솔직히 처음 대시보드를 열었을 때 "이 파드는 CPU 500m 잡혀 있는데 target이 80m이네" 하는 걸 보고 꽤 충격받았습니다.
전체 최적화 루프는 이런 흐름입니다.
실제 사용량 관찰 (VPA Recommender — 히스토그램 백분위수 기반)
↓
Goldilocks 대시보드에서 추천값 확인
↓
Deployment YAML / GitOps에 requests 반영
↓
Karpenter가 줄어든 requests 기준으로 더 작은 노드 선택
↓
Consolidation으로 유휴 노드 자동 제거HPA와 VPA를 함께 쓸 때 반드시 알아야 할 것
HPA와 VPA를 같은 메트릭(CPU)으로 동시에 적용하면 충돌이 납니다. HPA는 CPU 사용률이 높으면 파드를 늘리고, VPA는 CPU requests를 바꾸려 하는데 두 컨트롤러가 서로 다른 목표를 향해 간섭합니다.
제가 경험해보니, 이걸 무시하고 CPU 메트릭으로 둘 다 붙여뒀다가 파드가 새벽에 계속 재시작되는 상황이 생겼습니다. 그래서 지금은 역할을 명확히 분리하는 패턴을 씁니다.
권장 패턴: CPU 스케일링은 HPA가 담당하고, Memory requests 튜닝은 VPA(Goldilocks)가 담당하는 방식으로 역할을 분리하면 충돌 없이 두 도구를 함께 사용할 수 있습니다.
클러스터에 붙여보기: 설치부터 추천값 반영까지
예시 1: Goldilocks + VPA 설치 및 기본 설정
먼저 VPA를 설치합니다. Goldilocks가 VPA의 Recommender를 내부적으로 활용하기 때문에 선행 설치가 필요합니다.
한 가지 짚고 넘어갈 것이 있는데요 — 여기서 사용하는 fairwinds-stable/vpa 헬름 차트는 Fairwinds가 래핑한 버전으로, 공식 kubernetes/autoscaler 저장소의 VPA와 설정 구조가 다소 다를 수 있습니다. 공식 VPA를 이미 쓰고 계신 분이라면 Goldilocks 공식 문서에서 연동 옵션을 먼저 확인해보시는 걸 권장합니다.
# VPA 설치 (Fairwinds 래퍼 버전)
helm repo add fairwinds-stable https://charts.fairwinds.com/stable
helm install vpa fairwinds-stable/vpa \
--namespace vpa \
--create-namespace
# Goldilocks 설치
helm install goldilocks fairwinds-stable/goldilocks \
--namespace goldilocks \
--create-namespace
# 모니터링할 네임스페이스에 레이블 추가
kubectl label namespace production goldilocks.fairwinds.com/enabled=true
# 대시보드 임시 접근 (개발/로컬 확인용)
kubectl port-forward svc/goldilocks-dashboard 8080:80 -n goldilocks보안 주의:
kubectl port-forward는 임시 로컬 접근 용도입니다. 프로덕션에서 대시보드를 지속적으로 노출하려면 Ingress나 LoadBalancer Service를 사용하고, 반드시 인증(OAuth, SSO 등)을 함께 구성하는 것을 권장합니다. 인증 없이 외부에 노출하면 클러스터 리소스 정보가 그대로 드러납니다.
레이블을 붙이고 나면 Goldilocks가 아래와 같은 VPA 객체를 자동으로 생성합니다.
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: my-app-goldilocks
namespace: production
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
updatePolicy:
updateMode: "Off" # Updater가 비활성화 — 추천값만 계산, 파드 재시작 없음대시보드에서 추천값을 확인한 뒤 Deployment에 반영하는 예시입니다.
# Before: 과잉 설정된 상태
# After: Goldilocks target 기준으로 조정
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: production
spec:
template:
spec:
containers:
- name: my-app
image: my-app:latest
resources:
requests:
cpu: "250m" # 기존 500m → 50% 감소
memory: "512Mi" # 기존 1Gi → 50% 감소
limits:
cpu: "800m" # limits는 requests보다 여유 있게 유지 (throttling 방지)
memory: "1Gi"limits를 requests와 동일하게 줄이지 않은 이유가 있습니다. CPU limits를 너무 낮게 잡으면 실제 사용량이 잠깐 치솟는 순간 throttling이 심화되고 레이턴시가 튀게 됩니다. limits 조정은 requests 조정과 별개로, 충분한 모니터링 후에 진행하는 것이 안전합니다.
| 변경 항목 | Before | After | 절감율 |
|---|---|---|---|
| CPU request | 500m | 250m | 50% |
| Memory request | 1Gi | 512Mi | 50% |
| 예상 노드 크기 | m5.xlarge | m5.large | ~40% 비용 감소 |
예시 2: Karpenter NodePool Consolidation 연동
requests가 줄어들면 Karpenter의 Consolidation이 더 적극적으로 작동합니다. Karpenter v1 GA 이후 spec.disruption.budgets로 업무 시간대 노드 중단을 제어할 수 있게 됐는데, 이 설정 없이 Consolidation을 켜두면 낮 시간에 파드가 예고 없이 재스케줄링되는 상황이 생깁니다.
consolidateAfter: 30s가 무슨 의미인지 처음엔 헷갈릴 수 있는데요 — 노드가 underutilized 상태로 판단된 뒤 30초가 지나면 Karpenter가 해당 노드의 파드를 다른 노드로 옮기고 노드를 제거하는 이벤트가 시작됩니다. 30초는 꽤 공격적인 값이라 트래픽 패턴을 먼저 충분히 관찰한 뒤 조정하는 것을 권장합니다.
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: default
spec:
disruption:
consolidationPolicy: WhenUnderutilized
consolidateAfter: 30s # underutilized 감지 후 30초 뒤 Consolidation 이벤트 시작
budgets:
- nodes: "10%" # 동시에 중단 가능한 노드 비율 (전체의 10%)
- nodes: "0" # 업무 시간(월~금 09:00~18:00)엔 노드 중단 완전 차단
schedule: "0 9 * * 1-5"
duration: 9hConsolidation: Karpenter가 여러 노드에 분산된 파드를 더 적은 수의 노드로 재배치하고 유휴 노드를 삭제하는 기능. requests가 실제 사용량에 가까울수록 더 공격적으로 작동합니다.
예시 3: HPA + VPA 역할 분리 패턴
CPU는 HPA가, Memory requests 튜닝은 Goldilocks(VPA)가 담당하는 분리 패턴입니다.
# HPA: CPU 사용률 기반 수평 스케일링
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: my-app-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # 파드 전체 평균 CPU 사용률이 70%를 넘으면 스케일 아웃averageUtilization: 70이 처음 보면 직관적이지 않을 수 있는데요 — 이 값은 "모든 파드의 실제 CPU 사용량 합계 ÷ (requests × 파드 수)"가 70%를 초과하면 파드를 추가로 띄우고, 낮으면 줄이는 방식입니다. requests가 정확할수록 HPA의 스케일링 판단도 정확해집니다.
# Deployment 내 리소스 설정
# CPU는 HPA 기준이므로 Goldilocks 추천값과 무관하게 유지
# Memory만 Goldilocks target으로 업데이트
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: production
spec:
template:
spec:
containers:
- name: my-app
image: my-app:latest
resources:
requests:
cpu: "250m" # HPA 동작 기준 — Goldilocks 추천값은 참고만
memory: "512Mi" # Goldilocks target 기준으로 업데이트
limits:
cpu: "800m"
memory: "1Gi"장단점 분석
장점
| 항목 | 내용 |
|---|---|
| Karpenter 효율 극대화 | 정밀한 requests → 더 작은 인스턴스 타입 선택 → bin-packing 밀도 향상 |
| 비용 절감 | 트래픽이 균일한 백엔드 기준 30~56% 인프라 비용 감소 (실제 사례: $52K → $23K/월) |
| 무중단 분석 | updateMode: Off로만 운영되므로 파드 재시작 없음 |
| 시각화 | 네임스페이스·Deployment별 추천값을 UI 대시보드로 한눈에 확인 |
| 점진적 적용 | GitOps(Argo CD, Flux) 연동 시 PR 기반 안전한 단계적 반영 가능 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 수동 개입 필요 | Goldilocks는 추천만 제공, 자동 적용은 별도 구현 필요 | Argo CD/Flux로 추천값을 PR 자동 생성하는 파이프라인 구축 |
| HPA 충돌 | VPA Auto 모드와 CPU 기반 HPA 동시 사용 시 상호 간섭 | CPU는 HPA, Memory는 VPA로 역할 분리 |
| 콜드 스타트 | p90 기준 추천값 생성에 최소 수 일간의 관찰 데이터 필요 | 설치 후 최소 3~7일 대기 후 추천값 신뢰 |
| Consolidation 과잉 | consolidateAfter 값이 너무 짧으면 파드 재스케줄링 과다 발생 |
30s~5m 사이로 설정, disruption budgets 필수 |
| Spot + PDB 미설정 | Karpenter + Spot 조합에서 PDB 없으면 서비스 불안정 | 모든 Deployment에 PodDisruptionBudget 설정 |
| 메모리 계절성 | 트래픽 패턴이 시간대별로 크게 다르면 VPA p90 추천값이 피크를 반영 못 함 | 피크 시간대 데이터를 충분히 포함한 후 추천값 반영 |
PodDisruptionBudget(PDB): Karpenter가 노드를 제거할 때 동시에 내려갈 수 있는 파드 수를 제한하는 설정.
minAvailable: 1만 잡아두어도 서비스 단절을 방지할 수 있습니다.
실무에서 가장 흔한 실수
-
VPA 설치 직후 바로 추천값을 적용하는 것. 데이터가 수 시간치밖에 없는 상태에서 반영하면 추천값이 실제 피크를 전혀 반영하지 못합니다. p90 기준 히스토그램이 안정화되려면 최소 일주일은 관찰한 뒤 반영하는 것을 권장합니다.
-
HPA와 VPA를 CPU 메트릭으로 동시에 연결하는 것. 저도 처음에 이걸 무시했다가 파드가 새벽에 계속 재시작되는 경험을 했습니다. 두 컨트롤러가 서로 다른 목표를 향해 requests를 조정하려 하면 파드가 불안정하게 됩니다. CPU는 HPA, Memory는 VPA로 역할을 분리하는 패턴을 따르는 것을 권장합니다.
-
Karpenter Consolidation을 켜놓고 disruption budgets를 설정하지 않는 것. 업무 시간 중 노드가 갑자기 내려가면서 트래픽이 몰리는 파드들이 동시에 재스케줄링되는 상황이 발생할 수 있습니다. NodePool에 업무 시간대
nodes: "0"설정은 거의 필수입니다.
마치며
Goldilocks + VPA로 requests를 정밀하게 조정하는 것은, Karpenter가 더 똑똑하게 일할 수 있도록 올바른 정보를 제공하는 작업입니다. 실제 사례에서 이 방법으로 $52K → $23K/월 수준의 절감이 보고됐습니다. 비용 최적화는 새로운 도구를 추가하는 것보다, 이미 갖고 있는 스케줄러에게 정확한 데이터를 주는 것에서 시작됩니다.
지금 바로 시작해볼 수 있는 3단계:
-
먼저 진단해보시면 좋습니다. 현재 클러스터에서 CPU 사용률이 50% 이하인 노드가 두 개 이상이라면, 이 글의 방법이 바로 효과를 낼 가능성이 높습니다. 가장 비용이 많이 나가는 네임스페이스 하나에만 Goldilocks 레이블을 붙이고 일주일간 추천값을 수집해보시면 현재 requests가 얼마나 과잉 설정됐는지 바로 확인할 수 있습니다.
-
낮은 트래픽 환경에서 하나의 Deployment에만 추천값을 반영해보시면 좋습니다.
lowerBound보다 약간 여유 있는 수준으로 requests를 낮추고, 며칠간 OOM이나 스로틀링이 발생하지 않는지 모니터링한 뒤 점진적으로 확대해 나가는 방식이 안전합니다. limits는 requests와 별개로 충분한 여유를 두는 것을 권장합니다. -
Karpenter NodePool에 disruption budgets를 설정하고 Consolidation을 활성화해보시면 좋습니다. requests가 줄어든 상태에서
consolidationPolicy: WhenUnderutilized를 켜면, Karpenter가 유휴 노드를 자동으로 정리하면서 실제 비용 절감 효과를 확인할 수 있습니다.
다음 글: Argo CD와 연동해 Goldilocks 추천값을 자동으로 PR로 생성하고 GitOps 파이프라인에 통합하는 방법
참고 자료
- Right-Sizing Kubernetes Resources with VPA and Karpenter | DEV Community
- Kubernetes Resource Optimization & Best Practices with Goldilocks | Fairwinds
- Right-size your Kubernetes Applications Using Open Source Goldilocks | AWS Open Source Blog
- How to Use Goldilocks VPA Recommendations to Right-Size Kubernetes Pod Resources | OneUptime
- GitHub - FairwindsOps/goldilocks
- Goldilocks Installation 공식 문서
- Karpenter 공식 문서 - Disruption
- Kubernetes Cost Optimization: From $50K to $22K/Month with Karpenter, Spot, and VPA | ZeonEdge
- From VPA & Goldilocks to Automation with ScaleOps
- Answering Your Goldilocks Questions About How HPA and VPA Work Together | Fairwinds
- Understanding Karpenter Consolidation | StormForge
- Goldilocks vs Karpenter vs KRR for Kubernetes | Overcast Blog