Платформенная инженерия Инфраструктура как API: декларативность, каталог, шаблоны сервисов
0%

Инфраструктура как API: декларативность, каталог, шаблоны сервисов

Инфраструктура как API: декларативность, каталог, шаблоны сервисов

Платформенная команда сделала «инфраструктуру по кнопке»: CLI-команда infra create-service, за которой четырнадцать последовательных вызовов к API облака — репозиторий, реестр образов, роль, политика доступа, база, балансировщик, DNS-запись, дашборд, канал алертов и так далее. Демо прошло отлично: тридцать секунд — и сервис есть.

Через месяц выяснилось следующее. На девятом вызове случается таймаут примерно в одном запуске из двадцати; половина ресурсов создана, половина нет. Повтор команды падает с AlreadyExists. Инженер идёт в веб-консоль облака и доделывает руками — быстрее, чем разбираться. Через полгода в облаке лежат сорок безымянных групп безопасности и одиннадцать балансировщиков, к которым не привязан ни один сервис; счёт вырос на заметную величину, и никто в компании не может объяснить, чей это ресурс и можно ли его удалить. Каталог сервисов на портале показывает восемьдесят записей, из них двадцать три — про сервисы, выключенные в прошлом году.

Команда сделала не API. Она сделала скрипт с сетевыми вызовами. Разница не в оформлении, а в свойствах: у скрипта нет понятия «желаемое состояние», поэтому нет ни повторяемости, ни сходимости, ни ответа на вопрос «что сейчас есть на самом деле».

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

Что значит «инфраструктура как API» и чем это не является

Полезное определение, от которого можно проверять себя: API платформы — это набор именованных ресурсов со схемой, семантикой применения и обещанием совместимости. Транспорт вторичен: это может быть HTTP-эндпоинт, kubectl, файл в git-репозитории или ваш CLI. Первичны четыре свойства.

  1. Описываемость. Желаемое состояние выражается данными в файле, а не последовательностью команд. Файл можно посмотреть в ревью, положить в git, сравнить между окружениями.
  2. Повторяемость. Применение одного и того же описания дважды даёт то же состояние. Это идемпотентность в чистом виде — фундаментальное свойство, которое ломается первым и чинится тяжелее всего (про идемпотентность и доставку).
  3. Наблюдаемость. У ресурса есть статус, по которому видно: применено, применяется, не применилось и почему.
  4. Совместимость. Схема версионирована, ломающие изменения объявляются заранее, старая версия продолжает работать оговорённый срок.

Уберите любое — и это уже не API, а автоматизация с человеческим лицом. Полезная, но с другими свойствами и другой ценой.

Поколение Как выглядит Что ломается Кто платит
Заявка в тикете «Заведите базу, реквизиты пришлите» Очередь, потеря контекста, разные результаты у разных дежурных Продуктовая команда — ожиданием
Скрипт create-service.sh с вызовами облака Частичное выполнение, повтор невозможен, сироты Платформа — разбором последствий
Императивный API POST /services, DELETE /services/{id} Ретраи, состояние гонки, дрейф после ручной правки Обе стороны — в инциденте
Декларативный ресурс service.yaml + контроллер сходимости Сложность реализации, риск «починки» ручных правок Платформа — постоянно, но предсказуемо

Заметьте, что таблица не заканчивается «декларативность всегда лучше». Она заканчивается перераспределением цены. Декларативный слой стоит дорого и постоянно; окупается он только при потоке однотипных операций. Про это будет отдельный раздел с расчётом.

И главный тест, который стоит применять к любому платформенному API до того, как вы напишете первую строчку: пользователи вашего API — инженеры продуктовых команд, у них есть альтернатива. Альтернатива — открыть консоль облака, написать свой Terraform, попросить знакомого из соседней команды. Если ваш API дороже альтернативы по времени или по когнитивной нагрузке, им не будут пользоваться, каким бы правильным он ни был. Это измеримо: доля ресурсов в облаке, созданных через платформу, против созданных мимо неё — самая честная метрика этой главы, и её можно посчитать по тегам ресурсов в первый же день.

