Skip to content
Published on

Kubernetes FinOps와 클라우드 비용 최적화 전략: 리소스 낭비를 잡는 실전 가이드

Authors
  • Name
    Twitter

Kubernetes FinOps와 클라우드 비용 최적화

들어가며: 왜 Kubernetes 비용 관리가 중요한가

Kubernetes 도입이 가속화되면서 클라우드 비용 문제가 심각해지고 있습니다. CNCF의 2025 FinOps 리포트에 따르면, 기업들의 Kubernetes 관련 클라우드 지출 중 평균 30-35%가 낭비되고 있습니다. 이는 단순히 리소스를 과다하게 할당하는 문제를 넘어, 비용에 대한 가시성 부족, 팀 간 책임 소재 불분명, 최적화 프로세스의 부재라는 구조적 문제에서 비롯됩니다.

FinOps Foundation에서 정의하는 FinOps는 **"엔지니어링, 재무, 비즈니스 팀이 데이터 기반으로 협업하여 클라우드의 비즈니스 가치를 극대화하는 운영 프레임워크"**입니다. 전통적인 IT 인프라에서는 CapEx(자본 지출) 모델로 한 번 구매하면 끝이었지만, 클라우드는 OpEx(운영 지출) 모델로 매 시간, 매 분 비용이 발생합니다. 이런 환경에서 FinOps는 선택이 아닌 필수입니다.

이 글에서는 Kubernetes 환경에 특화된 FinOps 전략을 다룹니다. 비용 가시성 확보부터 리소스 최적화, 자동 스케일링, 그리고 팀 문화까지 실전에서 바로 적용할 수 있는 가이드를 제공합니다.

FinOps의 핵심 원칙과 Kubernetes 적용

FinOps의 세 가지 원칙

FinOps Foundation이 제시하는 핵심 원칙은 다음과 같습니다.

  1. 팀은 자신의 클라우드 사용량에 대해 책임을 진다 - 엔지니어링 팀이 비용을 인식하고 최적화 결정을 내려야 합니다
  2. 의사결정은 클라우드의 비즈니스 가치에 기반한다 - 단순 비용 절감이 아닌 비즈니스 가치 대비 비용 효율을 추구합니다
  3. FinOps는 중앙 집중형 팀이 주도한다 - 도구, 프로세스, 모범 사례를 중앙에서 관리하되, 실행은 각 팀이 합니다

Inform, Optimize, Operate 사이클

FinOps는 세 단계의 반복 사이클로 운영됩니다.

단계목표Kubernetes 적용
Inform비용 가시성 확보Kubecost/OpenCost 도입, 네임스페이스별 비용 대시보드
Optimize비용 최적화 실행Request/Limit 튜닝, Spot 인스턴스, 유휴 리소스 정리
Operate지속적 거버넌스비용 알림, 정기 리뷰, 팀별 예산 관리

Kubernetes 리소스 낭비의 주요 원인

비용 최적화를 시작하기 전에, 어디서 낭비가 발생하는지 정확히 이해해야 합니다.

1. 과도한 Request/Limit 설정

가장 흔한 원인입니다. 개발자들이 "안전하게" 높은 값을 설정하는 경향이 있습니다.

# 문제: 실제 사용량 대비 과도한 리소스 요청
apiVersion: v1
kind: Pod
metadata:
  name: over-provisioned-app
spec:
  containers:
    - name: app
      image: my-app:latest
      resources:
        requests:
          cpu: '2' # 실제 사용량: 200m
          memory: '4Gi' # 실제 사용량: 512Mi
        limits:
          cpu: '4'
          memory: '8Gi'

위 예시에서 CPU는 실제 사용량의 10배, 메모리는 8배를 요청하고 있습니다. 이 Pod 하나만으로는 큰 문제가 아니지만, 100개의 Pod가 이런 식이라면 월 수천 달러의 낭비가 발생합니다.

2. 스케일링 정책 부재

트래픽이 감소해도 Pod 수가 줄어들지 않거나, 야간/주말에도 동일한 리소스가 유지되는 경우입니다.

3. 유휴 리소스 방치

더 이상 사용하지 않는 PersistentVolume, LoadBalancer Service, 테스트용 네임스페이스 등이 정리되지 않고 남아 있는 경우입니다.

