8 lines
22 KiB
JSON
8 lines
22 KiB
JSON
{
|
||
"index": 95,
|
||
"slug": "editorial-2025-05-mechanism-engineering-automation",
|
||
"title": "Почему preview и approval не делают автоматизацию безопасной",
|
||
"excerpt": "Preview показывает намерение, approval фиксирует согласие, журнал хранит событие. Без ограничения scope и независимой проверки состояния автоматизация всё равно может изменить не те объекты.",
|
||
"contentHtml": "<p>Pipeline завершился зелёным, preview показал ожидаемый diff, а reviewer нажал approve. После запуска изменились записи за пределами заявки. В журнале есть operation id, но он не отвечает на главный вопрос: какие объекты реально изменились. Команда тратит часы на восстановление списка целей, проверку частичного результата и ручной откат.</p>\n<p>Цена ошибки растёт с размером batch-операции. Неверный selector меняет не одну запись, а весь совпавший набор. Старый preview уже не описывает состояние системы. Автоматический rollback может затереть исправления, которые кто-то внёс между двумя запусками.</p>\n<p>Тезис простой: безопасный change требует разных доказательств на разных границах. Preview доказывает только рассчитанное намерение. Authority ограничивает допустимый scope. Approval связывает согласие с конкретной версией proposal. Audit trail сохраняет переходы. Verification читает целевое состояние после execute. Если один слой подменяет другой, runner должен остановиться.</p>\n<h2>Механизм: пять артефактов, пять вопросов</h2>\n<p>У каждой фазы свой владелец и свой вопрос. Preview отвечает, что планировщик рассчитал в фиксированный момент. Authority отвечает, имеет ли операция право работать с выбранным scope. Approval отвечает, согласовал ли reviewer именно эту операцию. Audit trail отвечает, какое событие записал исполнитель. Verification отвечает, видит ли авторитетный reader ожидаемый результат.</p>\n<p>Ни один ответ не следует из другого. Наличие approval не доказывает, что reviewer видел полный список целей. Запись execution не доказывает, что target system приняла каждое изменение. Успешный dry-run не доказывает, что следующий write применится к тому же состоянию.</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>Планировщик рассчитал proposed change для выбранного snapshot</td><td>Execute будет ровно таким же</td><td>Пересчитать preconditions перед write</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>Reviewer проверил каждую цель и побочный эффект</td><td>Сверить operation id и digests</td></tr><tr><td>Audit trail</td><td>Исполнитель записал событие по контракту</td><td>Target system уже находится в ожидаемом состоянии</td><td>Сделать независимый read</td></tr><tr><td>Verification</td><td>Reader увидел заданное условие</td><td>Все последствия change безопасны</td><td>Проверить остаточный риск и recovery</td></tr></tbody></table></div>\n<h2>Preview имеет срок годности</h2>\n<p>Preview — снимок намерения, а не разрешение на исполнение. Между расчётом и write другой job может изменить объект. Policy может обновиться. Selector может начать выбирать другой набор. Поэтому в карточке хранят не только человекочитаемый diff, но и operation id, snapshot version, selector, dense target list, target digest, expected precondition и proposal digest.</p>\n<p>Terraform разделяет speculative plan и план, который можно применить. Официальная документация прямо предупреждает: изменения в целевой системе после раннего speculative plan могут поменять финальный эффект, поэтому перед apply нужно проверить актуальный non-speculative plan. Этот принцип переносится на самописный runner без привязки к Terraform: старый diff нужно пересчитать или отклонить по TTL.</p>\n<p>Dry-run тоже бывает разным. В Kubernetes <code>kubectl apply --dry-run=client</code> только печатает объект и не отправляет его. Режим <code>--dry-run=server</code> отправляет server-side request, но не сохраняет ресурс. Первый проверяет локальную сериализацию. Второй проходит часть серверной обработки. Ни один режим не подтверждает будущий persistent change.</p>\n<h2>Authority ограничивает мощность</h2>\n<p>Authority не решает, стоит ли менять поле. Она ограничивает то, что исполнитель может сделать: допустимый selector, максимальное число целей, срок действия и исключения. Если policy разрешает два объекта, карточка с тремя не должна превращаться в warning. Runner возвращает stop до approval.</p>\n<p>Граница должна быть машинной. Сравнивайте exact selector и exact target digest. Не полагайтесь на текст «небольшой batch». Не уменьшайте список молча: reviewer должен увидеть тот же scope, который получит executor. Не расширяйте authority по умолчанию, если часть целей стала недоступна.</p>\n<h2>Учебный пример: остановка до write</h2>\n<p>Ниже — самостоятельная модель в памяти. Она не обращается к сети, не читает права и не меняет production. Карточка содержит три synthetic targets, а authority разрешает два. Отрицательный путь важнее счастливого: операция не доходит до approval.</p>\n<pre><code>const 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\nfunction requestApproval(op, policy) {\n if (op.selector !== policy.selector) {\n return { status: 'STOP', reason: 'selector-not-authorized' };\n }\n if (op.targets.length > policy.maxTargets) {\n return {\n status: 'STOP',\n reason: 'scope-exceeds-authority-limit',\n targetCount: op.targets.length\n };\n }\n return { status: 'READY_FOR_APPROVAL', operationId: op.id };\n}\n\nconsole.log(requestApproval(operation, authority));\n// { status: 'STOP', reason: 'scope-exceeds-authority-limit', targetCount: 3 }</code></pre>\n<p>Пример проверяет контракт, а не безопасность пользователя. В реальной системе policy получает identity из провайдера, срок из доверенных часов, а target list — из авторитетного inventory. Здесь значения фиксированы, чтобы показать один переход: превышение лимита останавливает цепочку до любого внешнего действия.</p>\n<h2>Approval должен быть связанным</h2>\n<p>Approval без binding легко переносится на другую операцию. Минимальная связка содержит operation id, proposal digest, target digest, authority id, reviewer role, decision и expiry. Executor сравнивает эти поля перед write. Любое несовпадение, отсутствие или неизвестное значение возвращает stop.</p>\n<p>Похожий gate есть в GitHub Environments: job, который ссылается на environment, проходит настроенные protection rules до запуска и доступа к environment secrets. Required reviewers и bypass — отдельные настройки. Это gate стадии, а не доказательство смысла изменения. Reviewer подтверждает разрешение на job, но не заменяет проверку конкретного target digest.</p>\n<p>Проверяйте также отрицательный путь. Если preview относится к <code>targets-a-b-v1</code>, а approval к <code>targets-a-c-v1</code>, процесс не должен выбирать «более похожий» список. Он возвращает <code>STOP: approval-does-not-bind-target</code> и требует новый preview. Иначе система превратит человеческую ошибку в скрытый write.</p>\n<h2>Audit trail не наблюдает состояние</h2>\n<p>Журнал связывает фазы одной операции. В нём полезны operation id, version runner, selector, digests, authority, approval, timestamps и результат каждого target. Это помогает восстановить причинную цепочку. Но журнал фиксирует сообщение исполнительной системы. Он не читает целевой объект и не доказывает, что тот сохранился.</p>\n<p>Разделяйте receipt и evidence. Receipt говорит: «executor отправил запрос и получил ответ». Evidence говорит: «authoritative reader прочитал поле после операции». Если dashboard помечает change как verified только по наличию audit event, он скрывает самый дорогой отказ — неизвестное состояние.</p>\n<h2>Verification и rollback</h2>\n<p>Verification начинается с явного expected state. Например: reader видит у двух объектов label <code>env=staging</code>, target digest совпадает с карточкой, отсутствующие объекты перечислены, а stale inventory имеет отдельный статус. Exit code исполнителя не заменяет этот read.</p>\n<p>Если reader недоступен, состояние неизвестно. Если один объект не совпал, операция остановлена. Не переводите эти исходы в «успех с предупреждением», если следующий запуск может затронуть тот же scope.</p>\n<p>Rollback не является обратной строкой в <code>finally</code>. После частичного выполнения исходное значение может быть устаревшим. Часть объектов может исправить человек. Новая policy может запретить обратную операцию. Безопасный rollback начинается с observed state, нового bounded scope, новой authority, нового preview и нового approval. Исходное approval не наследуется.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<div class=\"table-scroll\"><table><caption>Диагностика автоматизированного change</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>После approve изменились лишние объекты</td><td>Scope не связан с approval или selector расширился</td><td>Сравнить target digest, selector и preconditions перед write</td><td>Остановить операцию и создать новый bounded preview</td></tr><tr><td>Preview зелёный, а execute получил другой diff</td><td>Между фазами изменился target state</td><td>Проверить snapshot version и возраст preview</td><td>Пересчитать plan; старый approval не использовать</td></tr><tr><td>В audit есть success, но поле не изменилось</td><td>Receipt выдали за verification</td><td>Прочитать authoritative source после execute</td><td>Вернуть статус unknown или mismatch</td></tr><tr><td>Dry-run прошёл, write отклонён сервером</td><td>Локальная проверка не видела server-side policy</td><td>Сравнить client/server режим и ответ admission</td><td>Исправить вход или остановить до нового review</td></tr><tr><td>Rollback снова повредил часть данных</td><td>Использовали старый scope и старое состояние</td><td>Собрать observed inventory и проверить preconditions</td><td>Планировать rollback как отдельный change</td></tr></tbody></table></div>\n<h2>Иллюстрация границ</h2>\n<figure><img src=\"/assets/editorial/2025/engineering-automation-2025-authority-matrix.svg\" alt=\"Матрица показывает связь authority, target scope, preview digest, approval binding, execution и verification; несовпадение останавливает цепочку\" loading=\"lazy\" /><figcaption>Authority ограничивает scope, но не оценивает смысл идеи. Approval связывает карточку. Verification проверяет состояние после execute. Красная клетка означает STOP, а не автоматический rollback.</figcaption></figure>\n<h2>Порядок действий</h2>\n<ol><li><strong>Опишите операцию.</strong> Запишите intent, selector, target list, target digest, expected before и expected after.</li><li><strong>Зафиксируйте границу preview.</strong> Укажите snapshot version, источник данных, время расчёта и условие устаревания.</li><li><strong>Проверьте authority.</strong> Сравните selector, лимит, срок и исключения до запроса approval.</li><li><strong>Свяжите approval.</strong> Сохраните operation id, proposal digest, target digest и authority id. Любое несовпадение остановите.</li><li><strong>Повторите preconditions перед write.</strong> Проверьте существование целей, версию, scope и действительность policy.</li><li><strong>Запишите receipt отдельно.</strong> Не называйте событие исполнителя доказательством состояния.</li><li><strong>Сделайте независимую verification.</strong> Прочитайте authoritative source и сравните его с expected state.</li><li><strong>Остановите неизвестное.</strong> При mismatch или недоступном reader соберите observed inventory и спланируйте новый bounded change. Не запускайте широкий rollback автоматически.</li></ol>\n<h2>Ограничения</h2>\n<p>Эта схема не создаёт identity provider, неизменяемое хранилище журнала, криптографическую подпись, распределенный lock или гарантию идемпотентности. Учебный код не читает реальные targets и не даёт production-результатов. Digests в примере — строки для проверки связи, а не доказательство криптографической стойкости.</p>\n<p>Независимая verification тоже имеет границу. Reader может быть устаревшим, неполным или не видеть побочные системы. Назначьте источник истины и отдельно опишите, что означает unknown. Для destructive change нужны дополнительные ограничения: backup, ручное подтверждение, лимит скорости и владелец recovery.</p>\n<p>Approval не делает решение правильным. Он только фиксирует разрешение в определённой роли. Authority не проверяет бизнес-смысл. Audit trail не гарантирует сохранность без свойств storage. Эти ограничения нужно оставить рядом с контрактом, иначе короткий статус начнёт обещать больше, чем проверка.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Механизм готов к ограниченному запуску, если другой инженер может по одной карточке восстановить intent, selector, exact targets, digests, authority, approval и expected verification. Тест с несовпадающим target digest останавливается до write. Устаревший preview требует пересчёта. Недоступный reader возвращает unknown. Частичный результат не запускает обратную операцию по старому approval.</p>\n<p>Минимальный набор проверок состоит из четырёх случаев: bounded scope проходит к review; scope выше лимита останавливается; approval с другим digest останавливается; после execute verification читает target system и отдельно сообщает mismatch. Только после прохождения этих случаев можно обсуждать более широкий scope. В статье нет утверждения, что такой механизм уже дал результат в production.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://developer.hashicorp.com/terraform/cli/commands/plan\" target=\"_blank\" rel=\"noopener noreferrer\">HashiCorp Terraform: plan command</a> — официальная документация различает speculative plan и применяемый plan и предупреждает о расхождении после изменений в target system.</li><li><a href=\"https://kubernetes.io/docs/reference/kubectl/generated/kubectl_apply/\" target=\"_blank\" rel=\"noopener noreferrer\">Kubernetes: kubectl apply</a> — официальная reference различает client и server dry-run: один не отправляет объект, другой отправляет запрос без сохранения ресурса.</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, required reviewers и возможность запретить bypass; эти правила ограничивают запуск, но не доказывают корректность change.</li></ul>"
|
||
}
|