Декларативность: желаемое состояние и сходимость

Декларативная модель — это три множества и петля между ними: желаемое состояние (что просил пользователь), наблюдаемое состояние (что есть в реальности), действие (шаг в сторону желаемого).

Анатомия платформенного ресурса и петля сходимости

Ключевая деталь, которую чаще всего теряют: петля работает постоянно, а не один раз при создании. Тим Хокин из команды Kubernetes формулирует это как разницу между реакцией на событие и реакцией на состояние (Edge vs. Level Triggered Logic). Реакция на событие («пришёл запрос — создай») теряет работу при любом сбое доставки: событие пропало, и никто об этом не узнает. Реакция на состояние («сравни и подвинь») самовосстанавливается: следующий проход всё равно увидит расхождение.

Практически это значит, что контроллер обязан быть написан как функция «посмотреть — сравнить — сделать маленький шаг», а не как «выполнить сценарий создания». Псевдокод, который стоит держать в голове:

цикл каждые N секунд или по уведомлению:
    для каждого ресурса R:
        желаемое   = прочитать spec(R)
        наблюдаемое = опросить внешние системы по идентификаторам R
        расхождение = сравнить(желаемое, наблюдаемое)
        если расхождение пусто:
            записать status: Ready, observedGeneration = generation(R)
            продолжить
        если R помечен как приостановленный:
            записать status: Drifted + описание расхождения, НЕ трогать
            продолжить
        применить(один шаг расхождения)   # идемпотентно, с ключом запроса
        записать status: Progressing + причина + человекочитаемое сообщение

Четыре нюанса, которые отличают рабочую реализацию от демонстрационной.

Идемпотентность — обязанность реализации, а не свойство слова «декларативно». Вызов создания ресурса в облаке должен нести ключ запроса (clientToken, Idempotency-Key), иначе ретрай после таймаута создаст второй балансировщик. Google формулирует это как отдельное требование к API-дизайну (AIP-155). Ровно этой мелочи не хватало в истории из начала главы.

Декларативно не значит мгновенно. База данных создаётся минуты, сертификат выпускается десятки секунд, запись DNS расходится по резолверам. Значит, у ресурса обязан быть асинхронный статус с условиями (conditions), и observedGeneration — номер поколения спецификации, которое контроллер уже видел. Без него пользователь смотрит на Ready: true, относящийся к позавчерашней версии описания, и делает неверные выводы. Конвенции статусов у Kubernetes проработаны и годятся как готовый образец: API conventions.

Удаление — самая опасная часть петли. Логика «привести к желаемому» симметрична: убрали строчку из файла — контроллер уберёт ресурс. Для очереди сообщений это нормально, для продовой базы данных — катастрофа. Обязательны: политика удаления на уровне ресурса (deletionPolicy: Retain по умолчанию для всего, что хранит данные), защита от удаления (finalizer или флаг), явное подтверждение в CLI и предпросмотр разрушающих действий в проверке пул-реквеста. Правило простое: разрушающая операция никогда не выполняется как побочный эффект слияния PR без отдельного явного согласия.

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

Жизненный цикл ресурса удобно держать перед глазами как конечный автомат — он же определяет, какие состояния обязан различать ваш статус.

Отдельно стоит сказать про владение полями. Как только над одним объектом работают и человек, и контроллер, и другой контроллер, возникает вопрос «кто последний записал и имел ли право». Наивная реализация «перезаписать целиком» стирает чужие изменения молча. Kubernetes решает это через server-side apply с менеджерами полей (документация); в своём API минимальный аналог — хранить, кто последний менял каждую ветку спецификации, и при конфликте выдавать понятную ошибку вместо тихой перезаписи.

Проектирование самого API: ресурсы, схема, ошибки

Ресурсы называются на языке пользователя

Первый и самый частый провал платформенных API — обёртка над облаком один в один. Появляются ресурсы S3BucketRequest, RDSInstanceRequest, IAMRoleRequest, поля которых повторяют поля облачного API, только через свою валидацию и с задержкой в неделю на добавление каждого нового параметра. Пользователь получает всё то же самое, но хуже: документации в интернете нет, примеров нет, поддержка новых возможностей отстаёт, а обойти нельзя.

