Стоимость инфраструктуры: облако против своего железа, FinOps, реальные расчёты
Предыдущие четыре статьи трека разбирали площадки: https://courses.digitable.life/post/devops/11-aws/, https://courses.digitable.life/post/devops/12-azure/, https://courses.digitable.life/post/devops/13-gcp-and-others/ и https://courses.digitable.life/post/devops/14-vps-vds-and-bare-metal/. Каждая из них заканчивалась одним и тем же неудобным вопросом: а сколько это стоит на самом деле и с чем это сравнивать?
Эта статья — про деньги как инженерную дисциплину. Не про «облако дорогое» и не про «своё железо — прошлый век». Оба тезиса ложны в общем виде и оба верны при конкретных числах. Задача — научиться получать эти числа: строить модель стоимости, находить точку безубыточности, распределять счёт по командам и продуктам, встраивать оценку в конвейер до мержа, а не после счёта.
Ключевая мысль, вокруг которой построено всё остальное: счёт за инфраструктуру — это не финансовый показатель, а следствие архитектурных решений. Скидка, которую вы выторговали у вендора, даёт 20–30 %. Смена топологии даёт 3–10 раз. FinOps существует, чтобы инженер увидел это раньше финансиста.
Первый принцип: за что вообще берут деньги
Любой счёт — комбинация пяти базовых товаров. Полезно держать их в голове раздельно, потому что оптимизируются они по-разному.
| Товар | Единица | Кто платит | Как оптимизируется |
|---|---|---|---|
| Вычисления | vCPU-час, GB-RAM-час, GPU-час | все | rightsizing, коммиты, spot, плотность упаковки |
| Хранение | ГБ-месяц, IOPS, throughput | все | классы хранения, lifecycle, компрессия, retention |
| Передача данных | ГБ egress, ГБ между зонами, NAT | почти все, но незаметно | топология, CDN, приватные endpoints, IPv6 |
| Операции | 1000 запросов к API, вызов функции | serverless и объектные хранилища | батчинг, кэш, укрупнение объектов |
| Управляемость | фиксированная надбавка за сервис | тот, кто выбрал managed | сравнение с self-hosted с учётом людей |
Первые четыре — переменные и почти линейные. Пятая — то, за что на самом деле идёт спор в дискуссии «облако против железа»: вы покупаете не CPU, а чужой труд, чужие запчасти на складе, чужую резервную площадку и чужой пейджер в три часа ночи.
Отсюда правило, которое сэкономит вам много бессмысленных споров: сравнивать цену сервера с ценой инстанса некорректно, это разные наборы услуг. Корректно сравнивать полные TCO при одинаковом уровне надёжности, скорости изменений и человеческой нагрузки.
Считаем TCO своего железа честно
Возьмём конкретную задачу: ёмкость примерно 100 vCPU, 400 ГБ RAM, 20 ТБ NVMe. Это середина — не пет-проект и не Netflix. Соберём стоимость собственного варианта, ничего не забыв.
Разбор по строкам:
- Железо. Два двухпроцессорных сервера (по 2 × 32 ядра, 512 ГБ RAM, 4 × 3,84 ТБ NVMe) — порядка 5 500–6 500 $ за штуку у интегратора. Амортизация линейная на 60 месяцев: ~190 $/мес. Пятилетний срок — компромисс: гарантия вендора обычно 3 года с опцией продления, реальная деградация NVMe и рост энергопотребления на ядро делают седьмой год сомнительным.
- Колокация. Считают не по юнитам, а по гарантированной мощности. 2 кВт с резервированием по питанию и охлаждению в приличном ДЦ — 250–350 $/мес в Европе. Дешёвые предложения «юнит за 40 $» обычно означают 300 Вт на юнит, и вы упрётесь в лимит раньше, чем в место.
- Транзит. Гигабит с двумя аплинками и своей AS — 100–150 $/мес. Здесь же главный экономический аргумент против облака: этот гигабит даёт до 300 ТБ трафика в месяц. Те же 300 ТБ egress из AWS — порядка 25 000 $. Разница в двести раз, и она не объясняется себестоимостью.
- Сеть и обвязка. Пара коммутаторов, PDU, оптика, кабели, патч-панель — 5 000 $ единовременно, ~85 $/мес амортизации.
- ЗИП. Диски выходят из строя. При 8 NVMe на систему AFR порядка 1–2 % означает отказ примерно раз в 6–10 лет на диск, то есть один-два инцидента в год на парк. Полка запасных дисков и БП — это не паранойя, а условие того, чтобы SLA держался без ночного заказа с доставкой.
- Бэкапы и вторая площадка. Строка, которую пропускают чаще всего. Без неё сравнение с облаком нечестно: у managed-БД репликация в другую зону включена в цену. Минимум — бэкапы в объектное хранилище другого провайдера плюс холодный резерв.
- Люди. 0,2–0,4 FTE инженера на парк такого размера, если всё уже автоматизировано и ничего не горит. В деньгах — 2 000–3 500 $/мес полной стоимости сотрудника. Это самая крупная и самая невидимая строка.
Итог: 1 150 $/мес по счетам, 3 650 $/мес с людьми. Аналогичная ёмкость в AWS on-demand (скажем, 4 × m6i.8xlarge + EBS gp3) — порядка 5 000–5 400 $/мес; с трёхлетним Compute Savings Plan — около 2 300–2 600 $. Выделенные серверы Hetzner сопоставимой мощности — 500–700 $/мес, и это середина между колокацией и облаком: железо чужое, руки чужие, эластичности почти нет.
Главная переменная: коэффициент загрузки
Все таблицы выше сравнивают статические цены и поэтому лгут. Настоящий вопрос — какую долю времени ёмкость реально нужна. Облако продаёт часы; своё железо продаёт себя целиком и заранее.
Модель проста и её стоит уметь считать в уме. Пусть C_cloud — цена ёмкости в облаке за месяц при постоянной работе, d — доля времени, когда ёмкость занята (duty cycle), C_own — полный TCO своего железа за месяц. Тогда облако дешевле, пока выполняется C_cloud · d < C_own, то есть пока d < C_own / C_cloud.
Подставим наши числа: 3 650 / 5 200 ≈ 0,70. До 70 % загрузки облако с реально работающим автоскейлингом дешевле собственного железа с учётом людей. Только выше этой отметки железо начинает выигрывать — и только если нагрузка предсказуема на горизонте амортизации.
Три оговорки, которые превращают эту формулу из лозунга в инструмент.
- Эластичность нужно заслужить. Формула предполагает, что вы действительно гасите ёмкость. Если ноды работают 24/7 при средней утилизации CPU 12 % — ваш эффективный
dравен 1,0, и вы платите за облако как за пик, получая экономику худшую, чем у железа. Именно так выглядит большинство «дорогих облаков»: не тариф плохой, а автоскейлинг не настроен. - Пиковость важнее среднего. Полезная величина — отношение пика к среднему (peak-to-average ratio). У B2B-SaaS с рабочим днём одного часового пояса оно 4–6: ночью и в выходные нагрузка падает почти до нуля. У инфраструктурного сервиса с глобальной аудиторией — 1,3–1,8. Первому облако почти всегда выгодно, второму — почти никогда.
- Не всё в системе имеет одинаковый
d. Реляционная база работает 24/7 приd = 1. CI-раннеры — приd = 0,15. Батчи ML-обучения —d = 0,05, но с гигантским пиком. Правильный ответ почти никогда не «всё в облако» или «всё на железо», а гибрид с разной площадкой под разные профили.
нагрузки на 12+ мес?} B -->|Нет, гипотеза| C[Облако on-demand
оптимизируем потом] B -->|Да| D{Peak / average} D -->|больше 3| E[Облако + автоскейлинг
коммит только на базовый слой] D -->|1 - 2| F{Требуется ли
эластичность по регионам?} F -->|Да| G[Облако с коммитами
1-3 года] F -->|Нет| H{Есть ли инженер,
готовый держать железо?} H -->|Нет| I[Выделенные серверы
Hetzner / OVH] H -->|Да| J{Трафик больше 50 ТБ/мес
или данные больше 100 ТБ?} J -->|Да| K[Колокация со своим железом] J -->|Нет| I C --> L[Через 2 квартала:
пересчитать по факту] E --> L G --> L I --> L K --> L style C fill:#3d8bcd,color:#0e1116 style K fill:#e9c46a,color:#0e1116 style I fill:#2a9d8f,color:#0e1116 style L fill:#e76f51,color:#0e1116
Обратите внимание на нижний узел: любое решение о площадке — временное. Профиль нагрузки меняется, цены меняются, команда растёт. Решение, принятое один раз и не пересматриваемое, гарантированно устареет.
Скрипт, который стоит держать под рукой
Модель полезно уметь считать, а не чувствовать. Ниже — маленький калькулятор, который отвечает на вопрос «при какой загрузке что дешевле» и заодно показывает чувствительность к зарплате инженера.
"""Сравнение вариантов размещения по коэффициенту загрузки.
Сложность: O(len(scenarios) * len(grid)) по времени, O(1) по памяти —
это арифметика, а не алгоритм; ценность в честности входных данных.
"""
from dataclasses import dataclass
@dataclass
class Option:
name: str
fixed: float # $/мес, платится независимо от загрузки
elastic: float # $/мес при загрузке 100 %, масштабируется линейно
fte: float # доля инженера, которую вариант съедает
FTE_COST = 8_300.0 # полная стоимость инженера в месяц: оклад, налоги, оборудование
def monthly(opt: Option, duty: float) -> float:
"""Счёт за месяц при заданном коэффициенте загрузки duty (0..1)."""
return opt.fixed + opt.elastic * duty + opt.fte * FTE_COST
def breakeven(a: Option, b: Option) -> float | None:
"""Загрузка, при которой варианты стоят одинаково. None, если не пересекаются."""
# a.fixed + a.elastic*d + a.fte*F == b.fixed + b.elastic*d + b.fte*F
delta_elastic = a.elastic - b.elastic
if abs(delta_elastic) < 1e-9:
return None
const = (b.fixed - a.fixed) + (b.fte - a.fte) * FTE_COST
d = const / delta_elastic
return d if 0.0 <= d <= 1.0 else None
OPTIONS = [
Option("AWS on-demand + автоскейлинг", fixed=350, elastic=5_200, fte=0.05),
Option("AWS + 3-летний Savings Plan", fixed=2_300, elastic=0, fte=0.05),
Option("Hetzner dedicated", fixed=640, elastic=0, fte=0.12),
Option("Колокация, своё железо", fixed=1_150, elastic=0, fte=0.30),
]
for duty in (0.10, 0.25, 0.50, 0.75, 1.00):
row = [f"{o.name}: {monthly(o, duty):>8,.0f} $" for o in OPTIONS]
best = min(OPTIONS, key=lambda o: monthly(o, duty))
print(f"загрузка {duty:>4.0%} | дешевле всего: {best.name}")
print("\nТочки безубыточности против on-demand:")
for o in OPTIONS[1:]:
d = breakeven(OPTIONS[0], o)
print(f" {o.name:<32} {'—' if d is None else f'{d:.0%}'}")
загрузка 10% | дешевле всего: AWS on-demand + автоскейлинг
загрузка 25% | дешевле всего: AWS on-demand + автоскейлинг
загрузка 50% | дешевле всего: Hetzner dedicated
загрузка 75% | дешевле всего: Hetzner dedicated
загрузка 100% | дешевле всего: Hetzner dedicated
Точки безубыточности против on-demand:
AWS + 3-летний Savings Plan 38%
Hetzner dedicated 18%
Колокация, своё железо 26%
Результат поучителен: при честном учёте зарплаты выделенные серверы бьют и облако, и колокацию почти во всём диапазоне — потому что дают низкую цену без полной эксплуатационной нагрузки. Это и есть причина, по которой Hetzner и OVH держат целый пласт рынка (подробнее — в https://courses.digitable.life/post/devops/14-vps-vds-and-bare-metal/). Меняйте FTE_COST на реальную для вашего рынка — картина сдвигается сильно.
Что говорят те, кто действительно переезжал
Две публичные истории, на которые все ссылаются, и обе стоит понимать точно, а не по заголовкам.
37signals (Basecamp, HEY). DHH описал уход из облака в серии постов, начиная с «Why we’re leaving the cloud». Числа: расход на облако порядка 3,2 млн $ в год, закупка собственного железа примерно на 600 тыс. $, заявленная экономия — около 2 млн $ в год и порядка 10 млн $ за пять лет с учётом ухода с S3. Важный контекст, который теряется: у них стабильная, предсказуемая нагрузка зрелого продукта, сильная инфраструктурная команда и собственный инструмент развёртывания Kamal. Это d ≈ 1 и наличие людей — ровно правая часть нашего графика.
Dropbox. Проект Magic Pocket — переезд с S3 на собственное хранилище (инженерный блог). В форме S-1 при выходе на биржу указана экономия порядка 74,6 млн $ за два года. Здесь двигатель другой: экстремальная гравитация данных. При сотнях петабайт стоимость хранения и egress перестаёт быть строкой в счёте и становится бизнес-моделью.
Контрпример столь же важен. a16z в «The Cost of Cloud, a Trillion Dollar Paradox» утверждают, что облако «отжимает» 50 % и более валовой маржи у зрелых инфраструктурных компаний — но там же сказано, что репатриация имеет смысл на поздней стадии, при масштабе и предсказуемости. Для компании, которая ещё ищет product-market fit, эта же операция — способ потратить полгода инженерного времени на экономию, которая меньше зарплаты одного разработчика.
Академическая база под всем этим — «Above the Clouds: A Berkeley View of Cloud Computing» (2009). Там же сформулирован главный аргумент, который не устарел: облако продаёт устранение риска переоценки и недооценки спроса, а не дешёвые ядра.
FinOps: цикл, а не отдел
FinOps — операционная практика управления переменными затратами. FinOps Foundation описывает её как три фазы, повторяющиеся бесконечно: Inform → Optimize → Operate. Канонический текст — «Cloud FinOps» Дж. Стормента и М. Фуллера (O’Reilly, 2-е издание).
владельцы найдены Optimize --> Operate: изменения внедрены
эффект измерен Operate --> Inform: новая нагрузка,
новые сервисы, новые цены Optimize --> Inform: данных не хватает,
непонятно чьё Operate --> Optimize: бюджет пробит,
сработала аномалия note right of Inform Пропуск этой фазы — самая частая ошибка: оптимизируют то, что видно, а не то, что дорого. end note
Смысл цикла — в порядке. Команды почти всегда начинают с Optimize: «давайте выключим неиспользуемые диски». Это даёт единоразовые проценты и не меняет траекторию. Работает обратный порядок: сначала сделать счёт видимым и адресным, и тогда оптимизация начинает происходить сама — инженер, который видит, что его сервис стоит 4 000 $/мес, задаёт вопросы без всякого распоряжения сверху.
Три уровня зрелости, по которым практика описывает себя: Crawl (счёт разложен помесячно, есть базовые теги), Walk (аллокация выше 90 %, unit-метрики, прогноз), Run (стоимость — часть design review, автоматические политики, чарджбэк). Прыгать через ступень бессмысленно.
Inform: аллокация — фундамент всего
Пока счёт не разложен по владельцам, любая работа со стоимостью — это гадание. Основной механизм — теги (labels в GCP, tags в AWS/Azure). Требования к схеме тегов:
- обязательный минимум:
owner,service,env,cost-center; - теги активируются как ключи распределения затрат (в AWS — Cost Allocation Tags, иначе они не попадут в отчёты);
- нетегируемые ресурсы существуют всегда (трафик между зонами, часть сетевых сервисов) — их распределяют пропорционально или относят на общий пул;
- соблюдение тегов обеспечивается в конвейере, а не проверками постфактум.
Принуждение через Terraform — самый надёжный способ (см. https://courses.digitable.life/post/devops/09-terraform-and-iac/):
# tagging/variables.tf — единая точка правды для тегов
variable "service" {
type = string
description = "Имя сервиса как в каталоге сервисов"
validation {
condition = can(regex("^[a-z][a-z0-9-]{2,30}$", var.service))
error_message = "service: строчные буквы, цифры и дефис, 3-31 символ."
}
}
variable "owner_team" {
type = string
description = "Slack-канал команды-владельца, например #team-payments"
validation {
condition = startswith(var.owner_team, "#")
error_message = "owner_team должен быть slack-каналом и начинаться с #."
}
}
variable "env" {
type = string
validation {
condition = contains(["prod", "staging", "dev", "sandbox"], var.env)
error_message = "env: prod | staging | dev | sandbox."
}
}
locals {
# Эти теги приклеиваются ко всему через default_tags провайдера.
mandatory_tags = {
Service = var.service
Owner = var.owner_team
Env = var.env
CostCenter = var.cost_center
ManagedBy = "terraform"
Repo = var.repo_url
}
}
provider "aws" {
region = var.region
default_tags {
tags = local.mandatory_tags
}
}
Дополнительно на уровне организации ставится Service Control Policy, запрещающая создание инстансов и томов без тега Service. Это грубо, но работает: нетегированные ресурсы просто не появляются.
Проверить покрытие можно прямо из CLI:
# Какая доля затрат прошлого месяца имеет тег Service
aws ce get-cost-and-usage \
--time-period Start=2026-06-01,End=2026-07-01 \
--granularity MONTHLY \
--metrics UnblendedCost \
--group-by Type=TAG,Key=Service \
--query 'ResultsByTime[0].Groups[?Keys[0]==`Service$`].Metrics.UnblendedCost.Amount' \
--output text
18342.77
18 342 $ из счёта не имеют владельца. Это и есть первая задача фазы Inform — не «сэкономить», а выяснить, чьё это.
Athena поверх Cost and Usage Report: настоящий инструмент
Cost Explorer хорош для обзора, но врёт в деталях и не умеет соединяться с вашими данными. Серьёзная работа идёт через Cost and Usage Report (CUR) — построчную выгрузку в S3, которую читают Athena. Один час одного ресурса — одна строка.
-- Топ-15 источников роста: сравниваем июнь с маем по (сервис, тип использования, команда)
WITH monthly AS (
SELECT
date_trunc('month', line_item_usage_start_date) AS month,
product_product_name AS service,
line_item_usage_type AS usage_type,
COALESCE(resource_tags_user_owner, '(без владельца)') AS owner,
SUM(line_item_unblended_cost) AS cost
FROM cur.my_org_cur
WHERE line_item_line_item_type IN ('Usage', 'DiscountedUsage', 'SavingsPlanCoveredUsage')
AND line_item_usage_start_date >= TIMESTAMP '2026-05-01'
AND line_item_usage_start_date < TIMESTAMP '2026-07-01'
GROUP BY 1, 2, 3, 4
)
SELECT
service, usage_type, owner,
ROUND(SUM(CASE WHEN month = TIMESTAMP '2026-05-01' THEN cost END), 2) AS may_cost,
ROUND(SUM(CASE WHEN month = TIMESTAMP '2026-06-01' THEN cost END), 2) AS jun_cost,
ROUND(SUM(CASE WHEN month = TIMESTAMP '2026-06-01' THEN cost END)
- SUM(CASE WHEN month = TIMESTAMP '2026-05-01' THEN cost END), 2) AS delta
FROM monthly
GROUP BY service, usage_type, owner
HAVING SUM(CASE WHEN month = TIMESTAMP '2026-06-01' THEN cost END)
- SUM(CASE WHEN month = TIMESTAMP '2026-05-01' THEN cost END) > 100
ORDER BY delta DESC
LIMIT 15;
service | usage_type | owner | may_cost | jun_cost | delta
------------------------+-----------------------------+----------------+----------+----------+--------
Amazon EC2 | EU-NatGateway-Bytes | #team-search | 410.22 | 2891.60 | 2481.38
AmazonCloudWatch | EU-DataProcessing-Bytes | (без владельца)| 612.05 | 1904.88 | 1292.83
Amazon S3 | EU-Requests-Tier1 | #team-media | 188.40 | 974.15 | 785.75
Amazon EC2 | EU-DataTransfer-Regional-Bytes | #team-search| 120.66 | 690.10 | 569.44
Amazon RDS | EU-InstanceUsage:db.r6g.4xl | #team-billing | 1420.00 | 1893.33 | 473.33
Читается это так: команда #team-search в июне выкатила что-то, что гоняет трафик через NAT Gateway и между зонами. Скорее всего — новый индексатор, который тянет данные из S3 без VPC-endpoint и раскидан по зонам без учёта локальности. Строка счёта — 3 000 $/мес, стоимость исправления — два часа работы. Такие находки делает не отдел финансов, а SQL-запрос раз в неделю.
Отдельно стоит знать про FOCUS — открытую спецификацию единого формата биллинга, которую поддерживают AWS, Azure, GCP и OCI. Если у вас мультиоблако, начинайте сразу с неё: одна схема вместо трёх несовместимых выгрузок.
Unit-экономика: единственная метрика, которая не врёт
Абсолютный счёт бесполезен как показатель здоровья: он растёт вместе с бизнесом, и «рост на 15 %» ничего не говорит. Смысл появляется, когда стоимость нормируется на единицу ценности.
| Тип продукта | Разумная unit-метрика | Что показывает |
|---|---|---|
| SaaS B2B | $ на активный аккаунт в месяц | масштабируется ли продукт с ростом клиентов |
| Маркетплейс | $ на 1000 заказов | не съедает ли инфраструктура комиссию |
| API-платформа | $ на миллион запросов | сходится ли тариф с себестоимостью |
| Медиа | $ на 1000 просмотров, отдельно CDN | эффективность доставки |
| ML-инференс | $ на 1000 предсказаний, отдельно GPU | окупается ли модель |
| Внутренняя платформа | $ на команду-потребителя | честна ли внутренняя цена |
Практическое правило: абсолютный счёт может расти, unit-стоимость должна падать или держаться. Растущая unit-стоимость означает, что архитектура масштабируется хуже линейного — и это инженерный дефект, который стоит чинить до того, как он станет проблемой бизнеса.
Считается это соединением CUR с продуктовыми метриками. Простой вариант — витрина, куда раз в сутки складывается сумма затрат по сервису и число бизнес-событий, а дальше делится одно на другое. Сложный — Kubecost/OpenCost с внешним источником юнитов. Начинайте с простого: одна таблица и один график в дашборде команды дают 80 % эффекта.
Optimize: рычаги в порядке убывания эффекта
Это самая практичная таблица статьи. Порядок неслучаен — он отражает и величину эффекта, и его устойчивость.
| Рычаг | Типичный эффект | Усилие | Долговечность |
|---|---|---|---|
| Смена архитектуры (убрать NAT, кэш, сжать хранение) | 30–70 % на затронутых строках | высокое | навсегда |
| Выключить то, что не нужно (зомби, dev по ночам) | 10–25 % общего счёта | низкое | требует автоматизации |
| Rightsizing (инстансы, requests/limits, классы дисков) | 15–35 % на вычислениях | среднее | деградирует, нужен цикл |
| Коммиты (Savings Plans, RI, CUD) | 25–60 % на покрытой части | низкое, но риск | 1–3 года |
| Spot / preemptible | 60–90 % на подходящих нагрузках | среднее | требует устойчивости к прерываниям |
| Смена поколения инстансов (Graviton, AMD) | 10–25 % при равной производительности | среднее (пересборка) | до следующего поколения |
| Тюнинг хранения (классы, lifecycle, retention) | 30–80 % на хранении | низкое | навсегда |
| Переговоры с вендором (EDP, private pricing) | 5–15 % | высокое, нужен объём | срок контракта |
Правый нижний квадрант заслуживает комментария. Мультиоблако ради переговорной позиции — почти всегда убыточно: вы платите двойной сложностью, двойной экспертизой и межоблачным трафиком за скидку, которой не получите. Смена региона ради цены ломается о задержки и требования к резидентности данных. Ручной аудит раз в квартал даёт эффект ровно до следующего квартала — заменяйте его автоматикой.
Коммиты: как не купить себе тюрьму
Коммиты — самый быстрый способ снять 30–50 % с вычислений и самый быстрый способ на три года зафиксировать ошибку. Матрица инструментов:
| Инструмент | Гибкость | Скидка | Риск |
|---|---|---|---|
| AWS Compute Savings Plan | любые EC2/Fargate/Lambda, любой регион и семейство | до ~54 % (3 года, предоплата) | обязательство в $/час |
| AWS EC2 Instance SP | одно семейство в одном регионе | до ~72 % | привязка к семейству |
| Reserved Instances (standard) | конкретный тип | до ~72 % | почти нулевая гибкость, но есть маркетплейс |
| GCP Committed Use Discounts | ресурсные (vCPU/RAM) или flexible | ~37–70 % | ресурсные привязаны к региону |
| Azure Reservations + Savings Plan | похоже на AWS, есть обмен | ~40–65 % | обмен ограничен |
Рабочие правила:
- Коммитьте только базовый слой. Возьмите почасовой график расхода за 90 дней, найдите 5-й перцентиль — это ваш неснижаемый минимум. Коммит на 70–80 % от него — безопасный старт. Покрывать 100 % пика бессмысленно: сверху пик закрывается on-demand и spot.
- Лестница вместо одного прыжка. Покупайте порциями раз в квартал с разными датами окончания. Так вы не окажетесь в ситуации, когда 100 % обязательств истекают в один день и вам нужно принять решение на всю сумму сразу.
- Следите за двумя числами. Coverage — какая доля потребления покрыта коммитами (цель 70–85 %). Utilization — какая доля купленного обязательства использована (цель 98–100 %). Низкая утилизация означает, что вы платите за воздух, и это хуже, чем отсутствие скидки.
- Сначала rightsizing, потом коммит. Коммит, купленный поверх раздутых инстансов, консервирует раздутость на три года: сжимать теперь невыгодно, обязательство никуда не денется.
# Утилизация и покрытие Savings Plans за прошлый месяц
aws ce get-savings-plans-utilization \
--time-period Start=2026-06-01,End=2026-07-01 \
--granularity MONTHLY \
--query 'SavingsPlansUtilizationsByTime[0].Utilization'
{
"TotalCommitment": "34200.00",
"UsedCommitment": "31805.42",
"UnusedCommitment": "2394.58",
"UtilizationPercentage": "92.99"
}
93 % утилизации — это 2 394 $ выброшенных за месяц. Причина обычно одна: команда мигрировала часть нагрузки на spot или на Graviton, не пересчитав обязательство. Целевое значение — выше 98 %.
Spot: где 80 % скидки не стоят ничего
Spot-инстансы (в GCP — preemptible/Spot VM, в Azure — Spot VM) продаются со скидкой 60–90 % с условием, что их могут отобрать с уведомлением в две минуты. Spot Instance Advisor публикует частоту прерываний по типам — она сильно различается, и выбор пула по этому показателю важнее выбора «самого дешёвого».
Подходят: CI-раннеры (см. https://courses.digitable.life/post/devops/01-ci-fundamentals/), stateless-веб за балансировщиком, батчи с чекпоинтами, ML-обучение с сохранением состояния, обработка очередей. Не подходят: базы данных, синглтоны без реплик, всё, где потеря узла означает потерю данных, и всё, что не умеет корректно завершаться за 120 секунд.
Пример продакшн-конфигурации для Karpenter в EKS — диверсификация по типам и зонам, что и обеспечивает устойчивость:
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: spot-general
spec:
template:
metadata:
labels:
capacity-type: spot
spec:
# Пул должен быть широким: чем больше типов, тем реже отбирают всё сразу.
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"]
- key: kubernetes.io/arch
operator: In
values: ["amd64", "arm64"] # Graviton дешевле ещё на 10-20 %
- key: karpenter.k8s.aws/instance-family
operator: In
values: ["m6i", "m6a", "m7i", "m7g", "c6i", "c6a", "c7g", "r6i", "r6g"]
- key: karpenter.k8s.aws/instance-size
operator: In
values: ["xlarge", "2xlarge", "4xlarge"]
- key: topology.kubernetes.io/zone
operator: In
values: ["eu-central-1a", "eu-central-1b", "eu-central-1c"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
# Плановая ротация: узел старше 7 дней заменяется в рабочее время,
# а не тогда, когда его отберёт AWS в пятницу вечером.
expireAfter: 168h
terminationGracePeriod: 5m
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 1m
budgets:
- nodes: "10%" # не выселять больше 10 % парка разом
- nodes: "0" # заморозка в час пик
schedule: "0 9 * * mon-fri"
duration: 4h
limits:
cpu: "2000"
memory: 8000Gi
Обязательное дополнение со стороны приложения — PodDisruptionBudget и корректная обработка SIGTERM; без них скидка оборачивается пятиминутными окнами недоступности. Подробности жизненного цикла подов — в https://courses.digitable.life/post/devops/06-kubernetes/.
Гибридная стратегия, которая работает в проде: базовый слой (примерно 30–40 % ёмкости) — on-demand под Savings Plan, всё остальное — spot с несколькими пулами. Кластер переживает отбор целого пула без деградации, а средняя цена ядра падает в 2,5–3 раза.
Kubernetes: где деньги теряются незаметно
Кластер добавляет свой слой потерь, невидимый в счёте облака, потому что облако выставляет счёт за ноды, а тратят его поды.
replicas: 12 P->>I: terraform plan / манифесты I->>I: считает дельту по прайсу I-->>P: комментарий: +2 480 $/мес (+31 %) P->>R: запрос ревью R->>D: «пик по CPU 0,4 ядра, зачем 4?» D->>P: requests: cpu 500m, memory 2Gi I-->>P: комментарий: +310 $/мес (+4 %) R->>P: approve P->>K: мерж и выкатка K->>O: метрики фактического потребления O-->>D: через 7 дней: факт 0,35 ядра,
рекомендация requests 400m Note over I,O: Оценка до мержа ловит грубые ошибки,
факт после выкатки — тонкие.
Три источника потерь в Kubernetes:
1. Разрыв между requests и фактом. Планировщик резервирует по requests, а вы платите за ноду. Если суммарные requests — 60 % ёмкости нод, а реальное потребление — 15 %, вы выбрасываете 45 % денег на воздух, и ни один дашборд облака этого не покажет. Лечится VPA в режиме рекомендаций и регулярным пересмотром.
2. Плохая упаковка. Поды с requests в 3 vCPU на нодах по 4 vCPU оставляют по 1 ядру мусора на каждой ноде. Karpenter с консолидацией решает это, подбирая типы нод под форму подов, а не наоборот.
3. Накладные расходы платформы. Системные демонсеты, service mesh, агенты логов и метрик съедают 10–20 % каждой ноды. На мелких нодах доля выше: сайдкар в 200 МБ на ноде в 2 ГБ — это 10 %, на ноде в 16 ГБ — 1,2 %. Это аргумент в пользу нод покрупнее, ограниченный радиусом взрыва.
Инструмент для измерения — OpenCost (CNCF, открытый; Kubecost — коммерческая надстройка над ним). Он соединяет данные Prometheus с прайсом провайдера и раскладывает счёт по неймспейсам, деплойментам и лейблам.
helm install opencost opencost/opencost \
--namespace opencost --create-namespace \
--set opencost.prometheus.internal.enabled=false \
--set opencost.prometheus.external.url=http://prometheus-server.monitoring:80 \
--set opencost.exporter.defaultClusterId=prod-eu-central-1
# Стоимость по неймспейсам за 7 дней с показом эффективности
kubectl cost namespace --window 7d --show-efficiency=true --historical
+---------------+----------+-----------+----------+-----------+------------+
| NAMESPACE | CPU | MEMORY | PV | TOTAL | EFFICIENCY |
+---------------+----------+-----------+----------+-----------+------------+
| search | 412.88 | 288.10 | 96.40 | 797.38 | 11.2% |
| checkout | 190.44 | 142.02 | 61.20 | 393.66 | 58.9% |
| media | 121.60 | 88.75 | 340.10 | 550.45 | 44.3% |
| observability | 208.31 | 410.66 | 188.90 | 807.87 | 72.4% |
| ci-runners | 344.02 | 102.55 | 0.00 | 446.57 | 81.0% |
+---------------+----------+-----------+----------+-----------+------------+
Колонка EFFICIENCY — отношение фактического потребления к запрошенному. search с 11 % — не «дорогой сервис», а сервис с requests, завышенными в девять раз. Возврат — примерно 700 $/мес за один PR с правкой манифеста. Что характерно, observability стоит дороже всех — это нормальная и часто недооценённая статья, к которой мы вернёмся в следующей статье трека.
Shift left: стоимость в pull request
Самый дешёвый момент повлиять на счёт — до мержа. Infracost считает дельту стоимости по Terraform-плану и пишет её комментарием в PR.
# .github/workflows/infracost.yml
name: infracost
on:
pull_request:
paths: ["infra/**", ".github/workflows/infracost.yml"]
permissions:
contents: read
pull-requests: write
jobs:
cost:
runs-on: ubuntu-latest
steps:
- uses: infracost/actions/setup@v3
with:
api-key: ${{ secrets.INFRACOST_API_KEY }}
- name: Базовая ветка
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.ref }}
- name: Стоимость до изменений
run: |
infracost breakdown --path=infra \
--format=json --out-file=/tmp/base.json
- name: Ветка PR
uses: actions/checkout@v4
- name: Дельта стоимости
run: |
infracost diff --path=infra \
--compare-to=/tmp/base.json \
--format=json --out-file=/tmp/diff.json
# Политика: рост дороже 500 $/мес требует явного одобрения FinOps.
- name: Проверка порога
run: |
DELTA=$(jq -r '.diffTotalMonthlyCost // "0"' /tmp/diff.json)
echo "Дельта: ${DELTA} USD/мес"
if (( $(echo "$DELTA > 500" | bc -l) )); then
echo "::error::Рост стоимости ${DELTA} USD/мес превышает порог 500"
gh pr edit "${{ github.event.number }}" --add-label needs-finops-review
exit 1
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Комментарий в PR
run: |
infracost comment github --path=/tmp/diff.json \
--repo="$GITHUB_REPOSITORY" \
--pull-request="${{ github.event.number }}" \
--github-token="${{ secrets.GITHUB_TOKEN }}" \
--behavior=update
Локально это выглядит так:
infracost diff --path=infra --compare-to=/tmp/base.json
Project: infra/prod
~ module.search.aws_instance.indexer[0]
+487 $/мес (было 122 $, стало 609 $)
~ Instance usage (Linux/UNIX, on-demand, r6i.4xlarge)
+487 $/мес
+ aws_nat_gateway.private[1]
+142 $/мес
+ NAT gateway (hours) +33 $/мес
+ Data processed (18 000 GB) +109 $/мес [оценка по usage-файлу]
Monthly cost change for infra/prod
Amount: +629 $ (+2 480 $ → 3 109 $)
Percent: +25%
Две детали, без которых инструмент бесполезен. Первая: usage-файл. Infracost не знает объёма трафика и числа запросов — эти величины задаются в infracost-usage.yml, иначе самые дорогие строки (egress, NAT, запросы к S3, вызовы Lambda) оцениваются в ноль. Вторая: порог, а не просто комментарий. Комментарий, который никого ни к чему не обязывает, через месяц перестают читать.
Operate: бюджеты, аномалии и защита от катастроф
Инцидент со счётом — такой же инцидент, как падение сервиса, и обрабатывается так же: детект, алерт, дежурный, постмортем.
# Бюджет на сервис с реакцией на прогноз, а не на факт.
resource "aws_budgets_budget" "search_service" {
name = "search-monthly"
budget_type = "COST"
limit_amount = "6000"
limit_unit = "USD"
time_unit = "MONTHLY"
cost_filter {
name = "TagKeyValue"
values = ["user:Service$search"]
}
# 80 % факта — предупреждение команде.
notification {
comparison_operator = "GREATER_THAN"
threshold = 80
threshold_type = "PERCENTAGE"
notification_type = "ACTUAL"
subscriber_sns_topic_arns = [aws_sns_topic.finops_alerts.arn]
}
# 100 % прогноза — реакция до конца месяца, а не после.
notification {
comparison_operator = "GREATER_THAN"
threshold = 100
threshold_type = "PERCENTAGE"
notification_type = "FORECASTED"
subscriber_sns_topic_arns = [aws_sns_topic.finops_alerts.arn]
}
}
# Детектор аномалий: ловит скачки в пределах бюджета.
resource "aws_ce_anomaly_monitor" "by_service" {
name = "monitor-by-service"
monitor_type = "DIMENSIONAL"
monitor_dimension = "SERVICE"
}
resource "aws_ce_anomaly_subscription" "daily" {
name = "anomaly-daily"
frequency = "DAILY"
monitor_arn_list = [aws_ce_anomaly_monitor.by_service.arn]
subscriber {
type = "SNS"
address = aws_sns_topic.finops_alerts.arn
}
# Порог в абсолютном выражении: шум ниже 150 $ не нужен.
threshold_expression {
dimension {
key = "ANOMALY_TOTAL_IMPACT_ABSOLUTE"
match_options = ["GREATER_THAN_OR_EQUAL"]
values = ["150"]
}
}
}
Бюджеты ловят медленный дрейф. Детектор аномалий ловит быстрые скачки внутри бюджета — рекурсивную Lambda, забытый цикл, включённое подробное логирование. Задержка биллинга в AWS — 8–24 часа, и это не лечится: за сутки рекурсивная функция способна выписать пятизначный счёт. Поэтому дополнительно нужны предохранители на уровне сервисов: конкурентность Lambda, maxScale в managed-платформах, квоты на GPU, hard limits в квотах аккаунта.
Отдельная категория трат — зомби-ресурсы. У них предсказуемый жизненный цикл:
«временно, на пару дней» Создан --> Активен: реально используется Активен --> Осиротел: автор ушёл из команды,
проект закрыт Создан --> Осиротел: эксперимент кончился,
ресурс остался Осиротел --> Забыт: нет тегов, нет владельца,
боятся удалять Забыт --> Оплачивается: годами, тихо Оплачивается --> Забыт: обнаружен, но
«вдруг что-то сломается» Забыт --> Помечен: политика: нет тега owner
больше 14 дней Помечен --> Остановлен: остановка,
уведомление в Slack Остановлен --> Активен: кто-то отозвался
и поставил теги Остановлен --> Удалён: 30 дней тишины,
снимок сохранён Удалён --> [*] note right of Оплачивается Типичные жители: EBS без инстанса, снимки старше двух лет, старые AMI, NAT в пустой VPC, простаивающие балансировщики, ElastiCache «на тест». end note
Ключ к безопасному удалению — снимок перед удалением и окно тишины. Инженеры не удаляют ресурсы не из лени, а из страха; процедура, в которой всё обратимо в течение 30 дней, снимает страх и делает уборку рутиной.
Хранение и трафик: где прячется вторая половина счёта
Вычисления оптимизируют все, потому что они очевидны. Хранение и трафик растут монотонно и почти всегда без владельца.
Классы хранения. Объектные хранилища предлагают лестницу: горячий → нечастый доступ → архив → глубокий архив, с разницей в цене до 20 раз. Ловушка — стоимость извлечения и минимальный срок хранения: объект, переведённый в архив и прочитанный через неделю, обойдётся дороже, чем если бы он лежал в горячем. Работает Intelligent-Tiering для непредсказуемых паттернов и явные lifecycle-правила для предсказуемых.
{
"Rules": [
{
"ID": "logs-lifecycle",
"Filter": { "Prefix": "logs/" },
"Status": "Enabled",
"Transitions": [
{ "Days": 30, "StorageClass": "STANDARD_IA" },
{ "Days": 90, "StorageClass": "GLACIER_IR" },
{ "Days": 365, "StorageClass": "DEEP_ARCHIVE" }
],
"Expiration": { "Days": 2555 }
},
{
"ID": "abort-incomplete-uploads",
"Filter": {},
"Status": "Enabled",
"AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 }
},
{
"ID": "expire-old-versions",
"Filter": {},
"Status": "Enabled",
"NoncurrentVersionExpiration": { "NoncurrentDays": 90 }
}
]
}
Последние два правила окупаются чаще первого. Незавершённые multipart-загрузки не видны в листинге бакета, но занимают место и оплачиваются — в бакетах возрастом в несколько лет это регулярно десятки процентов объёма. То же с некурируемыми версиями при включённом версионировании.
Egress. Плата за исходящий трафик — самая спорная строка облачного счёта. Порядок: 0,08–0,12 $/ГБ у гиперскейлеров, 0,01 $/ГБ у DigitalOcean и Hetzner сверх включённого объёма, 0 $ у Cloudflare R2 и Backblaze B2 при отдаче через партнёрскую сеть. Для медийного проекта с 200 ТБ отдачи в месяц разница между S3 и R2 — порядка 16 000 $/мес, что превышает всю остальную инфраструктуру.
Практические шаги, по убыванию отдачи: вынести отдачу статики на CDN с дешёвым или бесплатным egress; поставить VPC-endpoints, чтобы трафик к S3 и другим сервисам не шёл через NAT; сделать сервисы AZ-aware, чтобы реплики читали из своей зоны; включить сжатие на границе; для больших архивов рассмотреть хранилище с нулевым egress.
Регуляторное давление здесь тоже работает: European Data Act, вступивший в силу в 2024 году, обязывает провайдеров убрать плату за перенос данных при смене поставщика, и крупные облака отменили egress при полной миграции. Это касается ухода, а не повседневной отдачи, но снижает главный барьер vendor lock-in.
Типичные ошибки
- Оптимизируют то, что понятно, а не то, что дорого. Неделя на сжатие Docker-образов при том, что 40 % счёта — межзонный трафик.
- Считают железо без людей. Сравнение «сервер за 190 $ против инстанса за 730 $» игнорирует зарплату, дежурства и вторую площадку. С ними разрыв обычно исчезает.
- Считают облако без эластичности. Если вы держите ноды 24/7 на пиковую ёмкость, вы платите за облако цену железа, не получая ни одного его преимущества.
- Коммит поверх раздутых ресурсов. Сначала rightsizing, потом Savings Plan. Иначе вы на три года зафиксировали неэффективность.
- Нет аллокации — есть общий котёл. Пока счёт не адресный, у него нет владельца, а у команд нет обратной связи. Это не финансовая проблема, а проблема проектирования организации.
- Экономят на наблюдаемости и бэкапах. Самые лёгкие для урезания строки, самые дорогие последствия. Один инцидент, отлаженный на четыре часа дольше из-за отсутствия трейсов, съедает годовую экономию на логах.
- Мультиоблако ради переговорной позиции. Двойная сложность и двойная экспертиза стоят дороже скидки, которую вы, скорее всего, не получите.
- Репатриация «потому что дорого» без модели. Переезд на своё железо на стадии поиска product-market fit — это полгода инженерного времени в обмен на неопределённость.
- Забывают про плоскость управления. Один kubernetes-кластер на команду выглядит аккуратно, пока не выяснится, что вы платите за десять control plane и десять комплектов системных демонсетов.
- Считают только прямые затраты. Скорость выхода изменений — тоже деньги. Инфраструктура, которая экономит 30 % и замедляет релизы вдвое, убыточна.
Как это выглядит в зрелой команде
Опишу практику, которую видно у команд, доросших до уровня Run.
Стоимость — часть design review. В шаблоне RFC есть раздел «стоимость» с оценкой: сколько будет стоить решение при текущей нагрузке и при десятикратной. Это ловит нелинейности до написания кода.
Раз в неделю — автоматический отчёт в канал команды: сумма за неделю, дельта к предыдущей, unit-стоимость, топ-3 источника роста. Не письмо от финансов, а сообщение в том же канале, где обсуждаются инциденты.
Раз в месяц — FinOps-ретро на 30 минут: что выросло и почему, что из запланированного сделано, какой эффект получен. Эффект измеряется до и после, а не по обещанию.
Ежеквартально — пересмотр коммитов, отчёт по coverage/utilization и решение о следующей ступени лестницы.
Постоянно — бюджет как SLO: у сервиса есть целевая unit-стоимость, и её пробитие рассматривается наравне с пробитием бюджета ошибок. Это ставит деньги в тот же ряд, что и надёжность, вместо ежеквартальной кампании по сокращению расходов.
Последняя строка важна для тех, кто строит ML-инфраструктуру: там экономика перевёрнута. GPU в дефиците, скидки за коммит достигают 60 %, но главный риск — не переплатить, а не получить ёмкость вообще. Модель «плати по часам, масштабируйся мгновенно» на GPU работает плохо, и логика ближе к закупке железа, чем к облачной эластичности.
Мини-итог
- Счёт за инфраструктуру — следствие архитектуры. Скидки дают проценты, топология — разы.
- Спор «облако против железа» решается одним числом: коэффициентом загрузки. Ниже 20–25 % облако выигрывает почти всегда, выше 70 % — своё железо, между ними ответ зависит от людей и предсказуемости.
- TCO железа включает колокацию, транзит, ЗИП, вторую площадку и зарплату инженера. Без последней строки сравнение бессмысленно.
- Выделенные серверы — недооценённая середина: цена близка к своему железу, эксплуатационная нагрузка близка к облачной.
- FinOps работает в порядке Inform → Optimize → Operate. Пропуск Inform превращает практику в кампанейщину.
- Аллокация через обязательные теги, принуждаемые в Terraform и SCP, — фундамент. Без неё нет владельцев, без владельцев нет оптимизации.
- Unit-экономика важнее абсолютного счёта: абсолют может расти, стоимость единицы — нет.
- Rightsizing перед коммитом. Coverage 70–85 %, utilization выше 98 %, лестница вместо одного прыжка.
- Spot даёт 60–90 % на подходящих нагрузках при широкой диверсификации пулов и корректном завершении подов.
- Стоимость проверяется до мержа (Infracost с порогом) и измеряется после выкатки (OpenCost по неймспейсам).
- Хранение и egress — вторая половина счёта, и почти всегда без владельца. Lifecycle, VPC-endpoints, AZ-локальность, CDN.
- Инцидент со счётом обрабатывается как инцидент: детект, алерт, дежурный, постмортем, предохранители.
Источники
- FinOps Framework — FinOps Foundation — фазы, домены, модель зрелости.
- J. R. Storment, M. Fuller. Cloud FinOps, 2nd ed., O’Reilly, 2023 — канонический текст практики.
- FOCUS — FinOps Open Cost and Usage Specification — единый формат биллинга для мультиоблака.
- AWS Cost and Usage Report — документация и AWS Well-Architected: Cost Optimization Pillar.
- Above the Clouds: A Berkeley View of Cloud Computing (2009) — экономика эластичности с первых принципов.
- a16z: The Cost of Cloud, a Trillion Dollar Paradox — аргумент за репатриацию на масштабе.
- DHH: Why we’re leaving the cloud и The Big Cloud Exit FAQ — публичные числа 37signals.
- Dropbox: Magic Pocket infrastructure — гравитация данных как драйвер переезда.
- OpenCost — документация и Infracost — документация.
- Karpenter и EC2 Spot Instance Advisor — практика spot в Kubernetes.
- Google Cloud: Committed use discounts и AWS Savings Plans — механика обязательств.
Что дальше
Мы научились видеть, во что обходится инфраструктура, и управлять этим числом. Но у инфраструктуры есть вторая цена — цена незнания того, что в ней происходит: часы, потраченные на диагностику вслепую, и инциденты, замеченные клиентами раньше вас. Следующая статья — про то, как построить наблюдаемость, которая окупается, и дежурства, которые не выжигают команду.
Наблюдаемость и дежурства: метрики, логи, трейсы, алерты, SLO и постмортемы