# Endpoint отката для автономных развертываний без конфликтов

Автономным агентам развертывания нужно разрешение безопасно ошибаться, а не право гадать. Опасный откат редко выглядит как команда, которая завершается ошибкой. Гораздо опаснее команда, которая успешно выполняется после изменения обстановки и возвращает старый артефакт поверх релиза, созданного не этим агентом.

Безопасное действие отката указывает на конкретную попытку развертывания, называет конкретный предыдущий релиз и отказывается работать, если его представление о текущем состоянии устарело. Относитесь к откату как к защищенному переходу состояния, а не как к короткому пути к «последней рабочей версии». От этого зависит, исправит ли агент собственную работу или сотрет чужую.

## Откат должен относиться к одной попытке развертывания

Endpoint отката должен связывать отмену с той попыткой развертывания, которая вызвала предполагаемый сбой. Одного номера версии для этого недостаточно.

Команды часто хранят последовательность вроде `1.8.4`, `1.8.5` и `1.8.6`, а затем предоставляют операцию «развернуть 1.8.4 в production». Агент развертывает `1.8.5`, получает оповещение и вызывает эту операцию. Тем временем инженер выпускает срочный релиз `1.8.6`. Endpoint принимает запрос, потому что `1.8.4` существует. Теперь в production работает артефакт, созданный до обоих изменений. API сделал ровно то, что ему сказали, и в этом проблема.

Разделяйте следующие идентификаторы:

- **Релиз** - неизменяемый пакет кода, ссылок на конфигурацию и метаданных. Присвойте ему постоянный ID и дайджест артефакта.
- **Попытка развертывания** - отдельный запрос на размещение релиза в указанной целевой среде. У нее есть ID развертывания, инициатор и жизненный цикл.
- **Состояние цели** - то, что сейчас запущено в среде. Оно включает ревизию или поколение, которое меняется при каждом принятом переходе.
- **Намерение отката** - указание, что попытка развертывания `D` может восстановить целевой релиз `R`, только если `D` все еще владеет текущим состоянием.

Чаще всего забывают о связи владения. Когда развертывание `dep_842` продвигает релиз `rel_105`, зафиксируйте, что `dep_842` создало текущее поколение цели `gen_913`. Откат, связанный с `dep_842`, может выполняться только пока `gen_913` остается текущим. Если более позднее развертывание создает `gen_914`, сервис должен отклонить старый запрос.

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

Это также отвечает на неудобный операционный вопрос: может ли агент откатить развертывание другого агента? Обычно нет. Отдельный уполномоченный может явно разрешить такое вмешательство, но обычная возможность отката должна охватывать только действия, инициированные самим вызывающим. Широкие полномочия кажутся удобными, пока два цикла развертывания не начинают реагировать на один и тот же инцидент.

## Неизменяемая история происхождения помогает точно определить предыдущий релиз

Сервис должен сохранить происхождение отката до изменения цели, потому что после продвижения слово «предыдущий» становится неоднозначным. Запрос истории релизов после оповещения почти неизбежно приводит к выбору записи, которая случайно оказалась рядом в списке.

Когда сервис развертывания принимает продвижение, он должен создать запись с предыдущим стабильным релизом, выбранным в этот момент. Этот релиз может отличаться от события, которое произошло непосредственно перед ним. Например, canary может продвинуть `rel_105`, пока `rel_103` остается стабильной базовой версией, а `rel_104` был прерванным экспериментом. Правильной целью восстановления может быть `rel_103`, а не строка непосредственно перед `rel_105`.

Минимальная запись может выглядеть так:

```json
{
  "deployment_id": "dep_842",
  "environment": "production",
  "release_id": "rel_105",
  "artifact_digest": "sha256:8b2c...",
  "source_revision": "4f1d9c7",
  "config_digest": "sha256:1a06...",
  "prior_release_id": "rel_103",
  "created_target_generation": "gen_913",
  "migration_set_id": "mig_77",
  "actor_id": "agent-run-27"
}
```

