← 전체 글

EKS FinOps: Kubecost + Karpenter로 쿠버네티스 컴퓨팅 비용 최적화하기


환경: EKS, Karpenter v1.x, Kubecost v2.x
핵심 주제: Kubernetes 비용 가시화, Consolidation 전략, Spot 인스턴스, 리소스 요청 최적화

배경 / 문제 상황

EKS 클러스터를 어느 정도 운영하다 보면 어느 날 AWS 청구서가 생각보다 훨씬 크다는 것을 알게 된다. 인스턴스는 분명 돌아가고 있는데, 실제로 어디서 비용이 발생하는지 알 수 없다는 것이 가장 큰 문제다.

처음에는 감각으로 관리했다. “이 서비스가 좀 큰 것 같으니 리소스 요청을 낮춰보자”, “야간에는 트래픽이 없으니 수동으로 스케일 다운하자”. 이런 식의 관리는 한계가 명확하다. 어떤 네임스페이스가 얼마를 쓰는지, 어떤 워크로드가 요청 대비 실제 사용률이 낮은지, 노드 빈 공간이 얼마나 낭비되는지 — 모두 눈에 보이지 않았다.

EKS FinOps를 제대로 하려면 두 가지가 필요하다. 비용을 보이게 하는 도구와 비용을 자동으로 줄이는 메커니즘이다. Kubecost가 전자를, Karpenter Consolidation이 후자를 담당한다.

기술 선택 배경 — Kubecost를 선택한 이유

AWS 비용 관리에는 여러 도구가 있다. AWS Cost Explorer, CloudWatch, AWS Compute Optimizer… 그런데 이들의 공통된 한계가 있다: Kubernetes Pod 단위의 비용을 볼 수 없다.

EC2 인스턴스 하나에 10개의 Pod가 돌아가면, AWS 입장에서는 인스턴스 비용만 보인다. 어떤 네임스페이스의 어떤 Pod가 그 인스턴스의 몇 %를 사용하는지는 전혀 알 수 없다.

Kubecost는 이 문제를 해결하는 오픈소스 Kubernetes 비용 분석 도구다.

  • Pod/Deployment/Namespace 단위 비용 분해: 각 워크로드가 실제로 얼마를 쓰는지 계산
  • 요청 대비 실제 사용률 비교: CPU/메모리 요청이 과도한 워크로드 식별
  • 팀/서비스 단위 비용 할당: 레이블 기반으로 비용을 조직 구조에 맞게 분류
  • Savings 추천: 사용률 데이터 기반으로 리소스 요청 다운사이징 자동 추천
  • EKS 통합: AWS가 EKS에서 공식 지원하는 비용 모니터링 솔루션

Kubecost는 Kubernetes 클러스터 내에서 동작하기 때문에 Pod 단위 메트릭을 직접 수집한다. Prometheus와 연동해서 리소스 사용 이력을 분석하고, AWS 가격 정보와 결합해서 실제 달러 비용으로 환산한다.

무료 버전(Community Edition)도 기본적인 비용 분석 기능을 충분히 제공한다. 단일 클러스터 분석이라면 무료 버전으로 시작하기에 충분하다.

Kubecost 설치

AWS가 EKS용으로 최적화한 Helm 차트를 ECR Public에 제공한다:

# EKS 최적화 Kubecost 차트 다운로드
helm pull oci://public.ecr.aws/kubecost/cost-analyzer --untar

# EKS 통합 values 파일을 기본으로 사용
cp -rf ./cost-analyzer/values-eks-cost-monitoring.yaml ./values.yaml

# 설치
helm upgrade -i kubecost \
  -n kubecost \
  --create-namespace \
  ./cost-analyzer \
  -f ./values.yaml

values-eks-cost-monitoring.yaml은 EKS 환경에 최적화된 설정이 들어있다. Prometheus 연동, AWS Spot 인스턴스 가격 통합, EKS 클러스터 ID 자동 감지 등이 포함된다.

주의사항: Kubecost는 내부적으로 Prometheus에 의존한다. cost-analyzer Pod가 Ready 상태가 되기 위해서는 Prometheus가 먼저 정상적으로 동작해야 한다. 설치 직후 Probe가 실패하면 잠시 기다리거나 kubectl rollout restart로 재시작한다.

