Files
progcode/editorial/agent-rewrites/095.json
T

8 lines
23 KiB
JSON
Raw 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": 95,
"slug": "editorial-2025-05-mechanism-engineering-automation",
"title": "Почему preview и approval не делают автоматизацию безопасной",
"excerpt": "Preview показывает рассчитанное намерение, authority ограничивает допустимый scope, approval связывает согласие с конкретной операцией, а verification читает результат. Если эти доказательства смешать, batch-автоматизация может изменить лишние объекты.",
"contentHtml": "<p>Симптом обнаруживается после зелёного pipeline: preview показал ожидаемый diff, а reviewer нажал approve. После запуска изменились записи за пределами заявки. В журнале есть operation id, но из него не видно, какие объекты реально изменились. Команда тратит часы на восстановление списка целей и рискует запустить ещё один широкий change под видом rollback.</p>\n<p>Причина не в отсутствии ещё одной кнопки подтверждения. Разные этапы отвечают на разные вопросы: preview описывает предложение в определённый момент, authority задаёт предел полномочий, approval фиксирует согласие, audit trail сохраняет событие, а verification читает целевое состояние после записи. Без явной связи между ними зелёный статус доказывает меньше, чем кажется.</p>\n<h2>Пять артефактов — пять разных утверждений</h2>\n<p>Начните с разделения доказательств. Это не бюрократия, а способ не выдать одно наблюдение за другое. Наличие preview не говорит, что scope разрешён. Approval не говорит, что target list остался прежним. Receipt от executor не доказывает, что целевая система сохранила значение.</p>\n<div class='table-scroll'><table><caption>Что доказывает каждый артефакт и какой вопрос остаётся открытым</caption><thead><tr><th scope='col'>Артефакт</th><th scope='col'>Честное утверждение</th><th scope='col'>Опасная подмена</th><th scope='col'>Следующая проверка</th></tr></thead><tbody><tr><td>Preview</td><td>Для зафиксированного snapshot рассчитан proposed change</td><td>Execute даст тот же эффект</td><td>Проверить свежесть и preconditions перед записью</td></tr><tr><td>Authority</td><td>Policy разрешает selector и размер scope</td><td>Change полезен и технически корректен</td><td>Сверить intent и содержимое preview</td></tr><tr><td>Approval</td><td>Reviewer разрешил связанную карточку в своей роли</td><td>Согласие переносится на похожую операцию</td><td>Сравнить operation, proposal, target и authority digests</td></tr><tr><td>Audit trail</td><td>Executor записал событие по своему контракту</td><td>Target system уже в ожидаемом состоянии</td><td>Прочитать authoritative source после execute</td></tr><tr><td>Verification</td><td>Reader наблюдал заданное expected state</td><td>Все побочные последствия устранены</td><td>Проверить residual risk и план recovery</td></tr></tbody></table></div>\n<p>В карточке операции храните как минимум идентификатор, selector, точный список targets, digest списка, ожидаемое состояние до и после, версию логики изменения и срок действия. Человеческое описание остаётся полезным для review, но executor должен сравнивать машинные поля буквально.</p>\n<h2>Preview — снимок, а не разрешение</h2>\n<p>Preview устаревает, когда меняется объект, inventory или policy. Поэтому его ценность ограничена snapshot version и временем расчёта. Если между preview и execute другой job обновил target, старый diff больше не является описанием текущего действия. В безопасном процессе есть явное условие: пересчитать предложение или остановиться, если precondition больше не выполняется.</p>\n<p>Это соответствует модели Terraform. Официальная документация описывает <code>terraform plan</code> как расчёт изменений без их выполнения. План без <code>-out</code> является speculative plan и не содержит намерения применяться. Документация отдельно предупреждает, что изменения в target system между ранним speculative plan и финальным применением могут изменить результат; перед apply нужно снова проверить актуальный non-speculative plan. Это узкий факт о Terraform, а не готовая гарантия для любого самописного runner.</p>\n<p>Сохраняйте не только красивый diff. Практический минимум выглядит так: <code>operationId</code>, <code>snapshotVersion</code>, <code>selector</code>, dense target list, <code>targetDigest</code>, expected precondition и <code>proposalDigest</code>. Digest здесь связывает две фазы и позволяет обнаружить расхождение. Он не заменяет криптографическую подпись, проверку identity или защиту хранилища.</p>\n<h2>Dry-run имеет конкретную границу</h2>\n<p>Слово dry-run описывает режим, но не обещает одинаковый объём проверки. В официальной reference для <code>kubectl apply</code> режим <code>--dry-run=client</code> только печатает объект, который был бы отправлен, без отправки. Режим <code>--dry-run=server</code> передаёт запрос на сервер, но не сохраняет ресурс. Значит, client mode проверяет локальную подготовку объекта, а server mode проходит часть серверного пути. Ни один режим сам по себе не подтверждает будущий persistent change.</p>\n<p>В карточке записывайте источник данных и границу побочного эффекта: «локальная модель, внешний read не выполняется», «server admission, сохранение запрещено» или «read-only inventory со snapshot». Тогда reviewer понимает, какую проверку ещё нужно сделать. Если этого поля нет, слово preview создаёт ложное ощущение полноты.</p>\n<figure><img src='/assets/editorial/2025/engineering-automation-2025-authority-matrix.svg' alt='Матрица показывает, как authority, target scope, preview digest и approval binding ведут к execute и verification; несовпадение любой границы останавливает операцию' loading='lazy' /><figcaption>Authority ограничивает scope, approval связывает конкретную карточку, а verification проверяет состояние после execute. Красная клетка означает остановку до следующей записи, а не автоматический rollback.</figcaption></figure>\n<h2>Authority ограничивает мощность операции</h2>\n<p>Authority — это машинная граница: допустимый selector, максимальное число targets, срок действия и исключения. Она отвечает на вопрос «можно ли этой роли работать с таким scope», но не решает, правильно ли менять поле. Если policy разрешает два объекта, карточка с тремя должна остановиться до approval. Список нельзя молча уменьшать: reviewer должен видеть ровно тот scope, который получит executor.</p>\n<p>Сравнивайте exact selector и exact target digest. Не подменяйте их словами «маленький batch» или «почти тот же список». Не расширяйте authority, если часть целей исчезла из inventory. Не превращайте превышение лимита в warning, когда последующая операция может затронуть больше объектов, чем видел reviewer.</p>\n<h2>Воспроизводимый gate до записи</h2>\n<p>Ниже — автономный пример на Node.js. Он работает только с объектами в памяти, не читает сеть, не проверяет реальные права и ничего не меняет во внешней системе. В нём три synthetic target, authority разрешает два, а approval связан с точным operation и target digest. Скопируйте блок в shell с Node.js 18+:</p>\n<pre><code>node --input-type=module &lt;&lt;'NODE'\nconst operation = {\n id: 'op-normalize-labels-v1',\n selector: 'labels.env=staging',\n targets: ['service-a', 'service-b', 'service-c'],\n targetDigest: 'targets-a-b-c-v1',\n proposalDigest: 'proposal-7f2a'\n};\n\nconst authority = {\n id: 'authority-l2-v1',\n selector: 'labels.env=staging',\n maxTargets: 2\n};\n\nconst approval = {\n operationId: 'op-normalize-labels-v1',\n targetDigest: 'targets-a-b-c-v1',\n proposalDigest: 'proposal-7f2a',\n authorityId: 'authority-l2-v1'\n};\n\nfunction gate(op, policy, decision) {\n const reasons = [];\n if (op.selector !== policy.selector) reasons.push('selector-not-authorized');\n if (op.targets.length &gt; policy.maxTargets) reasons.push('scope-exceeds-authority-limit');\n if (decision.operationId !== op.id\n || decision.targetDigest !== op.targetDigest\n || decision.proposalDigest !== op.proposalDigest\n || decision.authorityId !== policy.id) {\n reasons.push('approval-does-not-bind-operation');\n }\n return reasons.length === 0\n ? { status: 'READY_FOR_EXECUTE', operationId: op.id }\n : { status: 'STOP', reasons };\n}\n\nconsole.log(gate(operation, authority, approval));\n// { status: 'STOP', reasons: [ 'scope-exceeds-authority-limit' ] }\n// No external target is read or changed.\nNODE</code></pre>\n<p>Команда возвращает <code>STOP</code>, потому что три цели превышают лимит два. Чтобы проверить вторую отрицательную ветку, замените в <code>approval</code> значение <code>targetDigest</code> на <code>targets-a-c-v1</code>: результат дополнится <code>approval-does-not-bind-operation</code>. В настоящей системе digest вычисляет канонизатор данных, authority приходит из доверенного policy engine, а identity и срок решения проверяются отдельно.</p>\n<h2>Approval связывает согласие с карточкой</h2>\n<p>Approval должен содержать operation id, proposal digest, target digest и authority id. Полезны также роль reviewer, decision, reason и expiry. Executor сравнивает эти значения перед write; «похожий» selector или частичное совпадение не подходят. Если approval относится к targets A и C, а preview — к A и B, процесс возвращает stop и требует новый preview.</p>\n<p>У GitHub Environments есть похожая, но более узкая граница: job, который ссылается на environment, должен пройти настроенные protection rules до запуска или доступа к environment secrets. В документации required reviewers и запрет административного bypass описаны как отдельные настройки. Это пример gate запуска, а не доказательство корректности diff. Доступность функций зависит от типа репозитория и плана GitHub, поэтому переносить его поведение в собственный runner без проверки нельзя.</p>\n<p>Bypass тоже является событием с владельцем, причиной и audit record. Если исключение скрыто внутри общего поля <code>approved=true</code>, расследование не отличит обычное согласие от обхода. Для destructive operation отсутствие reviewer или истёкший expiry должны вести к stop, а не к default-решению.</p>\n<h2>Receipt и verification — разные результаты</h2>\n<p>Audit trail восстанавливает причинную цепочку: operation id, версии runner и policy, selector, digests, authority, approval, время и результат по каждой цели. Но запись «executor отправил запрос» — это receipt. Она не подтверждает, что target system приняла значение, что controller не откатил его сразу и что побочный объект не изменился.</p>\n<p>Verification начинается с ожидаемого состояния. Например: authoritative reader видит label <code>env=staging</code> на двух конкретных targets, digest списка совпадает, а отсутствующие объекты перечислены. У проверки должны быть три различимых исхода: <code>matched</code>, <code>mismatched</code> и <code>unknown</code>. Недоступный reader — это unknown, а не «успех с предупреждением».</p>\n<div class='table-scroll'><table><caption>Диагностика после запуска</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>Selector расширился или scope не был связан с approval</td><td>Сравнить target digest до write и фактический список</td><td>Остановить цепочку и собрать bounded inventory</td></tr><tr><td>Preview и execute показывают разный diff</td><td>Target state изменился между фазами</td><td>Сверить snapshot version, precondition и возраст preview</td><td>Пересчитать preview, старое approval не использовать</td></tr><tr><td>В audit есть success, но поле прежнее</td><td>Receipt выдали за verification</td><td>Прочитать authoritative source отдельным путём</td><td>Вернуть mismatched или unknown</td></tr><tr><td>Client dry-run прошёл, сервер отказал</td><td>Локальная проверка не видела server-side policy</td><td>Сравнить режим dry-run и ответ admission</td><td>Исправить вход или остановиться до нового review</td></tr><tr><td>Rollback снова меняет не те targets</td><td>Обратный write использует старый scope</td><td>Проверить observed state и новую authority</td><td>Оформить rollback как новую операцию</td></tr></tbody></table></div>\n<h2>Rollback — новый change</h2>\n<p>После частичного результата нельзя считать исходное значение безопасной обратной командой. Человек мог исправить часть targets, исходное состояние могло стать устаревшим, а новая policy может запретить обратную запись. Поэтому первый шаг — observed inventory: что изменилось, где состояние неизвестно, какие зависимости затронуты и кто владеет recovery.</p>\n<p>Затем rollback проходит тот же bounded маршрут: новый scope, новые preconditions, новая authority, новый preview, отдельный approval и последующая verification. Статус «rollback planned» не означает «rollback executed». Автоматический inverse в блоке <code>finally</code> опасен именно потому, что исполняется без свежего знания о состоянии.</p>\n<h2>Практический порядок внедрения</h2>\n<ol><li><strong>Выберите одну операцию.</strong> Начните с обратимого batch на двух учебных или тестовых targets, а не с широкого selector.</li><li><strong>Опишите contract.</strong> Запишите intent, selector, exclusions, exact target list, target digest и expected before/after.</li><li><strong>Зафиксируйте preview boundary.</strong> Укажите snapshot version, источник, время расчёта и условие, при котором нужен новый preview.</li><li><strong>Проверьте authority.</strong> Сравните selector, лимит, срок и исключения до запроса approval.</li><li><strong>Привяжите approval.</strong> Сверьте operation, proposal, target и authority ids буквально; любое отсутствие означает stop.</li><li><strong>Повторите preconditions перед write.</strong> Проверьте существование targets, версию, scope и действительность policy.</li><li><strong>Сохраните receipt отдельно.</strong> Не называйте событие executor доказательством состояния.</li><li><strong>Сделайте observed verification.</strong> Прочитайте authoritative source и зафиксируйте matched, mismatched или unknown.</li><li><strong>При unknown остановитесь.</strong> Соберите observed inventory и подготовьте новый bounded change; не используйте старое approval для rollback.</li></ol>\n<h2>Границы применимости</h2>\n<p>Схема не создаёт identity provider, policy engine, неизменяемое хранилище журнала, распределённую блокировку, гарантию идемпотентности или реальный rollback. Учебный код не доказывает права пользователя, свежесть inventory, криптографическую стойкость digest и фактическое состояние production. Он проверяет только связь полей и отказ до внешней записи.</p>\n<p>Даже независимая verification имеет границу: reader может быть устаревшим, неполным или не видеть побочные системы. Для каждого change назовите источник истины, допустимую задержку и владельца unknown. Для удаления и других необратимых действий добавьте отдельные меры: резервную копию, лимит скорости, ручное подтверждение и план восстановления.</p>\n<p>Документация Terraform, Kubernetes и GitHub меняется; флаги и доступность функций нужно сверять с версией и тарифом своего окружения. Примеры в статье иллюстрируют узкие свойства официальных инструментов, а не сертифицируют единый безопасный процесс. Готовность определяется собственными тестами: scope выше лимита останавливается, чужой digest не проходит, недоступный reader остаётся unknown, а старое approval не запускает rollback.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href='https://developer.hashicorp.com/terraform/cli/commands/plan' target='_blank' rel='noopener noreferrer'>HashiCorp Terraform: plan command</a> — назначение <code>plan</code>, speculative plan, сохранённый план через <code>-out</code> и предупреждение о повторной проверке после изменений в target system.</li><li><a href='https://kubernetes.io/docs/reference/kubectl/generated/kubectl_apply/' target='_blank' rel='noopener noreferrer'>Kubernetes: kubectl apply reference</a> — официальные границы <code>--dry-run=client</code> и <code>--dry-run=server</code>, включая отсутствие сохранения ресурса в server mode.</li><li><a href='https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments' target='_blank' rel='noopener noreferrer'>GitHub Docs: managing environments for deployment</a> — protection rules до запуска job и доступа к secrets, required reviewers и настройка bypass.</li></ul>"
}