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

Абстракции: где скрывать сложность, а где вредно

Абстракции: где скрывать сложность, а где вредно

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

Потом пришла команда платежей. Им нужен topologySpreadConstraints по зонам — требование из разбора инцидента, где обе реплики оказались в одной зоне отказа. В схеме контракта такого поля нет. Заводят задачу платформе. Платформа отвечает честно: «в спринт не влезет, возьмём через две недели, потом ещё неделя на раскатку контроллера». Команда платежей ждёт три дня, потом кладёт рядом с сервисом обычный deployment.yaml, добавляет в свой пайплайн kubectl apply и уходит. Сервис работает. Платформа об этом сервисе больше ничего не знает: он не в каталоге, у него нет платформенных алертов, его затраты не попадают в отчёт, при следующей централизованной миграции ингресса он молча сломается.

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

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

Что такое абстракция и чем она не является

Определение, от которого удобно считать: абстракция — это перенос решения. Было решение, которое принимал пользователь; стало решение, которое принято за него — один раз, кем-то другим, и зафиксировано в контракте. Всё остальное — следствия.

Джон Оустерхаут в «A Philosophy of Software Design» (книга) даёт критерий, который для платформ работает лучше любого другого: модуль полезен, если он глубокий — простой интерфейс поверх существенной функциональности. Мелкий модуль (интерфейс не проще того, что он скрывает) имеет отрицательную ценность: он добавляет сущность, которую надо изучить, ничего не убрав.

Проверка: сколько решений пользователь НЕ принимает благодаря вашему контракту?

replicas: 3 в вашем контракте вместо spec.replicas: 3 в манифесте
    → ноль перенесённых решений. Это переименование.

tier: critical, из которого следуют три реплики, антиаффинити, разнос по зонам,
PDB, отдельный пул узлов и страница дежурного
    → шесть перенесённых решений. Это абстракция.

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

Дейкстра сформулировал, зачем это вообще нужно, в «The Humble Programmer» (1972): цель абстрагирования — не быть расплывчатым, а создать новый смысловой уровень, на котором можно быть абсолютно точным. Для платформы это значит: у контракта должен быть свой словарь и свои гарантии, а не пересказ нижнего слоя своими словами. «Критичный сервис», «внутренний API», «фоновая задача с гарантией однократной обработки» — это словарь. «Deployment с ресурсными лимитами» — это пересказ.

Абстракция, шаблон и политика — три разные вещи

Их постоянно путают, а цена владения у них отличается на порядок — сначала выбор формы, потом таблица со счётом.

Форма Что связывает вас с пользователем Цена владения Обратимость Кто отлаживает протечку
Шаблон, скаффолд Момент генерации Низкая Высокая: файлы уже у команды Команда
Библиотека, SDK Версия в сборке Средняя Средняя: нужен апгрейд Обе стороны
Контракт + контроллер Каждый прогон реконсиляции Высокая Низкая: снять нельзя без миграции Платформа
CLI-обёртка над API облака Каждый вызов Средняя, растёт с API Средняя Платформа
Политика, guardrail Момент проверки Низкая Высокая Команда, но с подсказкой

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

Закон протекания и обязанности абстракции

Джоэл Спольски в «The Law of Leaky Abstractions» (2002) сформулировал: все нетривиальные абстракции в какой-то степени протекают. Для платформы это не пессимизм, а проектное требование: протечка — это штатный режим, а не сбой. Вопрос не «протечёт ли», а «что произойдёт в момент протечки».

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

Плохо:

$ svc deploy checkout
Error: reconciliation failed (attempt 4/5)
  status: Degraded
  reason: ResourceNotReady

Хорошо:

$ svc deploy checkout
✗ Выкладка checkout остановлена на шаге "проверка готовности"

  Контейнер стартовал и упал 4 раза подряд с кодом 1.
  Последние строки stdout:
      fatal: config key "PAYMENTS_API_URL" is not set

  Похоже, переменная окружения не объявлена в контракте сервиса.
  Добавьте её в раздел env или в секреты:  svc secret set checkout PAYMENTS_API_URL
  Полный лог:      svc logs checkout --revision 41 --previous
  Нижний слой:     svc debug checkout --raw   (покажет объекты Kubernetes как есть)

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

Восемнадцать минут — не выдумка, это типичная стоимость лишнего звена эскалации ночью. Считать её надо в тех же терминах, что и остальную надёжность: см. SLO и бюджет ошибок и разбор инцидентов. У платформы есть собственные SLO, и «время до понятного диагноза» — законный SLI, который стоит измерять наравне с доступностью. Отсюда правило, которое стоит записать в принципы команды:

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

