{ "index": 95, "slug": "editorial-2025-05-mechanism-engineering-automation", "title": "Почему preview и approval не делают автоматизацию безопасной", "excerpt": "Preview показывает рассчитанное намерение, authority ограничивает допустимый scope, approval связывает согласие с конкретной операцией, а verification читает результат. Если эти доказательства смешать, batch-автоматизация может изменить лишние объекты.", "contentHtml": "

Симптом обнаруживается после зелёного pipeline: preview показал ожидаемый diff, а reviewer нажал approve. После запуска изменились записи за пределами заявки. В журнале есть operation id, но из него не видно, какие объекты реально изменились. Команда тратит часы на восстановление списка целей и рискует запустить ещё один широкий change под видом rollback.

\n

Причина не в отсутствии ещё одной кнопки подтверждения. Разные этапы отвечают на разные вопросы: preview описывает предложение в определённый момент, authority задаёт предел полномочий, approval фиксирует согласие, audit trail сохраняет событие, а verification читает целевое состояние после записи. Без явной связи между ними зелёный статус доказывает меньше, чем кажется.

\n

Пять артефактов — пять разных утверждений

\n

Начните с разделения доказательств. Это не бюрократия, а способ не выдать одно наблюдение за другое. Наличие preview не говорит, что scope разрешён. Approval не говорит, что target list остался прежним. Receipt от executor не доказывает, что целевая система сохранила значение.

\n
Что доказывает каждый артефакт и какой вопрос остаётся открытым
АртефактЧестное утверждениеОпасная подменаСледующая проверка
PreviewДля зафиксированного snapshot рассчитан proposed changeExecute даст тот же эффектПроверить свежесть и preconditions перед записью
AuthorityPolicy разрешает selector и размер scopeChange полезен и технически корректенСверить intent и содержимое preview
ApprovalReviewer разрешил связанную карточку в своей ролиСогласие переносится на похожую операциюСравнить operation, proposal, target и authority digests
Audit trailExecutor записал событие по своему контрактуTarget system уже в ожидаемом состоянииПрочитать authoritative source после execute
VerificationReader наблюдал заданное expected stateВсе побочные последствия устраненыПроверить residual risk и план recovery
\n

В карточке операции храните как минимум идентификатор, selector, точный список targets, digest списка, ожидаемое состояние до и после, версию логики изменения и срок действия. Человеческое описание остаётся полезным для review, но executor должен сравнивать машинные поля буквально.

\n

Preview — снимок, а не разрешение

\n

Preview устаревает, когда меняется объект, inventory или policy. Поэтому его ценность ограничена snapshot version и временем расчёта. Если между preview и execute другой job обновил target, старый diff больше не является описанием текущего действия. В безопасном процессе есть явное условие: пересчитать предложение или остановиться, если precondition больше не выполняется.

\n

Это соответствует модели Terraform. Официальная документация описывает terraform plan как расчёт изменений без их выполнения. План без -out является speculative plan и не содержит намерения применяться. Документация отдельно предупреждает, что изменения в target system между ранним speculative plan и финальным применением могут изменить результат; перед apply нужно снова проверить актуальный non-speculative plan. Это узкий факт о Terraform, а не готовая гарантия для любого самописного runner.

\n

Сохраняйте не только красивый diff. Практический минимум выглядит так: operationId, snapshotVersion, selector, dense target list, targetDigest, expected precondition и proposalDigest. Digest здесь связывает две фазы и позволяет обнаружить расхождение. Он не заменяет криптографическую подпись, проверку identity или защиту хранилища.

\n

Dry-run имеет конкретную границу

\n

Слово dry-run описывает режим, но не обещает одинаковый объём проверки. В официальной reference для kubectl apply режим --dry-run=client только печатает объект, который был бы отправлен, без отправки. Режим --dry-run=server передаёт запрос на сервер, но не сохраняет ресурс. Значит, client mode проверяет локальную подготовку объекта, а server mode проходит часть серверного пути. Ни один режим сам по себе не подтверждает будущий persistent change.

\n

В карточке записывайте источник данных и границу побочного эффекта: «локальная модель, внешний read не выполняется», «server admission, сохранение запрещено» или «read-only inventory со snapshot». Тогда reviewer понимает, какую проверку ещё нужно сделать. Если этого поля нет, слово preview создаёт ложное ощущение полноты.

\n
Матрица показывает, как authority, target scope, preview digest и approval binding ведут к execute и verification; несовпадение любой границы останавливает операцию
Authority ограничивает scope, approval связывает конкретную карточку, а verification проверяет состояние после execute. Красная клетка означает остановку до следующей записи, а не автоматический rollback.
\n

Authority ограничивает мощность операции

\n

Authority — это машинная граница: допустимый selector, максимальное число targets, срок действия и исключения. Она отвечает на вопрос «можно ли этой роли работать с таким scope», но не решает, правильно ли менять поле. Если policy разрешает два объекта, карточка с тремя должна остановиться до approval. Список нельзя молча уменьшать: reviewer должен видеть ровно тот scope, который получит executor.

\n

Сравнивайте exact selector и exact target digest. Не подменяйте их словами «маленький batch» или «почти тот же список». Не расширяйте authority, если часть целей исчезла из inventory. Не превращайте превышение лимита в warning, когда последующая операция может затронуть больше объектов, чем видел reviewer.

\n

Воспроизводимый gate до записи

\n

Ниже — автономный пример на Node.js. Он работает только с объектами в памяти, не читает сеть, не проверяет реальные права и ничего не меняет во внешней системе. В нём три synthetic target, authority разрешает два, а approval связан с точным operation и target digest. Скопируйте блок в shell с Node.js 18+:

\n
node --input-type=module <<'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 > 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
\n

Команда возвращает STOP, потому что три цели превышают лимит два. Чтобы проверить вторую отрицательную ветку, замените в approval значение targetDigest на targets-a-c-v1: результат дополнится approval-does-not-bind-operation. В настоящей системе digest вычисляет канонизатор данных, authority приходит из доверенного policy engine, а identity и срок решения проверяются отдельно.

\n

Approval связывает согласие с карточкой

\n

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.

\n

У GitHub Environments есть похожая, но более узкая граница: job, который ссылается на environment, должен пройти настроенные protection rules до запуска или доступа к environment secrets. В документации required reviewers и запрет административного bypass описаны как отдельные настройки. Это пример gate запуска, а не доказательство корректности diff. Доступность функций зависит от типа репозитория и плана GitHub, поэтому переносить его поведение в собственный runner без проверки нельзя.

\n

Bypass тоже является событием с владельцем, причиной и audit record. Если исключение скрыто внутри общего поля approved=true, расследование не отличит обычное согласие от обхода. Для destructive operation отсутствие reviewer или истёкший expiry должны вести к stop, а не к default-решению.

\n

Receipt и verification — разные результаты

\n

Audit trail восстанавливает причинную цепочку: operation id, версии runner и policy, selector, digests, authority, approval, время и результат по каждой цели. Но запись «executor отправил запрос» — это receipt. Она не подтверждает, что target system приняла значение, что controller не откатил его сразу и что побочный объект не изменился.

\n

Verification начинается с ожидаемого состояния. Например: authoritative reader видит label env=staging на двух конкретных targets, digest списка совпадает, а отсутствующие объекты перечислены. У проверки должны быть три различимых исхода: matched, mismatched и unknown. Недоступный reader — это unknown, а не «успех с предупреждением».

\n
Диагностика после запуска
СимптомГипотезаПроверкаДействие
Изменились лишние объектыSelector расширился или scope не был связан с approvalСравнить target digest до write и фактический списокОстановить цепочку и собрать bounded inventory
Preview и execute показывают разный diffTarget state изменился между фазамиСверить snapshot version, precondition и возраст previewПересчитать preview, старое approval не использовать
В audit есть success, но поле прежнееReceipt выдали за verificationПрочитать authoritative source отдельным путёмВернуть mismatched или unknown
Client dry-run прошёл, сервер отказалЛокальная проверка не видела server-side policyСравнить режим dry-run и ответ admissionИсправить вход или остановиться до нового review
Rollback снова меняет не те targetsОбратный write использует старый scopeПроверить observed state и новую authorityОформить rollback как новую операцию
\n

Rollback — новый change

\n

После частичного результата нельзя считать исходное значение безопасной обратной командой. Человек мог исправить часть targets, исходное состояние могло стать устаревшим, а новая policy может запретить обратную запись. Поэтому первый шаг — observed inventory: что изменилось, где состояние неизвестно, какие зависимости затронуты и кто владеет recovery.

\n

Затем rollback проходит тот же bounded маршрут: новый scope, новые preconditions, новая authority, новый preview, отдельный approval и последующая verification. Статус «rollback planned» не означает «rollback executed». Автоматический inverse в блоке finally опасен именно потому, что исполняется без свежего знания о состоянии.

\n

Практический порядок внедрения

\n
  1. Выберите одну операцию. Начните с обратимого batch на двух учебных или тестовых targets, а не с широкого selector.
  2. Опишите contract. Запишите intent, selector, exclusions, exact target list, target digest и expected before/after.
  3. Зафиксируйте preview boundary. Укажите snapshot version, источник, время расчёта и условие, при котором нужен новый preview.
  4. Проверьте authority. Сравните selector, лимит, срок и исключения до запроса approval.
  5. Привяжите approval. Сверьте operation, proposal, target и authority ids буквально; любое отсутствие означает stop.
  6. Повторите preconditions перед write. Проверьте существование targets, версию, scope и действительность policy.
  7. Сохраните receipt отдельно. Не называйте событие executor доказательством состояния.
  8. Сделайте observed verification. Прочитайте authoritative source и зафиксируйте matched, mismatched или unknown.
  9. При unknown остановитесь. Соберите observed inventory и подготовьте новый bounded change; не используйте старое approval для rollback.
\n

Границы применимости

\n

Схема не создаёт identity provider, policy engine, неизменяемое хранилище журнала, распределённую блокировку, гарантию идемпотентности или реальный rollback. Учебный код не доказывает права пользователя, свежесть inventory, криптографическую стойкость digest и фактическое состояние production. Он проверяет только связь полей и отказ до внешней записи.

\n

Даже независимая verification имеет границу: reader может быть устаревшим, неполным или не видеть побочные системы. Для каждого change назовите источник истины, допустимую задержку и владельца unknown. Для удаления и других необратимых действий добавьте отдельные меры: резервную копию, лимит скорости, ручное подтверждение и план восстановления.

\n

Документация Terraform, Kubernetes и GitHub меняется; флаги и доступность функций нужно сверять с версией и тарифом своего окружения. Примеры в статье иллюстрируют узкие свойства официальных инструментов, а не сертифицируют единый безопасный процесс. Готовность определяется собственными тестами: scope выше лимита останавливается, чужой digest не проходит, недоступный reader остаётся unknown, а старое approval не запускает rollback.

\n

Проверяемые источники

\n" }