Проверка на обёртку занимает минуту: возьмите свою схему и схему облака и посчитайте поля, которые пользователь больше не заполняет. Если таких нет — вы построили косвенность и берёте за неё плату чужим временем.

Хорошие имена ресурсов приходят из предметной области ваших пользователей, а не из прайс-листа облака: Service, ScheduledJob, Topic, ObjectStore, Database, Environment. Google описывает этот подход как ресурсо-ориентированный дизайн (AIP-121), и он ровно так же работает для внутренних платформ.

# service.yaml — то, что пишет продуктовая команда.
apiVersion: platform/v1
kind: Service
metadata:
  name: checkout
  owner: team-payments          # группа в корпоративном каталоге, а не строка
spec:
  tier: critical                # enum: critical | standard | batch
  language: go
  exposure: internal            # enum: public | internal | private
  resources:
    cpu: "2"
    memory: 2Gi
  dependsOn:                    # из этого строится граф каталога
    - service/ledger
    - topic/orders.v2
  datastores:
    - kind: postgres
      name: checkout-main
      size: small               # enum вместо класса инстанса конкретного облака
      deletionPolicy: Retain    # разрушающее действие требует явной смены
  slo:
    availability: "99.9"        # связывает контракт с дежурством и алертами
    latencyP99Ms: 300

Здесь ни одного поля из словаря облака: size: small вместо db.r6g.large, tier: critical вместо шести отдельных настроек надёжности. Пользователь принимает решения в своих терминах; трансляцию в «инстанс такого-то класса в трёх зонах» платформа делает сама и может поменять поставщика, не трогая сорок репозиториев. Связь slo с дежурством и алертами — прямое продолжение главы про SLI и SLO: класс сервиса перестаёт быть словом в вики и становится данными, из которых генерируются пороги.

Схема — это тоже интерфейс

Схема должна быть строгой там, где строгость помогает, и молчаливой там, где решение уже принято за пользователя.

# Фрагмент схемы ресурса. Показательны три вещи:
# перечисления вместо свободных строк, дефолты и запрет неизвестных полей.
openAPIV3Schema:
  type: object
  required: [tier, exposure]
  additionalProperties: false        # опечатка в имени поля - ошибка, а не тишина
  properties:
    tier:
      type: string
      enum: [critical, standard, batch]
      description: "класс сервиса; определяет реплики, разнос по зонам, дежурство"
    exposure:
      type: string
      enum: [public, internal, private]
      default: internal
    resources:
      type: object
      properties:
        cpu:    { type: string, pattern: '^[0-9]+(\.[0-9]+)?$', default: "1" }
        memory: { type: string, pattern: '^[0-9]+(Mi|Gi)$',    default: "512Mi" }
    datastores:
      type: array
      maxItems: 5                    # ограничитель, а не бесконечная свобода
      items:
        type: object
        required: [kind, name]
        properties:
          kind: { type: string, enum: [postgres, redis, objectstore] }
          # безопасное значение по умолчанию: удаление требует явной смены
          deletionPolicy: { type: string, enum: [Retain, Delete], default: Retain }

additionalProperties: false заслуживает отдельного слова. Схема, принимающая неизвестные поля, порождает самый неприятный класс обращений в поддержку: «я написал replicaCount, а ничего не изменилось». Пользователь считает, что задал параметр; система его молча проигнорировала. Строгая схема превращает это в ошибку на этапе проверки PR — за секунды, а не за час отладки.

Ошибки: три эшелона проверки

Правило: чем раньше пользователь узнает об ошибке, тем дешевле она стоит, и разница здесь на порядки.

Эшелон Когда срабатывает Что ловит Стоимость ошибки для пользователя
Схема локально и в проверке PR, секунды опечатки, типы, диапазоны, обязательные поля секунды
Политика в проверке PR, секунды «публичный доступ запрещён для tier critical без ревью безопасности», лимиты, именование минуты, до слияния
Реконсиляция после слияния, минуты квоты, конфликты имён, недоступность внешней системы десятки минут, уже в общей ветке