# 설치 상태 확인
kubectl get pods -n kubecost

# cost-analyzer 재시작 (Prometheus 초기화 대기 후)
kubectl rollout restart deployment/kubecost-cost-analyzer -n kubecost

ArgoCD로 관리하는 경우 ApplicationSet에 Kubecost를 추가한다:

# management applicationset에 추가
elements:
  - name: kubecost
    namespace: kubecost
    chart: cost-analyzer
    repoURL: 'oci://public.ecr.aws/kubecost'
    valueFiles: 'values.yaml'

Kubecost로 비용 구조 파악하기

설치가 완료되면 kubectl port-forward로 대시보드에 접근한다:

kubectl port-forward svc/kubecost-cost-analyzer 9090 -n kubecost
# http://localhost:9090 접근

프로덕션 환경에서는 Ingress를 통해 내부 도메인으로 노출하거나, Backstage 플러그인으로 연동하면 편하다.

핵심 분석: Allocation 대시보드

Kubecost의 가장 강력한 기능은 Allocation(비용 할당) 대시보드다. 비용을 다음 차원으로 분해할 수 있다:

  • Namespace별: 어떤 네임스페이스가 가장 많은 비용을 쓰는가
  • Deployment/StatefulSet별: 어떤 워크로드가 비용 주범인가
  • Label별: 팀/서비스 단위로 비용 할당

실제로 분석해 보면 흥미로운 패턴이 드러난다. CPU 요청은 많이 설정했는데 실제 사용률이 10% 미만인 워크로드, 야간에도 레플리카를 그대로 유지하는 서비스, 개발 환경임에도 프로덕션 사이즈 인스턴스를 사용하는 워크로드 등이 보인다.

Savings 추천 활용

Kubecost의 Savings 메뉴는 자동으로 비용 절감 기회를 식별한다:

  • Right-sizing 추천: CPU/메모리 요청이 실제 사용량 대비 과도한 워크로드 목록과 권장 조정값
  • 유휴 노드: 할당된 리소스가 없거나 매우 낮은 노드
  • 오래된 리소스: 장기간 트래픽이 없는 서비스

Right-sizing 추천은 신중하게 적용해야 한다. Kubecost가 제시하는 값은 관측 기간의 평균/p95 기반이므로, 피크 트래픽이 갑자기 증가하는 서비스라면 여유를 남겨야 한다. 추천값의 70~80%를 목표로 점진적으로 줄이는 것을 권장한다.

Karpenter Consolidation

Kubecost로 비용을 보이게 했다면, Karpenter Consolidation은 비용을 자동으로 줄인다. 이것이 EKS FinOps의 핵심 메커니즘이다.

Consolidation이란

클러스터를 운영하다 보면 노드 사용률이 들쭉날쭉해진다. Pod가 스케일 다운되면 일부 노드는 드문드문 Pod만 남게 된다. 이 Pod들을 더 적은 노드에 모아서 빈 노드를 제거하면 비용이 절감된다.

Karpenter의 Consolidation은 이 과정을 자동화한다. 주기적으로 클러스터 상태를 분석해서 “이 Pod들을 더 적은 노드에 배치할 수 있는가?“를 계산하고, 가능하다면 Pod를 옮기고 빈 노드를 제거한다.

NodePool Consolidation 설정

apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: default
spec:
  template:
    spec:
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["on-demand", "spot"]  # Spot 포함으로 비용 절감
        - key: kubernetes.io/arch
          operator: In
          values: ["amd64"]
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ["c", "m", "r"]  # 범용 인스턴스 패밀리
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default

  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized  # 핵심 설정
    consolidateAfter: 30s  # 빈 노드 제거까지 대기 시간
    budgets:
      - nodes: "10%"  # 동시에 교체 가능한 최대 노드 비율

consolidationPolicy는 두 가지 옵션이 있다:

  • WhenEmpty: 완전히 빈 노드만 제거. 보수적, 안전
  • WhenEmptyOrUnderutilized: Pod 재배치를 통해 저사용 노드까지 제거. 더 공격적인 비용 절감

프로덕션에서는 WhenEmptyOrUnderutilized를 사용하되, budgets으로 동시 교체 노드 수를 제한해서 안정성을 확보한다.

Spot 인스턴스 + On-Demand 혼합 전략