4. 노드 단편화(Fragmentation)

작은 Pod들이 여러 노드에 분산되어 각 노드의 활용률이 낮아지는 현상입니다.

# 노드별 리소스 활용률 확인
kubectl top nodes

# 결과 예시
# NAME           CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
# node-1         800m         20%    2Gi             25%
# node-2         600m         15%    1.5Gi           18%
# node-3         400m         10%    1Gi             12%
# => 3개 노드 모두 활용률 25% 이하 - 1개 노드로 통합 가능

비용 가시성 확보: Kubecost와 OpenCost

비용 최적화의 첫 단계는 **"지금 얼마를 쓰고 있는지"**를 아는 것입니다.

OpenCost 설치 및 설정

OpenCost는 CNCF 공식 프로젝트로, Kubernetes 비용 모니터링을 위한 오픈소스 표준입니다. Prometheus와 통합되어 실시간 비용 데이터를 수집합니다.

# Helm으로 OpenCost 설치
helm repo add opencost https://opencost.github.io/opencost-helm-chart
helm repo update

helm install opencost opencost/opencost \
  --namespace opencost-system \
  --create-namespace \
  --set opencost.prometheus.internal.enabled=true \
  --set opencost.ui.enabled=true

OpenCost의 커스텀 가격 설정을 통해 실제 클라우드 요금을 반영할 수 있습니다.

# opencost-custom-pricing.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: opencost-custom-pricing
  namespace: opencost-system
data:
  default.json: |
    {
      "provider": "custom",
      "description": "Custom pricing for on-prem + cloud hybrid",
      "CPU": "0.031611",
      "spotCPU": "0.012644",
      "RAM": "0.004237",
      "spotRAM": "0.001694",
      "storage": "0.000138888",
      "GPU": "0.95"
    }
kubectl apply -f opencost-custom-pricing.yaml

Kubecost 설치 및 설정

Kubecost는 OpenCost를 기반으로 더 풍부한 기능(알림, 추천, 거버넌스)을 제공하는 상용/오픈소스 하이브리드 도구입니다.

# Kubecost 설치 (Free Tier)
helm repo add kubecost https://kubecost.github.io/cost-analyzer/
helm repo update

helm install kubecost kubecost/cost-analyzer \
  --namespace kubecost \
  --create-namespace \
  --set kubecostToken="YOUR_TOKEN" \
  --set prometheus.server.persistentVolume.enabled=true \
  --set prometheus.server.persistentVolume.size=32Gi

Kubecost API를 통해 프로그래밍 방식으로 비용 데이터를 조회할 수 있습니다.

# 네임스페이스별 비용 조회 (최근 7일)
curl -s "http://kubecost.example.com/model/allocation?window=7d&aggregate=namespace" | \
  python3 -m json.tool

# 출력 예시 (간소화)
# {
#   "data": [{
#     "production": {
#       "cpuCost": 245.67,
#       "ramCost": 123.45,
#       "pvCost": 34.56,
#       "totalCost": 403.68
#     },
#     "staging": {
#       "cpuCost": 89.12,
#       "ramCost": 45.23,
#       "pvCost": 12.34,
#       "totalCost": 146.69
#     }
#   }]
# }

비용 모니터링 도구 비교표

기능OpenCostKubecost FreeKubecost EnterpriseCloudHealthSpot.io (NetApp)
라이선스오픈소스 (Apache 2.0)무료 (15일 데이터)상용상용상용
CNCF 프로젝트OX (기반)XXX
실시간 비용 모니터링OOOOO
네임스페이스별 비용OOOOO
비용 절감 추천X기본고급고급고급
멀티클러스터 지원O (수동)XOOO
알림/알람X기본고급고급고급
Spot 인스턴스 관리XXXXO
데이터 보관 기간Prometheus 의존15일무제한무제한무제한
비용 할당 정확도높음높음매우 높음높음높음
설치 난이도낮음낮음중간높음중간
월 비용무료무료클러스터당협의절감액 %

권장 사항: 소규모 팀은 OpenCost로 시작하고, 비용 규모가 커지면 Kubecost Enterprise나 Spot.io로 전환하는 것이 현실적입니다. FinOps Foundation의 멤버 기업 중 68%가 이 경로를 따랐습니다.

리소스 최적화 전략