Из этого следует практический вывод: всё, что можно проверить по данным, проверяется до слияния. Проверка политик на этапе PR — та же идея, что и тесты в конвейере (тесты в CI), только для описаний инфраструктуры.

И обязательный элемент, отсутствие которого делает декларативный API страшным: предпросмотр. Пользователь должен видеть, что произойдёт, до того как это произойдёт.

$ svc plan --env prod
Изменения для checkout (окружение prod):

  ~ Service/checkout
      tier: standard → critical
        ⤷ реплики 2 → 4, разнос по зонам, PDB minAvailable 2
        ⤷ дежурство: подключается страница team-payments
  + Topic/orders.dlq            создать
  + Alert/checkout-availability создать из SLO 99.9

1 разрушающее действие:
  - Database/checkout-legacy    БУДЕТ УДАЛЕНА (deletionPolicy: Delete)
      данных: 41 GB, последняя запись: 3 дня назад
      подтвердить: svc apply --confirm-destroy Database/checkout-legacy

Оценка изменения расходов: +180 USD в месяц (реплики и второй узел).

Три вещи, за которые этот вывод стоит своих денег: развёрнутые последствия однострочной правки (tier тянет за собой пять решений), явное выделение разрушающего действия с отдельным подтверждением и стоимость изменения прямо в плане — иначе разговор о деньгах случается через месяц, когда приходит счёт (глава про стоимость).

Версионирование: почему API — это обещание на годы

Вернер Фогельс сформулировал самый неудобный факт про API одной фразой: API навсегда (10 уроков за 10 лет AWS). Для внутренней платформы это чуть мягче — вы можете договориться с сорока командами, — но каждый такой договор стоит недель календарного времени и репутации.

К этому добавляется закон Хайрума (hyrumslaw.com): при достаточном числе пользователей неважно, что вы обещали в контракте, — все наблюдаемые особенности поведения станут чьей-то зависимостью. Кто-то заметил, что имя балансировщика формируется как svc-<имя>-lb, и захардкодил его в скрипте. Вы поменяли шаблон имени — сломался чужой скрипт. Формально вы ничего не обещали; практически вы сломали поставку.

Изменение Совместимо Что делать
Добавить необязательное поле с дефолтом да выпускать в минорной версии
Добавить значение в enum нет, если старые клиенты валидируют строго новая минорная + время на обновление валидаторов
Сделать поле обязательным нет новая мажорная версия + миграция
Ужесточить дефолт, меняющий поведение нет, хуже всего — это тихое изменение объявление, окно, автоматический PR
Переименовать поле нет оба имени какое-то время, конверсия на входе
Убрать поле нет объявление, окно не меньше двух кварталов, отключение
Изменить формат генерируемых имён формально да, фактически нет считать ломающим, см. закон Хайрума

Механика, которая работает на практике и почти не имеет альтернатив:

  1. Одно внутреннее представление, несколько внешних версий. Контроллер работает с внутренней структурой; на входе и выходе — конверсия из v1beta1, v1 и так далее. Не пытайтесь поддерживать две реализации.
  2. Политика устаревания, записанная заранее. Kubernetes публикует свою и придерживается её (deprecation policy); её можно взять почти дословно. Ключевое — объявить окно до того, как оно понадобилось.
  3. Мигрирует платформа, а не сорок команд. Это правило, которое отделяет платформу-продукт от платформы-налога. Меняете схему — пишете codemod и отправляете сорок пул-реквестов сами, с зелёными проверками; команде остаётся нажать «слить». Если вы вместо этого рассылаете письмо «до конца квартала всем перейти на v2», вы просто переложили свою работу на пользователей и сделали это в масштабе. Подробно о механике массовых переводов — глава про миграции.
  4. Метрика вместо ощущения. Доля сервисов на актуальной версии API и возраст самой старой живой версии — два числа, которые должны висеть на дашборде платформы рядом с метриками принятия.

Каталог: не витрина, а модель данных