모든 워크로드를 Spot으로 돌리면 비용이 크게 줄지만, 인터럽트(강제 종료) 위험이 있다. 실용적인 접근은 중단 허용 워크로드는 Spot, 중단 불가 워크로드는 On-Demand로 분리하는 것이다.

# Spot 허용 NodePool (배치 작업, Stateless 서비스)
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: spot-pool
spec:
  template:
    metadata:
      labels:
        node-type: spot
    spec:
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot"]
      taints:
        - key: spot-instance
          effect: PreferNoSchedule  # Spot 허용 Pod만 우선 배치

# On-Demand 전용 NodePool (DB, 캐시, 크리티컬 서비스)
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: on-demand-pool
spec:
  template:
    spec:
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["on-demand"]

Spot 인스턴스는 On-Demand 대비 평균 60~70% 저렴하다. Stateless 웹 서비스, 배치 작업, 개발 환경 워크로드를 Spot으로 전환하면 상당한 비용 절감이 가능하다.

Consolidation 시 중요한 설정들

실제 운영에서 Consolidation과 관련해서 신경 써야 할 설정들이 있다.

do-not-disrupt 어노테이션: 중요 Pod가 Consolidation 대상이 되지 않도록 보호한다.

metadata:
  annotations:
    karpenter.sh/do-not-disrupt: "true"  # Redis, MySQL 등 상태 유지 워크로드

PDB(PodDisruptionBudget) 설정: Consolidation 시 가용성을 보장한다.

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-pdb
spec:
  minAvailable: "50%"  # 절댓값(1)보다 비율이 Consolidation에 더 유리
  selector:
    matchLabels:
      app: api-server

minAvailable을 절댓값으로 설정하면(예: 1) 레플리카가 1개일 때 Consolidation이 완전히 막힌다. 비율("50%")로 설정하면 레플리카가 줄어들어도 Consolidation이 계속 진행될 수 있다.

실제 비용 분석 결과와 적용 내용

Kubecost를 통해 파악한 비용 구조와 개선 내용을 정리하면:

발견 1: 개발 환경 야간 비용

Stage 클러스터가 야간에도 동일한 규모로 운영되고 있었다. KEDA + CronJob 기반 야간 Scale-to-zero를 적용해서 오전 9시~오후 10시 외 시간에는 워크로드를 0으로 줄였다. Karpenter가 빈 노드를 자동으로 제거해서 노드 비용이 절감됐다.

발견 2: 과도한 리소스 요청

초기 배포 시 “일단 크게”로 설정한 메모리 요청이 실제 사용량의 34배인 워크로드들이 여럿 있었다. Kubecost Right-sizing 추천을 참고해서 12주 모니터링 후 실제 p95 사용량 × 1.5배로 조정했다. 노드 bin-packing 효율이 높아지면서 동일한 워크로드를 더 적은 노드에 배치할 수 있게 됐다.

발견 3: 인스턴스 타입 최적화

Karpenter의 karpenter.k8s.aws/instance-category 요건을 넓게 설정해서 메모리 최적화 인스턴스(r 계열)와 컴퓨팅 최적화 인스턴스(c 계열)도 후보에 포함시켰다. 범용(m 계열)만 쓸 때보다 워크로드 특성에 맞는 인스턴스를 Karpenter가 자동으로 선택해서 비용 대비 성능이 개선됐다.

발견 4: Spot 전환 가능 워크로드

Stateless 웹 서비스와 배치 처리 파드는 Spot NodePool로 전환했다. 인터럽트 발생 시 Karpenter가 자동으로 새 Spot 노드를 프로비저닝하고, ALB의 Connection Draining으로 트래픽 중단을 최소화했다.

핵심 트러블슈팅

문제: Kubecost Probe 실패

증상: 설치 후 cost-analyzer Pod의 Readiness Probe가 계속 실패해서 CrashLoopBackOff 또는 NotReady 상태.

원인: cost-analyzer는 내부 Prometheus가 준비될 때까지 자체 초기화를 완료할 수 없다. 처음 설치 시 Prometheus Pod가 완전히 기동하는 데 시간이 필요하다.

해결: cost-analyzer Pod를 Prometheus가 Ready된 후에 재시작한다.

kubectl wait --for=condition=ready pod -l app=prometheus \
  -n kubecost --timeout=120s