Request/Limit 튜닝: VPA를 활용한 자동 적정화

Vertical Pod Autoscaler(VPA)는 실제 리소스 사용 패턴을 분석하여 적절한 Request/Limit 값을 추천합니다.

# vpa-recommendation.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: app-vpa
  namespace: production
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-app
  updatePolicy:
    updateMode: 'Off' # 추천만 받고 자동 적용하지 않음 (안전)
  resourcePolicy:
    containerPolicies:
      - containerName: app
        minAllowed:
          cpu: '100m'
          memory: '128Mi'
        maxAllowed:
          cpu: '2'
          memory: '4Gi'
        controlledResources: ['cpu', 'memory']
# VPA 추천 값 확인
kubectl describe vpa app-vpa -n production

# 출력 예시
# Recommendation:
#   Container Recommendations:
#     Container Name: app
#     Lower Bound:
#       Cpu:     150m
#       Memory:  256Mi
#     Target:
#       Cpu:     250m
#       Memory:  512Mi
#     Uncapped Target:
#       Cpu:     250m
#       Memory:  512Mi
#     Upper Bound:
#       Cpu:     800m
#       Memory:  1Gi

주의사항: VPA의 updateMode: "Auto"는 Pod를 재시작합니다. Kubernetes 1.33부터 지원되는 In-Place Resource Resize(KEP-1287)를 활용하면 재시작 없이 리소스를 조정할 수 있습니다. 프로덕션에서는 반드시 "Off" 모드로 시작하여 추천값을 검토한 후 점진적으로 적용해야 합니다.

네임스페이스별 ResourceQuota

팀별 리소스 사용량을 제한하여 비용 폭주를 방지합니다.

# resourcequota-team-backend.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-backend-quota
  namespace: team-backend
spec:
  hard:
    requests.cpu: '20'
    requests.memory: '40Gi'
    limits.cpu: '40'
    limits.memory: '80Gi'
    persistentvolumeclaims: '10'
    services.loadbalancers: '2'
    pods: '50'
    # 비용 관점: LoadBalancer 서비스 수 제한 (AWS에서 각각 약 월 $18)
# ResourceQuota 사용 현황 확인
kubectl describe resourcequota team-backend-quota -n team-backend

# 출력 예시
# Name:                    team-backend-quota
# Namespace:               team-backend
# Resource                 Used   Hard
# --------                 ----   ----
# limits.cpu               12     40
# limits.memory            24Gi   80Gi
# persistentvolumeclaims   3      10
# pods                     15     50
# requests.cpu             6      20
# requests.memory          12Gi   40Gi
# services.loadbalancers   1      2

LimitRange 설정

개별 Pod/Container 단위에서 기본 리소스 값과 범위를 강제합니다.

# limitrange-default.yaml
apiVersion: v1
kind: LimitRange
metadata:
  name: default-limits
  namespace: team-backend
spec:
  limits:
    - type: Container
      default: # Limit 기본값 (Request 미설정 시 자동 적용)
        cpu: '500m'
        memory: '512Mi'
      defaultRequest: # Request 기본값
        cpu: '100m'
        memory: '128Mi'
      min: # 최소값 (이하 불가)
        cpu: '50m'
        memory: '64Mi'
      max: # 최대값 (이상 불가)
        cpu: '4'
        memory: '8Gi'
    - type: PersistentVolumeClaim
      min:
        storage: '1Gi'
      max:
        storage: '100Gi' # PVC 최대 크기 제한
kubectl apply -f limitrange-default.yaml

# Request/Limit 미설정 Pod 생성 시 자동으로 기본값 적용
kubectl run test-pod --image=nginx -n team-backend
kubectl describe pod test-pod -n team-backend | grep -A 5 "Limits\|Requests"

노드 풀 최적화: Spot/Preemptible 인스턴스 활용

Spot 인스턴스는 On-Demand 대비 60-90% 저렴하지만, 클라우드 제공자가 언제든 회수할 수 있습니다. 올바르게 활용하면 Kubernetes 비용을 획기적으로 줄일 수 있습니다.

# spot-tolerant-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: batch-processor
  namespace: production