Где скрывать полезно, а где вредно

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

Скрывать полезно:

  • Обязательный обвес, одинаковый для всех: разметка ресурсов, теги для учёта затрат, базовый набор меток, экспортёры метрик, инструментирование трейсинга, ротация журналов. Ни одно из этих решений не делает продукт лучше, но каждое обязано быть принято. Идеальный кандидат.
  • Решения, где ошибка дорогая и обнаруживается поздно: сетевые политики, права IAM, сроки хранения данных, шифрование в покое. Здесь абстракция ценна не экономией времени, а тем, что убирает класс ошибок — подробнее в главе о безопасности по умолчанию.
  • Решения, требующие редкой экспертизы: пулы соединений к базе, параметры HPA под конкретный профиль нагрузки, работа с квотами облака. Сюда же сопряжение с быстро меняющимся нижним API — но только если вы готовы держать стабильный контракт годами: пообещать стабильность и не удержать хуже, чем не обещать.

Скрывать вредно:

  • Деньги. Если команда не видит, что её сервис стоит 4 200 USD в месяц, она не может принять ни одного разумного решения о ресурсах. Абстракция может убрать выбор типа инстанса, но обязана показать счёт. Подробно — в главе о стоимости, общий контекст цен облака — в devops.
  • Надёжность. Нельзя спрятать от команды, что её сервис не переживёт отказ зоны. Уровень отказоустойчивости — продуктовое решение, а не платформенное.
  • Физика распределённых систем. Задержки, частичные отказы, порядок сообщений, консистентность — всё это протекает через любой слой и обязано протекать. Абстракция, делающая сетевой вызов похожим на локальный, вредна: она стирает ровно ту информацию, которая нужна для проектирования. См. модели отказов и модели согласованности.
  • Схема данных и миграции. Платформа может дать команде базу за одну строку в контракте; она не может решить за команду, как мигрировать таблицу на десять миллиардов строк без простоя — см. databases.
  • То, что меняется быстрее вашего цикла разработки. Если облако выпускает возможность раз в месяц, а вы протаскиваете её в контракт за квартал, ваша обёртка — тормоз по определению.

Раскладывать конкретных кандидатов по этим двум спискам удобно по двум осям: однородность потребностей и стабильность нижнего слоя.

Правый верхний угол — то, что даёт платформе основную окупаемость. Левый нижний — то, где попытка абстрагировать даст вам вечный поток исключений и злых пользователей.

Провал первый: обёртка над облаком, которая только мешает

Самый распространённый способ потратить два человеко-года впустую выглядит так. Платформа решает, что «Terraform слишком сложный для продуктовых команд», и делает свой YAML, из которого генерируется Terraform. Первые три месяца всё прекрасно: команды пишут двадцать строк вместо двухсот.

Дальше начинается арифметика, которую редко делают заранее:

  • Провайдер AWS выпускает релиз примерно раз в неделю, добавляя аргументы к ресурсам. Каждый нужный командам аргумент придётся протащить через ваш слой руками.
  • Пользователь ищет в интернете «как сделать X в S3» — находит ответы про Terraform, которые к вашему YAML не применимы. Вы только что отрезали команды от экосистемы: модулей, документации, готовых ответов, статей, ИИ-ассистентов, обученных на публичных конфигурациях.
  • Любая ошибка провайдера теперь выглядит как ошибка вашей обёртки: первую линию поддержки держите вы. А ваш YAML не поддерживает import существующих ресурсов, moved-блоки и таргетированный apply — каждый инцидент с расхождением состояния становится задачей платформы.

Правило, которое отсекает большинство таких проектов на входе:

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

Для Terraform «модуль на языке нижнего слоя» — это модули: та же экосистема, та же документация, тот же отладчик, но решения приняты за пользователя. Это глубокая абстракция при почти нулевой цене владения: вы не сопровождаете транслятор. Аналогично в Kubernetes собственный CRD с контроллером стоит на порядок дороже, чем набор проверенных манифестов плюс политика — и оправдан ровно тогда, когда решение нужно переисполнять при изменении окружения, а не только при создании сервиса. Технический фундамент — в devops про Kubernetes и Helm и GitOps; как из этого собирается инфраструктурное API — в главе 09.

Провал второй: одна абстракция поверх трёх разных потребностей

