{ "index": 132, "slug": "editorial-2024-05-practice-platform-templates", "title": "Платформенный шаблон без ловушки универсальности: контракт, расширение и отказ", "excerpt": "Шаблон экономит время только там, где повторяется один и тот же класс решений. Разбираем границы golden path, named extension, отрицательный путь и проверяемый критерий готовности.", "contentHtml": "

Новая команда открывает заявку на сервис и получает знакомый ответ: возьмите соседний репозиторий, замените имя, а остальное поправьте по месту. Через неделю два сервиса уже расходятся по CI, healthcheck и владельцам конфигурации. Ошибка проявляется не при создании, а на первом изменении общего правила. Review приходится сравнивать с несколькими копиями, исправление нужно переносить вручную, а rollback зависит от того, какая копия стала «правильной». Цена ошибки — не лишний файл. Команда теряет время на восстановление контракта и получает несколько путей, за которые никто не отвечает.

\n

Тезис простой: платформенный шаблон должен фиксировать только повторяемый класс решений. У класса есть owner, версия, обязательные входы, инварианты и ограниченный выход. Всё, что меняет этот класс, выносят в именованное расширение. Всё неизвестное отправляют на отдельное архитектурное решение. Такой шаблон остаётся коротким golden path и не превращается в форму со скрытой политикой.

\n

Что именно делает шаблон

\n

Шаблон — это не обещание готового сервиса. Он может положить согласованную структуру каталогов, подставить имя, подготовить описание компонента и передать результат в выбранное место. Backstage называет такой механизм Software Templates: он загружает skeleton, подставляет переменные и может опубликовать результат в GitHub или GitLab. Это полезная граница автоматизации. Она отвечает на вопрос «как получить одинаковый старт», но не отвечает на вопросы о доступе к данным, security review, capacity или разрешении на выпуск.

\n

Поэтому сначала описывают договор, а потом файлы. Для внутреннего HTTP-сервиса договор может содержать owner, approved runtime, класс данных, обязательный health endpoint и способ регистрации в каталоге. Он также должен содержать отрицательную часть: шаблон не выдаёт production approval, не создаёт секреты, не выбирает retention и не меняет права. Без этой части любой новый параметр легко станет незаметным исключением.

\n

Симптом → причина → проверка → действие

\n
Диагностика шаблона до его расширения
СимптомПричинаПроверкаДействие
В форме появляется «ещё одна галочка» для особого runtime.В один шаблон поместили два разных класса компонентов.Сравнить owner, runtime, data class и обязательные проверки для обоих вариантов.Оставить один класс. Для второго открыть отдельный design path.
После создания каждый репозиторий правят вручную.Выход шаблона считают договором, хотя он содержит только стартовые файлы.Показать, какие инварианты сохраняются после правки и кто владеет каждым правилом.Вынести повторяемую правку в версионированный шаблон или named extension.
Один generated repository нужно синхронизировать с базой.Создание из template перепутали с fork или наследованием.Проверить историю и способ доставки изменений из исходного репозитория.Не обещать автоматическую синхронизацию. Выбрать обновление вручную или другой механизм.
Неизвестный владелец всё равно проходит форму.Проверку ownership оставили на потом.Сделать owner обязательным входом и остановить запуск при пустом значении.Вернуть заявку на уточнение ответственности.
Шаблон должен выбрать retention или access policy.Архитектурное решение спрятали в параметр интерфейса.Спросить, кто утвердил policy и действует ли она для всего класса.Убрать параметр из base path и провести отдельную проверку.
\n

Таблица нужна не для оценки удобства формы. Она разделяет повторяемое правило и исключение. Если проверка не может назвать owner и одинаковый смысл поля для нескольких задач, поле ещё не готово для base template. Если выход нельзя проверить без устной истории конкретной команды, шаблон выдаёт слишком много обещаний.

\n

Пример короткого контракта

\n

Ниже учебный JavaScript-объект. Он не создаёт репозиторий, не вызывает CI, не меняет кластер и не подтверждает работу сервиса. Его задача — показать форму проверки. В настоящем проекте значения должны ссылаться на реальные справочники и правила доступа, а не на строки из примера.

\n
const contract = {\n  templateId: 'internal-http-service',\n  version: 3,\n  owner: 'platform-team',\n  requires: [\n    'service-name',\n    'component-owner',\n    'approved-runtime',\n    'internal-data-class',\n  ],\n  produces: [\n    'repository-skeleton',\n    'catalog-metadata-draft',\n    'review-checklist',\n  ],\n  refuses: [\n    'unknown-owner',\n    'new-retention-policy',\n    'one-off-data-migration',\n  ],\n};\n\nfunction canUseTemplate(input) {\n  return contract.requires.every((field) => input[field])\n    && !contract.refuses.some((field) => input[field]);\n}\n\n// Учебные данные. true означает только согласованный вход.\nconsole.log(canUseTemplate({\n  'service-name': 'billing-api',\n  'component-owner': 'billing-team',\n  'approved-runtime': 'node-approved',\n  'internal-data-class': 'internal',\n}));
\n

Проверка возвращает true только для входа, который удовлетворяет договору. Она не говорит, что сервис безопасен или готов к выкладке. Если добавить new-retention-policy, функция должна вернуть false. Это важнее положительной ветки: неизвестная политика не должна незаметно превращаться в настройку по умолчанию.

\n

Имена полей также задают границы ответственности. component-owner отвечает за доменный компонент. owner контракта отвечает за сам шаблон. Эти роли могут принадлежать одной группе, но смешивать их в одно не стоит. Иначе пользователь сможет создать компонент без владельца, потому что «платформа же владеет формой».

\n

Узкий golden path

\n

Узкий путь не означает бедный путь. Он может включать структуру документации, labels, базовый health endpoint, регистрацию компонента и обязательные проверки. Ограничение относится к смыслу выбора. Каждое поле должно иметь одинаковое значение для заявленного класса. Поле «выбрать любую базу» не является частью golden path, если разные базы меняют отказоустойчивость, хранение данных и эксплуатацию. Поле «выбрать approved runtime из двух поддерживаемых» может быть допустимым, если правила для обоих вариантов уже определены и owner готов их поддерживать.

\n
\"Схема
Учебная схема отделяет стабильный base contract от policy-вопроса. Она не показывает живую платформу, статистику использования или результат выпуска.
\n

У каждого правила должен быть ответ на вопрос «кто изменит его, если оно устареет?». Ответом может быть команда платформы, владелец каталога или отдельная группа безопасности. Если ответа нет, правило нельзя считать общим. Копирование файла не назначает владельца. Число успешных запусков тоже не доказывает, что договор корректен.

\n

Расширение вместо локальной копии

\n

Иногда компонент остаётся в том же классе, но требует одного дополнительного подключения. Например, approved runtime и internal data class сохраняются, а команде нужен заранее описанный observability adapter. Тогда нужен named extension. У него есть имя, owner, входные условия, граница изменений, rollback и срок пересмотра. Extension добавляет output к base contract, но не заменяет owner, runtime и data policy.

\n

Не называйте расширением любое изменение после создания. Локальный patch без имени и владельца не оставляет следа для следующей команды. Он быстро становится новой копией шаблона, а исправление базы не доходит до него. Если одинаковый patch повторился несколько раз, соберите факты: одинаков ли класс, одинаковы ли условия и можно ли описать rollback. Только после этого решайте, стал ли patch частью extension или отдельным шаблоном.

\n

У механики template repository есть ещё одна ловушка. GitHub предупреждает: ветви репозитория, созданного из template, имеют несвязанную историю, поэтому между ними нельзя строить обычные pull request и merge. Это не дефект GitHub. Это свойство операции создания. Значит, шаблон нельзя рекламировать как канал синхронизации с исходным репозиторием. Для долгоживущего общего кода нужен другой механизм, например пакет, зависимость или явно поддерживаемый upstream-процесс.

\n

Порядок принятия решения

\n
  1. Назовите класс. Одним предложением опишите компонент, owner, runtime и класс данных. Если описание требует слова «обычно» или «иногда», граница ещё не определена.
  2. Запишите инварианты. Перечислите то, что должно остаться истинным после применения шаблона: обязательные метаданные, тип компонента, проверки и границы доступа.
  3. Отделите выход от разрешения. Укажите, что шаблон создаёт skeleton или draft, но не выдаёт approval, секреты и право на изменение среды.
  4. Проверьте расширение. Для единственного известного отклонения назовите extension, owner, условия входа и rollback. Если отклонений несколько и они меняют класс, не прячьте их в одной форме.
  5. Проверьте отрицательную ветку. Передайте неизвестного owner, новую retention policy и неподдерживаемый runtime. Каждый вход должен получить ясный stop, а не молча выбранный default.
  6. Выберите механизм доставки. Если результат должен жить независимо, template repository подходит для старта. Если изменения должны приходить из общего источника, используйте механизм с проверяемой связью, а не обещание синхронизации.
  7. Назначьте проверку результата. Укажите, кто сверяет входы, созданные файлы, metadata и права. Положительный ответ открывает следующий review, но не заменяет его.
  8. Зафиксируйте версию. Запишите версию контракта и действие при обновлении. Не меняйте смысл старого входа под тем же номером: добавьте новую версию или отдельный путь.
\n

Когда отказ — правильный результат

\n

Заявка на разовую миграцию регулируемых данных обычно не готова к service template. У неё могут быть неизвестны owner и lifecycle, а access и retention требуют отдельного решения. Если вставить такую задачу в форму, пользователь заполнит поля, но архитектура останется неразрешённой. Шаблон создаст уверенный вид, а вопросы проявятся после получения файлов.

\n

Остановка не означает запрет на работу. Она возвращает заявку на правильную границу: design record, security review или обсуждение владельца данных. После нескольких одинаковых решений может появиться новый класс. Тогда его можно оформить отдельным контрактом с собственным owner и проверками. Не делайте этот вывод по одной удачной заявке.

\n

Ограничения и критерий готовности

\n

Шаблон не заменяет threat modeling, capacity planning, CI, тесты, миграционный план и проверку прав. Он не доказывает, что созданный сервис можно выпускать. Он также не обязан поддерживать каждый исторический репозиторий. Base contract задаёт повторяемый старт, а не универсальную архитектуру компании.

\n

Rollback должен быть определён на уровне изменения. Для draft удаляют draft. Для extension возвращаются к versioned base contract и удаляют подключение по его инструкции. Для уже созданного репозитория отдельно проверяют, что возвращается: файлы, зависимость, конфигурация или данные. Возврат шаблона не откатывает автоматически изменения, которые команда внесла после создания.

\n

Материал готов к применению как учебная схема, когда второй инженер без устных пояснений может показать класс, owner, версию, обязательные входы, инварианты, выход, refusal criterion и rollback. Для каждой ссылки есть источник. Для каждого неизвестного входа есть stop. В примере выше все имена и значения вымышлены; они не описывают измерение, внедрение или результат в production.

\n

Проверяемые источники

\n" }