OpenCost + Karpenter로 EKS Spot 비용 56% 줄인 패턴 가이드
EKS 클러스터를 운영하다 보면 어느 순간 이런 생각이 듭니다. "분명히 스케일링도 잘 되고 있고, Spot도 쓰고 있는데… 왜 비용이 이렇게 많이 나오지?" 저도 처음엔 Karpenter만 붙이면 다 해결될 줄 알았거든요. 근데 막상 뚜껑을 열어보면 유휴 노드가 방치돼 있거나, On-Demand 가격으로 과금되는 구간이 슬금슬금 쌓여 있는 경우가 많습니다.
이 글에서는 OpenCost로 낭비 구간을 정확히 짚어내고, Karpenter의 통합(Consolidation) 정책을 함께 튜닝해서 Spot 인스턴스 활용률을 실질적으로 높이는 패턴을 다룹니다. ZeonEdge 팀이 월 $50K → $22K로 56%를 줄인 것도, Tinybird가 전체 AWS 비용 20%를 절감하면서 오히려 스케일을 키운 것도 정확히 이 흐름을 따랐습니다. 각 사례 링크는 글 하단 참고 자료에 달아뒀으니 직접 확인해보시면 좋을 것 같습니다.
이 글의 대상 독자: Kubernetes + EKS 기본 지식이 있는 DevOps/인프라 엔지니어를 대상으로 작성했습니다. EC2, Spot 인스턴스, kubectl 명령어에 어느 정도 익숙하다는 가정 하에 진행됩니다.
핵심 개념
OpenCost와 Karpenter가 만드는 피드백 루프
OpenCost를 처음 붙였을 때 제일 놀랐던 건 네임스페이스별로 비용이 쪼개져 보인다는 거였어요. 그냥 전체 AWS 청구서 금액이 아니라, "이 네임스페이스가 지난 7일간 $340를 썼고 CPU 효율은 18%입니다"라고 나오거든요. CNCF 샌드박스 프로젝트라 벤더 종속성 없이 AWS, GCP, Azure 모두 지원하고, Prometheus 익스포터로도 동작하기 때문에 기존 모니터링 스택에 자연스럽게 끼워 넣을 수 있습니다.
OpenCost — Kubernetes 비용을 Pod 단위까지 할당(allocation)하는 벤더 중립 오픈소스.
karpenter.sh/nodepool레이블을 자동 인식해 NodePool별 시간당 비용을 PromQL로 집계할 수 있습니다.
Karpenter 쪽은 어떠냐면, 기존 클러스터 오토스케일러(CA)와 달리 미리 정의된 노드 그룹 없이 클라우드 API를 직접 호출해 Pending Pod 요건에 맞는 최적 인스턴스를 즉시 프로비저닝합니다. 2024년 하반기 v1.0 GA로 NodePool과 EC2NodeClass API가 안정화됐고, 가장 중요한 SpotToSpotConsolidation 기능이 Feature Gate로 정식 제공됩니다.
Spot 인스턴스 — AWS가 남는 컴퓨팅 자원을 On-Demand 대비 최대 90% 저렴하게 제공하는 방식. 단, AWS가 해당 자원이 필요해지면 2분 전 통보 후 회수할 수 있습니다.
Spot-to-Spot 통합(SpotToSpotConsolidation) — 현재 실행 중인 Spot 인스턴스를 더 저렴한 Spot 타입으로 자동 교체하는 기능. 인스턴스 타입 15종 이상 지정 시에만 활성화됩니다. 이 조건을 몰라서 비활성화된 상태로 운영하는 경우가 꽤 많습니다.
솔직히 말하면, Karpenter만 쓰는 것도 나쁘지 않습니다. 그런데 OpenCost 없이는 "지금 내 클러스터가 어디서 돈을 낭비하는지"를 정확히 알기가 어렵습니다. 반대로 OpenCost만 있으면 낭비를 발견해도 자동으로 고쳐지지 않죠. 두 도구를 함께 쓰면 다음 루프가 완성됩니다.
| 단계 | 도구 | 역할 |
|---|---|---|
| ① 가시성 확보 | OpenCost | 네임스페이스·NodePool별 낭비 구간 식별 |
| ② 정책 튜닝 | Karpenter NodePool | 인스턴스 다양성 확대, 통합 정책 설정 |
| ③ 자동 최적화 | Karpenter Consolidation | 더 저렴한 Spot으로 자동 교체·유휴 노드 삭제 |
| ④ 효과 측정 | OpenCost + Grafana | 절감액 추적, 다음 튜닝 사이클 진입 |
아래 실전 예시 3개가 각각 이 루프의 어느 단계에 해당하는지 의식하면서 읽어보시면 흐름이 더 잘 잡힙니다.
실전 적용
시작 전 주의사항: Spot 인스턴스는 인터럽션(중단) 핸들링이 전제 조건입니다. Karpenter가 SQS + EventBridge를 통해 인터럽션 이벤트를 받아 미리 Pod를 옮길 수 있도록 설정해두지 않으면, 인스턴스가 갑자기 사라질 때 서비스에 영향이 생깁니다. 설정 방법은 AWS Spot Instances with Karpenter 공식 블로그에 잘 정리돼 있으니, 아래 예시를 적용하기 전에 먼저 확인해보시기를 권장합니다.
② 정책 튜닝 — Spot-to-Spot 통합 NodePool 구성
이 예시가 루프의 ② 단계, 즉 NodePool 정책 튜닝에 해당합니다. 실무에서 가장 먼저 해야 할 건 인스턴스 타입을 15종 이상으로 열어두는 겁니다. 저도 처음엔 "관리가 복잡해지지 않을까?" 하고 3~4종만 넣었다가, Spot-to-Spot 통합이 아예 안 돼서 On-Demand 노드가 계속 남아있는 걸 나중에야 발견했습니다.
SpotToSpotConsolidation Feature Gate를 활성화하려면 Karpenter 컨트롤러 플래그를 추가해야 합니다. Helm으로 설치하는 경우 values.yaml에 다음을 추가하면 됩니다.
# karpenter values.yaml
controller:
extraArgs:
- --feature-gates=SpotToSpotConsolidation=trueFeature Gate 설정 후 NodePool은 이런 형태로 구성해볼 수 있습니다.
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: spot-general
spec:
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 5m # 너무 짧으면 불필요한 재시작 유발
budgets:
- nodes: "20%" # 한 번에 최대 20% 노드만 교체
template:
metadata:
labels:
workload-type: general # OpenCost 비용 분류용 레이블
spec:
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"] # Karpenter가 비용 기준으로 Spot 우선 선택, 재고 없을 때 On-Demand fallback
- key: node.kubernetes.io/instance-type
operator: In
values:
- m5.xlarge
- m5a.xlarge
- m5d.xlarge
- m6i.xlarge
- m6a.xlarge
- m6in.xlarge
- m7i.xlarge
- m7a.xlarge
- c5.2xlarge
- c5a.2xlarge
- c6i.2xlarge
- c6a.2xlarge
- r5.xlarge
- r6i.xlarge
- r6a.xlarge
# 15종 이상 — SpotToSpotConsolidation 활성화 요건한 가지 궁금할 수 있는 점이 있는데요, capacity-type에 spot과 on-demand를 함께 지정하면 Karpenter가 랜덤으로 고르는 건지 의문이 드실 수 있습니다. 실제로는 Karpenter가 각 인스턴스 타입의 현재 Spot 가격을 실시간으로 조회해서 가장 저렴한 조합을 선택하기 때문에, Spot 재고가 있는 상황이라면 자동으로 Spot이 먼저 선택됩니다.
각 설정 의도를 정리하면 다음과 같습니다.
| 설정 키 | 값 | 의도 |
|---|---|---|
consolidationPolicy |
WhenEmptyOrUnderutilized |
빈 노드 + 활용률 낮은 노드 모두 교체 대상 |
consolidateAfter |
5m |
1m은 너무 공격적 — 버스트 워크로드에 5m 권장 |
budgets.nodes |
20% |
한 번에 너무 많은 노드 교체 시 서비스 영향 방지 |
capacity-type |
spot, on-demand |
Karpenter 비용 기반 알고리즘이 Spot 우선 선택 |
① 가시성 확보 — OpenCost로 낭비 네임스페이스 탐지
이제 루프의 ① 단계로 돌아가서, 실제로 어디서 돈이 새고 있는지 찾는 방법입니다. Karpenter를 잘 설정했더라도, CPU/Memory Request가 실제 사용량보다 과도하게 잡혀 있으면 Karpenter는 "이 노드는 꽉 찼다"고 판단해서 통합 대상에서 제외합니다. OpenCost API로 이걸 찾아낼 수 있습니다.
API를 로컬에서 호출하려면 먼저 포트 포워딩이 필요합니다.
kubectl port-forward -n opencost svc/opencost 9090:9090포트 포워딩이 된 상태에서 아래 커맨드를 실행해보시면 네임스페이스별 비용 효율성 리포트가 나옵니다.
# OpenCost API로 네임스페이스별 비용 효율성 조회 (지난 7일)
curl "http://localhost:9090/model/allocation" \
--data-urlencode 'window=7d' \
--data-urlencode 'aggregate=namespace' \
--data-urlencode 'accumulate=true' | \
jq '.data[0] | to_entries |
map({
namespace: .key,
cpuEfficiency: .value.cpuEfficiency,
memEfficiency: .value.memEfficiency,
totalCost: .value.totalCost
}) |
sort_by(.totalCost) | reverse | .[:10]'jq 파이프라인이 익숙하지 않으신 분들을 위해 출력 예시를 보면 이런 형태입니다.
[
{
"namespace": "data-pipeline",
"cpuEfficiency": 0.18,
"memEfficiency": 0.42,
"totalCost": 340.21
},
{
"namespace": "api-server",
"cpuEfficiency": 0.71,
"memEfficiency": 0.68,
"totalCost": 112.05
}
]cpuEfficiency가 0.3 이하(30% 미만)인 네임스페이스가 있다면, 해당 팀에 VPA 우사이징을 검토해볼 수 있습니다. Request가 줄면 Karpenter가 더 작은 Spot 노드로 재통합할 수 있거든요. ZeonEdge가 $50K → $22K를 달성한 것도 정확히 이 흐름이었습니다.
VPA 우사이징 권고를 받기 위해서는 VPA를 Recommendation 모드로 먼저 설치해보시면 좋습니다. 실제 Request 변경 없이 권고치만 확인하는 안전한 방식입니다.
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: data-pipeline-vpa
namespace: data-pipeline
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: data-pipeline
updatePolicy:
updateMode: "Off" # Recommendation 모드 — 자동 변경 없이 권고치만 수집kubectl describe vpa data-pipeline-vpa -n data-pipeline으로 권고 Request 값을 확인한 뒤, 실제 값과 차이가 크다면 점진적으로 줄여나가는 방식을 권장합니다.
NodePool별 시간당 비용도 PromQL로 집계할 수 있습니다. OpenCost 메트릭 이름은 배포 방식에 따라 다를 수 있어서, 정확한 메트릭 목록은 OpenCost Prometheus 익스포터 공식 문서에서 확인하시기를 권장합니다. 일반적으로 container_cpu_allocation 같은 메트릭은 대부분의 배포 환경에서 검증된 형태입니다.
③ 자동 최적화 — 워크로드 특성별 NodePool 분리
루프의 ③ 단계, 즉 자동화된 비용 최적화를 최대한 끌어올리는 패턴입니다. 배치성 워크로드(CI/CD, 미디어 인코딩 등)와 프론트엔드 API를 같은 NodePool에 두는 건 실무에서 자주 보이는 비효율입니다. 통합 정책이 API 서버를 중단시킬 수 있고, 배치 작업은 Spot 중단에 상대적으로 관대한데도 불구하고 비싼 On-Demand 노드를 쓰게 됩니다.
실제로 이 설정을 처음 적용했을 때 팀 내에서 "Spot이 불안정하다"는 불만이 있었는데, 알고 보니 배치와 API가 같은 풀을 쓰고 있어서 배치 인터럽션이 API 레이턴시에도 영향을 주고 있었던 상황이었습니다. 분리하고 나서 API 서버 안정성 이슈가 사라졌습니다.
# 배치 전용 Spot NodePool — 공격적인 통합 허용
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: spot-batch
spec:
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 1m # 배치는 짧아도 OK
budgets:
- nodes: "30%"
template:
metadata:
labels:
workload-type: batch
spec:
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
taints:
- key: workload-type
value: batch
effect: NoSchedule # 배치 Pod만 이 노드에 스케줄
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"] # 배치는 Spot만
- key: node.kubernetes.io/instance-type
operator: In
values:
- c5.2xlarge
- c5a.2xlarge
- c6i.2xlarge
- c6a.2xlarge
- m5.2xlarge
- m6i.2xlarge
- m6a.2xlarge
- m7i.2xlarge
- r5.xlarge
- r6i.xlarge
- r6a.xlarge
- r7i.xlarge
- c7i.2xlarge
- c7a.2xlarge
- m7a.2xlarge
# 15종 이상 유지
---
# API 서버용 On-Demand NodePool — 안정성 우선
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: ondemand-api
spec:
disruption:
consolidationPolicy: WhenEmpty # 비어있을 때만 삭제
consolidateAfter: 30m
budgets:
- nodes: "10%" # 안정성을 위해 더 보수적으로
template:
spec:
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"]OpenCost에서 label_workload_type 기준으로 비용을 나눠보면 Spot 배치 NodePool의 절감액이 얼마인지 한눈에 확인됩니다. 이게 루프의 ④ 효과 측정 단계로 자연스럽게 이어지는 흐름입니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 비용 절감 폭 | Spot은 On-Demand 대비 최대 90% 저렴. 실제 운영 사례에서 전체 20~56% 절감 |
| 완전 자동화 | 통합 루프가 주기적으로 더 저렴한 Spot 탐색·교체 — 수동 개입 불필요 |
| 세분화된 가시성 | NodePool·네임스페이스·레이블 단위 비용 트래킹으로 낭비 구간 정밀 식별 가능 |
| 오픈소스 스택 | 둘 다 라이선스 비용 없음. OpenCost는 CNCF 프로젝트로 벤더 종속 없음 |
| 기존 스택 통합 | Prometheus + Grafana 기반이라 별도 모니터링 인프라 없이 통합 가능 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| Spot 중단 위험 | AWS가 2분 전 통보 후 회수 가능 | SQS + EventBridge 인터럽션 핸들링 필수 설정 |
| 15종 미만 인스턴스 | Spot-to-Spot 통합 비활성화 | NodePool에 15종 이상 인스턴스 타입 지정 |
| OpenCost 가격 정확도 | 미설정 시 On-Demand 가격으로 fallback, 절감액 과소 계산 | S3 데이터 피드 + IAM 권한 설정 필요 |
| Stateful 워크로드 부적합 | DB, 분산 스토리지는 Spot 중단 시 데이터 위험 | PDB + tolerations 설계, On-Demand NodePool 분리 |
| 과도한 재스케줄링 | consolidateAfter가 너무 짧으면 불필요한 Pod 재시작 |
버스트성 워크로드는 5m 이상 권장 |
| 비용 데이터 지연 | AWS 가격 피드 갱신 주기에 의존, 완전 실시간 아님 | 대시보드 해석 시 수 분~수십 분 지연 감안 필요 |
PodDisruptionBudget(PDB) — 특정 시점에 동시에 중단될 수 있는 Pod의 최소/최대 수를 제한하는 Kubernetes 리소스. Spot 통합 과정에서 서비스 가용성을 보장하는 핵심 장치입니다.
이 중에서 제가 실제로 가장 자주 보는 실수를 따로 정리해봤습니다.
실무에서 가장 흔한 실수
-
인스턴스 타입을 3~5종만 지정하는 것 — Spot-to-Spot 통합이 조용히 비활성화되어 있어도 로그에 잘 안 잡힙니다. NodePool 적용 후
kubectl get nodepool spot-general -o yaml로 status 필드를 꼭 확인해보시기를 권장합니다. -
OpenCost Spot 가격 피드 설정을 빠뜨리는 것 — 설정 없이는 On-Demand 가격으로 fallback되어 대시보드상 절감액이 0으로 보이거나 실제보다 훨씬 적게 나옵니다. AWS IAM 권한 설정이 빠지기 쉬운 부분인데, OpenCost AWS 설정 가이드에 필요한 권한 목록이 정리돼 있습니다.
-
Stateful 워크로드에 Spot을 적용하는 것 — DB나 Kafka 같은 상태 유지 워크로드를 Spot NodePool에 올리면 중단 시 데이터 손실 위험이 있습니다.
nodeSelector나affinity로 반드시 On-Demand NodePool로 고정하는 것을 권장합니다.
마치며
OpenCost로 낭비 구간을 정확히 짚고, Karpenter의 Spot-to-Spot 통합 정책을 튜닝하는 가시성 → 분석 → 최적화 루프가 EKS 비용 절감의 핵심 패턴입니다. 처음부터 완벽한 설정을 목표로 하기보다는, 아래 순서로 가볍게 시작해보시면 좋습니다.
지금 바로 시작해볼 수 있는 4단계
- OpenCost 설치 —
helm install opencost opencost/opencost -n opencost --create-namespace로 설치하고 포트 포워딩으로 UI에 접속해보시면 됩니다. - 낭비 네임스페이스 파악 — 위
curl + jq예시로cpuEfficiency30% 미만 네임스페이스를 찾아 VPA 우사이징 후보를 식별해보시기를 권장합니다. - NodePool 인스턴스 타입 확대 — AWS Spot Instance Advisor에서 중단율
<5%인스턴스를 골라 15종 이상으로 늘리고SpotToSpotConsolidationFeature Gate를 활성화해보시면 됩니다. - Grafana 대시보드 연결 —
opencost-mixin+kubernetes-autoscaling-mixin대시보드를 추가하면 NodePool별 Spot 절감액과 유휴 노드 비율을 단일 뷰로 모니터링할 수 있습니다.
이 루프를 한 사이클 돌리고 나면 "우리 클러스터에서 어디가 문제였는지"가 숫자로 보이기 시작합니다. 그 시점부터 다음 최적화 사이클이 훨씬 빠르게 돌아간다는 게 이 패턴의 진짜 장점입니다.
다음 글: Goldilocks + VPA로 CPU/Memory Request를 자동 우사이징해 Karpenter 통합 효율을 추가로 높이는 방법
참고 자료
- Using Amazon EC2 Spot Instances with Karpenter | AWS Containers Blog
- Applying Spot-to-Spot consolidation best practices with Karpenter | AWS Compute Blog
- Karpenter NodePool 공식 문서
- Amazon EKS Karpenter Best Practices | AWS 공식 문서
- OpenCost 공식 사이트
- OpenCost AWS 설정 가이드
- OpenCost Prometheus 익스포터 통합
- Cut AWS costs by 20% while scaling with EKS, Karpenter and Spot Instances | Tinybird
- Kubernetes Cost Optimization: From $50K to $22K/Month | ZeonEdge
- Karpenter Monitoring: Spot Savings and Node Pool Cost Breakdown | hodovi.cc
- Monitoring EKS costs with OpenCost and AWS Managed Prometheus/Grafana | automat-it
- Spot-to-Spot Consolidation in Karpenter: Best Practices | nOps
- OpenCost GitHub 리포지토리