К8s Cost
Optimization
Guide

для on-premise кластера с частичным cloud, техники, команды, роли

Self-managed K8s On-prem + Cloud VM FinOps практики HPA / VPA / Autoscaling Resource Rightsizing Orphan Cleanup
30–40%
средняя утилизация без оптимизации
80%
экономии возможно с autoscaling
68%
компаний видят рост K8s-расходов
2–5×
cascading orphan waste vs direct

// 01

Контекст: on-prem + cloud ВМ

Самоуправляемый Kubernetes — это принципиально другая экономика по сравнению с managed-сервисами (EKS/GKE/AKS). Деньги здесь другие: не счёт cloud-провайдера, а утилизация железа, лицензии, человеко-часы и облачные ВМ поверх этого.

On-prem ВМ (bare metal / hypervisor)

Капитальные затраты уже оплачены. Основной cost — утилизация. Если узлы простаивают, деньги просто не работают. Цель: плотность и утилизация 60–80%.

Cloud ВМ (overspend-зона)

Счёт идёт по часам. Каждый незакрытый idle-узел — реальные деньги. Приоритет: rightsizing, spot/preemptible, schedule-based scale-down.

Главная ошибка — overprovisioning

Разработчики ставят requests «с запасом», лимиты неадекватно высокие. Кластер тратит 60% ресурсов на «просто в случае чего».

Гибридный подход

On-prem — для стабильных baseline-нагрузок. Cloud ВМ — для пиков и dev/staging. Cluster federation или node pool разделение.

On-prem экономия — это не снижение счёта, а увеличение полезной нагрузки на то же железо. Метрика успеха: сколько workloads умещается в кластер без деградации производительности.

// 02

Шаг 0: видимость и baseline

Нельзя оптимизировать то, что не измерено. Первый шаг всегда — понять, где вы сейчас. Без baseline данных любые действия — угадывание.

Команды для немедленного диагноза

bash# baseline_check.sh
# 1. Утилизация ресурсов по узлам
kubectl top nodes

# 2. Запросы vs лимиты по namespace
kubectl describe nodes | grep -A6 "Allocated resources"

# 3. Поды без requests/limits (КРИТИЧНО)
kubectl get pods -A -o json | jq '.items[] |
  select(.spec.containers[].resources.requests == null) |
  "\(.metadata.namespace)/\(.metadata.name)"'

# 4. Idle pods (CPU < 10m за 30 мин)
kubectl top pods -A --sort-by=cpu | head -50

# 5. Orphaned PVC (не привязанные к активным подам)
kubectl get pvc -A | grep -v Bound

# 6. Namespace resource summary
kubectl get resourcequota -A
bash# utilization_report.sh — быстрый отчёт
# Установить kube-capacity (лучший инструмент для baseline)
kubectl krew install resource-capacity

# По узлам — requests vs actual utilization
kubectl resource-capacity --util --pods

# По namespace с CPU/Memory details
kubectl resource-capacity --util --namespace-labels

Что искать в baseline

request_utilization < 30%

Pods запрашивают в 3× больше, чем реально используют. Главная причина «жирных» узлов.

Pods без requests/limits

Scheduler не может нормально разместить. Узлы перегружаются без предупреждения. OOM kills.

Namespaces без ResourceQuota

Любая команда может захватить всё железо. Один раннавей-под гладит весь кластер.

Узлы с утилизацией < 20%

Пустые узлы (особенно cloud ВМ) — прямые расходы. В on-prem — замороженный ресурс.

// 03

Rightsizing: requests & limits

Overprovisioning — причина №1 неэффективности K8s-кластеров. Типичный кластер без оптимизации использует 30–40% запрошенных ресурсов. Rightsizing — это выравнивание requests по реальному потреблению.

Ключевое правило: request = что scheduler резервирует на узле. Limit = максимум, который pod может использовать. Если requests завышены — scheduler создаёт «дыры» на узлах, которые никогда не используются.

Как выставлять requests правильно

Алгоритм rightsizing вручную (без VPA)
EngineerOn-prem

Собрать Prometheus-данные за 7–14 дней, взять P95 как базу для requests, P99 или max — для limits.

  • CPU request = P95 использования за типичный день (не peak)
  • Memory request = P95; Memory limit = P99 + 20% буфер (OOM kill дороже чем чуть больше памяти)
  • CPU limit = осторожно: throttling хуже, чем чуть более высокий лимит. Для latency-sensitive: limit = 2–3× request
  • Для batch/worker процессов: CPU limit можно ставить tight — throttling некритичен