Каталог обычно вспоминают как «нам нужен портал». Это неправильный порядок мыслей. Каталог — это модель данных о вашей инженерной реальности, а портал — одно из окон в неё, причём далеко не первое по важности.

Три свойства отличают полезный каталог от мёртвого.

Каталог — производная, а не форма. Всё, что человек должен заполнить руками в отдельном интерфейсе, устаревает за квартал. Источники правды должны быть теми же, где инженер уже работает: service.yaml в репозитории (владелец, класс, зависимости, SLO), факты выкаток из конвейера (что реально крутится), теги ресурсов в облаке (деньги), корпоративный каталог групп (живые ли люди в команде-владельце), система дежурств. Каталог собирается из них — и никакой кнопки «зарегистрировать сервис».

У каталога есть API. Каталог, доступный только глазами через веб-страницу, не участвует в автоматизации, а значит, не окупается. Ценность появляется тогда, когда владелец сервиса подставляется в алерт автоматически, страница инцидента сразу показывает граф зависимостей, а бот перед сменой общего базового образа сам находит затронутые репозитории. Ровно поэтому портал стоит делать последним: сначала данные и программный доступ, потом витрина.

Каталог сверяется с реальностью. Три множества — объявленное, развёрнутое и оплачиваемое — почти никогда не совпадают, и разница между ними и есть настоящая работа.

"""Сверка каталога с реальностью: три источника, четыре вида расхождений.

Сложность: O(n + m + k) по времени и по памяти — только хеш-множества,
без вложенных проходов; n, m, k — размеры источников.
"""

def сверить(объявленные: set, развёрнутые: set, оплачиваемые: set) -> dict[str, set]:
    return {
        # описание есть, в проде ничего нет: чистить каталог или доводить выкатку
        "призраки": объявленные - развёрнутые,
        # работает мимо платформы: найти владельца, предложить переезд, не ломать
        "теневые": развёрнутые - объявленные,
        # платим и не используем: кандидаты на удаление после подтверждения
        "сироты": оплачиваемые - развёрнутые - объявленные,
        # владелец расформирован: эскалировать до руководителя направления
        "безхозные": {s for s in объявленные & развёрнутые if not владелец_жив(s)},
    }


def владелец_жив(сервис: str) -> bool:
    """Владелец жив, если это группа с активными участниками и дежурством.
    Строка «команда платежей» в поле owner владельцем не является."""
    группа = каталог_групп.получить(владелец_сервиса(сервис))
    return группа is not None and группа.активные_участники and группа.дежурство

Метрика «доля сервисов с проверяемым владельцем» — одна из немногих, ради которых каталог вообще имеет смысл. Проверяется она не наличием строки в поле owner, а тем, что за этой строкой стоит группа с живыми людьми и настроенным дежурством. В момент инцидента ценность каталога равна вероятности за минуту ответить на вопрос «кому звонить» (дежурства).

Про инструменты — без рекламы. Backstage даёт готовую модель каталога и механизмы автоматического обнаружения сущностей (документация каталога), и это разумный выбор, если у вас десятки команд и есть кому сопровождать приложение на TypeScript с постоянными обновлениями плагинов. При десяти сервисах ту же задачу закрывает таблица в базе, наполняемая из репозиториев по расписанию, плюс команда svc who-owns в CLI — и стоит это не человека в год, а неделю работы. Выбор инструмента определяется масштабом и наличием сопровождающего, а не тем, что о нём написано в отраслевых обзорах.

Шаблоны сервисов: скаффолд — это форк

Шаблон нового сервиса — самая заметная и самая недооценённая часть платформенного API. Заметная, потому что именно с неё начинается опыт нового пользователя. Недооценённая, потому что момент генерации решает задачу первого дня, а платформа живёт с последствиями двухсотого.

Дрейф шаблона: доля репозиториев на актуальной версии

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

Поэтому первое проектное решение по шаблону — что в нём является копией, а что зависимостью.