Второй классический провал заметнее всего по темпам роста схемы. Начинается с наблюдения «у нас же везде сервисы, сделаем единый контракт Service». Под одним именем оказываются три разные вещи: HTTP-API без состояния, задача по расписанию и консьюмер очереди.

Как выглядит контракт на седьмом квартале:

# ПЛОХО: один kind поверх трёх разных потребностей
apiVersion: platform.acme/v1
kind: Service
metadata: { name: checkout }
spec:
  type: http                 # http | cron | consumer — от этого зависит смысл половины полей ниже
  replicas: 3                # для cron не имеет смысла, но валидатор не ругается
  schedule: ""               # только для cron; пустая строка — «не применимо»
  topic: ""                  # только для consumer
  maxInFlight: 0             # только для consumer; 0 значит «по умолчанию», а не «ноль»
  ingress: { enabled: true, path: /api/checkout }   # для cron enabled всегда false
  scaling:
    mode: cpu                # queue-depth валиден только для consumer
    targetCpu: 60
    targetLag: 0             # осмыслен только при mode: queue-depth
  legacyProbePaths: false    # флаг совместимости для двух старых сервисов
  useNewIngressController: true   # флаг миграции, живёт тут уже год
  # ...ещё 40 полей, из которых для конкретного сервиса осмысленны 11

Симптомы, которые видно без разговоров с командами:

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

Сэнди Метц в заметке «The Wrong Abstraction» (2016) формулирует то же самое для кода: дублирование гораздо дешевле неправильной абстракции. Механика идентична — сначала три случая склеивают по внешнему сходству, потом каждое новое требование добавляет параметр, и в итоге абстракция описывает не общее, а объединение частных.

Общая реализация дешевле общего интерфейса

Правильный ответ почти всегда один: разделить контракты, переиспользовать внутренности.

# ХОРОШО: три узких контракта, у каждого — свой словарь
apiVersion: platform.acme/v1
kind: HttpService
metadata: { name: checkout }
spec:
  tier: critical             # из tier выводятся реплики, зоны, PDB, страница дежурного
  port: 8080
  path: /api/checkout
  scaling: { min: 3, max: 30, targetCpu: 60 }
---
apiVersion: platform.acme/v1
kind: ScheduledJob
metadata: { name: nightly-reconcile }
spec:
  tier: standard
  schedule: "17 3 * * *"
  timeout: 45m
  onOverlap: skip            # skip | queue — единственное решение, которое реально принимает пользователь
---
apiVersion: platform.acme/v1
kind: StreamConsumer
metadata: { name: payments-events }
spec:
  tier: critical
  topic: payments.events
  group: checkout-v2
  scaling: { min: 2, max: 40, targetLagSeconds: 30 }

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

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

Люки: как выйти из абстракции, не выходя из платформы

Вернёмся к сцене из начала главы. Команде платежей нужно было одно поле, которого нет в схеме. Есть ровно четыре честных ответа, и «подождите три недели» среди них нет.

Уровни абстракции, люки и инварианты платформы

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

Люк, который работает

Люк — это не «поле raw: для чего угодно». Такое поле через полгода содержит половину продакшна, и вы уже не можете менять контроллер. Работающий люк имеет четыре свойства:

apiVersion: platform.acme/v1
kind: HttpService
metadata: { name: checkout }
spec:
  tier: critical
  port: 8080
  scaling: { min: 3, max: 30, targetCpu: 60 }

  # Уровень 1: расширение контракта, а не выход из платформы.
  # Обязательны все четыре поля: причина словами, срок пересмотра,
  # владелец и патч в терминах нижнего слоя — честно и проверяемо.
  overrides:
    reason: >-
      После инцидента INC-2411 требуется разнос реплик по зонам отказа:
      обе реплики оказались в eu-central-1a.      
    expires: 2026-11-01
    owner: team-payments
    podPatch: |
      spec:
        topologySpreadConstraints:
          - maxSkew: 1
            topologyKey: topology.kubernetes.io/zone
            whenUnsatisfiable: DoNotSchedule
            labelSelector:
              matchLabels: { app: checkout }      

Что даёт такая конструкция платформе:

  • Расширение видно в каталоге — никто ничего не обходит молча. Патч применяется поверх сгенерированного объекта, а не вместо него: инварианты сохраняются, метки, метрики и лимиты никуда не деваются.
  • Появляется реестр люков — и это самый честный продуктовый бэклог, какой может быть у платформенной команды. Один запрос к каталогу даёт вам список того, чего людям не хватает, отсортированный по числу команд:
