阅读需 8 分钟

为自主部署设计无冲突的回滚端点

为自主部署设计回滚端点,确保恢复正确的发布、拒绝过时状态,并保留生产环境中无关的后续变更。

为自主部署设计无冲突的回滚端点

自主部署代理需要的是安全失败的权限,而不是猜测的权限。危险的回滚很少是因为命令执行失败。更危险的情况是,系统环境已经发生变化,命令却仍然成功,把旧制品恢复到一个并非由该代理创建的发布之上。

安全的回滚操作会明确某次部署尝试,指定某个之前的发布,并在它对当前状态的判断已经过时时拒绝执行。应把回滚视为一种受保护的状态转换,而不是通往“最后一个正常版本”的捷径。这个区别决定了代理是在修复自己的工作,还是抹掉别人的变更。

回滚必须属于某次部署尝试

回滚端点必须把撤销动作绑定到导致疑似故障的那次部署尝试。单靠版本号无法完成这项工作。

团队经常保存 1.8.41.8.51.8.6 这样的序列,然后提供一个“将 1.8.4 部署到生产环境”的操作。代理部署了 1.8.5,收到告警后调用该操作。与此同时,一名工程师已经部署了紧急发布 1.8.6。回滚端点因为 1.8.4 存在而接受请求。此时生产环境运行的是早于这两次变更的制品。API 完全按照请求执行,而问题正在于此。

请分开保存以下身份:

  • 发布是一个不可变的代码包,包含配置引用和元数据。为它分配持久 ID 和制品摘要。
  • 部署尝试是一次将发布放入指定目标环境的请求。它拥有部署 ID、执行者和生命周期。
  • 目标状态是环境当前运行的内容。它包含一个每次接受状态转换都会变化的修订号或代数。
  • 回滚意图表示:只有在部署尝试 D 仍然拥有当前状态时,才允许它将目标恢复为发布 R

人们经常遗漏的是所有权关系。当部署 dep_842 推进发布 rel_105 时,应记录 dep_842 创建了当前目标代数 gen_913。绑定到 dep_842 的回滚只能在 gen_913 仍是当前代数时执行。如果后续部署创建了 gen_914,服务就必须拒绝旧请求。

不要通过比较时间戳来推断所有权。重试、排队的工作进程、人工修复,以及允许用户选择旧发布的系统,都会让时序判断失效。在接受推进操作时,直接保存这种关系。

这也回答了一个棘手的运维问题:代理能否回滚另一个代理的部署?通常不能。独立的授权方可以明确批准这种干预,但普通回滚能力应只覆盖调用者自己发起的操作。广泛的权限看起来很方便,直到两个部署循环同时响应同一事故。

不可变的来源记录让之前的发布可被确定

服务必须在改变目标之前记录回滚来源,因为推进完成后,“之前”这个词就变得含糊。告警发生后再查询发布历史,很容易从列表中选中碰巧相邻的项目。

部署服务接受推进操作时,应创建一条记录,其中包含当时选定的之前稳定发布。这个发布可能不同于紧邻的上一个事件。例如,金丝雀发布可以在 rel_103 仍是稳定基线时推进 rel_105,而 rel_104 只是一次中止的实验。正确的恢复目标可能是 rel_103,而不是 rel_105 之前紧邻的那一行。

最小记录可以如下所示:

{
  "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 必须解析到推进时捕获的摘要。

配置也需要同样严谨。只恢复应用字节,却保留已经变化的功能开关、运行时密钥引用、镜像拉取策略或资源设置,可能会创建一个测试中从未存在过的系统。你不必在部署记录中复制每个值,但应记录不可变的配置版本或摘要,并明确回滚是否会恢复它。

数据库状态是另一个边界。只新增可为空列的发布,通常允许应用回滚。删除列、重写值或改变语义的发布则可能不允许。若发布规划器无法证明兼容性,就不要承诺通用的“完整回滚”。应将部署标记为可回滚应用、可回滚流量,或需要修复。拒绝执行总比让旧代码面对无法理解的架构更好。

端点需要预期的当前发布

回滚请求必须同时携带目标发布,以及它预期要替换的实时状态。没有这个前置条件,端点就无法区分有效恢复和过时指令。

请求契约可以设计成这样:

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"
}

服务应尽可能根据 origin_deployment_id 推导 restore_release_id,然后将请求中的值与保存的 prior_release_id 比较。请求中同时保留这两个字段,有助于审计人员了解代理声称的意图,但最终以服务器记录为准。绝不能让调用者把自己发起的部署变成任意选择历史制品的许可证。

