CI/CD, инфраструктура и облака Стоимость инфраструктуры: облако против своего железа, FinOps, реальные расчёты
0%

Стоимость инфраструктуры: облако против своего железа, FinOps, реальные расчёты

Стоимость инфраструктуры: облако против своего железа, 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. Соберём стоимость собственного варианта, ничего не забыв.

Структура TCO своего железа в колокации

Разбор по строкам:

  • Железо. Два двухпроцессорных сервера (по 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 % загрузки облако с реально работающим автоскейлингом дешевле собственного железа с учётом людей. Только выше этой отметки железо начинает выигрывать — и только если нагрузка предсказуема на горизонте амортизации.

Три оговорки, которые превращают эту формулу из лозунга в инструмент.

  1. Эластичность нужно заслужить. Формула предполагает, что вы действительно гасите ёмкость. Если ноды работают 24/7 при средней утилизации CPU 12 % — ваш эффективный d равен 1,0, и вы платите за облако как за пик, получая экономику худшую, чем у железа. Именно так выглядит большинство «дорогих облаков»: не тариф плохой, а автоскейлинг не настроен.
  2. Пиковость важнее среднего. Полезная величина — отношение пика к среднему (peak-to-average ratio). У B2B-SaaS с рабочим днём одного часового пояса оно 4–6: ночью и в выходные нагрузка падает почти до нуля. У инфраструктурного сервиса с глобальной аудиторией — 1,3–1,8. Первому облако почти всегда выгодно, второму — почти никогда.
  3. Не всё в системе имеет одинаковый d. Реляционная база работает 24/7 при d = 1. CI-раннеры — при d = 0,15. Батчи ML-обучения — d = 0,05, но с гигантским пиком. Правильный ответ почти никогда не «всё в облако» или «всё на железо», а гибрид с разной площадкой под разные профили.

Обратите внимание на нижний узел: любое решение о площадке — временное. Профиль нагрузки меняется, цены меняются, команда растёт. Решение, принятое один раз и не пересматриваемое, гарантированно устареет.

Скрипт, который стоит держать под рукой

Модель полезно уметь считать, а не чувствовать. Ниже — маленький калькулятор, который отвечает на вопрос «при какой загрузке что дешевле» и заодно показывает чувствительность к зарплате инженера.

"""Сравнение вариантов размещения по коэффициенту загрузки.

Сложность: 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: «давайте выключим неиспользуемые диски». Это даёт единоразовые проценты и не меняет траекторию. Работает обратный порядок: сначала сделать счёт видимым и адресным, и тогда оптимизация начинает происходить сама — инженер, который видит, что его сервис стоит 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 % обмен ограничен

Рабочие правила:

  1. Коммитьте только базовый слой. Возьмите почасовой график расхода за 90 дней, найдите 5-й перцентиль — это ваш неснижаемый минимум. Коммит на 70–80 % от него — безопасный старт. Покрывать 100 % пика бессмысленно: сверху пик закрывается on-demand и spot.
  2. Лестница вместо одного прыжка. Покупайте порциями раз в квартал с разными датами окончания. Так вы не окажетесь в ситуации, когда 100 % обязательств истекают в один день и вам нужно принять решение на всю сумму сразу.
  3. Следите за двумя числами. Coverage — какая доля потребления покрыта коммитами (цель 70–85 %). Utilization — какая доля купленного обязательства использована (цель 98–100 %). Низкая утилизация означает, что вы платите за воздух, и это хуже, чем отсутствие скидки.
  4. Сначала 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: где деньги теряются незаметно

Кластер добавляет свой слой потерь, невидимый в счёте облака, потому что облако выставляет счёт за ноды, а тратят его поды.

Три источника потерь в 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 в квотах аккаунта.

Отдельная категория трат — зомби-ресурсы. У них предсказуемый жизненный цикл:

Ключ к безопасному удалению — снимок перед удалением и окно тишины. Инженеры не удаляют ресурсы не из лени, а из страха; процедура, в которой всё обратимо в течение 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.
  • Инцидент со счётом обрабатывается как инцидент: детект, алерт, дежурный, постмортем, предохранители.

Источники

Что дальше

Мы научились видеть, во что обходится инфраструктура, и управлять этим числом. Но у инфраструктуры есть вторая цена — цена незнания того, что в ней происходит: часы, потраченные на диагностику вслепую, и инциденты, замеченные клиентами раньше вас. Следующая статья — про то, как построить наблюдаемость, которая окупается, и дежурства, которые не выжигают команду.

Наблюдаемость и дежурства: метрики, логи, трейсы, алерты, SLO и постмортемы

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов