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

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

\n

Цена ошибки растёт с размером batch-операции. Неверный selector меняет не одну запись, а весь совпавший набор. Старый preview уже не описывает состояние системы. Автоматический rollback может затереть исправления, которые кто-то внёс между двумя запусками.

\n

Тезис простой: безопасный change требует разных доказательств на разных границах. Preview доказывает только рассчитанное намерение. Authority ограничивает допустимый scope. Approval связывает согласие с конкретной версией proposal. Audit trail сохраняет переходы. Verification читает целевое состояние после execute. Если один слой подменяет другой, runner должен остановиться.

\n

Механизм: пять артефактов, пять вопросов

\n

У каждой фазы свой владелец и свой вопрос. Preview отвечает, что планировщик рассчитал в фиксированный момент. Authority отвечает, имеет ли операция право работать с выбранным scope. Approval отвечает, согласовал ли reviewer именно эту операцию. Audit trail отвечает, какое событие записал исполнитель. Verification отвечает, видит ли авторитетный reader ожидаемый результат.

\n

Ни один ответ не следует из другого. Наличие approval не доказывает, что reviewer видел полный список целей. Запись execution не доказывает, что target system приняла каждое изменение. Успешный dry-run не доказывает, что следующий write применится к тому же состоянию.

\n
Что на самом деле доказывает каждый артефакт
АртефактЧестное утверждениеОпасная подменаСледующая проверка
PreviewПланировщик рассчитал proposed change для выбранного snapshotExecute будет ровно таким жеПересчитать preconditions перед write
AuthorityPolicy разрешает selector и размер scopeChange полезен и технически корректенПроверить intent и содержимое preview
ApprovalReviewer разрешил связанную карточкуReviewer проверил каждую цель и побочный эффектСверить operation id и digests
Audit trailИсполнитель записал событие по контрактуTarget system уже находится в ожидаемом состоянииСделать независимый read
VerificationReader увидел заданное условиеВсе последствия change безопасныПроверить остаточный риск и recovery
\n

Preview имеет срок годности

\n

Preview — снимок намерения, а не разрешение на исполнение. Между расчётом и write другой job может изменить объект. Policy может обновиться. Selector может начать выбирать другой набор. Поэтому в карточке хранят не только человекочитаемый diff, но и operation id, snapshot version, selector, dense target list, target digest, expected precondition и proposal digest.

\n

Terraform разделяет speculative plan и план, который можно применить. Официальная документация прямо предупреждает: изменения в целевой системе после раннего speculative plan могут поменять финальный эффект, поэтому перед apply нужно проверить актуальный non-speculative plan. Этот принцип переносится на самописный runner без привязки к Terraform: старый diff нужно пересчитать или отклонить по TTL.

\n

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

\n

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

\n

Authority не решает, стоит ли менять поле. Она ограничивает то, что исполнитель может сделать: допустимый selector, максимальное число целей, срок действия и исключения. Если policy разрешает два объекта, карточка с тремя не должна превращаться в warning. Runner возвращает stop до approval.

\n

Граница должна быть машинной. Сравнивайте exact selector и exact target digest. Не полагайтесь на текст «небольшой batch». Не уменьшайте список молча: reviewer должен увидеть тот же scope, который получит executor. Не расширяйте authority по умолчанию, если часть целей стала недоступна.

\n

Учебный пример: остановка до write

\n

Ниже — самостоятельная модель в памяти. Она не обращается к сети, не читает права и не меняет production. Карточка содержит три synthetic targets, а authority разрешает два. Отрицательный путь важнее счастливого: операция не доходит до approval.

\n
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 }
\n

Пример проверяет контракт, а не безопасность пользователя. В реальной системе policy получает identity из провайдера, срок из доверенных часов, а target list — из авторитетного inventory. Здесь значения фиксированы, чтобы показать один переход: превышение лимита останавливает цепочку до любого внешнего действия.

\n

Approval должен быть связанным

\n

Approval без binding легко переносится на другую операцию. Минимальная связка содержит operation id, proposal digest, target digest, authority id, reviewer role, decision и expiry. Executor сравнивает эти поля перед write. Любое несовпадение, отсутствие или неизвестное значение возвращает stop.

\n

Похожий gate есть в GitHub Environments: job, который ссылается на environment, проходит настроенные protection rules до запуска и доступа к environment secrets. Required reviewers и bypass — отдельные настройки. Это gate стадии, а не доказательство смысла изменения. Reviewer подтверждает разрешение на job, но не заменяет проверку конкретного target digest.

\n

Проверяйте также отрицательный путь. Если preview относится к targets-a-b-v1, а approval к targets-a-c-v1, процесс не должен выбирать «более похожий» список. Он возвращает STOP: approval-does-not-bind-target и требует новый preview. Иначе система превратит человеческую ошибку в скрытый write.

\n

Audit trail не наблюдает состояние

\n