spec:
  replicas: 5
  selector:
    matchLabels:
      app: batch-processor
  template:
    metadata:
      labels:
        app: batch-processor
    spec:
      # Spot 노드에 스케줄링
      nodeSelector:
        node.kubernetes.io/capacity-type: spot
      tolerations:
        - key: 'spot'
          operator: 'Equal'
          value: 'true'
          effect: 'NoSchedule'
      # Graceful shutdown - Spot 인스턴스 회수 시 정상 종료
      terminationGracePeriodSeconds: 120
      containers:
        - name: processor
          image: batch-processor:latest
          resources:
            requests:
              cpu: '500m'
              memory: '1Gi'
            limits:
              cpu: '1'
              memory: '2Gi'
          # Spot 인스턴스 회수 신호 처리
          lifecycle:
            preStop:
              exec:
                command: ['/bin/sh', '-c', 'kill -SIGTERM 1 && sleep 90']
      # Pod Disruption Budget 설정
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: batch-processor

Spot 인스턴스 적합한 워크로드와 부적합한 워크로드 비교

적합한 워크로드부적합한 워크로드
Batch 작업/데이터 처리단일 인스턴스 데이터베이스
CI/CD 파이프라인스테이트풀 서비스 (Kafka, Redis)
스테이트리스 웹 서버 (여러 복제본)장시간 실행 트랜잭션
개발/테스트 환경실시간 스트리밍 처리
ML 학습 (체크포인트 지원)리더 선출 기반 서비스

자동 스케일링을 통한 비용 절감

Karpenter: 차세대 노드 프로비저닝

Karpenter는 AWS에서 시작하여 현재 멀티클라우드로 확장 중인 노드 프로비저너입니다. Cluster Autoscaler보다 빠르고 유연한 노드 관리를 제공합니다.

# karpenter-nodepool.yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: cost-optimized
spec:
  template:
    spec:
      requirements:
        - key: kubernetes.io/arch
          operator: In
          values: ['amd64']
        - key: karpenter.sh/capacity-type
          operator: In
          values: ['spot', 'on-demand'] # Spot 우선, 실패 시 On-Demand
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ['c', 'm', 'r'] # 컴퓨팅/범용/메모리 최적화
        - key: karpenter.k8s.aws/instance-generation
          operator: Gt
          values: ['5'] # 6세대 이상만 (가성비 우수)
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
  limits:
    cpu: '100'
    memory: '400Gi'
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 30s
    # 미활용 노드 30초 후 자동 통합
  weight: 10 # 다른 NodePool보다 우선 사용
# karpenter-ec2nodeclass.yaml
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: default
spec:
  amiSelectorTerms:
    - alias: 'al2023@latest'
  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: 'my-cluster'
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: 'my-cluster'
  blockDeviceMappings:
    - deviceName: /dev/xvda
      ebs:
        volumeSize: 50Gi
        volumeType: gp3
        encrypted: true

Karpenter vs Cluster Autoscaler 비교

항목KarpenterCluster Autoscaler
스케일업 속도수 초 ~ 1분2-5분
인스턴스 타입 선택자동 최적 선택 (다양한 타입)노드 그룹별 고정
스케일다운적극적 통합 (consolidation)보수적 (10분+ 대기)
Spot 처리네이티브 지원, 자동 전환별도 노드 그룹 필요
멀티 AZ 분산자동노드 그룹별 설정
빈 패킹 효율높음 (Pod 크기 기반 선택)낮음 (고정 노드 크기)
노드 파편화 해소자동 (consolidation)수동
클라우드 지원AWS(GA), Azure(Preview)AWS, GCP, Azure 모두

실전 팁: Karpenter의 consolidationPolicy: WhenEmptyOrUnderutilized는 리소스 활용률이 낮은 노드의 Pod를 다른 노드로 자동 이동하고 해당 노드를 종료합니다. 이것만으로도 노드 비용을 20-30% 절감할 수 있습니다 (Karpenter 공식 문서의 best practices 참고).

야간/주말 스케일다운 자동화

비-프로덕션 환경의 워크로드를 업무 시간 외에 자동으로 축소하면 상당한 비용을 절감할 수 있습니다.

# cronjob-scaledown.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: nighttime-scaledown
  namespace: kube-system
