- Published on
Kubernetes FinOps와 클라우드 비용 최적화 전략: 리소스 낭비를 잡는 실전 가이드
- Authors
- Name
- 들어가며: 왜 Kubernetes 비용 관리가 중요한가
- FinOps의 핵심 원칙과 Kubernetes 적용
- Kubernetes 리소스 낭비의 주요 원인
- 비용 가시성 확보: Kubecost와 OpenCost
- 리소스 최적화 전략
- 자동 스케일링을 통한 비용 절감
- 이미지 최적화와 스토리지 비용 절감
- Prometheus를 활용한 비용 메트릭 모니터링
- 클라우드별 비용 최적화 전략
- 실패 사례: 과도한 비용 절감으로 인한 서비스 장애
- FinOps 운영 체크리스트
- FinOps 팀 문화와 비용 인식
- FinOps 성숙도 모델
- 정리 및 권장 사항
- 참고 자료

들어가며: 왜 Kubernetes 비용 관리가 중요한가
Kubernetes 도입이 가속화되면서 클라우드 비용 문제가 심각해지고 있습니다. CNCF의 2025 FinOps 리포트에 따르면, 기업들의 Kubernetes 관련 클라우드 지출 중 평균 30-35%가 낭비되고 있습니다. 이는 단순히 리소스를 과다하게 할당하는 문제를 넘어, 비용에 대한 가시성 부족, 팀 간 책임 소재 불분명, 최적화 프로세스의 부재라는 구조적 문제에서 비롯됩니다.
FinOps Foundation에서 정의하는 FinOps는 **"엔지니어링, 재무, 비즈니스 팀이 데이터 기반으로 협업하여 클라우드의 비즈니스 가치를 극대화하는 운영 프레임워크"**입니다. 전통적인 IT 인프라에서는 CapEx(자본 지출) 모델로 한 번 구매하면 끝이었지만, 클라우드는 OpEx(운영 지출) 모델로 매 시간, 매 분 비용이 발생합니다. 이런 환경에서 FinOps는 선택이 아닌 필수입니다.
이 글에서는 Kubernetes 환경에 특화된 FinOps 전략을 다룹니다. 비용 가시성 확보부터 리소스 최적화, 자동 스케일링, 그리고 팀 문화까지 실전에서 바로 적용할 수 있는 가이드를 제공합니다.
FinOps의 핵심 원칙과 Kubernetes 적용
FinOps의 세 가지 원칙
FinOps Foundation이 제시하는 핵심 원칙은 다음과 같습니다.
- 팀은 자신의 클라우드 사용량에 대해 책임을 진다 - 엔지니어링 팀이 비용을 인식하고 최적화 결정을 내려야 합니다
- 의사결정은 클라우드의 비즈니스 가치에 기반한다 - 단순 비용 절감이 아닌 비즈니스 가치 대비 비용 효율을 추구합니다
- 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
# }
# }]
# }
비용 모니터링 도구 비교표
| 기능 | OpenCost | Kubecost Free | Kubecost Enterprise | CloudHealth | Spot.io (NetApp) |
|---|---|---|---|---|---|
| 라이선스 | 오픈소스 (Apache 2.0) | 무료 (15일 데이터) | 상용 | 상용 | 상용 |
| CNCF 프로젝트 | O | X (기반) | X | X | X |
| 실시간 비용 모니터링 | O | O | O | O | O |
| 네임스페이스별 비용 | O | O | O | O | O |
| 비용 절감 추천 | X | 기본 | 고급 | 고급 | 고급 |
| 멀티클러스터 지원 | O (수동) | X | O | O | O |
| 알림/알람 | X | 기본 | 고급 | 고급 | 고급 |
| Spot 인스턴스 관리 | X | X | X | X | O |
| 데이터 보관 기간 | 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 비교
| 항목 | Karpenter | Cluster 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 /app/dist ./dist
COPY /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 EKS | GCP GKE | Azure AKS |
|---|---|---|---|
| 컨트롤 플레인 비용 | 월 $73 (클러스터당) | 무료 (Standard) / 월 $73 (Enterprise) | 무료 (Standard) / 월 $73 (Premium) |
| Spot 할인율 | 60-90% | 60-91% | 60-90% |
| Spot 최소 보장 | 2분 경고 | 30초 경고 | 30초 경고 |
| Savings Plan/CUD | Compute Savings Plans | Committed Use Discounts | Azure Reservations |
| 최대 약정 할인 | 72% (3년 전액 선결제) | 70% (3년 약정) | 72% (3년 예약) |
| 오토파일럿/서버리스 | Fargate | GKE Autopilot | AKS 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 리스트 | SRE | Prometheus/Grafana |
| Spot 인스턴스 비율 확인 (목표: 50-70%) | Infra | 클라우드 콘솔 |
| 미사용 PV/PVC 정리 | 개발팀 | kubectl 스크립트 |
| LoadBalancer Service 수 확인 | Infra | kubectl |
| 이미지 레지스트리 오래된 태그 정리 | DevOps | 레지스트리 API |
월간 비용 리뷰 체크리스트
| 체크 항목 | 담당 | 도구 |
|---|---|---|
| 전월 대비 비용 증감 분석 | FinOps 팀 | 클라우드 Billing |
| Savings Plan/CUD 커버리지 확인 | FinOps 팀 | 클라우드 콘솔 |
| VPA 추천값 기반 Request/Limit 업데이트 | 개발팀 | VPA Recommender |
| 노드 인스턴스 타입 최적화 검토 | Infra | Karpenter 로그 |
| 팀별 비용 할당 리포트 공유 | 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는 도구만으로는 성공할 수 없습니다. 팀 전체가 비용을 인식하고 최적화에 참여하는 문화가 필요합니다.
비용 인식 문화의 핵심 요소:
- 비용 가시성: 매주 팀별 비용 대시보드를 공유합니다
- 비용 책임: 각 팀이 자신의 네임스페이스 비용에 대해 책임집니다
- 인센티브: 비용 절감 사례를 공유하고 인정하는 문화를 만듭니다
- 교육: 개발자가 리소스 Request/Limit의 의미와 영향을 이해해야 합니다
- 자동화: 수동 작업을 줄이고, 정책 기반 자동 최적화를 추구합니다
FinOps 성숙도 모델
FinOps Foundation은 조직의 FinOps 성숙도를 세 단계로 정의합니다.
| 단계 | 특징 | Kubernetes 지표 |
|---|---|---|
| Crawl (시작) | 기본 비용 가시성 확보, 수동 최적화 | OpenCost 도입, 월간 비용 리뷰 시작 |
| Walk (발전) | 팀별 비용 할당, 자동화 시작 | Kubecost 알림, VPA 추천 적용, Spot 50%+ |
| Run (성숙) | 실시간 최적화, 비용 예측, 문화 정착 | Karpenter 자동 통합, 비용 예측 모델, FinOps 팀 운영 |
정리 및 권장 사항
즉시 실행 가능한 Quick Win
- OpenCost 설치 (1시간): 비용 가시성 즉시 확보
- VPA 추천 모드 배포 (30분): 리소스 적정화 데이터 수집 시작
- 미사용 리소스 정리 (2시간): Released PV, 빈 네임스페이스, 오래된 Job 삭제
- LimitRange 적용 (1시간): Request 미설정 Pod에 기본값 강제
중기 최적화 (1-3개월)
- Kubecost 또는 상용 도구 도입하여 팀별 비용 할당 시작
- Spot 인스턴스 도입 (비-프로덕션부터 시작, 점진적으로 프로덕션 확대)
- Karpenter 도입하여 노드 자동 통합 활성화
- 야간/주말 스케일다운 CronJob 배포
장기 전략 (3-12개월)
- FinOps 전담 팀 또는 역할 구성
- Savings Plan/CUD 최적화
- 비용 예측 모델 구축 (과거 데이터 기반)
- 비용 인식 문화 정착 (교육, 대시보드, 인센티브)
Kubernetes 비용 최적화는 한 번의 프로젝트가 아니라 지속적인 프로세스입니다. FinOps의 Inform-Optimize-Operate 사이클을 반복하면서 점진적으로 성숙도를 높여가는 것이 핵심입니다. 가장 중요한 첫 단계는 **"지금 얼마를 쓰고 있는지 아는 것"**입니다. OpenCost를 설치하고, 이 글의 체크리스트를 따라 첫 번째 비용 리뷰를 시작해 보시기 바랍니다.
참고 자료
- FinOps Foundation - Kubernetes Best Practices - FinOps의 Kubernetes 적용 가이드라인
- Kubecost Documentation - Kubecost 설치, 설정, API 활용법
- OpenCost - CNCF Project - OpenCost 아키텍처와 설치 가이드
- AWS EKS Best Practices - Cost Optimization - AWS EKS 비용 최적화 공식 가이드
- Karpenter Documentation - Best Practices - Karpenter 노드 프로비저닝과 통합 전략
- GCP GKE Cost Optimization - GKE 비용 최적화 공식 문서
- Azure AKS Cost Optimization - AKS 비용 관리 모범 사례