yamldeployment-rightsized.yaml
resources:
  requests:
    cpu: "100m"      # P95 реального использования
    memory: "128Mi"   # P95 реального использования
  limits:
    cpu: "500m"      # Позволяем burst, CPU throttle некритичен
    memory: "256Mi"   # P99 + буфер; OOM kill нежелателен

QoS классы и их стоимость

QoS классУсловиеПоведение при нехваткеРекомендация
Guaranteed requests == limits для CPU и memory Убивается последним. Предсказуемо. prod / critical
Burstable requests < limits, заданы оба Убивается при нехватке памяти, если превысил request default для большинства
BestEffort Нет ни requests ни limits Первым убивается при нехватке. Scheduler игнорирует при размещении. НИКОГДА в prod

VPA в рекомендательном режиме (безопасный старт)

yamlvpa-recommendation-only.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: my-app-vpa
spec:
  targetRef:
    apiVersion: "apps/v1"
    kind:       Deployment
    name:       my-app
  updatePolicy:
    updateMode: "Off"   # Только рекомендации, без изменений
  resourcePolicy:
    containerPolicies:
    - containerName: "*"
      minAllowed:
        cpu:    50m
        memory: 64Mi
      maxAllowed:
        cpu:    "4"
        memory: 4Gi
bash# Просмотр рекомендаций VPA
kubectl describe vpa my-app-vpa -n production
# Смотрим: status.recommendation.containerRecommendations
# lowerBound = минимально безопасные значения
# target = рекомендуемые
# upperBound = максимальный потолок

Не запускайте VPA в updateMode: "Auto" на одном deployment вместе с HPA по CPU/Memory — они будут конфликтовать. Безопасный паттерн: HPA по CPU → VPA только для Memory. Или VPA в Off mode как советник.

// 04

Autoscaling: HPA, VPA, KEDA, Cluster Autoscaler

Autoscaling — ключевой механизм cost optimization: незанятые ресурсы не потребляются. На on-prem это плотность пода на узел, в cloud — количество активных ВМ.

Когда что использовать

ИнструментДля чегоОграниченияПриоритет
HPA Горизонтальный scale по CPU/memory/custom metrics. Stateless workloads, API-сервисы. Реактивен. Lag 30–60 сек. Плохо для spike-трафика. Всегда
VPA Вертикальный rightsizing. Memory-bound: Java, ML, Spark. Stateful, batch. В Auto-режиме перезапускает поды. Конфликт с HPA по одной метрике. Off/Recommend
KEDA Scale on event-driven metrics: очереди RabbitMQ/Kafka, cron, latency. Требует настройки triggers. Зависимость от внешних систем. Async / batch
Cluster Autoscaler Добавляет/удаляет узлы при нехватке/избытке. Критично для cloud ВМ. Медленно (1–3 мин на узел). На on-prem требует интеграции с hypervisor API. Cloud ВМ

HPA — правильная конфигурация

yamlhpa-production.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-service
  minReplicas: 2
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 65   # Не 80% — оставляем буфер
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300  # 5 мин стабилизация вниз
      policies:
      - type: Pods
        value: 1
        periodSeconds: 60    # Удалять не более 1 пода в минуту
    scaleUp:
      stabilizationWindowSeconds: 0   # Scale up немедленно

KEDA для scale-to-zero (dev/staging экономия)

yamlkeda-scaledobject-cron.yaml — scale to zero ночью
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
spec:
  minReplicaCount: 0   # ← scale to zero!
  maxReplicaCount: 5
  triggers:
  - type: cron
    metadata:
      timezone: "Europe/Moscow"
      start:   "0 9 * * 1-5"    # Старт в 9:00 пн-пт
      end:     "0 20 * * 1-5"   # Стоп в 20:00
      desiredReplicas: "3"    # Рабочее количество

Scale-to-zero для dev/staging окружений через KEDA cron trigger — один из самых быстрых wins. Dev-среды простаивают 60–70% времени. Для cloud ВМ это прямая экономия.

// 05

Namespace quotas & governance

Без ResourceQuota одна команда или один раннавей-деплой может забрать все ресурсы кластера. Quota — это граница, LimitRange — это умолчание. Нужны оба.

ResourceQuota + LimitRange — стандартный шаблон
EngineerManager