`prior_release_id` - это принятое решение, а не вспомогательное поле. Контроллер продвижения должен выбирать его по правилам, которые операторы могут проверить: например, последний подтвержденный стабильный релиз для этой цели с совместимой конфигурацией и подходящими требованиями к миграциям. Операция отката использует сохраненное решение и не пересчитывает его после изменения таблицы истории.

Для идентификации артефакта недостаточно понятной человеку метки релиза. Теги могут перемещаться, а метки сборок случайно переиспользоваться. При откате нужно развертывать адрес содержимого или другой неизменяемый идентификатор артефакта, сохраненный в исходной попытке. Если реестр позволяет позже направить тег на другие байты, ID целевого релиза должен разрешаться в дайджест, зафиксированный во время продвижения.

К конфигурации нужна такая же строгость. Возврат байтов приложения при сохранении измененного feature flag, ссылки на секрет среды выполнения, политики загрузки образа или настройки ресурсов может создать систему, которой никогда не было в тестах. Не обязательно дублировать каждое значение в записи развертывания, но нужно сохранять неизменяемую ревизию или дайджест конфигурации и определить, восстанавливает ли откат эту конфигурацию.

Состояние базы данных образует отдельную границу. Релиз, который только добавляет допускающие NULL столбцы, часто позволяет откатить приложение. Релиз, который удаляет столбец, переписывает значения или меняет смысл данных, может этого не позволять. Не обещайте общий «полный откат», если планировщик релизов не может доказать совместимость. Отмечайте, можно ли откатить приложение, трафик или требуется исправление. Отказ менее неприятен, чем запуск старого кода со схемой, которую он не понимает.

## Endpoint должен принимать ожидаемый текущий релиз

Запрос на откат должен содержать и желаемый релиз, и текущее состояние, которое он собирается заменить. Без этого предварительного условия endpoint не отличит корректное восстановление от устаревшей инструкции.

Используйте контракт запроса такого вида:

```http
POST /v1/environments/production/rollbacks
Idempotency-Key: 7e4cd1ee-62cb-4efa-985f-4ee0b77d577b
Content-Type: application/json

{
  "origin_deployment_id": "dep_842",
  "expected_current_release_id": "rel_105",
  "expected_target_generation": "gen_913",
  "restore_release_id": "rel_103",
  "reason": "error rate exceeded release threshold",
  "approval_id": "apr_551"
}
```

По возможности сервис должен получать `restore_release_id` из `origin_deployment_id`, а затем сравнивать переданное значение с сохраненным `prior_release_id`. Наличие обоих полей в запросе помогает аудиторам увидеть заявленное агентом намерение, но приоритет имеет запись на сервере. Нельзя позволять вызывающему превращать собственное развертывание в право выбирать любой исторический артефакт.

При успехе возвращайте созданную попытку развертывания и новое поколение цели. Не возвращайте расплывчатый ответ `accepted`, если система может синхронно зарезервировать переход.

```json
{
  "rollback_deployment_id": "dep_849",
  "reverted_deployment_id": "dep_842",
  "previous_release_id": "rel_105",
  "current_release_id": "rel_103",
  "target_generation": "gen_914",
  "status": "running"
}
```

Если текущее состояние цели больше не совпадает с ожидаемым, возвращайте конфликт. В теле ответа должно быть достаточно информации, чтобы агент сообщил о произошедшем, но недостаточно полномочий, чтобы самостоятельно придумать новое действие.

```http
HTTP/1.1 409 Conflict
Content-Type: application/json

{
  "error": "stale_rollback",
  "origin_deployment_id": "dep_842",
  "expected_target_generation": "gen_913",
  "observed_target_generation": "gen_914",
  "observed_release_id": "rel_106"
}
```

Код 409 - это успешный результат с точки зрения безопасности. Обучите агента останавливаться при таком ответе, прикреплять его к записи об инциденте и запрашивать новое решение. Не давайте ему запасную инструкцию вроде «повторить без ожидаемого поколения». Иначе защитная проверка превратится в формальность.