spec:
  schedule: '0 22 * * 1-5' # 평일 22시 (KST 기준)
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: scaler-sa
          containers:
            - name: scaler
              image: bitnami/kubectl:latest
              command:
                - /bin/sh
                - -c
                - |
                  # staging 네임스페이스의 모든 Deployment를 0으로 축소
                  for deploy in $(kubectl get deploy -n staging -o name); do
                    kubectl scale $deploy --replicas=0 -n staging
                  done
                  echo "Scaled down staging at $(date)"
          restartPolicy: OnFailure
---
apiVersion: batch/v1
kind: CronJob
metadata:
  name: morning-scaleup
  namespace: kube-system
spec:
  schedule: '0 8 * * 1-5' # 평일 08시 (KST 기준)
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: scaler-sa
          containers:
            - name: scaler
              image: bitnami/kubectl:latest
              command:
                - /bin/sh
                - -c
                - |
                  # staging 네임스페이스의 Deployment를 원래 상태로 복구
                  kubectl scale deploy/api-server --replicas=3 -n staging
                  kubectl scale deploy/web-frontend --replicas=2 -n staging
                  kubectl scale deploy/worker --replicas=2 -n staging
                  echo "Scaled up staging at $(date)"
          restartPolicy: OnFailure

이미지 최적화와 스토리지 비용 절감

컨테이너 이미지 최적화

컨테이너 이미지 크기가 크면 레지스트리 저장 비용, 이미지 풀 시간, 네트워크 전송 비용이 모두 증가합니다.

# Bad: 거대한 이미지 (1.2GB+)
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]

# Good: 멀티스테이지 빌드로 최적화 (150MB 이하)
FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["dist/server.js"]

미사용 PersistentVolume 정리

방치된 PV/PVC는 클라우드 스토리지 비용을 지속적으로 발생시킵니다.

# Released 상태의 PV 확인 (바인딩 해제되었지만 삭제되지 않은 PV)
kubectl get pv --field-selector=status.phase=Released

# 사용 중이지 않은 PVC 찾기 (어떤 Pod에도 마운트되지 않은 PVC)
kubectl get pvc --all-namespaces -o json | \
  python3 -c "
import json, sys
data = json.load(sys.stdin)
for pvc in data['items']:
    ns = pvc['metadata']['namespace']
    name = pvc['metadata']['name']
    phase = pvc['status'].get('phase', 'Unknown')
    if phase == 'Bound':
        print(f'{ns}/{name} - Bound but check if any pod uses it')
"

# 특정 PVC를 사용하는 Pod가 있는지 확인
kubectl get pods --all-namespaces -o json | \
  python3 -c "
import json, sys
data = json.load(sys.stdin)
used_pvcs = set()
for pod in data['items']:
    volumes = pod['spec'].get('volumes', [])
    for vol in volumes:
        if 'persistentVolumeClaim' in vol:
            ns = pod['metadata']['namespace']
            pvc_name = vol['persistentVolumeClaim']['claimName']
            used_pvcs.add(f'{ns}/{pvc_name}')
for pvc in sorted(used_pvcs):
    print(f'IN USE: {pvc}')
"

idle 리소스 탐지 스크립트

#!/bin/bash
# idle-resource-detector.sh
# 유휴 리소스를 탐지하여 비용 절감 기회를 찾는 스크립트

echo "=== 유휴 리소스 탐지 리포트 ==="
echo "날짜: $(date)"
echo ""

# 1. 0 레플리카인데 삭제되지 않은 Deployment
echo "--- 레플리카 0인 Deployment ---"
kubectl get deploy --all-namespaces -o json | \
  python3 -c "