kubectl rollout restart deployment/kubecost-cost-analyzer -n kubecost

문제: Consolidation이 예상보다 느리게 동작

증상: 명백히 비어있거나 저사용 노드가 있는데 Karpenter가 수십 분 이상 제거하지 않음.

원인: 여러 조건이 Consolidation을 지연시킨다. (1) do-not-disrupt 어노테이션이 있는 Pod, (2) minAvailable 절댓값 PDB로 인해 Pod를 이동할 수 없는 상태, (3) topologySpreadConstraints가 이동 후 불충족되는 경우, (4) consolidateAfter 대기 시간.

진단: Karpenter 로그에서 Consolidation 시도 이유와 실패 원인을 확인한다.

kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter \
  --since=30m | grep -E "consolidat|disrupt"

해결: PDB를 절댓값에서 비율로 변경하고, topologySpreadConstraintswhenUnsatisfiable을 재검토한다.

문제: Spot 인터럽트 시 서비스 다운

증상: Spot 인스턴스가 인터럽트될 때 해당 노드의 Pod가 잠시 Terminating 상태가 되면서 요청 실패 발생.

원인: Spot 인터럽트 알림(2분 전 경고)을 처리하는 로직이 없으면 Pod가 갑자기 종료된다.

해결: AWS Node Termination Handler(NTH) 또는 Karpenter 내장 인터럽트 처리를 활성화한다. Karpenter는 Spot 인터럽트 알림을 EventBridge를 통해 수신하고, 알림 수신 즉시 노드를 cordoned 상태로 전환하여 새 Pod 배치를 막고, 기존 Pod를 다른 노드로 안전하게 드레인한다.

# EC2NodeClass에 인터럽트 큐 설정
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: default
spec:
  # Spot 인터럽트 핸들링을 위한 SQS 큐
  # Karpenter Helm 설치 시 settings.interruptionQueue 설정 필요

마무리 및 다음 스텝

EKS FinOps는 “한 번 설정하면 끝”이 아니라 지속적인 관찰과 조정의 과정이다. Kubecost가 현재 상태를 보여주고, Karpenter Consolidation이 자동으로 최적화하지만, 워크로드는 계속 변하기 때문에 정기적인 검토가 필요하다.

실용적인 FinOps 루틴을 제안하면:

  • 주간: Kubecost Savings 대시보드에서 새로운 Right-sizing 기회 확인
  • 월간: Namespace별 비용 추이를 팀과 공유 (비용 오너십 강화)
  • 분기별: 인스턴스 타입/패밀리 재검토 (AWS 신규 인스턴스 타입 활용)

다음으로 적용할 FinOps 고도화 방향은 두 가지다.

첫째, Spot 커버리지 확대. 현재 일부 워크로드만 Spot을 사용하는데, 더 많은 Stateless 서비스를 분석해서 Spot 전환 가능 여부를 판단할 예정이다. 특히 레플리카가 3개 이상인 서비스는 인터럽트 허용 가능성이 높다.

둘째, KEDA 기반 야간 Scale-to-zero 확대. 현재 Stage 환경에만 적용된 야간 스케일링을 Prod의 특정 배치 워크로드에도 확대 적용할 계획이다. Karpenter Consolidation과 함께 사용하면 워크로드가 0이 되는 시간대에 노드도 0으로 줄어드는 완전한 탄력적 비용 구조를 만들 수 있다.

비용 최적화는 성능과 안정성의 트레이드오프를 수반한다. Kubecost로 데이터 기반 의사결정을 하고, Karpenter로 자동화하되, 항상 PDB와 모니터링으로 안전망을 유지하는 것이 핵심이다.

돌아보며

  • 비용을 감이 아니라 데이터(Kubecost Allocation·Right-sizing, p95 사용량)로 진단하고 Karpenter로 자동화한 데이터 기반 의사결정 능력 — 야간 Scale-to-zero, 실사용 3~4배 과다 요청 조정, 인스턴스 카테고리 확장, Spot 전환의 4개 레버 도출
  • 비용 절감이 안정성과의 트레이드오프임을 이해하고 PDB·인터럽트 핸들링·모니터링 안전망과 함께 설계한 균형 감각
  • 주간/월간/분기 FinOps 루틴으로 일회성 최적화가 아닌 지속 가능한 비용 운영 체계를 만든 경험