$ platformctl overrides report --group-by field --since 180d
FIELD                              TEAMS  SERVICES  OLDEST      STATUS
topologySpreadConstraints              6        14  2026-02-11  → в контракт, спринт 34
initContainers                         4         9  2026-03-02  → в контракт, спринт 36
podSecurityContext.sysctls             1         1  2026-05-19  остаётся расширением
hostNetwork                            1         2  2026-01-08  требует пересмотра: ломает сетевые политики

Шесть команд с одинаковым патчем — это не шесть исключений, это отсутствующая функция контракта. Так поток исключений превращается в источник приоритизации; техника из product-management про метрики и приоритизацию работает здесь без изменений.

Что делать с командой, которой золотой путь не подходит

Четыре варианта, в порядке предпочтения:

  1. Расширить контракт. Годится, когда потребность повторяемая и совместима с вашей моделью. Критерий «повторяемая» стоит зафиксировать заранее: например, три независимые команды или очевидное регуляторное требование. Это применение к платформе известного по рефакторингу правила трёх: обобщать после третьего реального случая, а не после первого предчувствия.
  2. Дать расширение с причиной и сроком. Годится, когда потребность реальная, но редкая. Стоит вам почти ничего, а команде разблокирует поставку сегодня.
  3. Явный договор о спуске. Годится, когда сервис принципиально не укладывается в модель: ML-инференс на GPU, легаси с собственным жизненным циклом, сервис с жёсткими требованиями к сети. Договор фиксирует, какие обязанности команда берёт на себя (сама держит манифесты, сама отвечает за апгрейды рантайма) и какие гарантии платформа снимает (например, часть SLO), но при этом инварианты сохраняются: секреты, наблюдаемость, учёт, каталог. Здесь помогает язык ADR — решение записывается вместе с контекстом и последствиями.
  4. Сказать «нет» и назвать цену. Легальный ответ, если требование ломает инварианты, за которые платформа отвечает перед компанией (например, hostNetwork в кластере, где сетевые политики — часть аудита). «Нет» обязано сопровождаться объяснением, что именно сломается, и предложением альтернативы. «Нет, потому что у нас так принято» — начало теневой платформы.

Чего делать нельзя: молча оставлять команду ждать. Из ожидания рождается ровно один исход — параллельный мир, о котором вы узнаете позже и дороже.

У контракта есть пользователи: версии, закон Хайрама и устаревание

Как только контрактом пользуются, он становится публичным API со всеми вытекающими. Закон Хайрама в платформенной версии звучит так: при достаточном числе пользователей неважно, что вы обещали в схеме — люди будут зависеть от всего наблюдаемого поведения. От порядка контейнеров в поде. От имени сгенерированного сервиса. От того, что дефолтный terminationGracePeriodSeconds равен тридцати. От формата имён метрик.

Практические следствия:

  • Версионируйте с первого дня. apiVersion: platform.acme/v1alpha1 стоит ноль, а возможность сломать совместимость — дорого. Модель Kubernetes с alpha/beta/stable (документация) проверена практикой и понятна пользователям.
  • Фиксируйте, что НЕ является контрактом, — явный раздел «на это опираться нельзя»: имена генерируемых объектов, внутренние метки, порядок применения. Полностью не спасёт, но переводит спор из «вы сломали» в «вы опирались на неподдерживаемое».
  • Считайте дефолты частью контракта. Смена дефолта — ломающее изменение, даже если схема не поменялась; особенно если дефолт влияет на деньги или на поведение при отказе.
  • Устаревание — процесс, а не письмо: объявление, инструмент миграции, отчёт по оставшимся, дата отключения, помощь отстающим. Как это делается на десятках команд без срыва поставки — глава про миграции.

Сколько стоит абстракция

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

Годовая чистая экономия абстракции =
  + N_сервисов × F_сквозных_изменений × T_ручного_изменения   — экономия на сопровождении
  + N_новых_сервисов × T_экономии_на_старте                   — экономия на старте
  + N_предотвращённых_инцидентов × T_инцидента                — экономия на классе ошибок
  − C_владения                          (люди платформы на эту абстракцию)
  − N_обходов × T_обхода                (кто-то всё равно делает руками)
  − N_блокировок × T_ожидания           (в нижнем слое есть, в контракте нет)
  − N_инцидентов × T_лишней_эскалации   (отладка через слой)