ResourceQuota ограничивает суммарное потребление namespace. LimitRange задаёт дефолты для подов без явных requests/limits — без этого quota не работает корректно.

yamlnamespace-quota-template.yaml
---
apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-quota
  namespace: team-backend
spec:
  hard:
    # Compute
    requests.cpu:     "20"
    requests.memory:  40Gi
    limits.cpu:       "40"
    limits.memory:    80Gi
    # Object count (namespace sprawl protection)
    pods: "100"
    persistentvolumeclaims: "20"
    requests.storage: 200Gi
---
apiVersion: v1
kind: LimitRange
metadata:
  name: default-limits
  namespace: team-backend
spec:
  limits:
  - type: Container
    default:         # Limits если не указаны
      cpu: 500m
      memory: 512Mi
    defaultRequest:  # Requests если не указаны
      cpu: 100m
      memory: 128Mi
    max:             # Потолок на контейнер
      cpu: "4"
      memory: 8Gi
    min:
      cpu: 50m
      memory: 64Mi
    maxLimitRequestRatio:  # Limit не может превышать request в N раз
      cpu: 10
      memory: 4

Окружения — разные квоты

production

Квоты с хорошим запасом. LimitRange жёсткий. QoS: Guaranteed для критичных, Burstable для остальных.

staging

50% от production квоты. Scale-to-zero вечером. Spot-нода если в cloud.

dev / feature-branch

Минимальные квоты. TTL на namespace (kube-janitor). Scale-to-zero aggressively. Нет persistent storage по умолчанию.

Policy-as-code: автоматическое управление

yamlkyverno-require-resources.yaml — блокировать поды без requests
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-resource-requests
spec:
  rules:
  - name: check-resource-requests
    match:
      resources:
        kinds: [Pod]
    validate:
      message: "CPU и memory requests обязательны"
      pattern:
        spec:
          containers:
          - resources:
              requests:
                memory: "?*"
                cpu:    "?*"

// 06

Orphaned resources & cleanup

Orphaned (осиротевшие) ресурсы — тихие пожиратели. PVC продолжает занимать storage, Services держат load balancer open, ConfigMaps и Secrets раздувают etcd. Cascading orphans (цепочки брошенных ресурсов) дают 2–5× больше waste, чем прямые сироты.

Типичные источники

Orphaned PVC

После удаления pod/deployment PVC остаётся. В cloud — продолжает тарифицироваться. На on-prem — блокирует дисковое место.

Feature-branch namespaces

CI/CD создаёт namespace для ветки, не удаляет после merge. Весь Helm-release остаётся жить.

Zombie ConfigMaps/Secrets

Deployments обновились, старые ConfigMaps остались. Раздувают etcd, замедляют API-server.

Completed/Failed Jobs

По умолчанию K8s не удаляет завершённые Jobs. Накапливаются сотнями, засоряют API.

Диагностика и очистка

bashorphan_cleanup.sh
# 1. Найти orphaned PVC (не привязаны к запущенным подам)
kubectl get pvc -A -o json | jq '.items[] |
  select(.status.phase != "Bound") |
  "\(.metadata.namespace)/\(.metadata.name): \(.spec.resources.requests.storage)"'

# 2. Найти Completed/Failed Jobs старше 24 часов
kubectl get jobs -A -o json | jq '.items[] |
  select(.status.completionTime != null) |
  "\(.metadata.namespace)/\(.metadata.name)"'

# 3. Инструмент kor — находит всё сразу
# github.com/yonahd/kor
kor all --namespace production

# 4. TTL-based cleanup для Jobs автоматически
# В spec Jobs добавить:
# ttlSecondsAfterFinished: 3600  (удалить через 1 час после завершения)

# 5. kube-janitor для namespace TTL
# Аннотация на namespace:
# janitor/ttl: 7d  — удалить namespace через 7 дней
yamljob-with-ttl.yaml — правильный шаблон Job
apiVersion: batch/v1
kind: Job
spec:
  ttlSecondsAfterFinished: 3600  # Автоудаление
  backoffLimit: 3
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: worker
        resources:           # Обязательно для batch!
          requests:
            cpu: 200m
            memory: 256Mi
          limits:
            cpu: "2"
            memory: 1Gi

Предотвращение: ownerReferences и GitOps

