{"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 extension | Proposal; публикация только после разрешения |
| Появился внешний доступ или другой класс данных | Сравнить инварианты и права; найти отдельный policy record | Decline | Только причина отказа и следующий вопрос |
| Поле или значение неизвестно | Проверить схему и список разрешённых action | Decline | Репозиторий не создаётся |
Термин «шаблон» описывает и механизм, и начальный набор файлов, поэтому здесь легко ошибиться. 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 — повторяемая часть решения. В нашем случае он фиксирует внутренний 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.
internal и none. Проверьте, что skeleton содержит ожидаемые metadata и список проверок.extension на observability-adapter. Сравните diff и убедитесь, что не появились внешний доступ, другой runtime или новая политика хранения.publish:github и вернуть понятную причину.Для различия 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. Для каждого сохраните вход, вердикт, созданные артефакты и причину отказа. Такой набор даёт команде воспроизводимый контракт и показывает, где шаблон заканчивается. Всё, что требует нового класса данных, доступа или жизненного цикла, должно выйти за эту границу явно.
apiVersion, spec.parameters, spec.steps, actions и условия выполнения шагов.