Некоторые команды используют HTTP-заголовок `If-Match` с ETag вместо поля JSON. Это работает, если ETag представляет состояние цели и меняется при каждом переходе. Механизм менее важен, чем инвариант: команда должна называть состояние, которое ей разрешено заменить.

## Сериализация не дает двум корректным запросам столкнуться

Одной предварительной проверки недостаточно, если два рабочих процесса могут пройти ее до фиксации хотя бы одного из них. Сервис развертывания должен сериализовать изменения одной среды и выполнять атомарную операцию сравнения и замены в хранилище состояния.

Представим, что цель сейчас имеет значение `(rel_105, gen_913)`. Агент отправляет запрос на откат, а оператор отправляет `rel_106`. Оба запроса читают `gen_913`. Если сервис проверяет значение в памяти приложения, а затем безусловно записывает результат, оба вызова могут заявить об успехе. Побеждает более поздняя запись, а журнал аудита описывает состояние, которого пользователи, возможно, никогда не видели.

Поместите сравнение и изменение в одну транзакцию или условную запись. Реляционная реализация может использовать такой шаблон:

```sql
UPDATE environment_targets
SET release_id = :restore_release_id,
    generation = generation + 1,
    active_deployment_id = :rollback_deployment_id,
    updated_at = CURRENT_TIMESTAMP
WHERE environment = :environment
  AND generation = :expected_generation
  AND release_id = :expected_release_id;
```

Сервис проверяет число измененных строк. Одна измененная строка резервирует переход состояния. Ноль строк означает конфликт. Затем нужно прочитать текущую цель и вернуть наблюдаемые значения в ответе 409.

Очередь не заменяет это условие. Очереди снижают вероятность одновременной работы, но повторная доставка, ручной путь вне очереди или повторная попытка рабочего процесса все равно могут создать конкурирующие команды. Условная запись должна выполняться там, где хранится состояние.

Идемпотентность решает другую проблему. Агент может потерять ответ после того, как сервис примет откат. При повторной отправке с тем же ключом идемпотентности сервис должен вернуть исходное развертывание отката и его статус. Нельзя резервировать новое поколение или запускать второе выполнение.

Ограничьте идемпотентность вызывающим и endpoint, сохраняйте дайджест тела запроса и отклоняйте повторно использованный ключ при другом теле. Иначе ошибочный клиент сможет прикрепить новое намерение к старому запросу, повторно используя идентификатор.

## Откат может сохранить некорректное состояние зависимостей

Откат приложения и восстановление среды - разные операции. Endpoint, который развертывает старый код, не может автоматически сделать совместимыми все зависимости.

Я видел типичный вариант такого сбоя: релиз `rel_105` добавил код, записывающий новое значение перечисления. Затем миграция ужесточила ограничение базы данных и разрешила только новые значения. Релиз завершился ошибкой по другой причине, и оператор вернул `rel_103`. Старый код записал прежнее значение, база данных его отклонила, а инцидент расширился, потому что на панели развертывания откат выглядел завершенным.

Endpoint не вызвал изменение схемы, но его успешный ответ создал ложное впечатление. Чтобы этого избежать, требуйте от метаданных релиза конкретного описания совместимости. Как минимум нужно зафиксировать, может ли восстанавливаемый релиз читать текущие данные, записывать текущие данные и работать с текущей ревизией конфигурации цели.

У управления трафиком есть собственная ловушка. Откат canary обычно должен менять только распределение трафика, которым владеет этот canary. Если независимый релиз изменил стабильный пул или другой контроллер поменял правило маршрутизации, endpoint отката, записывающий весь документ маршрутизации, может стереть эти изменения. Используйте версии на уровне ресурсов или изменяйте только те поля распределения, которые зарезервировало развертывание.

Тот же принцип действует для инфраструктуры. Если релиз создал очередь, bucket, роль или правило firewall, которое позже использовал другой компонент, удаление этого ресурса при откате может повредить отдельный сервис. Для очистки нужна запись о владении ресурсом и проверка, что ни одно более позднее развертывание не заявило на него права. Если установить владение нельзя, оставьте ресурс и создайте задачу на исправление.