Ключевое про масштабирование слагаемых: первые три растут линейно по числу однородных пользователей; C_владения почти не зависит от числа пользователей, но растёт с числом поддерживаемых случаев; последние три растут по разнообразию, а не по количеству. Отсюда прямой ответ на вопрос «когда абстракция окупается»: при большом числе похожих пользователей. Двадцать одинаковых сервисов — да. Шесть очень разных — почти наверняка нет, там дешевле шаблон и хорошая документация. Это тот же аргумент, что и в системном мышлении про локальную оптимизацию: платформа, оптимизирующая свою метрику «покрытие контрактом», легко ухудшает сквозной поток поставки.

Простой калькулятор, который стоит прогнать до начала работы, а не после:

"""Окупаемость платформенной абстракции: точка безубыточности по числу сервисов."""

HOURS_PER_FTE_YEAR = 1_600  # часов инженера в год за вычетом отпусков, дежурств и совещаний


def net_hours(n, *, changes, h_manual, h_batch, new_share, h_start,
              owners_fte, bypass_share, h_bypass, blocked, h_wait):
    """Чистая экономия часов за год; отрицательное значение — абстракция дороже, чем экономит."""
    saved = (
        n * changes * h_manual         # сквозные изменения: раньше каждый сервис правили руками
        - changes * h_batch            # теперь платформа катит их централизованно
        + n * new_share * h_start      # экономия на старте новых сервисов
    )
    cost = (
        owners_fte * HOURS_PER_FTE_YEAR   # люди платформы, занятые именно этой абстракцией
        + n * bypass_share * h_bypass     # обходы, которые всё равно делают руками
        + n * blocked * h_wait            # ожидание «в нижнем слое есть, в контракте нет»
    )
    return saved - cost


def breakeven(limit=2_000, **kw):
    """Минимальное число сервисов, при котором абстракция выходит в плюс."""
    return next((n for n in range(1, limit + 1) if net_hours(n, **kw) > 0), None)


# Однородные пользователи: почти все сервисы укладываются в один контракт
homogeneous = dict(changes=4, h_manual=3, h_batch=40, new_share=0.4, h_start=16,
                   owners_fte=1.5, bypass_share=0.05, h_bypass=8, blocked=0.5, h_wait=6)

# Разнородные: польза та же, но дороже поддержка, больше обходов и ожидания
heterogeneous = dict(homogeneous, owners_fte=2.5, bypass_share=0.35, blocked=3.0)

print(breakeven(**homogeneous))       # 171 — до этого числа сервисов абстракция в минусе
print(net_hours(60, **homogeneous))   # -1660.0 — на шестидесяти сервисах ещё не окупилась
print(breakeven(**heterogeneous))     # None — не окупается ни при каком числе сервисов

Числа условные, но структура вывода — нет, и она неприятна: на реалистичных допущениях рантайм-абстракция с полутора выделенными инженерами начинает окупаться далеко не с первого десятка сервисов. Из этого не следует «не делайте абстракций». Следует ровно три вещи:

  1. Считайте до начала работы и вслух. Если вы не можете назвать число сервисов, при котором абстракция окупится, вы не проектируете платформу, а строите хобби за счёт компании.
  2. Пока пользователей мало, самая выгодная форма — шаблон плюс политика плюс документация. Team Topologies называет это тончайшей жизнеспособной платформой (книга Скелтона и Пайса); минимум, который решает задачу, а не максимум, который вы можете построить. Про границы команд и когнитивную нагрузку — engineering-leadership.
  3. В расчёт обязательно входят отрицательные слагаемые. Модель, где посчитана экономия и не посчитано ожидание, — это не расчёт, а презентация.

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

Метрики: как понять, что абстракция работает

Полнота функций — плохая метрика. У абстракции есть пользователи, и мерить надо их поведение. Минимальный набор, который стоит выводить на дашборд платформы:

Метрика Как считать Что означает тревожное значение
Доля сервисов на уровне 0 Сервисы без расширений / все сервисы Падает → контракт отстаёт от потребностей
Отток вниз Переходы L0→L1 и L1→L2 за квартал Растёт при неизменной схеме → абстракция стареет
Возврат наверх Переходы L1→L0 после добавления поля Ноль → расширения превратились в постоянные исключения
Лаг возможностей Медиана времени от «нужно» до «доступно в контракте» Больше длины спринта → платформа стала тормозом
Рост схемы Число полей контракта по кварталам Монотонный рост → под одним kind живут разные потребности
Доля «непонятных» ошибок Обращения вида «что означает это сообщение» / все обращения Растёт → проблема с диагностируемостью, не с функциями
Обходы Сервисы в проде вне каталога платформы Любое ненулевое → есть неучтённая теневая платформа
Время до первого деплоя От создания репозитория до продакшна, медиана Растёт → усложнили контракт, не заметив

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

