Files

8 lines
23 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": 132,
"slug": "editorial-2024-05-practice-platform-templates",
"title": "Платформенный шаблон без ловушки универсальности: контракт, расширение и отказ",
"excerpt": "Шаблон экономит время только там, где повторяется один и тот же класс решений. Разбираем границы golden path, именованного расширения, отрицательного пути и проверяемой готовности.",
"contentHtml": "<p>Новая команда просит создать внутренний сервис и получает знакомый совет: скопируйте соседний репозиторий, замените имя, остальное поправьте после запуска. Сначала это выглядит дешевле платформенного шаблона. Через несколько недель копии расходятся по CI, healthcheck и владельцам конфигурации. Когда меняется общее правило, его приходится искать в каждом репозитории. Review сравнивает несколько версий «правильного» старта, а rollback зависит от того, какая копия успела стать образцом.</p>\n<p>Проблема не в количестве файлов. Команда потеряла контракт: какие входы обязательны, что именно создаётся, кто владеет правилом и когда генератор должен остановиться. Платформенный шаблон полезен, пока фиксирует повторяемый класс решений. Для известного варианта нужен узкий golden path, для одного контролируемого отклонения — именованное расширение, а неизвестная политика должна уйти в отдельное решение.</p>\n<h2>Симптом: форма обещает больше, чем система знает</h2>\n<p>Первый сигнал — в форму добавляют «ещё одну галочку»: новый runtime, другую базу, особый retention или исключение из CI. Второй — после генерации каждый владелец сервиса правит один и тот же файл вручную. Третий — от шаблона ждут синхронизации уже созданного репозитория с исходным. Эти просьбы выглядят разными, но у них один источник: стартовый набор файлов перепутали с архитектурным договором.</p>\n<p>Проверка должна быть предметной. Для каждого поля назовите его owner, одинаковый смысл для заявленного класса и обратимое действие при ошибке. Если значение зависит от конкретной команды или ещё не утверждённой политики, поле не принадлежит base template. Если после создания результат нужно регулярно подтягивать из общего источника, это уже задача управления зависимостью или upstream-процесса, а не простого копирования.</p>\n<h2>Сначала контракт, затем генерация</h2>\n<p>У рабочего шаблона есть небольшой закрытый договор. Он описывает идентификатор и версию, целевой тип компонента, обязательные входы, создаваемые артефакты и явные отказы. Например, для внутреннего HTTP-сервиса обязательными входами могут быть имя сервиса, владелец компонента, одобренный класс runtime и класс данных. Выходом будет skeleton репозитория, черновик метаданных каталога и checklist review.</p>\n<p>Отрицательная часть договора не менее важна. Шаблон не выдаёт production approval, не создаёт секреты по своему усмотрению, не выбирает новую retention policy и не меняет права доступа. Положительный результат проверки означает только «вход подходит для этого стартового пути». Он не означает, что сервис безопасен, выдержит нагрузку или готов к выпуску.</p>\n<div class=\"table-scroll\"><table><caption>Граница base contract для внутреннего HTTP-сервиса</caption><thead><tr><th scope=\"col\">Часть</th><th scope=\"col\">Фиксируем</th><th scope=\"col\">Не прячем в шаблон</th><th scope=\"col\">Проверка</th></tr></thead><tbody><tr><td>Идентичность</td><td>Имя сервиса, владелец компонента, версия шаблона</td><td>Будущее название команды или продукта</td><td>Владелец найден в разрешённом справочнике</td></tr><tr><td>Класс</td><td>Internal HTTP service и approved runtime</td><td>Новый runtime ради одной заявки</td><td>Класс и runtime входят в поддерживаемый список</td></tr><tr><td>Данные</td><td>Уже утверждённый internal data class</td><td>Новая retention или access policy</td><td>Ссылка на действующее правило и его owner</td></tr><tr><td>Выход</td><td>Skeleton, metadata draft, checklist review</td><td>Production approval, итог CI и разрешение на выпуск</td><td>Артефакты проверены отдельным review</td></tr><tr><td>Отклонение</td><td>Именованное расширение с owner и rollback</td><td>Безымянный patch в каждом generated repo</td><td>Описаны условия включения и удаления</td></tr></tbody></table></div>\n<p>Такой контракт превращает разговор «давайте сделаем универсально» в проверяемый вопрос. Для каждой строки можно показать источник значения, ответственного и действие при несовпадении. Если это невозможно, форма пока маскирует архитектурную неопределённость.</p>\n<h2>Что реально делает scaffolder</h2>\n<p>Backstage называет этот механизм Software Templates. Официальная документация описывает загрузку skeleton-кода, подстановку переменных и публикацию результата в такие места, как GitHub или GitLab. Шаблон состоит из шагов, а перед запуском пользователь может проверить введённые параметры на review-странице. Это хороший механизм повторяемого старта, но он не определяет политику конкретной компании сам по себе.</p>\n<p>Публикация требует настроенных интеграций и разрешений приложения. Поэтому из того, что Backstage умеет вызвать publish action, нельзя делать вывод о наличии прав у каждой команды или о прохождении security review. В вашем экземпляре нужно отдельно проверить, какие actions включены, куда им разрешено писать и какой процесс принимает созданный репозиторий. Версия Backstage также имеет значение: названия полей и доступные actions сверяйте с документацией установленного релиза.</p>\n<p>В этой статье слово «расширение» означает локальное соглашение команды, а не встроенный объект Backstage. Оно должно иметь имя, owner, условия входа, список изменяемых артефактов, способ удаления и дату пересмотра. Такое описание оставляет отличие видимым. Без него расширение быстро превращается в ещё одну копию base template.</p>\n<h2>Воспроизводимая отрицательная проверка</h2>\n<p>Ниже маленькая in-memory-проверка. Она не создаёт репозиторий, не обращается к Backstage, не меняет CI и не проверяет реальные права. Её можно вставить в файл и выполнить Node.js без зависимостей. Ожидаемый вывод — сначала <code>true</code>, затем <code>false</code>: новая политика не должна пройти через тот же путь, что и согласованный internal service.</p>\n<pre><code>const contract = {\n required: ['serviceName', 'componentOwner', 'approvedRuntime', 'dataClass'],\n allowedRuntimes: ['node-approved'],\n allowedDataClasses: ['internal'],\n};\n\nfunction accepts(input) {\n const hasRequired = contract.required.every((key) =&gt; Boolean(input[key]));\n const runtimeIsAllowed = contract.allowedRuntimes.includes(input.approvedRuntime);\n const dataClassIsAllowed = contract.allowedDataClasses.includes(input.dataClass);\n const hasNewPolicy = Boolean(input.newRetentionPolicy);\n return hasRequired &amp;&amp; runtimeIsAllowed &amp;&amp; dataClassIsAllowed &amp;&amp; !hasNewPolicy;\n}\n\nconst knownRequest = {\n serviceName: 'billing-api',\n componentOwner: 'billing-team',\n approvedRuntime: 'node-approved',\n dataClass: 'internal',\n};\n\nconsole.log(accepts(knownRequest));\nconsole.log(accepts({ ...knownRequest, newRetentionPolicy: '90-days' }));</code></pre>\n<p>Скопируйте блок в файл <code>contract-check.js</code> и запустите командой <code>node contract-check.js</code>. Вторая строка должна быть <code>false</code>. В реальном шаблоне проверка должна использовать справочники и policy вашей платформы; строка <code>node-approved</code> здесь учебная и не означает, что такой runtime существует у вас.</p>\n<h2>Узкий golden path</h2>\n<p>Узкий путь не означает бедный путь. В него можно включить структуру документации, обязательные labels, регистрацию компонента, базовый health endpoint и согласованный delivery-маршрут. Ограничение относится к смыслу выбора: каждое поле должно иметь одно и то же значение для всего класса. Поле «выберите любую базу» нарушает это правило, если базы меняют отказоустойчивость, хранение данных и эксплуатационные обязанности.</p>\n<figure><img src=\"/assets/editorial/2024/platform-templates-2024-golden-path.svg\" alt=\"Схема golden path: известный владелец, внутренний HTTP-сервис, одобренный runtime и класс данных проходят в версионированный base contract, а новая политика уходит в design path\" loading=\"lazy\" /><figcaption>Стабильные входы ведут в версионированный договор; неизвестный owner или новая policy не становятся ещё одним полем формы. Схема учебная и не описывает конкретную production-платформу.</figcaption></figure>\n<p>У каждого правила должен быть ответ на вопрос «кто изменит его, если оно устареет?». Для шаблона это может быть команда платформы, для data policy — владелец данных, для каталожных метаданных — отдельный owner. Число успешных запусков не доказывает правильность договора. Результат нужно сверить с созданными файлами, metadata, правами и обязательными проверками.</p>\n<h2>Именованное расширение вместо локальной копии</h2>\n<p>Отклонение допустимо как расширение, если базовый класс не меняется. Например, сервис остаётся внутренним HTTP-сервисом, сохраняет approved runtime и data class, но подключает заранее определённый observability adapter. У extension должны быть имя, owner, входное условие, граница изменений и rollback. Его подключение должно быть видно в результате генерации и в review, а не существовать только в памяти автора.</p>\n<p>Если расширение меняет класс данных, доступ, retention или право на выпуск, это уже не «одна дополнительная настройка». Такой вариант нужно вынести в собственный контракт с отдельными проверками. Если одинаковый локальный patch повторился несколько раз, соберите факты: совпадает ли класс задач, одинаковы ли условия и можно ли удалить изменение без ручного поиска по всем копиям. Только после этого решайте, стал ли patch расширением или отдельным шаблоном.</p>\n<h2>Template repository — не канал синхронизации</h2>\n<p>У GitHub есть важное ограничение, которое легко потерять в рекламном описании шаблона. Документация GitHub указывает, что ветви репозитория, созданного из template, имеют несвязанную историю. Поэтому между ветвями нельзя строить обычные pull request или merge. В отличие от этого fork сохраняет историю родительского репозитория.</p>\n<p>Следствие прикладное: template repository подходит, чтобы быстро начать независимый проект с одинаковой структурой. Он не обещает, что исправление в исходном шаблоне автоматически придёт во все созданные репозитории. Если общий код должен обновляться централизованно, используйте зависимость, пакет, generator с явным upgrade-процессом или другой механизм с проверяемой связью. Выбирайте его по требованию к жизненному циклу, а не по похожему начальному набору файлов.</p>\n<h2>Порядок принятия решения</h2>\n<ol><li><strong>Назовите класс.</strong> Одним предложением опишите компонент, его owner, runtime и класс данных. Слова «обычно» и «иногда» отмечают место, где граница ещё не доказана.</li><li><strong>Закройте вход.</strong> Перечислите обязательные поля и допустимые значения. Пустой owner, неизвестный runtime и новая policy должны иметь явный отказ.</li><li><strong>Опишите инварианты.</strong> Запишите, что останется истинным после генерации: тип компонента, обязательные метаданные, проверки и границы доступа.</li><li><strong>Отделите output от approval.</strong> Укажите, что создаёт шаблон, а какие решения принимаются следующим review. Generated файл не является доказательством запуска.</li><li><strong>Проверьте отклонение.</strong> Для одного варианта назовите extension, owner, условие входа, изменяемые артефакты и rollback. Если отклонение меняет класс, откройте новый путь.</li><li><strong>Проверьте механизм доставки.</strong> Независимый старт допускает template repository. Долгоживущая связь с общим кодом требует зависимости или процесса обновления, который можно проверить.</li><li><strong>Назначьте приёмку.</strong> Пусть отдельный участник сверит входы, созданные файлы, metadata, права и обязательные проверки. Положительный ответ открывает следующий этап, но не заменяет его.</li><li><strong>Зафиксируйте версию.</strong> Изменение смысла входа оформляйте новой версией или новым контрактом. Не меняйте старый default так, чтобы уже созданные проекты получили другой смысл задним числом.</li></ol>\n<h2>Когда отказ экономит время</h2>\n<p>Разовая миграция регулируемых данных обычно не подходит для общего service template. В ней могут быть неизвестны владелец данных и lifecycle, а access и retention требуют отдельного согласования. Если спрятать эти вопросы в форму, пользователь получит аккуратный skeleton, но архитектурная ответственность останется неразрешённой.</p>\n<p>Отказ возвращает задачу в правильную границу: design record, security review или обсуждение владельца данных. После нескольких одинаковых решений может появиться новый класс. Тогда его оформляют отдельно — с собственным owner, входами, инвариантами, источниками и rollback. Одной удачной заявкой такой класс не доказывается.</p>\n<h2>Ограничения и критерий готовности</h2>\n<p>Платформенный шаблон не заменяет threat modeling, capacity planning, тесты, CI, миграционный план и проверку прав. Он не гарантирует качество созданного сервиса и не откатывает изменения, внесённые командой после генерации. Для draft можно удалить draft; для extension нужен описанный способ отключения; для уже созданного репозитория отдельно определяют, возвращаются ли файлы, зависимость, конфигурация или данные.</p>\n<p>Шаблон готов к применению, если второй инженер без устных пояснений может показать класс, owner, версию, обязательные входы, инварианты, output, refusal criterion и rollback. Для каждой интеграции нужно дополнительно проверить установленную версию Backstage, разрешения publish action, целевое хранилище и правила вашей организации. Учебная команда выше подтверждает только отрицательную ветку в памяти Node.js; она не подтверждает живой scaffolder или production readiness.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://backstage.io/docs/features/software-templates/\" target=\"_blank\" rel=\"noopener noreferrer\">Backstage: Software Templates</a> — официальное описание skeleton, переменных, шагов и публикации результата в целевое хранилище.</li><li><a href=\"https://backstage.io/docs/features/software-templates/configuration/\" target=\"_blank\" rel=\"noopener noreferrer\">Backstage: Software Template Configuration</a> — официальные ограничения настройки интеграций, publish 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 repository и fork, включая несвязанную историю ветвей.</li></ul>"
}