Стратегии предотвращения накопления orphans
EngineerAutomation
  • Использовать Helm с корректным helm uninstall — он удаляет все ресурсы чарта
  • ArgoCD / Flux с prune: true — GitOps автоматически удаляет ресурсы, которых нет в git
  • ownerReferences в CustomResources — дочерние ресурсы удаляются при удалении родителя
  • Обязательный label owner: team-name на всех ресурсах — Kyverno политика на admission
  • CronJob для еженедельного запуска kor с отчётом в Slack

// 07

Оптимизация cloud ВМ

Cloud-узлы — самый быстрый источник savings. Каждый idle узел = реальные деньги по счёту. Три рычага: правильный размер, правильная цена (spot/reserved), правильное время работы.

Spot / Preemptible ВМ

Экономия 60–80% от On-Demand цены. Для stateless, batch, dev/staging. Требует PodDisruptionBudget и tolerations.

−70%

Reserved Instances / Committed Use

1–3 года резерв для baseline-нагрузки. Экономия 30–40% vs On-Demand. Только для стабильного baseline.

−35%

Rightsized VM types

Memory-optimized для Java/ML, Compute-optimized для CPU-bound. Неправильный тип = 30%+ переплаты.

−30%

Schedule scale-down

Dev/staging узлы выключены с 20:00 до 8:00 и в выходные. 65 часов из 168 в неделю — экономия 38%.

−38%

Tolerations для spot-нод

yamlspot-tolerations.yaml
spec:
  tolerations:
  - key: "kubernetes.io/spot"
    operator: "Equal"
    value: "true"
    effect: "NoSchedule"
  affinity:
    nodeAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        preference:
          matchExpressions:
          - key: "node.kubernetes.io/lifecycle"
            operator: "In"
            values: ["spot"]
  topologySpreadConstraints:  # Разнести по AZ для надёжности
  - maxSkew: 1
    topologyKey: "topology.kubernetes.io/zone"
    whenUnsatisfiable: "DoNotSchedule"

Spot ВМ могут быть прерваны в любой момент. Обязателен: PodDisruptionBudget, graceful shutdown (terminationGracePeriodSeconds), и distributed architecture — не запускать stateful workload только на spot без on-demand fallback.

// 08

Процесс: роли и ритм

Техника без процесса не работает. Экономия дрейфует обратно через 2–3 месяца без постоянного цикла мониторинга и корректировки.

Роли

Руководитель / Engineering Manager

Устанавливает цели экономии (target % utilization). Защищает время на оптимизацию в спринтах. Reviewing cost dashboard раз в месяц. Принимает решения о квотах для команд.

Platform / DevOps Engineer

Настройка VPA, HPA, quotas, autoscaler. Еженедельный аудит orphaned resources. Инструменты мониторинга (Prometheus, Kubecost/OpenCost). Внедрение policy-as-code.

Backend / App Developer

Выставление корректных requests/limits в манифестах. Graceful shutdown реализация. Не создавать BestEffort поды. Очищать dev-ресурсы после работы.

Ритм процесса

ЧастотаDAILY
Автоматический мониторинг

Алерты на: узлы с утилизацией <20%, pods с OOM, PVC без owner, jobs в failed state. Никакого ручного труда — всё через Alertmanager.

ЧастотаWEEKLY
Engineer: orphan audit + VPA review

Запуск kor для поиска orphaned ресурсов. Просмотр VPA recommendations — обновить requests если рекомендация стабильна 7+ дней. Slack digest: top-5 самых «тяжёлых» workloads.

ЧастотаMONTHLY
Manager + Engineer: cost review

Dashboard review: utilization по namespace, top spenders, waste ratio. Решения по квотам (повысить или понизить). Проверка cloud ВМ: есть ли idle узлы, нужно ли rightsizing VM типов.

ЧастотаQUARTERLY
Manager: архитектурный review

Пересмотр на/off-prem баланса. Reserved Instances — сколько зарезервировать. Нужен ли Cluster Autoscaler upgrade. Есть ли смысл в node pool реструктуризации.

Действия руководителя — конкретно

Что делает менеджер (не инженер)
Manager
  • Установить measurable цели: «Утилизация CPU requests ≥ 60% за квартал», «Нет idle cloud-нод дольше 24 часов»
  • Выделить capacity: cost optimization — не между делом. Минимум 20% времени platform engineer должно быть защищено от feature-работы
  • Чарджбэк / шоубэк: показывать командам их cost profile. Когда команда видит, сколько стоит их namespace — поведение меняется
  • Не допускать «и так сойдёт» requests: Kyverno политика + code review checklist для манифестов
  • Принимать решения по квотам: если команда просит поднять квоту — сначала данные: utilization текущей квоты
  • Контролировать cloud ВМ бюджет: знать ежемесячный счёт. Алерт при превышении threshold