Журнал связывает фазы одной операции. В нём полезны operation id, version runner, selector, digests, authority, approval, timestamps и результат каждого target. Это помогает восстановить причинную цепочку. Но журнал фиксирует сообщение исполнительной системы. Он не читает целевой объект и не доказывает, что тот сохранился.

\n

Разделяйте receipt и evidence. Receipt говорит: «executor отправил запрос и получил ответ». Evidence говорит: «authoritative reader прочитал поле после операции». Если dashboard помечает change как verified только по наличию audit event, он скрывает самый дорогой отказ — неизвестное состояние.

\n

Verification и rollback

\n

Verification начинается с явного expected state. Например: reader видит у двух объектов label env=staging, target digest совпадает с карточкой, отсутствующие объекты перечислены, а stale inventory имеет отдельный статус. Exit code исполнителя не заменяет этот read.

\n

Если reader недоступен, состояние неизвестно. Если один объект не совпал, операция остановлена. Не переводите эти исходы в «успех с предупреждением», если следующий запуск может затронуть тот же scope.

\n

Rollback не является обратной строкой в finally. После частичного выполнения исходное значение может быть устаревшим. Часть объектов может исправить человек. Новая policy может запретить обратную операцию. Безопасный rollback начинается с observed state, нового bounded scope, новой authority, нового preview и нового approval. Исходное approval не наследуется.

\n

Симптом → причина → проверка → действие

\n
Диагностика автоматизированного change
СимптомПричинаПроверкаДействие
После approve изменились лишние объектыScope не связан с approval или selector расширилсяСравнить target digest, selector и preconditions перед writeОстановить операцию и создать новый bounded preview
Preview зелёный, а execute получил другой diffМежду фазами изменился target stateПроверить snapshot version и возраст previewПересчитать plan; старый approval не использовать
В audit есть success, но поле не изменилосьReceipt выдали за verificationПрочитать authoritative source после executeВернуть статус unknown или mismatch
Dry-run прошёл, write отклонён серверомЛокальная проверка не видела server-side policyСравнить client/server режим и ответ admissionИсправить вход или остановить до нового review
Rollback снова повредил часть данныхИспользовали старый scope и старое состояниеСобрать observed inventory и проверить preconditionsПланировать rollback как отдельный change
\n

Иллюстрация границ

\n
\"Матрица
Authority ограничивает scope, но не оценивает смысл идеи. Approval связывает карточку. Verification проверяет состояние после execute. Красная клетка означает STOP, а не автоматический rollback.
\n

Порядок действий

\n
  1. Опишите операцию. Запишите intent, selector, target list, target digest, expected before и expected after.
  2. Зафиксируйте границу preview. Укажите snapshot version, источник данных, время расчёта и условие устаревания.
  3. Проверьте authority. Сравните selector, лимит, срок и исключения до запроса approval.
  4. Свяжите approval. Сохраните operation id, proposal digest, target digest и authority id. Любое несовпадение остановите.
  5. Повторите preconditions перед write. Проверьте существование целей, версию, scope и действительность policy.
  6. Запишите receipt отдельно. Не называйте событие исполнителя доказательством состояния.
  7. Сделайте независимую verification. Прочитайте authoritative source и сравните его с expected state.
  8. Остановите неизвестное. При mismatch или недоступном reader соберите observed inventory и спланируйте новый bounded change. Не запускайте широкий rollback автоматически.
\n

Ограничения

\n

Эта схема не создаёт identity provider, неизменяемое хранилище журнала, криптографическую подпись, распределенный lock или гарантию идемпотентности. Учебный код не читает реальные targets и не даёт production-результатов. Digests в примере — строки для проверки связи, а не доказательство криптографической стойкости.

\n

Независимая verification тоже имеет границу. Reader может быть устаревшим, неполным или не видеть побочные системы. Назначьте источник истины и отдельно опишите, что означает unknown. Для destructive change нужны дополнительные ограничения: backup, ручное подтверждение, лимит скорости и владелец recovery.

\n

Approval не делает решение правильным. Он только фиксирует разрешение в определённой роли. Authority не проверяет бизнес-смысл. Audit trail не гарантирует сохранность без свойств storage. Эти ограничения нужно оставить рядом с контрактом, иначе короткий статус начнёт обещать больше, чем проверка.

\n

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

\n

Механизм готов к ограниченному запуску, если другой инженер может по одной карточке восстановить intent, selector, exact targets, digests, authority, approval и expected verification. Тест с несовпадающим target digest останавливается до write. Устаревший preview требует пересчёта. Недоступный reader возвращает unknown. Частичный результат не запускает обратную операцию по старому approval.

\n

Минимальный набор проверок состоит из четырёх случаев: bounded scope проходит к review; scope выше лимита останавливается; approval с другим digest останавливается; после execute verification читает target system и отдельно сообщает mismatch. Только после прохождения этих случаев можно обсуждать более широкий scope. В статье нет утверждения, что такой механизм уже дал результат в production.

\n

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

" }