Верхний правый квадрант — всё, что меняется часто и одинаково у всех, — обязан быть зависимостью с версией, а не скопированным текстом: базовый образ вместо инструкций сборки в каждом Dockerfile, переиспользуемый workflow вместо копии шагов конвейера (общий конвейер как платформенный сервис), библиотека наблюдаемости вместо инструкций «добавьте эти двадцать строк» (наблюдаемость как платформа).

Стратегия Как обновляется Цена платформе Риск
Копия при генерации никак, только вручную командой почти ноль дрейф, ручная работа при каждом общем изменении
Зависимость с версией обновлением версии, бот шлёт PR сопровождение артефакта и совместимости ломающее изменение бьёт по всем сразу
Управляемый файл платформа шлёт PR на конкретные файлы инструмент обновления и разбор конфликтов ощущение, что «в моём репозитории хозяйничают»
Рантайм платформы ничего не лежит в репозитории максимальная, это уже контроллер негибкость, обходы

Между «копией» и «зависимостью» есть практичная середина — шаблон, умеющий обновляться. Инструмент copier хранит в репозитории отметку о версии шаблона и умеет накатывать разницу между версиями с разрешением конфликтов (документация по обновлению). Для файлов, которые платформа считает своими, работает связка «маркер владения в шапке файла плюс запись в CODEOWNERS плюс бот, который шлёт PR». Обновления зависимостей закрываются готовыми решениями вроде Renovate.

Ключевой критерий здоровья: доля репозиториев на актуальной версии шаблона и её распределение по возрасту. Если p90 возраста шаблона в репозиториях больше квартала, у вас не платформа, а исторический архив, и любое общее изменение будет стоить месяцы.

И честное ограничение: шаблон не делает архитектурную работу. «Сгенерировали сервис за минуту» — это про минуту, а не про то, что сервис нужен. Платформа, поощряющая создание сервисов, слишком легко получает сто сервисов при двадцати инженерах, а с ними операционную нагрузку, которую сама же потом будет разгребать. Это классическая локальная оптимизация — быстрее в одном месте, дороже в системе целиком (про локальную оптимизацию).

Цена этого слоя и как считать окупаемость

Платформенная команда не пишет продукт. Каждый её человеко-месяц — это человеко-месяц, не потраченный на функции, за которые компания получает деньги. Значит, слой «инфраструктура как API» обязан возвращать больше времени, чем потребляет, и этот расчёт нужно делать вслух, а не по ощущениям, иначе платформа превращается в налог, который просто никто не выставляет счётом. Порядок величин для планирования (уточняйте по своей ситуации, но не занижайте):

Компонент Разовые затраты Постоянные затраты Что даёт
Схема + валидация + план 1–2 человеко-месяца 0,1–0,2 человека ранние ошибки, предсказуемость
Контроллер сходимости 3–6 человеко-месяцев 0,5–1 человек, включая дежурство самообслуживание, отсутствие сирот
Каталог как производная 1–2 человеко-месяца 0,2–0,4 человека владельцы, граф, разложенные расходы
Шаблоны с автообновлением 0,5–1 человеко-месяц 0,2–0,3 человека общее изменение стоит один PR-бот
Портал поверх всего этого 2–4 человеко-месяца 0,5–1,5 человека витрина; сам по себе не окупается

Обратите внимание на постоянную колонку: контроллер — это дежурство. Как только сорок сервисов зависят от вашей петли сходимости, её падение — инцидент, и у вашей платформы появляются собственные SLO (SLI и SLO). Это не метафора: доступность API платформы, доля успешных применений, время от слияния PR до Ready в 95-м перцентиле — нормальные измеримые обещания, которые вы даёте своим пользователям.

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

Операция Раз в год Без платформы С платформой Экономия в год
Новый сервис от нуля до прода 25 3 дня инженера + 1 день платформы 2 часа около 90 человеко-дней
Общее изменение во всех репозиториях 6 40 команд × 2 часа = 80 часов 1 бот + 40 нажатий «слить» около 55 человеко-дней
Поиск владельца в инциденте 120 15 минут в среднем меньше минуты около 4 человеко-дней, но в самое дорогое время
Новая очередь или база 60 1,5 дня ожидания заявки 20 минут около 80 человеко-дней
Ответ аудиту «кто имеет доступ» 4 2 недели ручного сбора выгрузка из каталога около 35 человеко-дней

