{ "index": 96, "slug": "editorial-2025-05-practice-engineering-automation", "title": "Как безопасно автоматизировать массовые изменения: preview, approval и проверка результата", "excerpt": "Практический маршрут для операций, которые меняют сразу много объектов: зафиксировать состав targets, связать preview с разрешением, проверить отказоустойчивость и отделить receipt исполнителя от фактического состояния системы.", "contentHtml": "

Скрипт меняет label на двух документах и завершается за секунду. Через неделю тот же selector находит две тысячи объектов. Job получает статус success, но часть targets не подходит под новое правило.

Проблема возникает не из-за синтаксиса. Preview показывает только число объектов, approval хранит approved: true, а исполнитель заново вычисляет динамический selector после согласования. В журнале остаются время и имя оператора, но точный список изменений восстановить нельзя.

Безопасная автоматизация — это цепочка доказательств: что выбрано, что разрешено, что отправлено и что наблюдается после операции. Если связь между этими состояниями разорвана или evidence устарело, путь записи закрывается.

Сначала зафиксируйте границу операции

Массовая операция начинается с намерения, но не должна сразу получать право записи. Сначала система строит operation card: идентификатор, selector, точный список targets, digest списка, ожидаемый переход и лимит размера. Затем отдельные проверки связывают карточку с authority и approval.

У каждого барьера свой вопрос:

Эти ответы нельзя склеивать. Наличие preview не доказывает будущий результат. Approval не доказывает, что reviewer проверил смысл каждого изменения. Receipt исполнителя не доказывает состояние target system. Лог не делает операцию обратимой.

Preview фиксирует намерение, а не обещает эффект

Хороший preview показывает не фразу «обновить конфигурацию», а exact selection и proposed diff. Для списка targets нужны плотный массив идентификаторов, selector, exclusions и digest. Для изменения нужны expected before и expected after. Для расследования нужны operation id, версия правила и время построения.

Если selector вычисляется заново после approval, approval может относиться к другому набору. Поэтому исполнитель принимает только карточку с тем же operationId, targetDigest, selector и authority id. Любое несовпадение даёт stop. Список без digest можно прочитать, но его нельзя надёжно связать с последующим запросом.

У Terraform есть та же полезная граница: terraform plan создаёт execution plan и сам не выполняет предложенные изменения. Официальная документация отдельно предупреждает, что между speculative plan и применением состояние цели может измениться, поэтому перед применением нужен повторный non-speculative plan. Это пример того, почему preview нельзя считать вечным разрешением.

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

Диагностика массовой операции
СимптомПричинаПроверкаДействие
В preview есть только число targetsОперация не фиксирует exact selection и exclusionsСравнить список ids, selector и target digestОстановить approval до появления списка и лимита
Job получил approval, но selector вычисляется сноваApproval не связан с previewПроверить operation id, selector и digest перед executeОтклонить запрос при любом несовпадении
В журнале есть success, но состояние неизвестноReceipt исполнителя приняли за observed stateСделать независимое чтение authoritative sourceОставить статус stopped или verification-pending
Rollback запускает обратный payload автоматическиСтарый preview используют как новое разрешениеПроверить observed state и новый scopeСоздать отдельный rollback plan и новую authority
Операция затрагивает лишние targetsЛимит проверяют только в интерфейсеПодать scope больше лимита и проверить отказ до approvalПеренести проверку max targets в executor
\"Цепочка
Каждая карточка отвечает на свой вопрос. Красная ветка останавливает операцию; rollback начинается только с нового плана и новой проверки.

Учебная модель с fail-closed поведением

Ниже — учебный пример в памяти. Он не читает targets, не отправляет запросы и не выполняет запись. Его задача — показать границу между preview, approval, receipt и verification. В реальной системе отдельно задайте identity, durable storage, policy evaluation и контроль доступа; этот пример намеренно остаётся моделью в памяти.

node --input-type=module <<'EOF'\nimport { createHash } from 'node:crypto';\n\nconst canonicalTargets = (ids) => [...new Set(ids)].sort();\nconst digestTargets = (ids) => createHash('sha256')\n  .update(JSON.stringify(canonicalTargets(ids)))\n  .digest('hex');\n\nconst preview = {\n  operationId: 'op-demo-17',\n  selector: 'label=legacy',\n  targetIds: ['doc-b', 'doc-a'],\n  targetDigest: digestTargets(['doc-b', 'doc-a']),\n};\nconst authority = {\n  authorityId: 'role-maintainer',\n  selector: 'label=legacy',\n  maxTargets: 2,\n};\n\nfunction approve(p, a) {\n  const targets = canonicalTargets(p.targetIds);\n  if (targets.length !== p.targetIds.length) {\n    return { status: 'STOP', reason: 'duplicate-target' };\n  }\n  if (targets.length === 0 || targets.length > a.maxTargets) {\n    return { status: 'STOP', reason: 'scope-exceeds-authority-limit' };\n  }\n  if (p.selector !== a.selector) {\n    return { status: 'STOP', reason: 'selector-not-allowed' };\n  }\n  if (p.targetDigest !== digestTargets(p.targetIds)) {\n    return { status: 'STOP', reason: 'preview-digest-mismatch' };\n  }\n  return {\n    status: 'APPROVED',\n    operationId: p.operationId,\n    targetDigest: p.targetDigest,\n    authorityId: a.authorityId,\n  };\n}\n\nfunction execute(p, approval) {\n  if (approval.status !== 'APPROVED'\n      || approval.operationId !== p.operationId\n      || approval.targetDigest !== p.targetDigest) {\n    return { status: 'STOP', reason: 'approval-does-not-bind-preview' };\n  }\n  return { status: 'SIMULATED_RECEIPT', operationId: p.operationId };\n}\n\nconst approval = approve(preview, authority);\nconsole.log(approval);\nconsole.log(execute(preview, approval));\nconsole.log(execute(preview, {\n  ...approval,\n  targetDigest: digestTargets(['doc-a', 'doc-c']),\n}));\nEOF

Первый отказ должен произойти до approval, если карточка содержит третий target. Второй — до simulated execute, если кто-то подменил digest после согласования. Отсутствующее поле, неизвестный operation id и лишнее поле также должны закрывать путь. Fail-closed означает, что неполный или подозрительный input не получает безопасный-looking default.

SIMULATED_RECEIPT — только запись о прохождении учебной функции. Она не означает, что doc-a и doc-b изменились. Проверка результата потребовала бы отдельного чтения системы, которой в примере нет.

Dry-run имеет собственную границу

Preview и dry-run часто называют одним словом, хотя они отвечают за разные слои. Preview доменной операции может построить proposed diff из локальной модели. Dry-run конкретной команды может остановиться на клиенте или отправить проверяемый запрос серверу без сохранения.

Документация Kubernetes для kubectl apply разделяет режимы --dry-run=client и --dry-run=server. Client только печатает объект, который был бы отправлен. Server отправляет запрос без persistence ресурса. Значит, статус «dry-run прошёл» нужно сопровождать указанием режима, источника данных и границы. Ни один режим не гарантирует, что поздний реальный запрос встретит те же policy и состояние.

Если проверка использовала client dry-run, нельзя утверждать, что API-сервер примет запрос. Если server dry-run прошёл, нельзя утверждать, что через час selector вернёт тот же набор ресурсов. Следующее действие — повторить preconditions рядом с execute и сохранить новый digest.

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

Поле approved: true слишком слабое. Минимальная связь включает operation id, preview digest, target digest, authority id, reviewer или service identity, policy version и срок действия. Если digest другой, согласие относится к другой операции. Если authority другой, неизвестно, какой лимит применялся. Если истёк срок, старое решение не должно продолжать жить.

GitHub Actions даёт официальный пример узкой контрольной точки: job, который ссылается на environment с required reviewers, ждёт approval до старта; доступ к environment secrets появляется только после прохождения protection rules. Это контроль допуска к запуску. Он не доказывает правильность diff, качество selector или факт изменения целевых объектов. Содержательный review остаётся отдельной проверкой.

Audit trail и verification отвечают на разные вопросы

Audit trail связывает переходы: operation id, preview digest, authority, approval, actor, время, receipt и stop reason. Он помогает восстановить последовательность без поиска по чату и stdout. Но свойства «append-only» и «невозможно подделать» нельзя получить одним названием поля. Их нужно обеспечивать конкретным хранилищем, правами записи, retention и проверкой целостности.

Verification начинается после execute и использует authoritative read. Сначала задайте expectation: например, на каждом target поле label должно иметь значение current, а число отсутствующих targets равно нулю. Затем укажите источник, допустимую задержку и правило частичного результата. Если source недоступен, status должен быть verification-pending, а не verified.

Зелёный exit code не заменяет observed state. Receipt сообщает, что executor дошёл до своей точки завершения. Verification сообщает, что внешний объект сейчас выглядит ожидаемым образом. Эти записи нельзя объединять в одно поле.

Rollback — новая операция

Автоматический inverse payload опасен. Target мог измениться вручную после запуска. Исходное значение могло быть неправильным. Часть объектов могла принять новое состояние, а часть — нет. Внешняя зависимость могла изменить порядок восстановления.

При остановке безопасно создать только rollback plan: сохранить причину, прочитать observed state, определить новый bounded scope, выбрать owner, построить новый preview и получить новую authority и approval. Статус rollback-planned не означает rollback-executed. Старое approval не даёт права на новый write.

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

  1. Выберите одну операцию. Начните с малого обратимого batch, а не с широкого selector на всей системе.
  2. Назовите exact targets. Запишите ids, selector, exclusions, digest, expected before/after и maximum scope.
  3. Разделите capability. Preview и approval не должны иметь права записи. Execute принимает только связанный approval.
  4. Проверьте отказ. Добавьте лишний target, подмените digest, удалите authority id и передайте неизвестную карточку. Для каждого входа ожидайте STOP.
  5. Привяжите approval. Сравните operation id, selector, target digest, authority id, policy version и срок действия непосредственно перед execute.
  6. Запишите receipt отдельно. Сохраните переход и stop reason. Не называйте receipt подтверждением состояния внешней системы.
  7. Выполните независимую verification. Прочитайте authoritative source, сравните expected и observed, зафиксируйте partial result и задержку.
  8. Остановите неясный результат. При недоступном source или несовпадении не делайте автоматический retry-write.
  9. Планируйте rollback заново. Используйте observed state и новый scope. Получите новое разрешение на отдельную операцию.

Ограничения применимости

Учебная модель не проверяет реальные permissions, identity provider, состояние базы, гонки между preview и execute, сетевые повторы, durable audit storage, scheduler, очереди или фактический rollback. Она также не доказывает, что selector выражает правильное бизнес-условие. Все значения кода фиксированы в памяти; реальная запись намеренно не выполняется.

Даже exact digest не решает проблему сам по себе. Хэш связывает представления, но не говорит, что selection полон, authority разумна, expected state корректен или reviewer понял риск. Нужны владельцы данных, политика доступа и источник истины. Чем дороже ошибка, тем меньше должна быть граница первой операции.

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

Операция готова к ограниченному реальному запуску, если другой инженер может без устного объяснения восстановить exact targets, proposed diff, maximum scope, authority, approval binding, stop conditions, receipt и authoritative verification. Тест с подменой target digest останавливает executor до записи. Тест с лишним target останавливает approval до запуска. После execute существует отдельная проверка observed state. При несовпадении система не делает скрытый retry и создаёт только новый rollback plan.

Если хотя бы один пункт держится на внимательности оператора, автоматизацию не расширяют. Сначала переносят правило в contract и проверяют отрицательный путь. Практическая граница — не «job завершился успешно», а «система показывает, что именно было разрешено, что произошло и каким чтением проверен результат».

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

" }