{"index":131,"slug":"editorial-2024-05-mechanism-platform-templates","title":"Платформенный шаблон как контракт: базовый путь, расширение и отказ","excerpt":"Как превратить шаблон для команд в ограниченный контракт: отделить копию репозитория от наследования, закрыть входные параметры и остановить запрос, который требует отдельного архитектурного решения.","contentHtml":"

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

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

Рабочая модель проще: шаблон принимает закрытый класс задач и возвращает один из трёх вердиктов. Полное совпадение с базовым контрактом ведёт в golden path. Одно заранее описанное отличие отправляется на review extension. Несовместимый или неизвестный вход даёт decline до создания репозитория. Отказ — нормальный результат валидатора, а не ошибка интерфейса.

Симптом сначала, механизм потом

Перед изменением шаблона зафиксируйте наблюдаемый случай. Например, заявка содержит dataClass=internal, runtime из разрешённого списка и расширение observability-adapter. Такой запрос можно сравнить с базовым контрактом. Другая заявка содержит внешний доступ и индивидуальный retention. Она меняет границу риска и не должна проходить под тем же названием.

Как классифицировать заявку до генерации
НаблюдениеПроверкаВердиктЧто создаётся
Все обязательные поля совпали с base contractТип компонента, owner, runtime, класс данных и доступ входят в разрешённые значенияGolden pathСкелет, metadata и стандартные проверки
Добавлено одно известное отличиеЕсть identifier, владелец, граница, версия и условие удаленияReview extensionProposal; публикация только после разрешения
Появился внешний доступ или другой класс данныхСравнить инварианты и права; найти отдельный policy recordDeclineТолько причина отказа и следующий вопрос
Поле или значение неизвестноПроверить схему и список разрешённых actionDeclineРепозиторий не создаётся
Контур решения для платформенного шаблона: запись входа, сравнение с версионированным контрактом, golden path, именованное расширение или отказ, затем evidence и review
Сначала фиксируются факты и границы заявки, затем выбирается маршрут. Иллюстрация показывает порядок принятия решения, но не утверждает метрики внедрения или готовность конкретного репозитория.

Шаблон, template-repository и fork — разные вещи

Термин «шаблон» описывает и механизм, и начальный набор файлов, поэтому здесь легко ошибиться. GitHub пишет, что репозиторий, созданный из template, получает структуру и файлы выбранной ветки, а ветви такого репозитория имеют несвязанные истории. Это быстрый старт нового проекта, но не обещание получать изменения из исходного репозитория.

Fork отвечает на другой вопрос. Он сохраняет историю родительского репозитория и предназначен для работы с ним через общий граф коммитов. Если команде нужны pull request-ы из исходного проекта, template не заменяет fork. Если нужен самостоятельный сервис с начальным skeleton, fork создаёт лишнюю связь и другую модель владения.

Практическое правило: в описании платформы явно напишите, что распространяется после старта. Это может быть копия файлов, пакет, action, регулярные pull request-ы или самостоятельная версия. Само слово template не выбирает канал обновлений и не назначает владельца совместимости. Отсутствие такого договора и есть причина, по которой ручной patch позже принимают за «вариант шаблона».

Закрытый контракт входных параметров

Закрытый контракт отвечает на четыре вопроса: какой тип компонента создаётся, кто им владеет, какие значения разрешены и какое действие выполнится после проверки. Вход не должен принимать произвольный runtime-конфиг, credential, путь в файловой системе или политику retention. Каждый такой параметр расширяет область решений быстрее, чем растёт способность команды их проверять.

Ниже — учебный фрагмент для Backstage Software Template. В нём enum ограничивает класс данных и расширение, additionalProperties закрывает страницу формы, а allowedHosts и allowedOrganizations ограничивают место публикации. Имена организации, owner и runtime здесь проектные: их нужно заменить на значения своей Backstage-инсталляции.

apiVersion: scaffolder.backstage.io/v1beta3\nkind: Template\nmetadata:\n  name: internal-http-service\nspec:\n  owner: platform/catalog\n  type: service\n  parameters:\n    - title: Service contract\n      required: [name, owner, dataClass]\n      additionalProperties: false\n      properties:\n        name:\n          title: Service name\n          type: string\n          pattern: '^[a-z][a-z0-9-]{2,30}$'\n        owner:\n          title: Owner group\n          type: string\n          ui:field: OwnerPicker\n        dataClass:\n          title: Data class\n          type: string\n          enum: [internal]\n        extension:\n          title: Extension\n          type: string\n          enum: [none, observability-adapter]\n          default: none\n    - title: Repository\n      required: [repoUrl]\n      properties:\n        repoUrl:\n          title: Repository location\n          type: string\n          ui:field: RepoUrlPicker\n          ui:options:\n            allowedHosts: [github.com]\n            allowedOrganizations: [acme-platform]\n  steps:\n    - id: fetchBase\n      name: Fetch base\n      action: fetch:template\n      input:\n        url: ./skeleton\n        values:\n          name: ${{ parameters.name }}\n          owner: ${{ parameters.owner }}\n          extension: ${{ parameters.extension }}\n    - id: publish\n      name: Publish\n      action: publish:github\n      input:\n        repoUrl: ${{ parameters.repoUrl }}

Это не готовая policy организации. Backstage проверит форму и выполнит описанные actions, но не узнает сам, разрешён ли owner, достаточно ли прав у пользователя, соответствует ли healthcheck требованиям команды и можно ли откатить опубликованный ресурс. Эти инварианты должны проверяться отдельным action, permission policy, CI или review перед публикацией.

Проверка отрицательного пути обязательна. Подставьте dataClass=customer, неизвестное значение extension, пустой owner и лишний ключ. Ожидаемый результат — ошибка схемы до шага publish. Если неизвестный вход всё-таки доходит до публикации, закрытая форма существует только на экране, а не в контракте.

Base path и extension point

Base path — повторяемая часть решения. В нашем случае он фиксирует внутренний HTTP-сервис, назначенную группу-владельца, разрешённый runtime, внутренний класс данных и минимальный набор проверок. Версия base contract должна быть видна в документации или metadata. Иначе невозможно понять, к какой схеме относится сгенерированный проект.

Extension point разрешает узкое отличие от этой схемы. Для расширения заранее запишите пять полей: identifier, owner, границу изменения, версию и условие удаления. Observability adapter подходит как пример, если он добавляет известные метрики и не меняет доступ, класс данных или способ хранения. Внешний endpoint, новый runtime и другая retention policy уже меняют инвариант.

Такое разделение помогает не спорить о названиях. Сначала сравните заявку с canonical record. Затем проверьте, что отличие не пересекает границу base path. Если пересекает, верните decline и сформулируйте отдельный архитектурный вопрос. Если не пересекает, создайте review record, а не молча добавьте условие в основной шаблон.

Воспроизводимая проверка до публикации

У Backstage есть Template Editor с dry-run режимом. Он позволяет загрузить локальный каталог шаблона, заполнить форму и посмотреть результат выполнения actions без сохранения изменений в рабочем шаблоне. Это удобное место для проверки маршрутов, но dry-run не заменяет permission check, credentials, сетевые права и итоговый pipeline.

  1. Сохраните контракт. Зафиксируйте версию, обязательные поля, допустимые enum и инварианты, которые нельзя менять extension-ом.
  2. Запустите базовый dry-run. Передайте короткое имя, существующую группу, internal и none. Проверьте, что skeleton содержит ожидаемые metadata и список проверок.
  3. Запустите разрешённое расширение. Замените только extension на observability-adapter. Сравните diff и убедитесь, что не появились внешний доступ, другой runtime или новая политика хранения.
  4. Проверьте отказ. Подайте неизвестное значение, внешний класс данных и лишний ключ. Каждый случай должен остановиться до publish:github и вернуть понятную причину.
  5. Проверьте повтор. Повторите базовый запуск с теми же параметрами. Убедитесь, что генератор не создаёт второй обязательный action, дублирующую регистрацию или неожиданный ресурс.
  6. Проверьте реальную публикацию отдельно. После review подтвердите credentials, branch protection, CI, healthcheck, наблюдаемость и откат в окружении команды. Результат dry-run нельзя выдавать за результат production-развёртывания.

Для различия template и fork можно дополнительно проверить историю после создания репозитория. URL замените на адрес своего исходного template:

git clone https://github.com/acme-platform/internal-http-template.git template\ngit clone https://github.com/acme-team/payments-api.git generated\ngit -C generated remote add template https://github.com/acme-platform/internal-http-template.git\ngit -C generated fetch template main\nif git -C generated merge-base --is-ancestor template/main HEAD; then\n  echo 'generated наследует историю template'\nelse\n  echo 'истории не связаны: обновления нужно доставлять отдельным механизмом'\nfi

Команда проверяет именно граф коммитов, а не похожесть файлов. Для репозитория, созданного из GitHub template, ожидается ветка с несвязанной историей. Если команда требует постоянного наследования, зафиксируйте другой механизм: fork, пакет, генератор обновлений или pull request-ы с отдельным owner.

Отказ и границы применимости

Шаблон не назначает владельца за команду, не проводит threat modeling, legal review или capacity planning. Он не определяет SLO, не доказывает безопасность secrets и не решает, сколько стоит сопровождение внешней интеграции. Backstage предоставляет схему, actions и точки авторизации, а GitHub — механизм создания репозитория; policy конкретной организации остаётся за её владельцами.

Отказ должен быть информативным и безопасным. Сообщение вроде decline-template: внешний доступ меняет границу data class; назначьте policy owner и создайте отдельный design record объясняет следующий шаг. Репозиторий не создаётся, credentials не расходуются, а заявку можно вернуть после принятия решения. Удалять уже опубликованный чужой ресурс ради отката шаблона нельзя считать безопасным default.

Не расширяйте golden path ради редкого случая. Если одно и то же отличие повторяется, сохраняет исходные инварианты и проходит review, включите его в новую версию контракта. Если отличие одноразовое или требует другого доступа, оставьте его отдельным решением. Так количество вариантов отражает реальные договорённости, а не историю случайных ручных patch-ей.

Критерий готовности

Шаблон готов к использованию, когда две команды получают одинаковый вердикт по одинаковым фактам. Базовая заявка проходит закрытую схему. Разрешённое расширение имеет owner, границу, версию и rollback. Несовместимый запрос останавливается до публикации. Повторный запуск имеет заранее определённое поведение.

Проверяйте не количество созданных репозиториев, а четыре маршрута: base, разрешённое extension, неизвестный input и повтор base. Для каждого сохраните вход, вердикт, созданные артефакты и причину отказа. Такой набор даёт команде воспроизводимый контракт и показывает, где шаблон заканчивается. Всё, что требует нового класса данных, доступа или жизненного цикла, должно выйти за эту границу явно.

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

"}