Для необратимых операций выбирайте исправление вперед. Агент может отключить feature flag, убрать трафик с проблемного компонента или развернуть корректирующий релиз. Операторам не нравится такой ответ, потому что «откат» звучит быстрее, но аккуратное на вид восстановление, уничтожающее более поздние данные, отнимет больше времени, чем план исправления.

## Агентам нужны узкие полномочия и видимая точка остановки

Автономному агенту нужно предоставить минимальный набор действий, достаточный для выполнения назначенного развертывания. Ему не нужны учетные данные облака, общий shell с доступом к production или endpoint, принимающий произвольные ID релизов.

Выдавайте агенту дескриптор развертывания при запуске релиза. Этот дескриптор может разрешать чтение статуса, проверки работоспособности, изменения трафика в пределах распределения развертывания и один откат с указанием исходного развертывания. Сделайте его недействительным после перехода развертывания в конечное состояние или после отзыва запуска человеком. Сервис развертывания все равно должен проверять владение на стороне сервера, поскольку дескриптор можно скопировать, а клиент может работать неправильно.

Одобрение человека должно происходить до необратимой границы, а не после того, как агент уже подготовил необратимую команду. Разумная политика просит одобрение при запуске агентом развертывания в production, а затем позволяет отменить именно это развертывание, пока сохраняется предварительное условие владения. Если агент сталкивается с более поздним релизом, для любого вмешательства ему нужно новое одобрение. Это подходящее место для паузы: кто-то изменил ситуацию.

Sallyport не дает HTTP- и SSH-учетным данным попасть в процесс агента, пока человек одобряет запуск агента или помечает учетные данные для одобрения при каждом использовании. Эта мера помогает защитить путь действий, но самому сервису отката все равно нужны проверки релиза и поколения. Хранение учетных данных не определяет владение развертыванием.

Избегайте названий полномочий вроде `production:rollback:any`. Они предлагают вызывающему выбирать область действия во время выполнения. Лучше использовать выданное сервером полномочие, связанное с `dep_842`, средой `production` и конкретным маршрутом отката. Если агент запрашивает другую цель, уровень авторизации должен отклонить запрос до того, как его оценит контроллер развертывания.

Фиксируйте идентификатор процесса агента или рабочей нагрузки в каждом запросе. Человек должен иметь возможность ответить, кто инициировал `dep_842`, какой код подписал или аутентифицировал вызывающего, какое одобрение его покрывало и не отозвал ли кто-то доступ до завершения выполнения. Анонимные учетные записи автоматизации превращают каждый инцидент в археологические раскопки.

## Проверять нужно восстановленный релиз, а не запрос

Откат завершается только после того, как цель запускает нужный релиз и сервис проверяет условия, которые стали причиной восстановления. HTTP 202, успешное завершение команды или событие контроллера со статусом «применено» не доказывают, что старый релиз корректно обслуживает трафик.

Определяйте проверки с учетом реальной причины сбоя развертывания. Если откат вызвали задержка или ошибки, наблюдайте восстановленный сервис тем же способом измерения после перенаправления на него трафика. Если рабочий процесс обрабатывал некорректные задания, проверьте версию рабочего процесса и контролируемую нагрузку. Если ошибочная конфигурация вызвала сбои запуска, проверьте готовые экземпляры и загруженную ими ревизию конфигурации.

Задайте ограниченное окно наблюдения и фиксируйте его результат. Endpoint может возвращать `verifying`, затем `succeeded`, `failed` или `needs_operator`. Не ждите бесконечно метрику, которая может быть недоступна. По истечении времени нужно вернуть явный неопределенный результат, после чего решение принимает оператор.

Событие аудита должно связывать все этапы: оповещение или правило, запросившее отмену, исходное развертывание, ожидаемое состояние, условное резервирование, события выполнения, свидетельства работоспособности, итоговое состояние и возможный отзыв. Событиям нужны порядок и защита целостности, потому что удобной временной шкалы развертывания недостаточно во время разбирательства.