import json, sys
data = json.load(sys.stdin)
for d in data['items']:
    if d['spec'].get('replicas', 1) == 0:
        print(f\"  {d['metadata']['namespace']}/{d['metadata']['name']}\")
"

# 2. 7일 이상 Completed 상태인 Job
echo ""
echo "--- 완료 후 7일 이상 경과한 Job ---"
kubectl get jobs --all-namespaces --field-selector=status.successful=1 \
  -o custom-columns="NAMESPACE:.metadata.namespace,NAME:.metadata.name,COMPLETED:.status.completionTime"

# 3. 외부 트래픽 없는 LoadBalancer Service
echo ""
echo "--- LoadBalancer 타입 Service (비용 발생 중) ---"
kubectl get svc --all-namespaces --field-selector=spec.type=LoadBalancer \
  -o custom-columns="NAMESPACE:.metadata.namespace,NAME:.metadata.name,EXTERNAL-IP:.status.loadBalancer.ingress[0].hostname"

Prometheus를 활용한 비용 메트릭 모니터링

OpenCost와 Prometheus를 연동하면 Grafana 대시보드에서 실시간 비용 추이를 모니터링할 수 있습니다.

# prometheus-cost-alerts.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: cost-alerts
  namespace: monitoring
spec:
  groups:
    - name: cost-optimization
      rules:
        # CPU 요청 대비 실제 사용률이 20% 이하인 컨테이너 탐지
        - alert: LowCPUUtilization
          expr: |
            (
              sum(rate(container_cpu_usage_seconds_total[5m])) by (namespace, pod, container)
              /
              sum(kube_pod_container_resource_requests{resource="cpu"}) by (namespace, pod, container)
            ) < 0.2
          for: 24h
          labels:
            severity: warning
            category: cost
          annotations:
            summary: 'CPU 사용률이 Request의 20% 미만'
            description: '{{ `{{ $labels.namespace }}` }}/{{ `{{ $labels.pod }}` }}의 CPU 사용률이 24시간 동안 Request의 20% 미만입니다. Request 값 하향 조정을 권장합니다.'

        # 메모리 요청 대비 실제 사용률이 30% 이하인 컨테이너 탐지
        - alert: LowMemoryUtilization
          expr: |
            (
              sum(container_memory_working_set_bytes) by (namespace, pod, container)
              /
              sum(kube_pod_container_resource_requests{resource="memory"}) by (namespace, pod, container)
            ) < 0.3
          for: 24h
          labels:
            severity: warning
            category: cost
          annotations:
            summary: '메모리 사용률이 Request의 30% 미만'
            description: '{{ `{{ $labels.namespace }}` }}/{{ `{{ $labels.pod }}` }}의 메모리 사용률이 24시간 동안 Request의 30% 미만입니다.'

        # 네임스페이스별 일일 비용 임계값 초과
        - alert: NamespaceCostThresholdExceeded
          expr: |
            sum(
              sum_over_time(opencost_container_cost_cpu_hourly[24h]) +
              sum_over_time(opencost_container_cost_memory_hourly[24h])
            ) by (namespace) > 100
          labels:
            severity: critical
            category: cost
          annotations:
            summary: '네임스페이스 일일 비용 임계값 초과'
            description: '{{ `{{ $labels.namespace }}` }} 네임스페이스의 일일 비용이 100 USD를 초과했습니다.'

클라우드별 비용 최적화 전략

AWS EKS 비용 최적화

# AWS Savings Plans 추천 조회
aws ce get-savings-plans-purchase-recommendation \
  --savings-plans-type COMPUTE_SP \
  --term-in-years ONE_YEAR \
  --payment-option NO_UPFRONT \
  --lookback-period-in-days SIXTY_DAYS

# EKS 노드에 대한 Reserved Instance 추천
aws ce get-reservation-purchase-recommendation \
  --service "Amazon Elastic Compute Cloud - Compute" \
  --lookback-period-in-days SIXTY_DAYS

GCP GKE 비용 최적화

# GKE 비용 추천 확인
gcloud recommender recommendations list \
  --recommender=google.compute.instance.MachineTypeRecommender \
  --project=my-project \
  --location=asia-northeast3-a \
  --format="table(content.overview.resourceName, content.overview.recommendedMachineType.name, primaryImpact.costProjection.cost.units)"

# Committed Use Discounts 확인
gcloud compute commitments list --project=my-project

멀티클라우드 비용 비교

항목AWS EKSGCP GKEAzure AKS
컨트롤 플레인 비용월 $73 (클러스터당)무료 (Standard) / 월 $73 (Enterprise)무료 (Standard) / 월 $73 (Premium)
Spot 할인율60-90%60-91%60-90%
Spot 최소 보장2분 경고30초 경고30초 경고
Savings Plan/CUDCompute Savings PlansCommitted Use DiscountsAzure Reservations
최대 약정 할인72% (3년 전액 선결제)70% (3년 약정)72% (3년 예약)
오토파일럿/서버리스FargateGKE AutopilotAKS Virtual Nodes

실패 사례: 과도한 비용 절감으로 인한 서비스 장애

사례 1: Spot 인스턴스 100% 운영으로 인한 대규모 장애

한 스타트업에서 비용 절감을 극대화하기 위해 프로덕션 클러스터의 모든 노드를 Spot 인스턴스로 운영했습니다.

발생 상황:

  • AWS가 특정 리전에서 Spot 용량을 대규모로 회수
  • 전체 노드의 70%가 동시에 종료됨
  • Pod가 스케줄링될 노드가 없어 서비스 전면 장애 발생
  • On-Demand 노드 프로비저닝까지 8분 소요

교훈:

  • 프로덕션 핵심 서비스는 반드시 On-Demand 노드에 배치
  • Spot 비율은 전체의 70%를 넘기지 않을 것
  • PodDisruptionBudget으로 최소 가용 Pod 수를 보장할 것
# 필수: PodDisruptionBudget 설정
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-server-pdb
  namespace: production
spec:
  minAvailable: 2 # 최소 2개 Pod는 항상 유지
  selector:
    matchLabels:
      app: api-server

사례 2: 리소스 Request를 너무 낮게 설정하여 OOM 빈발

비용 최적화 과정에서 모든 Pod의 메모리 Request를 실사용량 기준으로 타이트하게 설정한 결과, 트래픽 피크 시간에 OOMKilled가 빈발했습니다.

교훈:

  • Request는 P99 사용량 기준이 아닌 P95 사용량 + 20% 버퍼로 설정
  • Limit은 Request의 1.5-2배로 설정하여 버스트 여유를 줌
  • VPA의 추천값을 무조건 따르지 말고, 트래픽 패턴을 고려하여 조정

사례 3: 테스트 환경 삭제로 인한 개발 생산성 저하

비용 절감을 위해 야간에 모든 개발/테스트 환경을 삭제하는 정책을 시행했으나, 해외 팀과의 시차로 인해 협업에 심각한 문제가 발생했습니다.

교훈:

  • 비용 절감과 개발자 경험(DX)의 균형이 필요
  • 완전 삭제가 아닌 스케일다운(replicas=0) 방식이 더 안전
  • 글로벌 팀의 경우 각 시간대를 고려한 스케줄 설정

FinOps 운영 체크리스트

주간 비용 리뷰 체크리스트

체크 항목담당도구
네임스페이스별 비용 추이 확인FinOps 팀Kubecost/OpenCost
CPU/메모리 활용률 20% 이하 Pod 리스트SREPrometheus/Grafana
Spot 인스턴스 비율 확인 (목표: 50-70%)Infra클라우드 콘솔
미사용 PV/PVC 정리개발팀kubectl 스크립트
LoadBalancer Service 수 확인Infrakubectl
이미지 레지스트리 오래된 태그 정리DevOps레지스트리 API

월간 비용 리뷰 체크리스트

체크 항목담당도구
전월 대비 비용 증감 분석FinOps 팀클라우드 Billing
Savings Plan/CUD 커버리지 확인FinOps 팀클라우드 콘솔
VPA 추천값 기반 Request/Limit 업데이트개발팀VPA Recommender
노드 인스턴스 타입 최적화 검토InfraKarpenter 로그
팀별 비용 할당 리포트 공유FinOps 팀Kubecost
비용 이상치(anomaly) 원인 분석SRE클라우드 Cost Explorer

분기별 비용 리뷰 체크리스트

체크 항목담당도구
Reserved Instance/CUD 갱신 검토FinOps 팀클라우드 콘솔
아키텍처 수준 비용 최적화 (마이크로서비스 통합 등)아키텍트설계 리뷰
비용 예측 모델 업데이트FinOps 팀스프레드시트/BI
FinOps 성숙도 자체 평가FinOps 팀FinOps Foundation 프레임워크

FinOps 팀 문화와 비용 인식

비용 태그 전략

모든 Kubernetes 리소스에 일관된 태그(label)를 부여하여 비용을 정확히 추적할 수 있어야 합니다.

# 비용 추적을 위한 표준 Label 정의
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
  namespace: production
  labels:
    app.kubernetes.io/name: api-server
    app.kubernetes.io/part-of: payment-platform
    # FinOps 비용 태그
    cost-center: 'engineering'
    team: 'backend'
    env: 'production'
    project: 'payment-v2'
spec:
  replicas: 3
  selector:
    matchLabels:
      app.kubernetes.io/name: api-server
  template:
    metadata:
      labels:
        app.kubernetes.io/name: api-server
        cost-center: 'engineering'
        team: 'backend'
        env: 'production'
        project: 'payment-v2'
    spec:
      containers:
        - name: api-server
          image: api-server:v2.1.0
          resources:
            requests:
              cpu: '500m'
              memory: '1Gi'
            limits:
              cpu: '1'
              memory: '2Gi'

OPA/Gatekeeper로 비용 태그 강제

# cost-label-constraint.yaml
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequiredcostlabels
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredCostLabels
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredcostlabels

        required_labels := {"cost-center", "team", "env", "project"}

        violation[{"msg": msg}] {
          provided := {label | input.review.object.metadata.labels[label]}
          missing := required_labels - provided
          count(missing) > 0
          msg := sprintf("필수 비용 태그가 누락되었습니다: %v", [missing])
        }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredCostLabels
metadata:
  name: require-cost-labels
spec:
  match:
    kinds:
      - apiGroups: ['apps']
        kinds: ['Deployment', 'StatefulSet', 'DaemonSet']
    namespaces: ['production', 'staging']

비용 인식 문화 구축

FinOps는 도구만으로는 성공할 수 없습니다. 팀 전체가 비용을 인식하고 최적화에 참여하는 문화가 필요합니다.

비용 인식 문화의 핵심 요소:

  1. 비용 가시성: 매주 팀별 비용 대시보드를 공유합니다
  2. 비용 책임: 각 팀이 자신의 네임스페이스 비용에 대해 책임집니다
  3. 인센티브: 비용 절감 사례를 공유하고 인정하는 문화를 만듭니다
  4. 교육: 개발자가 리소스 Request/Limit의 의미와 영향을 이해해야 합니다
  5. 자동화: 수동 작업을 줄이고, 정책 기반 자동 최적화를 추구합니다

FinOps 성숙도 모델

FinOps Foundation은 조직의 FinOps 성숙도를 세 단계로 정의합니다.

단계특징Kubernetes 지표
Crawl (시작)기본 비용 가시성 확보, 수동 최적화OpenCost 도입, 월간 비용 리뷰 시작
Walk (발전)팀별 비용 할당, 자동화 시작Kubecost 알림, VPA 추천 적용, Spot 50%+
Run (성숙)실시간 최적화, 비용 예측, 문화 정착Karpenter 자동 통합, 비용 예측 모델, FinOps 팀 운영

정리 및 권장 사항

즉시 실행 가능한 Quick Win

  1. OpenCost 설치 (1시간): 비용 가시성 즉시 확보
  2. VPA 추천 모드 배포 (30분): 리소스 적정화 데이터 수집 시작
  3. 미사용 리소스 정리 (2시간): Released PV, 빈 네임스페이스, 오래된 Job 삭제
  4. LimitRange 적용 (1시간): Request 미설정 Pod에 기본값 강제

중기 최적화 (1-3개월)

  1. Kubecost 또는 상용 도구 도입하여 팀별 비용 할당 시작
  2. Spot 인스턴스 도입 (비-프로덕션부터 시작, 점진적으로 프로덕션 확대)
  3. Karpenter 도입하여 노드 자동 통합 활성화
  4. 야간/주말 스케일다운 CronJob 배포

장기 전략 (3-12개월)

  1. FinOps 전담 팀 또는 역할 구성
  2. Savings Plan/CUD 최적화
  3. 비용 예측 모델 구축 (과거 데이터 기반)
  4. 비용 인식 문화 정착 (교육, 대시보드, 인센티브)

Kubernetes 비용 최적화는 한 번의 프로젝트가 아니라 지속적인 프로세스입니다. FinOps의 Inform-Optimize-Operate 사이클을 반복하면서 점진적으로 성숙도를 높여가는 것이 핵심입니다. 가장 중요한 첫 단계는 **"지금 얼마를 쓰고 있는지 아는 것"**입니다. OpenCost를 설치하고, 이 글의 체크리스트를 따라 첫 번째 비용 리뷰를 시작해 보시기 바랍니다.

참고 자료