Картина знакомая до боли. Команда разработки катит сервисы в кластер, всё работает, и в конце месяца прилетает счёт от облака на сумму, от которой у финдиректора дёргается глаз. При этом графики загрузки CPU стабильно показывают 12 процентов. То есть ты платишь за 8 ядер, а реально работаешь одним. Куда уходят деньги - вопрос, на который без инструментов учёта никто в команде ответить не может.
Это и есть тема урока: kubernetes стоимость и дисциплина под названием finops kubernetes. FinOps - это не "урезать всё до боли", а вернуть видимость: кто, за что и сколько платит, и где деньги горят впустую без всякой пользы для прода. Kubernetes по своей природе склонен прятать расходы. Ты просишь у планировщика ресурсы через requests, он находит узел, где они влезают, и резервирует их под тебя - неважно, используешь ты их или нет. Запросил 2 ядра, крутишь на 100 милликорах - полтора с лишним ядра выведены из оборота и оплачены. Умножь на сотню подов, и вот тебе три лишних узла, которые существуют только чтобы держать воздух.
Разберём по полочкам, откуда натекает счёт.
- Простаивающие узлы. Кластер масштабировали под пиковую нагрузку Чёрной пятницы, пятница прошла, узлы остались. Никто не выключил.
- Overcommit резерва без лимитов. requests завышены "с потолка", планировщик считает узел занятым, хотя на нём гуляет ветер. Низкий packing density - главный пожиратель бюджета.
- Лишние реплики. Поставили replicas: 6 "на всякий случай", хватило бы трёх. Каждая реплика тащит за собой свою долю requests.
- Дорогая обвязка. Отдельный облачный LoadBalancer на каждый сервис вместо общего Ingress/Gateway. Premium SSD-диски там, где хватило бы обычных. Снапшоты, которые никто не чистит. Egress-трафик между зонами.