Инструменты и их цена владения

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

Kubernetes. Даёт готовую модель расширения: CRD плюс контроллер — редкий случай, когда механизм «своей абстракции» уже реализован и проверен (паттерн оператора). Цена: три релиза в год и окно поддержки патчей около четырнадцати месяцев (политика релизов) — это минимум два-три апгрейда кластера ежегодно, каждый со своей проверкой совместимости CNI, CSI, ингресс-контроллера и ваших собственных контроллеров. Известная формулировка Келси Хайтауэра о том, что Kubernetes — платформа для построения платформ, а не конечная точка, ровно об этом: сам по себе он не отвечает ни на один вопрос этой главы.

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

Crossplane и подобное. Переносят управление ресурсами облака в реконсиляцию Kubernetes (документация). Мощно и рискованно: вы берёте на себя семантику расхождения состояния и удаления. Ошибка в композиции может удалить продовую базу — не гипотетически, а по логике «привести к желаемому состоянию».

Helm. Дёшев на входе, поэтому им пользуются все. Цена — шаблонизация строк поверх YAML: отладка идёт в терминах отрендеренного текста, условная логика в шаблонах растёт быстрее схемы. Годится как транспорт, плохо годится как язык вашего контракта. Модули Terraform/OpenTofu, наоборот, дают лучшее соотношение пользы и цены владения для инфраструктуры: абстракция на родном языке экосистемы, без транслятора (devops про IaC).

Общий фильтр перед добавлением любого из них — эссе Дэна Маккинли «Choose Boring Technology»: у команды есть ограниченное число «жетонов инноваций», и потратить их на способ описания деплоя — обычно плохая сделка. Разумная проверка: инструмент оправдан, если вы можете назвать, какое пользовательское решение он убирает, и во сколько человеко-часов в год обойдётся его сопровождение. Если ответ на первый вопрос — «он современный», сделки нет.

Типовые ошибки

  • Абстракция ради унификации отчётности. Контракт спроектирован так, чтобы платформе было удобно собирать статистику, а не чтобы пользователю было удобно описывать сервис. Признак: поля, которые никак не влияют на поведение системы.
  • Скрыли деньги и надёжность. Команды не видят стоимость и класс отказоустойчивости своего сервиса. Через год никто в компании не может объяснить счёт от облака.
  • Люк «поле raw для чего угодно». Без причины, без срока, без владельца: через год половина продакшна живёт в raw, а контроллер нельзя менять. Хуже только спуск, отключающий платформу: ушёл на уровень 2 — потерял секреты, метрики и место в каталоге.
  • Универсальный пайплайн с сорока флагами. Тот же провал, что и с контрактом сервиса, только в CI — разбор в главе 07. Рядом стоит обёртка ради красоты: свой язык поверх зрелого чужого, без единого убранного решения.
  • Ноль исключений как цель. Платформа, гордящаяся отсутствием исключений, обычно просто не знает о них. Реестр расширений с нулём записей при сорока командах — это не порядок, это слепота.
  • Абстракция без владельца. Контроллер написал энтузиаст, энтузиаст перешёл в другую команду. Теперь это инфраструктура без сопровождающего — худший вид технического долга.

Мини-итог

  • Абстракция — это перенос решения, а не переименование полей. Если пользователь не перестал принимать решение, вы добавили косвенность и оплатили её из чужого времени.
  • Шаблон, библиотека, контракт с контроллером и политика решают разные задачи и стоят по-разному. Начинайте с самого дешёвого, повышайте связанность только под доказанную нужду.
  • Протечка — штатный режим. Скрывайте механизм, никогда не скрывайте диагноз; отсутствие диагностического люка оплачивается минутами MTTR ночью. Скрывать полезно обязательное и неотличающее; вредно — деньги, надёжность, физику распределённых систем и всё, что меняется быстрее вашего цикла.
  • Одна абстракция поверх трёх разных потребностей всегда даёт объединение полей. Общая реализация дешевле общего интерфейса.
  • Люк с причиной, сроком и владельцем превращает исключения в бэклог. Спуск вниз обязан сохранять инварианты платформы, иначе рождается теневая платформа.
  • Абстракция окупается при большом числе однородных пользователей. Посчитайте точку безубыточности вслух и включите в расчёт ожидание, обходы и лишние эскалации.

Источники

Что дальше

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

Окружения: локальная разработка, превью, стенды и их цена

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

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

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

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