成功时,返回新创建的部署尝试和新的目标代数。如果系统能够同步预留该状态转换,就不要返回含糊的 accepted 响应。

{
  "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/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 是一次成功的安全结果。应让代理知道收到这个结果后必须停止,把响应附加到事故记录中,并请求新的决策。不要给它“去掉预期代数后再试一次”这样的备用指令。这样的备用路径会把你的防护变成表面功夫。

有些团队使用携带 ETag 的 If-Match HTTP 请求头,而不是 JSON 字段。只要 ETag 代表目标状态,并且每次状态转换都会变化,这种方式就可行。具体机制不如这个不变量重要:命令必须明确它可以替换的状态。

序列化可以阻止两个有效请求发生冲突

如果两个工作进程可以在任何一个进程提交之前都通过检查,那么单独的前置条件检查无法保护目标。部署服务必须串行处理同一环境的变更,并在状态存储中使用原子比较并交换。

假设目标当前为 (rel_105, gen_913)。代理提交回滚,操作员提交 rel_106。两个请求都读取到 gen_913。如果服务只在应用内存中检查,之后再无条件写入,那么两个调用都可能声称成功。后写入的值会生效,而审计记录可能报告一个用户从未实际看到过的状态。

应把比较和修改放在同一个事务或条件写入中。关系数据库实现可能使用这样的模式:

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 响应中返回实际观测到的值。

队列不能替代这个条件。队列可以降低并发工作的概率,但重复投递、队列之外的人工路径,或工作进程重试,仍可能产生相互竞争的命令。条件写入必须保留在状态实际存储的位置。

幂等性解决的是另一类故障。代理可能在服务接受回滚后丢失响应。如果它使用相同的幂等键重试,服务应返回原来的回滚部署和状态,不得再次预留一个代数或启动第二次执行。

幂等性应限定在调用者和端点范围内,保存请求正文的摘要,并在同一幂等键被不同正文复用时拒绝请求。否则,有缺陷的客户端可能通过复用标识符,把新的意图附加到早先的请求上。

回滚可能保留有问题的依赖状态

撤销高风险运行
当事故范围发生变化时,可立即从 Sessions journal 撤销代理运行。

应用回滚和环境恢复是两个不同的操作。一个负责部署旧代码的端点,无法自动让所有依赖再次兼容。

我见过一种很典型的故障:发布 rel_105 引入了写入新枚举值的代码。之后的迁移收紧了数据库约束,只允许这些新值。该发布因无关原因失败,操作员恢复了 rel_103。旧代码写入之前的值,数据库拒绝了它,事故因此扩大,因为部署面板看起来显示回滚已经完成。

端点没有造成架构变更,但它的成功响应作出了错误声明。应要求发布元数据以具体方式描述兼容性,避免这种误导。至少要记录恢复发布能否读取当前数据、写入当前数据,以及能否与目标的配置版本一起运行。

流量管理也有自己的陷阱。金丝雀回滚通常只应改变该金丝雀拥有的流量分配。如果另一个发布调整了稳定流量池,或另一个控制器改变了路由规则,那么回滚端点若写入整份路由文档,就可能抹掉这些变更。应使用资源级版本,或者只修改部署预留的分配字段。

基础设施也遵循同一原则。如果某个发布创建了队列、存储桶、角色或防火墙规则,而后续工作已经采用了这些资源,那么在撤销过程中删除它们可能会伤害另一个服务。清理前需要有资源所有权记录,并确认没有后续部署认领该资源。如果无法确认所有权,就保留资源并创建修复任务。

对于不可逆操作,应选择正向修复。代理可以关闭功能开关、转移流量,或部署纠正性发布。操作员不喜欢这个答案,因为“回滚”听起来更快,但一次看似干净、实际破坏了后续数据的撤销,耗时往往超过制定修复计划。

代理需要狭窄的权限和明确的停止点

自主代理应获得完成指定部署所需的最小操作权限。它不需要原始云凭据、拥有生产访问权限的通用 shell,也不需要一个接受任意发布 ID 的端点。

代理开始发布时,为它分配一个部署句柄。该句柄可以授权状态读取、健康检查、部署分配范围内的流量变更,以及一次明确指向原始部署的回滚。当部署进入终止状态,或人工撤销运行时,使句柄失效。部署服务仍必须在服务器端执行所有权检查,因为句柄可能被复制,客户端也可能发生故障。

人工审批应发生在不可逆边界之前,而不是代理已经准备好不可逆命令之后。合理的策略是在代理开始生产部署时请求审批,然后只要所有权前置条件仍然成立,就允许它撤销这次确切的部署。如果代理遇到后续发布,那么它对任何干预都需要新的审批。这是一个适合放慢速度的节点,因为有人已经改变了环境。

Sallyport 可以让 HTTP 和 SSH 凭据留在代理进程之外,同时由人员审批代理运行,或将凭据设置为每次使用都需要审批。这种控制有助于保护操作路径,但回滚服务仍需执行自身的发布和代数检查。凭据由谁保管,不能定义部署所有权。

避免使用 production:rollback:any 这样的能力名称。它会引导调用者在运行时选择范围。更好的做法是使用由服务器签发、绑定到 dep_842production 环境和具体回滚路由的能力。如果代理请求无关目标,授权层应在部署控制器评估请求之前拒绝它。

记录每次请求对应的代理进程或工作负载身份。人工人员应能回答:谁发起了 dep_842,调用者由什么代码签名或身份验证,哪项审批覆盖了该操作,以及是否有人在执行结束前撤销了访问权限。匿名自动化账户会让每次事故都变成考古工作。

验证必须测试恢复后的发布,而不是测试请求

控制自主恢复
让人工审批留在操作路径中,同时不把原始生产密钥交给自主代理。

只有在目标运行预期发布,并且服务验证了触发恢复的条件后,回滚才算完成。HTTP 202、命令成功退出,或控制器发出“已应用”事件,都不能证明旧发布正确地为流量提供服务。

应根据部署实际的故障模式定义验证方式。如果延迟或错误触发了回滚,就在流量切换到恢复服务后,通过同一测量路径观察它。如果工作进程发布消耗了格式错误的任务,就验证工作进程版本和受控工作负载。如果配置错误导致启动失败,就检查已就绪实例以及它们加载的配置版本。

设置有界的观察窗口,并记录结果。端点可以先报告 verifying,之后报告 succeededfailedneeds_operator。不要无限等待可能不可用的指标。达到时间限制后,应生成明确的“无法判断”结果,并交由操作员决定。

审计事件应连接每个阶段:请求撤销的告警或规则、原始部署、预期状态、条件预留、执行事件、健康证据、最终状态以及任何撤销操作。事件需要排序和完整性控制,因为一条看起来友好的部署时间线,在发生争议时远远不够。

Sallyport 会通过加密哈希链审计日志记录代理运行和单独的操作,sp audit verify 无需保险库密钥即可在离线环境中检查这条链。可以使用这类证据证明代理请求过某项操作,同时保留部署服务自身的状态转换和验证记录,作为实际变更内容的权威记录。

熟悉的回滚命令需要更安全的包装层

验证操作记录
无需保险库密钥,即可使用 sp audit verify 在离线环境中验证 Sallyport 的加密哈希链审计日志。

对于直接操作 Kubernetes Deployment 的人员来说,kubectl rollout undo 很有用,但它不是自主恢复的完整契约。Kubernetes 文档说明,除非调用者提供 --to-revision,否则 kubectl rollout undo 会回滚到之前的部署版本。这种默认行为适合人工排查,却不能证明之前的版本属于发生故障的代理运行。

Kubernetes Deployment 会在 ReplicaSet 中跟踪版本历史,而 revisionHistoryLimit 控制 Kubernetes 保留多少历史。那段历史是控制器的产物,不是你关于发布批准基线、配置兼容性或代理所有权的业务记录。历史被清理后,“撤销”也可能找不到外部部署流程所期待的版本。

不要给代理一份通用的 kubectl 凭据,然后把这个命令称为回滚端点。应在代理和集群之间放置控制器或部署服务。服务应解析原始部署记录,比较目标的实时代数,预留状态变更,并且只有通过这些检查后才调用底层平台。

对于声称“部署版本 X”的云厂商命令,或声称“重新运行之前发布”的 CI 控件,也有同样的问题。它们操作的是平台资源。除非你的控制平面提供上下文,否则它们不知道某个发布是否与代理当前的事故有关。

平台命令本身也应保持狭窄。如果服务只需修改指定工作负载的版本,就不要授予它集群范围的修改权限。在保留广泛凭据的情况下,包装层只是把危险藏到了另一个 API 后面。

在自动化恢复前建立发布台账

无需替换所有部署系统,也可以引入受保护的回滚。先让发布台账成为一个生产目标的权威记录,然后要求人工和代理路径都通过同一个条件转换。

实际的落地步骤可以分为五部分:

  1. 为发布和部署尝试分配不可变 ID,并在推进时记录之前批准的发布和目标代数。
  2. 增加一个端点,要求提供 origin_deployment_id、预期发布、预期代数和幂等键。
  3. 在数据库或控制存储中让目标更新遵循条件,并对每次不匹配都返回 409。
  4. 在推进前,分别评估每个发布对应用、配置、数据和流量的可回滚性。
  5. 要求控制器在将回滚标记为完成前提供验证证据。

在允许代理执行之前,先以报告模式运行这套契约。让服务计算它会恢复什么,以及它是否会拒绝请求。将这些决策与实际事故处理进行比较。这样可以在不给自动化覆盖生产环境机会的情况下,发现缺失的来源记录和隐藏的人工路径。

然后让拒绝变得平常。过时的回滚应创建一条易于理解的事故事项,其中包含实际观测到的发布和代数,而不是产生一个神秘的失败,诱使人绕过端点。端点只有在始终如一地拒绝不安全请求时才会赢得信任,包括拒绝构建它的人发出的请求。

首先要添加的字段不是 force,而是 expected_target_generation。一旦部署携带这个事实,并保存原始基线,代理就能在清晰的边界内撤销自己的发布。在此之前,自主回滚只是把旧的部署命令指向一个不断变化的目标。

常见问题

部署回滚应该针对什么目标?

部署回滚必须同时明确之前的制品,以及引入该制品的确切部署实例。仅说“之前的版本”不够明确,因为代理开始工作后,另一个发布可能已经改变了目标环境。

如何阻止回滚覆盖更新的部署?

使用 expected_current_release_id 这样的比较并交换前置条件。当前运行的发布与预期不同时,服务必须拒绝请求,因为继续执行会覆盖其他人后来完成的部署。

让 AI 代理调用回滚 API 安全吗?

只有在目标环境拥有唯一权威写入方、版本不可变,并且端点在执行任何变更前检查当前版本时,才适合让 AI 代理调用回滚 API。一个只接受环境和版本的普通端点无法做出这种保证。

要支持安全回滚,必须保存哪些数据?

保存包含制品摘要、源代码版本、配置摘要、迁移集合、部署 ID、执行者和时间戳的不可变发布目录。直接保存实际的前一发布 ID,不要让回滚服务在请求时自行推断历史。

回滚应该撤销数据库迁移吗?

不应自动回滚数据库迁移。回滚可以恢复应用代码、流量权重或配置值,但破坏性架构变更往往需要单独的正向修复。应把数据库兼容性视为发布属性,而不是应用回滚的自动副作用。

回滚发生冲突后,代理应该怎么做?

代理应返回明确的冲突结果,例如 HTTP 409,并附上当前观测到的发布 ID 和请求中的前置条件。代理应停止执行、报告冲突,并等待获得授权的人工处理或新的部署计划。

可以使用 kubectl rollout undo 进行自主回滚吗?

kubectl rollout undo 适合操作员调查某个 Deployment,但它默认回到之前的版本,并不会把请求绑定到代理准备撤销的那个发布。应在该命令前增加部署服务,并在那里执行所有权和版本检查。

回滚请求应该如何处理重试?

为一次回滚意图使用一个幂等键,并将最终结果与该键关联保存。代理超时后重试时,服务应返回之前的结果,而不是再次启动一次发布。

自主部署的审计日志应该包含什么?

记录请求、执行者身份、预期和实际的发布 ID、选定目标、审批信息、执行事件以及验证结果。证据必须足以解释一次成功的变更,也要能说明为何拒绝执行任何变更。

回滚端点应该有哪些字段?

实用的端点应接收预期当前发布、指定的之前发布、原因、幂等键和审批引用。如果 API 无法接收这些字段,就把它放在能够处理这些信息的控制器之后。

Sallyport

Sallyport 替你的 AI 智能体执行 API 调用和 SSH 命令。密钥留在你 Mac 上的本地密钥库里;每次运行由你批准,每个操作都落入一份密封的审计日志。

© 2026 Sallyport · 依据 Apache-2.0 开源 · Oleg Sotnikov