Действия инженера — конкретно

Что делает engineer / devops
EngineerAutomation
  • Установить Prometheus + kube-state-metrics + metrics-server. Это baseline — без этого ничего не работает
  • Развернуть OpenCost или Kubecost (self-hosted) для cost allocation по namespace
  • Включить VPA в Off mode на production namespace — 2 недели собирать данные, потом применить рекомендации
  • Настроить HPA на все stateless deployments с targetCPU: 65%, поведение scale-down stabilization 300s
  • Еженедельный cron-job с kor → репорт в Slack, автоудаление очевидных orphans с dry-run review
  • LimitRange + ResourceQuota на каждый namespace (скрипт автоматического создания при создании namespace)
  • kube-janitor для dev/feature namespaces с TTL 7–14 дней
  • Для cloud ВМ: включить Cluster Autoscaler с --scale-down-delay-after-add 10m, --scale-down-unneeded-time 5m

// 09

Метрики и алерты

МетрикаЦель / нормаТревожный сигналХорошо
CPU request utilization ≥ 60% < 30% — сильное overprovisioning 60–75% — здоровый диапазон
Memory request utilization ≥ 55% < 25% — requests завышены 55–70%
Node utilization (cloud) ≥ 50% < 20% — idle узел кандидат на удаление 50–75%
Pods без requests/limits 0 Любое значение > 0 в prod 0 в production
OOMKill rate ≈ 0 Рост — memory limits занижены 0 в течение недели
Orphaned PVC count 0 > 5 не обработанных 7 дней 0 или с owner label
Unallocated spend < 5% > 15% — ресурсы без владельца < 5%
Waste ratio (reserved–used) < 20% > 40% — пересмотр requests < 20%

Ключевые Prometheus-запросы

promqlkey_metrics.promql
# CPU utilization vs requests по namespace
sum(rate(container_cpu_usage_seconds_total[5m])) by (namespace)
  /
sum(kube_pod_container_resource_requests{resource="cpu"}) by (namespace)

# Pods без resource requests (алерт = любое значение > 0)
count(kube_pod_container_info) unless
  count(kube_pod_container_resource_requests{resource="cpu"})

# Idle nodes (utilization < 20%)
sum(kube_node_status_allocatable{resource="cpu"}) by (node)
  - sum(kube_pod_container_resource_requests{resource="cpu"}) by (node)

# OOMKill за последние 24h
increase(kube_pod_container_status_last_terminated_reason{
  reason="OOMKilled"}[24h])

// 10

Инструменты

ИнструментКатегорияДля чегоOn-prem / Cloud
OpenCost Cost Visibility Open-source cost allocation по namespace, workload, label. Self-hosted, нет cloud зависимости Оба
Kubecost Cost Visibility Более полный UI, showback/chargeback, рекомендации. Free tier до 1 кластера Оба
VPA (autoscaling.k8s.io) Rightsizing Рекомендации и автоматическое изменение requests/limits Оба
KEDA Autoscaling Event-driven autoscaling, scale-to-zero, интеграция с очередями Оба
Kyverno Governance Policy-as-code: enforce resources, labels, naming. Admission controller Оба
kor Orphan Cleanup Сканирование orphaned PVC, ConfigMaps, Services, Deployments Оба
kube-janitor TTL Cleanup TTL аннотации на namespace и resources. Автоудаление по расписанию Оба
kube-capacity Observability kubectl plugin для быстрого overview requests vs utilization по нодам Оба
Cluster Autoscaler Node Scaling Автоматическое добавление/удаление cloud ВМ-нод. Требует cloud API Cloud only

// 11

Чеклисты

Старт: первые 2 недели (Engineer)

Старт: первые 2 недели (Manager)

Регулярный (Engineer, еженедельно)

Регулярный (Manager, ежемесячно)

Манифест чеклист (Developer, на каждый PR)

Самые быстрые wins: (1) rightsizing requests на основе VPA рекомендаций — 2 недели данных и одно применение; (2) scale-to-zero для dev/staging через KEDA cron; (3) удаление orphaned PVC. Эти три действия обычно дают 30–40% improvement без архитектурных изменений.