Итог такой таблицы сравнивается с суммой постоянных затрат — примерно 1,5–2,5 человека при полном наборе компонентов. Порядок величин обычно сходится при трёх десятках команд и обычно не сходится при пяти: там дешевле общий репозиторий с модулями IaC и хорошая документация (Terraform и IaC). Аккуратные оговорки к расчёту: в экономию нельзя записывать время, которое команда не потратила бы вообще; нельзя считать выигрыш на сервисах, которых не было бы без платформы; и обязательно нужно вычитать время, которое команды тратят на обходы и на обучение вашему контракту. Детальнее эта арифметика разбирается в главе про стоимость и в главе про платформенную команду.

Отдельная строка расходов, о которой забывают, — цена владения инструментами. Kubernetes даёт готовый механизм расширения через собственные ресурсы и контроллеры (паттерн оператора), но требует нескольких обновлений кластера в год со сверкой совместимости всего, что в него воткнуто (Kubernetes). Crossplane переносит управление ресурсами облака в ту же петлю сходимости (документация) — вместе с семантикой удаления, ошибка в которой стоит продовой базы. Принципы GitOps (opengitops.dev) описывают дисциплину, а не продукт: декларативность, версионируемость, автоматическое получение изменений и непрерывная сходимость — их можно соблюдать и без модного стека. Фильтр перед добавлением любого инструмента один: назовите пользовательское решение, которое он убирает, и человеко-часы в год, которые он потребует. Если на первый вопрос ответ «он современный», сделки нет (Choose Boring Technology).

Золотой путь остаётся путём: что делать с теми, кому он не подходит

Декларативный API — самое удобное место, чтобы превратить золотой путь в забор. Схема строгая, политики жёсткие, обходов нет — красиво в презентации и убийственно на практике. Команда с инференсом моделей на GPU, команда с legacy на виртуалках, команда, которой регулятор предписал отдельный контур, — все они получают «нельзя» и уходят строить теневую платформу, о которой вы узнаете во время инцидента.

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

Технически частичное владение выражается прямо в API:

spec:
  tier: critical
  overrides:
    # Поле, которое платформа перестаёт вести. Не «raw для чего угодно»,
    # а конкретная ветка с обязательными атрибутами.
    topologySpreadConstraints:
      managedBy: team-payments
      reason: "требование из разбора инцидента INC-2291, обе реплики в одной зоне"
      expires: 2026-10-01          # дата пересмотра, а не «навсегда»
      issue: PLAT-874              # задача платформы на штатную поддержку
      value:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone

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

Типовые провалы этого слоя

  • Обёртка над облаком один в один. Ресурсы называются как в прайс-листе провайдера, поля повторяются, ни одно решение не убрано, зато добавлена неделя ожидания на каждый новый параметр. Проверка: сколько полей пользователь перестал заполнять. Ноль — значит, вы взяли деньги за косвенность.
  • Портал первым, данные потом. Красивая витрина поверх каталога, который заполняют вручную. Через квартал данные протухли, портал даёт неверные ответы в инциденте, и им перестают пользоваться. Единственное лечение — начинать с данных и программного доступа, витрину делать последней.
  • Одна абстракция поверх трёх разных потребностей. Веб-сервис, пакетное задание и потоковый обработчик засунуты в один kind: Service. Схема растёт объединением полей, половина из которых игнорируется в двух случаях из трёх, а документация начинается со слов «если у вас batch, то поля с 7 по 19 не используются». Дешевле общая реализация под тремя узкими контрактами, чем общий интерфейс.
  • API без версий. Схема меняется прямо в главной ветке, потому что «пользователи внутренние, договоримся». Первое же ломающее изменение в пятницу вечером стоит доверия, которое восстанавливается кварталами.
  • Реконсиляция без паузы и без предпросмотра. Контроллер откатывает ручные правки во время инцидента; PR сливают, не видя, что удалится база. Оба поведения технически корректны и оба непростительны.
  • Статус без причин. Degraded, ResourceNotReady, reconciliation failed (4/5) — пользователь не знает, что делать, и идёт к вам. Ваша поддержка становится узким местом чужой поставки. Рядом стоит каталог как отчётность для руководства: поля, которые не влияют ни на что и нужны только для слайда, заполняют один раз, а потом врут.
  • Шаблон без стратегии обновления. День 1 идеален, день 200 — сорок форков и невозможность выкатить общее изменение.
  • Метрики полноты вместо метрик принятия. «Поддержали двенадцать типов ресурсов» — активность. «Восемьдесят процентов продовых сервисов управляются через API, теневых нет» — результат (продуктовые метрики). Разница видна сразу: первая цифра растёт от работы платформы, вторая — только от решений её пользователей.

