Files

2 lines
21 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"index":131,"slug":"editorial-2024-05-mechanism-platform-templates","title":"Платформенный шаблон как контракт: базовый путь, расширение и отказ","excerpt":"Как превратить шаблон для команд в ограниченный контракт: отделить копию репозитория от наследования, закрыть входные параметры и остановить запрос, который требует отдельного архитектурного решения.","contentHtml":"<p>Команда просит создать внутренний HTTP-сервис. В форме есть имя, владелец и несколько флагов, поэтому старт выглядит безопасным. Через месяц в репозитории появляются ручные исключения в CI, другой healthcheck и особая политика хранения данных. Первый симптом обнаруживается в diff сразу после генерации: базовый шаблон уже не объясняет, почему проекту разрешены эти отличия.</p><p>Проблема не в самом генераторе файлов. Шаблон смешал три разных решения: какой класс компонента создаём, какие инварианты обязаны сохраниться и кто разрешает исключение. Из-за этого команда-потребитель принимает копию за наследника, а platform team получает набор неименованных вариантов без владельца обновлений. Чем шире форма, тем больше скрытых комбинаций приходится поддерживать.</p><p>Рабочая модель проще: шаблон принимает закрытый класс задач и возвращает один из трёх вердиктов. Полное совпадение с базовым контрактом ведёт в golden path. Одно заранее описанное отличие отправляется на review extension. Несовместимый или неизвестный вход даёт decline до создания репозитория. Отказ — нормальный результат валидатора, а не ошибка интерфейса.</p><h2>Симптом сначала, механизм потом</h2><p>Перед изменением шаблона зафиксируйте наблюдаемый случай. Например, заявка содержит <code>dataClass=internal</code>, runtime из разрешённого списка и расширение <code>observability-adapter</code>. Такой запрос можно сравнить с базовым контрактом. Другая заявка содержит внешний доступ и индивидуальный retention. Она меняет границу риска и не должна проходить под тем же названием.</p><table><caption>Как классифицировать заявку до генерации</caption><thead><tr><th>Наблюдение</th><th>Проверка</th><th>Вердикт</th><th>Что создаётся</th></tr></thead><tbody><tr><td>Все обязательные поля совпали с base contract</td><td>Тип компонента, owner, runtime, класс данных и доступ входят в разрешённые значения</td><td>Golden path</td><td>Скелет, metadata и стандартные проверки</td></tr><tr><td>Добавлено одно известное отличие</td><td>Есть identifier, владелец, граница, версия и условие удаления</td><td>Review extension</td><td>Proposal; публикация только после разрешения</td></tr><tr><td>Появился внешний доступ или другой класс данных</td><td>Сравнить инварианты и права; найти отдельный policy record</td><td>Decline</td><td>Только причина отказа и следующий вопрос</td></tr><tr><td>Поле или значение неизвестно</td><td>Проверить схему и список разрешённых action</td><td>Decline</td><td>Репозиторий не создаётся</td></tr></tbody></table><figure><img src='/assets/editorial/2024/platform-templates-2024-adoption-loop.svg' alt='Контур решения для платформенного шаблона: запись входа, сравнение с версионированным контрактом, golden path, именованное расширение или отказ, затем evidence и review'><figcaption>Сначала фиксируются факты и границы заявки, затем выбирается маршрут. Иллюстрация показывает порядок принятия решения, но не утверждает метрики внедрения или готовность конкретного репозитория.</figcaption></figure><h2>Шаблон, template-repository и fork — разные вещи</h2><p>Термин «шаблон» описывает и механизм, и начальный набор файлов, поэтому здесь легко ошибиться. GitHub пишет, что репозиторий, созданный из template, получает структуру и файлы выбранной ветки, а ветви такого репозитория имеют несвязанные истории. Это быстрый старт нового проекта, но не обещание получать изменения из исходного репозитория.</p><p>Fork отвечает на другой вопрос. Он сохраняет историю родительского репозитория и предназначен для работы с ним через общий граф коммитов. Если команде нужны pull request-ы из исходного проекта, template не заменяет fork. Если нужен самостоятельный сервис с начальным skeleton, fork создаёт лишнюю связь и другую модель владения.</p><p>Практическое правило: в описании платформы явно напишите, что распространяется после старта. Это может быть копия файлов, пакет, action, регулярные pull request-ы или самостоятельная версия. Само слово template не выбирает канал обновлений и не назначает владельца совместимости. Отсутствие такого договора и есть причина, по которой ручной patch позже принимают за «вариант шаблона».</p><h2>Закрытый контракт входных параметров</h2><p>Закрытый контракт отвечает на четыре вопроса: какой тип компонента создаётся, кто им владеет, какие значения разрешены и какое действие выполнится после проверки. Вход не должен принимать произвольный runtime-конфиг, credential, путь в файловой системе или политику retention. Каждый такой параметр расширяет область решений быстрее, чем растёт способность команды их проверять.</p><p>Ниже — учебный фрагмент для Backstage Software Template. В нём <code>enum</code> ограничивает класс данных и расширение, <code>additionalProperties</code> закрывает страницу формы, а <code>allowedHosts</code> и <code>allowedOrganizations</code> ограничивают место публикации. Имена организации, owner и runtime здесь проектные: их нужно заменить на значения своей Backstage-инсталляции.</p><pre><code>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 }}</code></pre><p>Это не готовая policy организации. Backstage проверит форму и выполнит описанные actions, но не узнает сам, разрешён ли owner, достаточно ли прав у пользователя, соответствует ли healthcheck требованиям команды и можно ли откатить опубликованный ресурс. Эти инварианты должны проверяться отдельным action, permission policy, CI или review перед публикацией.</p><p>Проверка отрицательного пути обязательна. Подставьте <code>dataClass=customer</code>, неизвестное значение <code>extension</code>, пустой owner и лишний ключ. Ожидаемый результат — ошибка схемы до шага <code>publish</code>. Если неизвестный вход всё-таки доходит до публикации, закрытая форма существует только на экране, а не в контракте.</p><h2>Base path и extension point</h2><p>Base path — повторяемая часть решения. В нашем случае он фиксирует внутренний HTTP-сервис, назначенную группу-владельца, разрешённый runtime, внутренний класс данных и минимальный набор проверок. Версия base contract должна быть видна в документации или metadata. Иначе невозможно понять, к какой схеме относится сгенерированный проект.</p><p>Extension point разрешает узкое отличие от этой схемы. Для расширения заранее запишите пять полей: <code>identifier</code>, owner, границу изменения, версию и условие удаления. Observability adapter подходит как пример, если он добавляет известные метрики и не меняет доступ, класс данных или способ хранения. Внешний endpoint, новый runtime и другая retention policy уже меняют инвариант.</p><p>Такое разделение помогает не спорить о названиях. Сначала сравните заявку с canonical record. Затем проверьте, что отличие не пересекает границу base path. Если пересекает, верните decline и сформулируйте отдельный архитектурный вопрос. Если не пересекает, создайте review record, а не молча добавьте условие в основной шаблон.</p><h2>Воспроизводимая проверка до публикации</h2><p>У Backstage есть Template Editor с dry-run режимом. Он позволяет загрузить локальный каталог шаблона, заполнить форму и посмотреть результат выполнения actions без сохранения изменений в рабочем шаблоне. Это удобное место для проверки маршрутов, но dry-run не заменяет permission check, credentials, сетевые права и итоговый pipeline.</p><ol><li><strong>Сохраните контракт.</strong> Зафиксируйте версию, обязательные поля, допустимые enum и инварианты, которые нельзя менять extension-ом.</li><li><strong>Запустите базовый dry-run.</strong> Передайте короткое имя, существующую группу, <code>internal</code> и <code>none</code>. Проверьте, что skeleton содержит ожидаемые metadata и список проверок.</li><li><strong>Запустите разрешённое расширение.</strong> Замените только <code>extension</code> на <code>observability-adapter</code>. Сравните diff и убедитесь, что не появились внешний доступ, другой runtime или новая политика хранения.</li><li><strong>Проверьте отказ.</strong> Подайте неизвестное значение, внешний класс данных и лишний ключ. Каждый случай должен остановиться до <code>publish:github</code> и вернуть понятную причину.</li><li><strong>Проверьте повтор.</strong> Повторите базовый запуск с теми же параметрами. Убедитесь, что генератор не создаёт второй обязательный action, дублирующую регистрацию или неожиданный ресурс.</li><li><strong>Проверьте реальную публикацию отдельно.</strong> После review подтвердите credentials, branch protection, CI, healthcheck, наблюдаемость и откат в окружении команды. Результат dry-run нельзя выдавать за результат production-развёртывания.</li></ol><p>Для различия template и fork можно дополнительно проверить историю после создания репозитория. URL замените на адрес своего исходного template:</p><pre><code>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</code></pre><p>Команда проверяет именно граф коммитов, а не похожесть файлов. Для репозитория, созданного из GitHub template, ожидается ветка с несвязанной историей. Если команда требует постоянного наследования, зафиксируйте другой механизм: fork, пакет, генератор обновлений или pull request-ы с отдельным owner.</p><h2>Отказ и границы применимости</h2><p>Шаблон не назначает владельца за команду, не проводит threat modeling, legal review или capacity planning. Он не определяет SLO, не доказывает безопасность secrets и не решает, сколько стоит сопровождение внешней интеграции. Backstage предоставляет схему, actions и точки авторизации, а GitHub — механизм создания репозитория; policy конкретной организации остаётся за её владельцами.</p><p>Отказ должен быть информативным и безопасным. Сообщение вроде <code>decline-template: внешний доступ меняет границу data class; назначьте policy owner и создайте отдельный design record</code> объясняет следующий шаг. Репозиторий не создаётся, credentials не расходуются, а заявку можно вернуть после принятия решения. Удалять уже опубликованный чужой ресурс ради отката шаблона нельзя считать безопасным default.</p><p>Не расширяйте golden path ради редкого случая. Если одно и то же отличие повторяется, сохраняет исходные инварианты и проходит review, включите его в новую версию контракта. Если отличие одноразовое или требует другого доступа, оставьте его отдельным решением. Так количество вариантов отражает реальные договорённости, а не историю случайных ручных patch-ей.</p><h2>Критерий готовности</h2><p>Шаблон готов к использованию, когда две команды получают одинаковый вердикт по одинаковым фактам. Базовая заявка проходит закрытую схему. Разрешённое расширение имеет owner, границу, версию и rollback. Несовместимый запрос останавливается до публикации. Повторный запуск имеет заранее определённое поведение.</p><p>Проверяйте не количество созданных репозиториев, а четыре маршрута: base, разрешённое extension, неизвестный input и повтор base. Для каждого сохраните вход, вердикт, созданные артефакты и причину отказа. Такой набор даёт команде воспроизводимый контракт и показывает, где шаблон заканчивается. Всё, что требует нового класса данных, доступа или жизненного цикла, должно выйти за эту границу явно.</p><h2>Проверяемые источники</h2><ul><li><a href='https://backstage.io/docs/features/software-templates/' target='_blank' rel='noopener noreferrer'>Backstage: Software Templates</a> — назначение шаблонов, параметры, шаги, dry-run и результаты выполнения.</li><li><a href='https://backstage.io/docs/features/software-templates/writing-templates/' target='_blank' rel='noopener noreferrer'>Backstage: Writing Templates</a> — структура <code>apiVersion</code>, <code>spec.parameters</code>, <code>spec.steps</code>, actions и условия выполнения шагов.</li><li><a href='https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-repository-from-a-template' target='_blank' rel='noopener noreferrer'>GitHub Docs: Creating a repository from a template</a> — различия template и fork, включая несвязанные истории ветвей.</li></ul>"}