Requests и лимиты: фундамент, на котором стоит весь биллинг
Чтобы понять FinOps в Kubernetes, надо твёрдо держать в голове разницу между requests и limits, потому что именно requests определяют, сколько ты платишь.
requests - это бронь. Планировщик (kube-scheduler) при размещении пода смотрит только на requests: суммирует requests всех подов на узле и сравнивает с allocatable-ёмкостью узла. Если новый под со своим request не влезает - узел для него закрыт, нужен новый узел. То есть requests напрямую определяют bin-packing - насколько плотно поды упакованы по узлам. Завысил requests - упаковка рыхлая - узлов нужно больше - счёт растёт. requests это и есть деньги.
limits - это потолок потребления. CPU сверх лимита троттлится (под не убивают, просто притормаживают через CFS-квоту ядра), память сверх лимита приводит к OOMKill. limits на bin-packing и на счёт напрямую не влияют - они про защиту соседей по узлу.
Важный нюанс 2026 года: классический Pod Security и устройство QoS никуда не делись. Класс QoS пода выводится из соотношения requests и limits:
- Guaranteed - requests равны limits для всех контейнеров. Убивают последним при нехватке памяти.
- Burstable - requests заданы, но меньше limits (или limits не заданы). Золотая середина для большинства.
- BestEffort - ни requests, ни limits. Прибьют первым. Для прода - яд.
Right-sizing kubernetes: ставим requests по факту, а не на глаз
Главная ошибка, из-за которой деньги текут рекой, - requests "от балды". Разработчик не знает реального аппетита сервиса, пишет 1000m CPU и 1Gi памяти "чтобы наверняка", а сервис ест 80m и 200Mi. Это не запас прочности, это сожжённый бюджет.
right-sizing kubernetes - это подгонка requests под реальное потребление с разумным буфером. Гадать не надо, надо измерить. Инструмент для этого - VPA (Vertical Pod Autoscaler) в режиме рекомендаций. Подчёркиваю: именно рекомендаций (Off / recommendation-only), не авто-применения. Авто-VPA пересоздаёт поды при смене requests и конфликтует с HPA по тем же метрикам - в проде это грабли. А вот как советчик VPA незаменим.
VPA крутится, собирает гистограммы реального потребления (нужно хотя бы 7-14 дней, чтобы поймать суточные и недельные циклы) и выдаёт рекомендации в полях target / lowerBound / upperBound.
Код: Выделить всё
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: api-vpa
namespace: shop
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: api
updatePolicy:
updateMode: "Off" # только рекомендации, поды не трогаем
Код: Выделить всё
$ kubectl describe vpa api-vpa -n shop
...
Recommendation:
Container Recommendations:
Container Name: api
Lower Bound:
Cpu: 76m
Memory: 180Mi
Target:
Cpu: 110m
Memory: 256Mi
Upper Bound:
Cpu: 480m
Memory: 610Mi
- Target - то, что VPA поставил бы прямо сейчас. Именно сюда целимся при правке requests. Видим 110m против заявленной 1000m - почти десятикратный перезаказ.
- Lower Bound - ниже этого начнётся нехватка ресурсов. Граница, под которую опускаться нельзя.
- Upper Bound - оценка пика. Полезна для прикидки limits.
Spot узлы kubernetes: платим в разы меньше за то же железо
spot узлы kubernetes (они же preemptible в Google Cloud, прерываемые ВМ в Yandex Cloud) - это невостребованные мощности облака, которые отдают со скидкой 60-90 процентов. Цена та же машина, дешевле в три-пять раз. Условие одно: облако вправе забрать узел в любой момент, обычно предупредив за 30-120 секунд через сигнал вытеснения.
Звучит страшно, но для stateless-нагрузки это почти бесплатные деньги. Реплика API-сервера за спиной балансировщика умерла на спот-узле - её перепланируют на другой узел, пользователь в худшем случае поймает один retry. Что хорошо живёт на споте: stateless web/API, воркеры очередей, CI-раннеры, батч-джобы, dev/stage целиком. Что туда нельзя: базы данных, stateful-системы с кворумом без серьёзной обвязки, синглтоны без реплик.
Чтобы спот не превратился в источник инцидентов, нужны три вещи.
1. Таргетинг через nodeSelector / tolerations. Спот-узлы обычно несут taint и лейбл капасити-типа. Pod должен явно соглашаться туда ехать:
Код: Выделить всё
spec:
nodeSelector:
karpenter.sh/capacity-type: spot
tolerations:
- key: "spot"
operator: "Equal"
value: "true"
effect: "NoSchedule"
Код: Выделить всё
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api-pdb
namespace: shop
spec:
minAvailable: 2
selector:
matchLabels:
app: api
Автоскейлинг узлов: Cluster Autoscaler и Karpenter, scale-to-zero
Right-sizing подогнал requests, спот удешевил железо. Но узлы всё ещё надо добавлять под рост нагрузки и убирать, когда нагрузка спала, - иначе ночью платишь за дневной пик. Этим занимается автоскейлер узлов, и тут два игрока.
Cluster Autoscaler - классика. Работает с заранее заданными node group / managed node group облака. Логика простая: есть Pending-поды, которым не хватило места, - добавь узел в подходящую группу; узел недозагружен дольше таймаута и его поды переедут - убери узел. Минус - он оперирует фиксированными группами одинаковых машин, поэтому упаковка не оптимальна: под просит чуть больше - поднимается целая большая машина.
Karpenter - новое поколение, в 2026-м фактически стандарт на EKS, и подход принципиально другой. Karpenter не привязан к node group. Он смотрит на конкретные Pending-поды, их requests, аффинити и таинты - и подбирает оптимальный по цене тип инстанса прямо под этот набор подов. Конфигурируется через ресурс NodePool (актуальная версия API - karpenter.sh/v1):
Код: Выделить всё
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: default
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
- key: kubernetes.io/arch
operator: In
values: ["amd64", "arm64"]
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 1m
Scale-to-zero - святой грааль экономии. Если у пула нагрузки нет ни одного пода (например, ночью все dev-деплойменты сжаты до replicas: 0), пул узлов схлопывается до нуля. Платишь строго за то, что работает. Для RU-реалий: в Yandex Managed Kubernetes автомасштабирование групп узлов с нижней границей 0 живёт давно, Deckhouse несёт свой механизм управления узлами, k3s на своём железе экономит иначе - там FinOps это про отказ от облачного оверхеда вообще.
ResourceQuota и учёт: чтобы команды не дрались за бюджет вслепую
Технику починили, теперь нужна организационная дисциплина, иначе одна команда снова закажет 64 ядра "про запас". Инструмент - ResourceQuota на namespace. Это жёсткий лимит на сумму requests/limits в пространстве имён: пробьёшь потолок - новые поды просто не создадутся, получишь ошибку при apply.
Код: Выделить всё
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-payments-quota
namespace: payments
spec:
hard:
requests.cpu: "20"
requests.memory: 40Gi
limits.cpu: "40"
limits.memory: 80Gi
persistentvolumeclaims: "10"
Но квоты режут будущее, а видеть надо прошлое: куда уже ушли деньги. Тут на сцену выходят инструменты учёта. OpenCost - это CNCF-проект, открытый стандарт распределения затрат: он берёт прайс облака, считает стоимость каждого узла и аллоцирует её на поды, деплойменты и namespace пропорционально их потреблению. kubecost - надстройка над той же моделью с удобным UI, отчётами и алертами (часть функций платная, ядро открытое). Оба отвечают на вопрос "сколько стоит namespace payments в день" и "какой деплоймент жжёт больше всех при нулевой загрузке".
Чтобы отчёты были осмысленными, нужна гигиена меток. Введи обязательные лейблы и добивайся их через политику (Kyverno умеет требовать наличие лейбла на любом workload):
Код: Выделить всё
metadata:
labels:
owner: "team-payments"
cost-center: "fin-2026"
env: "prod"
Грабли и антипаттерны из практики
- requests с потолка "чтобы наверняка". Самая дорогая привычка. Лечится только VPA-рекомендациями и культурой "request = факт плюс буфер".
- Лимиты CPU на латентном сервисе. Сервис мистически тормозит при низкой нагрузке - это троттлинг по CFS-квоте. Часто лучше снять CPU limit совсем, оставив request.
- Забытые ресурсы. Висящие LoadBalancer от снесённых сервисов, осиротевшие PVC и Released-тома, старые снапшоты, dev-кластер, который "временно" подняли полгода назад. Регулярный аудит kubectl get pvc,svc --all-namespaces и чистка дисков - прямые деньги.
- Спот без PDB и диверсификации. Облако забрало пул - сервис лёг. Спот это инструмент для тех, кто положил PDB и размазал реплики, а не просто включил галочку "дешевле".
- Dev работает 24/7. Разработчики спят 8 часов и не работают в выходные, а dev-кластер крутится. Простой CronJob, гасящий dev по ночам (kubectl scale --replicas=0 ... плюс scale-to-zero узлов), режет счёт за непрод почти вдвое.
- HPA + auto-VPA на одних метриках. Два автоскейлера дёргают requests и реплики по одной и той же метрике и входят в резонанс. VPA на проде - только в режиме рекомендаций.
Повтори руками на тестовом namespace:
- Подними простой Deployment с заведомо завышенными requests (например, cpu: 1, memory: 1Gi) и нагрузи его слегка (любой ab/hey на 5-10 RPS).
- Поставь VPA в updateMode: "Off" на этот Deployment. Подожди хотя бы 20-30 минут (для боевой оценки - дни, но для лабы хватит увидеть тренд).
- Выполни kubectl describe vpa и сравни Target с тем, что заказано. Посчитай, во сколько раз перезаказал.
- Поправь requests по формуле Target плюс 20 процентов, примени, убедись что под жив и не OOM.
- Навесь ResourceQuota на namespace и попробуй заказать больше потолка - поймай ошибку отказа в создании пода.
- Бонус: посчитай на калькуляторе облака, сколько в месяц стоит разница между старыми и новыми requests, если таких подов 50.
- Почему на счёт за кластер влияют именно requests, а не limits? Что планировщик использует при bin-packing?
- В каком режиме запускают VPA для right-sizing на проде и почему нельзя авто-применение вместе с HPA?
- Какие три обязательных условия нужны, чтобы безопасно вынести stateless-нагрузку на spot узлы?
- Чем подход Karpenter к выбору узлов принципиально отличается от Cluster Autoscaler и что такое consolidation?
FinOps в Kubernetes - это не разовая чистка, а петля: измерь (OpenCost/Kubecost плюс метки owner/team), подгони requests под факт (VPA-рекомендации), удешеви железо (spot для stateless с PDB), масштабируй узлы под реальную нагрузку с консолидацией и scale-to-zero (Karpenter), огради бюджет команд квотами и погаси непрод по ночам. Каждый шаг проверяемый и обратимый. Деньги в кластере не исчезают магически - они утекают в зарезервированный, но не используемый ресурс. Верни видимость - и счёт перестанет быть сюрпризом.