Что мерить

Метрика Как считать О чём говорит
Покрытие API ресурсы в облаке с меткой платформы / все ресурсы главная метрика принятия; растёт медленно, падает быстро
Теневые ресурсы продовые ресурсы без владельца в каталоге любое ненулевое значение — работа на ближайший квартал
Доля успешных применений applied без ручного вмешательства / все качество петли; ниже 95 % — пользователи начнут обходить
Время от merge до Ready p50 и p95 обещание платформы; p95 важнее среднего
Доля сервисов на актуальной версии API по полю apiVersion; плюс возраст старейшей живой версии если меньше 80 % — миграции не работают и долг не закрывается
Проверяемое владение сервисы с живой группой-владельцем / все ценность каталога в инциденте
Дрейф шаблона распределение репозиториев по версии, p90 возраста стоимость следующего общего изменения
Расширения с истёкшим сроком из реестра overrides превращаются ли исключения в бэклог

Мини-итог

  • «Инфраструктура как API» — это не эндпоинт, а четыре обязательства: описываемость, повторяемость, наблюдаемость, совместимость. Отсутствие любого превращает API обратно в скрипт с сетевыми вызовами.
  • Декларативность работает через петлю сходимости, которая крутится постоянно. Идемпотентность с ключом запроса, асинхронный статус с observedGeneration, безопасная политика удаления и режим паузы на время инцидента — не украшения, а условия работоспособности.
  • Ресурсы называются на языке пользователя: если ваша схема повторяет схему облака, вы построили косвенность и берёте за неё плату чужим временем. Схема, политики и предпросмотр ловят ошибки за секунды вместо десятков минут, а план изменений обязан показывать последствия, разрушающие действия и стоимость.
  • API — обещание на годы. Версии, окно устаревания и правило «мигрирует платформа, а не сорок команд» отличают продукт от налога. Каталог полезен, пока он производная от реальности и имеет программный доступ: ручной каталог опаснее отсутствующего, потому что даёт неверные ответы в момент инцидента.
  • Скаффолд — это форк. То, что меняется часто и одинаково у всех, обязано быть зависимостью с версией, иначе каждое общее изменение стоит сорок задач в чужих бэклогах.
  • Считайте окупаемость в потоке операций, а не в разовых победах, и вычитайте обходы и обучение. Полный набор компонентов — это 1,5–2,5 постоянных человека; он сходится при трёх десятках команд и обычно не сходится при пяти.
  • Золотой путь остаётся путём, пока в API есть частичное владение с владельцем, причиной, сроком и задачей. Пустой реестр исключений — это слепота, а не порядок.

Источники

Что дальше

Мы собрали инженерный каркас платформы: декларативный контракт с честной семантикой применения, версионирование как обещание, каталог, выведенный из реальности, и шаблоны, которые умеют обновляться. Всё это описывает, как ресурсы появляются и живут, — но ничего не говорит о том, кто имеет к ним доступ, где лежат секреты и что вы ответите аудитору. Хорошая новость в том, что декларативный API и каталог с проверяемым владением делают эти вопросы разрешимыми: требования безопасности перестают быть чек-листом в вики и становятся политикой, встроенной в тот самый золотой путь, — при условии, что она не превращает путь в забор.

Безопасность по умолчанию: секреты, доступы, соответствие требованиям

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

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

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

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