Sallyport записывает запуски агентов и отдельные действия в зашифрованный журнал аудита с цепочкой хешей, а `sp audit verify` проверяет эту цепочку офлайн без ключа хранилища. Используйте подобные свидетельства, чтобы показать, что агент запросил действие, а авторитетной записью о том, что именно изменилось, оставляйте собственные записи сервиса развертывания о переходе состояния и проверке.

## Знакомым командам отката нужна безопасная оболочка

`kubectl rollout undo` полезна оператору, который напрямую работает с Kubernetes Deployment, но для автономного восстановления это не полный контракт. В документации Kubernetes указано, что `kubectl rollout undo` возвращает предыдущую ревизию развертывания, если вызывающий не передал `--to-revision`. Такое поведение удобно во время ручной диагностики. Но оно не доказывает, что предыдущая ревизия принадлежит запуску агента, который завершился ошибкой.

Kubernetes хранит историю ревизий Deployment в ReplicaSet, а `revisionHistoryLimit` определяет, какой объем истории сохраняется. Это артефакт работы контроллера, а не ваш бизнес-журнал одобренной базовой версии релиза, совместимости конфигурации или владения агентом. После очистки истории команда «отменить» может не найти ревизию, которую ожидает внешний процесс развертывания.

Не выдавайте агенту общие учетные данные `kubectl`, называя саму команду endpoint отката. Поместите между агентом и кластером контроллер или сервис развертывания. Сервис должен найти запись исходного развертывания, сравнить текущее поколение цели, зарезервировать изменение состояния и только после этих проверок вызвать платформу.

То же относится к командам облачных провайдеров вроде «развернуть ревизию X» и средствам CI с действием «повторить предыдущий релиз». Они работают с ресурсом платформы. Они не знают, связан ли релиз с текущим инцидентом агента, если ваш управляющий контур не передаст им этот контекст.

Команда платформы тоже должна быть узкой. Если сервис может изменить ревизию указанной рабочей нагрузки, не выдавайте ему права на изменение всего кластера. Оболочка, сохраняющая широкие полномочия, лишь спрятала опасность за другим API.

## Создайте журнал релизов до автоматизации восстановления

Защищенный откат можно внедрить, не заменяя всю систему развертывания. Начните с того, что сделайте журнал релизов авторитетным для одной production-цели, а затем проведите через один и тот же условный переход пути человека и агента.

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

1. Присвойте неизменяемые ID релизам и попыткам развертывания, а затем во время продвижения сохраните предыдущий одобренный релиз и поколение цели.
2. Добавьте endpoint, требующий `origin_deployment_id`, ожидаемый релиз, ожидаемое поколение и ключ идемпотентности.
3. Сделайте обновление цели условным в базе данных или управляющем хранилище и возвращайте 409 при любом несовпадении.
4. До продвижения классифицируйте каждый релиз по обратимости приложения, конфигурации, данных и трафика.
5. Требуйте свидетельства проверки до того, как контроллер отметит откат завершенным.

Сначала запустите этот контракт в режиме отчета, до разрешения выполнения действий агентами. Пусть сервис рассчитывает, что он восстановил бы и отклонил ли запрос. Сравните эти решения с фактическими действиями во время инцидентов. Так вы обнаружите пропущенные записи о происхождении и скрытые ручные пути, не давая автоматизации возможности перезаписать production.

Затем сделайте отказы понятными. Устаревший откат должен создавать ясную запись об инциденте с наблюдаемыми релизом и поколением, а не загадочную ошибку, которая заставит кого-то обойти endpoint. Endpoint заслуживает доверия, когда он последовательно отклоняет небезопасные запросы, в том числе запросы от создавших его людей.

Первое поле, которое нужно добавить, не `force`, а `expected_target_generation`. Когда ваши развертывания начнут передавать этот факт и сохранять исходную базовую версию, агент сможет отменить собственный релиз в четко очерченных пределах. До этого автономный откат остается старой командой развертывания, направленной на движущуюся цель.
