亚马逊AWS官方博客

RDS for MySQL 8.0 升级 8.4 的下游 Binlog 消费处理实战(二)—— 用 Kiro Custom Agent 为割接 Runbook 加上可执行的门禁

摘要:上一篇,我们用 Canal 消费 RDS for MySQL 的 binlog,要把实例从 8.0 升到 8.4,并选择蓝绿部署完成切换。生产环境形态包含位点存 ZooKeeper、instance 配置由 Canal Admin 托管、Canal 订阅只读副本。在这篇文章里面, 我们讨论这次切割演练过程是很好的 Agent 协助运维的场景,如果使用 Kiro custom agent 应该如何实现。 当然,这篇 blog 也适用于我们开始考虑评估某个场景是否可以用 Agent 来解决,以及如何建设。


适用场景:上一篇,我们用 Canal 消费 RDS for MySQL 的 binlog,并计划将实例从 8.0 升级到 8.4,选择通过蓝绿部署完成切换。生产环境形态为:位点存储在 ZooKeeper、instance 配置由 Canal Admin 托管、Canal 订阅只读副本。这次割接演练很适合由 Agent 协助运维。本文讨论如何用 Kiro Custom Agent 实现这一流程,同时也可供在规划阶段评估某个场景是否适合交给 Agent,以及如何实现。

要解决的问题:前篇给出了 errno 1236 的成因、“并集修补”公式和一份生产割接 Runbook。即使有一份完善的 Runbook,也不一定能在割接当晚正确执行。 我们复盘过一次位点操作事故:“停实例 → 存档 → 清位点 → 验配置 → 启动”这五步一旦颠倒顺序,存量位点就会使新配置无法生效,而且这个错误要到下游对账时才能暴露。割接窗口的压力本身也是错误的放大器:试想,深夜要执行几十条命令,还有十几项校验结果需要人工逐一核对,整个团队都在等待结果,期间还可能随时出现故障。

  1. 用一张分工表判断自己的运维场景中哪些工作能交给 Agent、哪些必须由人完成,以及交给 Agent 前需要满足哪些前提条件;
  2. 如何用 Kiro Custom Agent 的能力级权限,配置哪些操作可以自动执行、哪些需要人工批准、哪些必须禁止,再通过实际测试确认权限限制是否生效。只有测试通过,才允许开始割接;
  3. 用 CDK 搭建一套按天计费、估算费用约为 $9/天($264/月,按 3.4 的分项表计算)的演练环境,按 Runbook 的顺序复现完整割接流程;

围绕这份 Runbook,我们完成了三项工作:搭建演练环境,编写按操作风险分级、以实际执行结果为依据的脚本,以及配置一个通过声明式权限规则控制操作的 Kiro Custom Agent。我们也在演练环境中完整执行了两轮割接:2026-07-23 意外进入故障路径,2026-08-15 跑通正常路径,取得零缺失的对账结果。

演练环境的搭建方式和 Kiro Custom Agent 的实现可参考 GitHub 仓库。仓库包含 Kiro Custom Agent 定义、Steering 文档、CDK 演练环境,以及两轮演练的完整报告;第三章的复现步骤也按该仓库的目录结构编写。

一、运维中哪些工作能交给 Agent,哪些不能

Gartner 的调研数据显示,企业在 Agent 落地过程中遇到的主要障碍是信任与集成,而不是 Agent 本身的开发;对基础设施与运维团队而言,AI Agent 会因为非确定性推理和动态工具调用而放大运维风险,需要用策略即代码、可观测性与护栏来实现“受治理的执行”。护栏的通行做法是分层防御:输入校验 → 工具与动作门控 → 输出校验 → 人工审批 → 回归评估(参考)。(三篇 Gartner 研究按标题与文档号引用,摘要经过改写,未转述原文结论;完整出处见文末参考资料。)

割接场景具有一次性、强时序、错误检查需要等到下游消费端对账才能发现。 这类场景的特点决定了可靠性不能仅靠更聪明的模型来保证:必须明确列出 Agent 可以执行哪些操作;它给出的每个结论,都要有可供核查的依据;不可逆的决定则必须由人作出。

这套演练方案主要从两个方面防止误操作:一是明确哪些工具和命令可以直接使用、哪些需要批准、哪些禁止使用;二是在需要人工审批的步骤设置门禁。输出由脚本自动校验,每个阶段都有明确的校验标准,可由程序自动检查,不采用模型自评。

1.1 human in the loop:人应该在什么时候参与

“human in the loop” 是 Agent 设计中常被提及的概念。在运维场景中,我们用一张分工表确定人应在何时参与 Agent 的执行过程。运维窗口中的工作可以按两个维度划分:后果是否可逆、是否可按明确标准自动校验,由此形成四类分工。各类之间的边界,就是需要设置门禁的位置。

象限 谁做 相关活动 归入这一类的理由 本项目中的实例 机器如何保障
后果可逆
机器可判定
Agent 自主 逐项核对校验结果、执行顺序约束、故障分诊、动态取证 认知负荷高、创造性低、可按明确标准自动校验 P0 五项环境断言、P1 GTID 预检、寻位日志核对 allow + 证据写入 state/
后果可逆
机器不可判定
Agent 提议,人批准 可逆但有副作用的变更 后果可回滚,但执行时机须由人决定 停 instance、删 ZK cursor、启动 instance ask + 契约要求出示动作 / 证据 / 回滚方式
后果不可逆
机器不可判定
由人独立执行,Agent 不得介入 云控制面变更、环境创建与销毁 影响面超出会话且不可逆 switchover、改 RDS 参数组、销毁演练环境 deny(跨作用域采用最严格的效果)
定义成功指标/规则等 只能由人完成 制定校验标准、规定执行顺序、授予权限 这是“谁来定义对错”的问题 Runbook 的校验标准表、permissions 规则本身 resources 预注入 + 运行时禁止 Agent 改写自己的权限文件

(最后一列的 allow / ask / deny 是 Kiro 的策略关键字,第二章展开。)

最后一行是表中最重要的一项,也是设计 Agent 时首先要考虑的问题。人在这个回路中的核心权力并不是“危险动作的批准权”。批准权属于第二类,通过确认菜单即可实现;真正必须由人决定的是:怎样才算校验通过、各项操作应按什么顺序执行,以及允许 Agent 使用哪些权限。这是四象限划分之外最重要的一步,这三项都必须在定义 Agent 之前明确:对于能改写自己权限文件的 Agent,deny 写得再严格,也无法形成有效约束。

前篇的 Runbook 最后给出了一份操作清单,但仅靠清单,还不能落实前面划定的人机分工和操作限制。要让 Agent 在生产环境中按这份 Runbook 执行,还需要做好以下准备:

  • 在哪里演练。 割接流程必须先在与生产形态一致的环境中完整跑通。形态一致并不仅仅是版本号一致:位点存 ZooKeeper 还是文件、配置由 Canal Admin 托管还是使用本地文件、Canal 订阅主库还是只读副本,每项关键配置都会改变操作路径与校验标准。
  • 由谁执行。 窗口内要执行几十条命令、逐一核对十几项校验结果。纯人工操作慢且容易出错;全自动执行也不可接受,停实例、删位点等动作属于第二类,何时触发操作必须由人决定。
  • 如何防止误操作。 遵守执行顺序和完成各项校验不能只靠操作人员小心谨慎。开头那次位点事故的关键就在于:顺序颠倒不会当场报错,问题要到下游对账时才暴露。因此,防线必须融入流程,也就是第三类中由机器强制执行的禁区限制。

归根结底,Runbook 不能只停留在操作文档上,还要落实为可验证、可复用的运维工具。哪些操作可以自动执行、哪些需要人工审批、哪些只能由人完成,都应按照分工表明确下来。

1.2 准入条件与蓝绿割接场景的逐项对照

理论上清晰,并不代表一定能落地。我们尝试用蓝绿割接场景验证这套思路。下表右列以蓝绿部署中的 Canal 位点修补为例,逐项对照准入条件;这些条件也是后文所有实现的前提。运维人员在规划和设计用于辅助日常运维的 Agent 时,也可以用这张表梳理需求。

准入条件 满足条件的标准 蓝绿割接位点修补
操作序列确定 不需要每次临场设计流程 “停 → 档 → 清 → 验 → 启”五步,执行顺序必须严格遵守
可按明确标准自动校验 有明确的字符串或数值标准,不依赖“看起来正常” GTID_SUBSET 返回 1、stat 返回节点不存在、寻位日志 gtid 等于并集、GTID_SUBTRACT 差值收敛、Kafka id 集合覆盖源表
认知负荷高、创造性低 大量比对、取证、状态跟踪,人容易疲劳出错 十几项校验结果核对、GTID 集合运算、跨日志取证,均属于比对工作
失败可检测、回滚明确 出错后能判定问题性质,也能回退 cursor 有存档可恢复;instance 可停;错误有 1236 这类明确信号
变更面可枚举 能写成允许 / 询问 / 禁止三类规则 变更仅通过三个脚本执行;禁区是 switchover 与 RDS/K8s 变更命令
每步有证据可留档 事后可审计 各阶段实时记录执行日志,流程结束后日志即为报告底稿

这个场景还有一项有利条件:割接窗口在深夜,人的状态最差,而 Agent 的状态恒定。将比对和取证交给 Agent,人就能把节省下来的注意力用于门禁判断。

1.3 哪些情形不适用这张表

除了上述准入条件,还需要明确四类不能让 Agent 参与的场景:1)需要业务权衡的决策(例如“是否延后割接窗口”)、2)不可回滚的一次性动作、3)判断只能依据模糊经验(“感觉不太对”)、4)影响面无法枚举,因而无法明确边界。

具体到这个 Agent ,包括以下情形:

  • RDS 割接的触发,以及继续/停止的决策:这些都属于禁区,一律由人执行。Agent 的任务到“预检全部通过”为止,连“提供命令供人粘贴”都不允许。
  • 演练环境的创建与销毁(cdk deploy / cleanup.sh),由人执行。
  • 环境形态不符:位点存文件而非 ZooKeeper、配置不由 Admin 托管、非 GTID 模式。这些情况下 Runbook 的校验标准和故障判定表都不再适用,Agent 会按错误的标准判定为通过。
  • 多 destination / 多 client id 的批量割接:当前 Runbook 只覆盖单条链路。
  • 需要 DDL 或数据修复的补偿动作:无法明确影响范围,不满足“变更面可枚举”的条件。
  • 无人值守模式:门禁的前提是有人应答。若会话无法向人请示,Agent 契约要求立即停止。
  • 尚未在对应演练环境中完整跑通的流程,不要直接用于生产。这是我们的准入规则,见第四章。

二、用 Kiro Custom Agent 把这分工表转化为机器执行的规则

分工表的前三项正好对应三种策略效果:Agent 自主是 allow,提议后批准是 ask,禁区是 deny。第四项不写成策略:它体现为“谁有权修改这个文件”。本章介绍前三项如何转化为运行时实际验证的规则,以及第四项通过什么机制确保 Agent 遵守。

2.1 一个 Markdown 文件:上半段管权限,下半段管行为

Kiro 是 AWS 的 Agentic AI 开发环境。在 Kiro 里定义一个 Custom Agent,写一个 .md 文件即可。这个文件分为两部分:

  • 上半段是 YAML,列出它可使用的工具,以及哪些命令可直接执行、哪些需要人工批准、哪些一律禁止。
  • 下半段是自然语言,也就是系统提示词,规定执行流程、校验未通过时的处理方式,以及哪些情况下必须停止并询问人。

两部分的约束力不同:上半段由 Kiro 运行时强制执行,Agent 无法绕过(2.3 已用三个探针验证);下半段约束它的判断方式,需要由它自行遵守。两部分放在同一个文件中,随代码一同纳入 git,评审时打开一个文件,就能同时看到“它能执行哪些操作”和“它应按什么方式执行”。

以这个割接 Agent 为例,Agents/canal-cutover.md 的基本结构如下(两部分均有删减,完整策略见 3.2,正文是契约条款的节选):

---
name: canal-cutover
tools: ["read", "write", "shell"]
resources:
  - "file://steering/canal-cutover-Runbook.md"
permissions:
  rules:
    - capability: shell                 # 只读脚本:直接跑,不打扰人
      effect: allow
      match: ["bash scripts/00_*", "bash scripts/10_*"]
    - capability: shell                 # 变更脚本:停下等人批准
      effect: ask
      match: ["bash scripts/22_*"]
    - capability: shell                 # 禁区:一律不许,人自己去执行
      effect: deny
      match: ["aws rds switchover-blue-green-deployment*"]
---

# 你的职责

按 Runbook 执行 RDS 蓝绿切换的 Canal 位点修补。

- Runbook 是唯一权威知识来源,严禁凭记忆或训练知识推断环境状态,
  任何结论必须引用本次会话中脚本或只读命令的实际输出。
- 执行变更脚本之前,先出示三项——将要执行的动作、当前证据摘要、
  回滚方式——然后停下等人批准。
- 判据不通过就停止流程,呈现失败证据,不要自行重试,也不要自行改配置。
- 若会话无法向人请示,立即停止。

这个例子体现了两部分的分工。scripts/22_* (比如:22_delete_cursor.sh) 执行前需要等待人工批准,依据的是 effect: ask 这一行,运行时弹出确认,Agent 没有“跳过”这个选项。但“等待批准时要出示动作、证据和回滚方式”无法写入 YAML:ask 只能表达“需要询问”,不能规定“询问时需要说明什么”,这部分只能写在正文中(图 1 展示了实际执行情况)。前半段决定是否需要询问(行为),后半段规定如何说明清楚,两部分缺一不可。

2.2 能力级权限:allow / ask / deny 与 deny-overrides

Kiro 的权限模型是能力级(capability-based)的:按能力(fs_readfs_writeshellweb_fetchweb_searchmcpsubAgent 等)声明匹配模式和效果,效果有三种:allow 静默执行、ask 弹出确认、deny 永久阻断。规则可写在用户级、工作区级,也可内嵌在 Agent 配置的 permissions 字段里(配置参考)。

Kiro 官方 Permissions 文档明确说明了以下两条规则,分别见“Scopes”和“Defining rules”小节。其中第一条还经过了本文的实际测试,结果见 2.3。

  • 优先级是 deny > ask > allow,作用域之间没有优先级,最终采用最严格的效果。只要存在一条 deny 规则,任何位置的 allow 都无法覆盖它。
  • 工作区级规则存储在仓库之外(~/.kiro/workspace-roots/<hash>/permissions.yaml),克隆仓库不会一并引入权限规则。将这套工具交给客户复现时,交付的是操作流程,并不意味着同时授予执行这些操作的权限。

2.3 实测:deny 能否真正阻断操作

我们通过反向测试验证了规则的稳定性。2026-08-17 在 Kiro IDE 1.0.309 上创建了一个探针 Agent,测试三种最容易绕过防护的路径:直接执行禁区命令、用 && 将禁区命令拼接在合法命令后、让 Agent 自行授予权限。

一、deny 稳定阻断操作,且提示中包含拒绝依据。 给探针 Agent 配一条 deny shell: "echo PROBE_DENY*",让它执行 echo PROBE_DENY_1,命令未执行,返回:

Tool call denied by user's permissions.
Rule: deny shell matching "echo PROBE_DENY*"  Source: subAgent-profile.

提示明确列出了命中的规则及其所属作用域。这有助于事后复盘——不是“Agent 声称自己没有执行”,而是运行时留下了拒绝记录。

二、不能通过 && 拼接命令绕过门禁。 执行 echo PROBE_ALLOW_2 && echo PROBE_DENY_2,整条命令被拒绝,前半段合法的部分也没有执行,提示还指出了触发拒绝的子命令:

Rule: deny shell matching "echo PROBE_DENY*"  Source: subAgent-profile.
Triggered by: "echo PROBE_DENY_2".

复合命令在匹配前会被拆分并逐条判定,不存在“前半段已经执行”的中间状态。

三、跨作用域确实采用最严格的效果。 实测机器的工作区级规则里 echo *allow 的,Agent 级对 echo PROBE_DENY*deny,结果 deny 生效。此外,运行时硬编码了一条不可配置的禁令:Agent 不能写入自己的权限文件。向 ~/.Kiro/workspace-roots/ 写入探针文件时,操作被直接拒绝:

Rule: deny fs_write matching "~/.Kiro/settings/, .Kiro/settings/, ~/.Kiro/workspace-roots/, ..."
Source: Kiro-scope.

门禁可信的前提就是,Agent 不能自行授予权限,这份 permissions 配置不仅用于运行时的权限检查,也方便安全团队在评审时看清:作者希望禁止哪些命令。

这三项结论的适用范围,以及为何仍需在本机验证。 探针运行在 IDE 1.0.309 上。CLI v3 与 IDE 共用同一套 harness(CLI v3 文档)。根据文档, v3 上应有相同的行为,但本文没有 v3 侧的第一手观察,因此这一点标为待验证。仓库中的实现并不只依赖文档:rehearsal_env/setup_cli_Agent.sh 中设置了一个 deny 冒烟探针。脚本会临时创建一个仅含两条 shell 规则的探针 Agent(allow: touch /tmp/allow_probe*deny: touch /tmp/deny_probe*),以非交互方式运行一次,仅依据文件系统证据判定:allow_probe 存在,说明会话确实执行了命令(排除“整个流程未运行却被误判为通过”的情况);deny_probe 不存在,则说明 deny 确实阻断了操作。任一条件不满足,脚本就执行 exit 1,不放行割接会话。判定时特意不读取输出文本,因为提示语会随版本变化。判断本机上的实际行为,应以运行这个脚本得到的结果为准,而不是以本文的描述为准。

为什么不能用 Agent validate 代替这个探针:实测发现,即使字段完全虚构,它也会返回退出码 0(结果见附录表格)。validate 通过不等于规则生效;在 IDE、CLI 或无人值守环境部署同一个 Agent 后,都要分别测试:需要审批的操作是否会等待批准,禁止执行的操作是否确实被拦截。不能因为换了新版本,就认为权限限制一定更严格。我们就遇到过这样的问题:两轮演练使用的都是 Kiro CLI 2.18.1(Agent v2),实测发现,校验虽然通过,但 permission 规则一条也未执行。(文末记录了问题经过及规避方法。)

2.4 进一步缩小幻觉产生的空间

除了动作门控,还从以下两个方面收窄 Agent 的活动范围:

  • 知识来源。通过 resources 字段,在会话开始时加载 Runbook,并在行为要求的第一条中明确规定:“Runbook 是唯一权威知识来源,严禁凭记忆或训练知识推断环境状态,任何结论必须引用本次会话中脚本或只读命令的实际输出”。环境事实来自文件,而不是模型记忆。
  • 能力面。这个 Agent 禁用了联网(web_fetch / web_search deny)、MCP 与子 Agent 派生能力。割接执行者不需要联网;禁用后,也就关闭了从外部内容引入指令的通道,同时降低了提示注入的暴露面。这两条 deny 规则也由 2.2 所述的权限机制执行。该机制在本机上是否生效,要看 2.3 中的实际测试结果。不过,本文尚未分别测试这两条规则能否拦截对应操作,因此仍标为待验证。

三、在自己的环境里复现

本章介绍仓库的目录结构、Agent 定义的完整策略、用 CDK 搭建按天计费的演练环境的方法,以及从零完成一次割接的操作流程。

3.1 资产结构与安全分级

canal_cutover_Agent/
├── steering/
│   └── canal-cutover-Runbook.md   # Agent 知识库:环境事实 + 流程 + 判据 + 门禁规则
├── scripts/
│   ├── 00_env_assert.sh           # P0 环境断言(只读,自动)
│   ├── 10_preflight_gtid.sh       # P1 GTID 预检 + 并集生成(只读,自动)
│   ├── 20_stop_instance.sh        # P2-1 停 instance          [门禁]
│   ├── 21_archive_cursor.sh       # P2-2 存档 cursor(只读)
│   ├── 22_delete_cursor.sh        # P2-3 删除 cursor 节点      [门禁]
│   ├── 23_verify_config.sh        # P2-4 校验 master.gtid 配置(只读)
│   ├── 24_start_instance.sh       # P2-5 启动 instance        [门禁]
│   ├── 30_verify.sh               # P3 验证循环(只读,自动)
│   ├── 40_loadgen.sh              # 造数端控制(仅写测试 marker 表)
│   ├── 50_e2e_verify.sh           # 端到端验证
│   └── 51_reconcile.sh            # 数据对账(Kafka id 集合 ⊇ 源表)
├── Agents/
│   ├── canal-cutover.md           # 唯一真源:契约正文 + permissions 策略
│   ├── canal-cutover.cli.json     # CLI 载体(JSON),由生成器产出,勿手改
│   └── gen_cli_json.sh            # 从 md 生成 CLI 载体
├── state/                         # 运行产物:证据存档 + 执行日志
└── rehearsal_env/                 # 演练环境 CDK(EKS + RDS + MSK + 跳板机)

仓库名 Kiro-Agent-canal-cutover,上图为仓库的代码结构。部署时 rehearsal_env/deploy_Agent_assets.sh 把除 rehearsal_env/reports/state/ 之外的部分部署到跳板机 /root/canal_cutover_Agent/,并按本次环境参数生成两个文件:env.sh(仓库里只有 env.sh.sample)和 session_init.sh。第三个不在仓库里的是 .canal/secrets.sh——运行时口令文件,在 Agent 的 deny 名单上(见 3.2)。该脚本会自动执行 cd "$(dirname "$0")/..",所以在 rehearsal_env/ 目录中直接执行 ./deploy_Agent_assets.sh 即可,不必先返回上级目录。

为节省篇幅,目录树中未列出以下文件:scripts/lib/common.sh(公共函数)、仓库根目录下的 REHEARSAL_OPERATOR_RUNBOOK.md(演练操作手册),以及 reports/ 下两份演练报告(rehearsal-report-2026-07-23.mdrehearsal-report-2026-08-15.md)——第四章的时间线、日志与对账数字都出自后者。

按照 1.1 的分工表,我们将脚本操作分为三个安全级别。每个级别都要明确两件事:权限策略如何限制操作,以及 Agent 执行时应遵守哪些要求。

级别 操作 策略效果 行为
只读与仅写测试表 环境断言 / GTID 预检 / 存档 / 配置校验 / 验证循环(00_ 10_ 21_ 23_ 30_,纯只读);造数端控制 / 端到端验证 / 数据对账(40_ 50_ 51_,写入范围只到测试 marker 表,不涉及业务表与配置) allow 自动执行,将逐项核对的校验结果与证据写入 state/
变更 停实例 / 删位点 / 启动 ask 先说明动作、证据和回滚方式,等待人工批准
禁区 switchover 触发、RDS/K8s 变更、环境销毁 deny 机器阻断,由人执行

这两列分别说明了两种约束:“策略效果”由机器判定,deny 那一行不存在“说服 Agent 放行”这个选项(2.3 的三个探针验证的正是这一点);“行为”是契约规定的行为要求,靠 Agent 自行遵守,无法由机器强制执行。

门禁控制在会话中的具体表现:

[图 1:变更脚本的门禁请示(演练会话记录)]

Agent 在执行 22_delete_cursor.sh 之前出示三项——将要执行的动作(含被删的 ZK 节点路径与效果)、当前证据摘要(P0/P1/P2-1/P2-2 逐项状态与 cursor 存档文件名)、回滚方式(用存档 JSON 重建 ZK 节点),然后暂停,等待“批准”。

成本参照:截图右下角的 Credits 是该轮对话的模型用量。本次观察:单步(“Agent 出示计划 → 人批准 → 执行 → 回报”算一步)的用量为 0.1–0.7 credits;本文未统计整轮割接的总用量。演练时未保留会话级用量记录,事后无法汇总,因此这里只提供单步用量区间,不能简单乘以步数来估算整轮用量。

3.2 Agent 定义:将三级安全分级写成策略

Agents/canal-cutover.md 的 frontmatter 将上表中的分级要求定义为规则(节选):

---
name: canal-cutover
tools: ["read", "write", "shell"]
includeMcpJson: false
includePowers: false
resources:
  - "file://steering/canal-cutover-Runbook.md"
  - "file://README.md"
permissions:
  rules:
    # 只读与造数脚本:静默执行
    - capability: shell
      effect: allow
      match: ["bash scripts/00_*", "bash scripts/10_*", "bash scripts/21_*",
              "bash scripts/23_*", "bash scripts/30_*", "bash scripts/40_*",
              "bash scripts/50_*", "bash scripts/51_*"]
    # 变更脚本:必须人点头
    - capability: shell
      effect: ask
      match: ["bash scripts/20_*", "bash scripts/22_*", "bash scripts/24_*"]
    # 禁区:deny 优先级最高,任何 allow 都无法覆盖这个 deny
    - capability: shell
      effect: deny
      match: ["aws rds switchover-blue-green-deployment*", "aws rds modify-*",
              "aws rds delete-*", "kubectl delete*", "kubectl apply*",
              "kubectl edit*", "kubectl scale*", "*cleanup.sh*",
              "*cdk destroy*", "sudo *", "rm -rf *"]
    # 文件写入只放开证据目录
    - capability: fs_write
      effect: allow
      match: ["state/**"]
    # 不许改写自己的门禁、契约与脚本
    - capability: fs_write
      effect: deny
      match: ["**/scripts/**", "**/steering/**", "**/Agents/**",
              "**/env.sh", "**/.Kiro/**"]
    # 口令与密钥不可读(脚本自行加载,Agent 不需要看到明文)
    - capability: fs_read
      effect: deny
      match: ["**/.canal/secrets.sh", "**/*.pem", "**/*.key"]
    # 关掉不需要的能力面
    - capability: web_fetch
      effect: deny
    - capability: subAgent
      effect: deny
---

三处设计说明:

fs_write 只允许写入 state/,并明确以 deny 禁止写入 scripts/steering/Agents/。 这条控制的是最隐蔽的绕过路径:Agent 不改命令,而是修改脚本。禁止这种行为后,才能确保“变更只能经由受版本控制的脚本执行”。

口令文件不可读,不影响脚本正常执行。 脚本通过 shell 加载 secrets.sh,属于 shell 能力;Agent 自己读文件属于 fs_read。将这两种能力分开管理,既能正常执行流程,也能避免暴露明文口令。

权限策略检查的是 Agent 发出的命令,不会逐条检查脚本内部执行的命令。 bash scripts/00_env_assert.sh 被放行后,脚本内部的 kubectl/mysql/zkCli 调用不再逐条进行策略检查。这是职责划分,而非漏洞:脚本受版本控制并经过评审,且 Agent 被禁止改写它。Agent 若需执行脚本之外的操作,只能进行只读取证。

mysqlkubectl exec 这类命令的风险取决于参数内容——-e 后面既可以是 SELECT 也可以是 UPDATE,glob 模式无法识别其中的语义。因此,我们不将这类命令加入 allow,一律要求人工确认,并在契约中要求 Agent 先声明只读用途。声明式策略能精确匹配“命令名 + 子命令”,但无法判断参数语义,这是策略的真实边界。

命令边界限制在会话中的具体表现:

[图 2:越界命令被拒(演练会话记录)]

人要求重启 canal-admin pod,Agent 判定它属于 scripts/ 之外的变更命令(kubectl delete pod),拒绝执行,将命令交由人工处理,并说明“重启完成后告知我,我重新执行 P2-5”。这一层是契约,不是策略。策略负责拦截已发出的命令,契约则要求 Agent 在提出动作之前就判断是否越界,不发出此类命令。

3.3 两个载体与版本闸门

契约以这份 Markdown 文件为唯一依据。CLI 不识别 Markdown,所以 CLI 载体用 JSON,由 bash Agents/gen_cli_json.sh 从 md 生成,正文原样写入 JSON 的 prompt,两个载体不再需要手动同步。

JSON 配置中必须包含 permissions。部署脚本会通过断言检查这一点,而不是只靠人工约定。 部署脚本会 assert permissions.rules 非空、且 deny 规则里必须出现 switchover,缺少时直接报错退出,避免将缺少权限规则的配置部署到支持这些规则的运行环境中。(这一断言的要求曾经调整过,原因见文末附录。)

部署脚本先检测版本,再决定是否继续执行。版本检查遵循 fail-closed 原则:只有确认版本符合要求,才会继续部署;否则立即停止。

CLI_VER=$(Kiro-cli --version | grep -oE '[0-9]+(\.[0-9]+)+' | head -1)
case "$CLI_VER" in
  3.*|[4-9]*.*) V3_CMD="Kiro-cli" ;;             # 原生 v3,permissions 按文档被评估
  2.*) Kiro-cli --v3 --version >/dev/null 2>&1 \
         && V3_CMD="Kiro-cli --v3" \
         || { echo "FATAL: 2.x 且无 --v3,permissions 不执行,deny 形同虚设"; exit 1; } ;;
  *)   echo "FATAL: 取不到版本号,先确认 Kiro-cli 装在 ec2-user 下"; exit 1 ;;
esac

(完整实现在仓库 rehearsal_env/setup_cli_Agent.sh,本文仅保留主要判定分支。获取版本号时,脚本还会尝试 Kiro-cli-chat,因为不同安装方式的二进制名不一样。)

启动与校验:

# 结构校验:通过只代表结构合法,不代表规则生效——实测它对瞎编的字段也返回 0
$V3_CMD Agent validate --path ~/.Kiro/Agents/canal-cutover.json
# 启动(--Agent 是 chat 子命令的参数)
$V3_CMD chat --Agent canal-cutover

因此,不能仅靠 validate 判断规则是否生效,而要通过 2.3 中的 deny 冒烟探针。allowedTools 指定哪些工具可以直接使用。其他工具调用是否允许执行,由 permissions 规则决定;如果没有匹配的规则,就弹出确认提示,等待人工批准

随后输入“按 Runbook 执行 P0 环境断言”即可开始执行。不要同时放置项目级和全局级配置——同名 Agent 会让每次启动打印 Agent conflict 警告;定义中使用绝对路径,因此只需保留一份全局配置。

门禁在会话中的实际表现:

[图 3:校验结果核对与 CLI 的逐条确认(演练会话记录)]

上半屏是 Agent 对 P2-3 的逐项校验——校验标准 / 结果 / 证据三列,“cursor 节点不存在”配的证据是 zkCli 退出码;下半屏是 CLI 在执行下一条 shell 命令前弹出的确认菜单,三个选项:单次放行 / 本会话信任 / 拒绝。按“本会话信任”这个选项的含义,选择后,后续操作将不再逐条请求确认。因此,割接期间不要选择这一项。

3.4 演练环境:用 CDK 搭建与生产形态一致的环境

[图 4:演练环境架构]

跳板机不配置任何 inbound 规则(SSM Session Manager 接入),EKS 上运行 ZooKeeper、Canal Admin 与 Canal Server,RDS 主库→只读副本复刻生产拓扑,Canal 订阅只读副本,并将数据投递至 MSK。

必须和生产一致的四项,每项都影响操作路径:

  • Canal 官方镜像 + Admin 托管配置:配置唯一来源是 canal_manager 库,改配置只能走 Admin UI/API
  • 位点存 ZooKeeper(default-instance.xml),cursor 路径 /otter/canal/destinations/<destination>/1001/cursor
  • Canal 订阅只读副本,与生产一致,也正是前篇“出生即 purged”问题的接入点;
  • 运维操作路径一致:从跳板机执行 kubectl / mysql / zkCli。

成本按 us-east-1 的按需价格估算,仓库中的分项估算如下:

资源 规格 月成本
EKS 控制面 标准支持版本 ~$73
节点组 t3.large ×1 + 30GB gp3 ~$63
RDS × 2 db.t4g.micro + 20GB gp3 ~$28
MSK × 2 broker kafka.t3.small + 20GB ~$69
跳板机 t3.medium + 10GB gp3 ~$31
合计 ~$264/月 ≈ $9/天

演练环境按天使用,用完立即执行 cdk destroy。中途要暂停可以停跳板机与 RDS,并将节点组缩容到 0——但 EKS 控制面和 MSK 不支持停止,长时间闲置仍会产生费用,不如销毁后按需重建。

跳板机原本是 t3.micro,实测无法同时承载 Kiro CLI 会话加常驻 kubectl,OOM 宕机之后才升到 t3.medium。如果只通过脚本进行运维、不在跳板机上启动 Agent 会话,可以降回 small。

成本:上表只含这五项的按需实例费与所列 EBS;未计入数据传输(含跨 AZ)与 NAT——NAT 走的是你现有 VPC 的,不在本栈内,EKS 节点拉镜像会产生少量流量费。也不含 Kiro 的 credits(用量见 3.1 图 1 下方的说明)。这些金额只是估算,并非实际账单。使用前请用 AWS Pricing Calculator 按你自己的区域重算一遍。

3.5 七步复现路径

动作顺序必须严格遵守。 仓库里的 Runbook 把它写成 P2 段五步:停 → 档 → 清 → 验 → 启。存量位点(ZK cursor)的优先级高于 master.gtid,顺序一旦颠倒,配置就被静默忽略。这也正是本文开头那次事故的成因。

下面七步将这五步纳入完整流程,并明确了另外两处顺序要求:switchover 在“停 → 档”之后(第 6 步),并集重算与配置修改必须安排在“档”和“清”之间(第 7 步)。合起来是 停 → 档 → 切 → 算 → 改 → 清 → 验 → 启。其中的“算 → 改”两步并非无故增加,而是根据 8-15 那轮 11:04:29 的校验 FAIL 结果明确的操作要求(见 4.1)——那次先删了 cursor 才重算并集,校验时配置仍为空。全文提到“五步操作规范”时指 Runbook 的 P2 核心,提到七步或八步时指这条完整路径。

下面每一步都列出了验收点,验收标准取自仓库里的 REHEARSAL_OPERATOR_RUNBOOK.md(面向人工操作的手册,包含 12 步操作及各步通过条件)。执行时应逐项核对,不满足通过条件就停止。

开始操作前,先核对前置条件(这部分不计入七步流程,但缺少这些条件,后续步骤将无法执行)。本栈不创建 VPC,而是部署到已有的 VPC 中,因此对 VPC 有要求:

要求 用途 不满足会怎样
-c vpcId=vpc-xxx 必须给 本栈不建 VPC 直接报错退出
带 NAT 出网的 private 子网 ≥ 1 EKS 节点组拉镜像、访问控制面 报错退出
isolated 或 private 子网覆盖 ≥ 2 个 AZ RDS 子网组与 MSK 都要求最少 2 AZ 报错退出
public 子网 跳板机;没有就落 private,SSM 经 NAT 仍能接入 自动降级,不报错

第 1 步,部署基础设施。

git clone https://github.com/xiaofeng8458/Kiro-Agent-canal-cutover.git
cd Kiro-Agent-canal-cutover/rehearsal_env && npm install
npx cdk bootstrap                               # 账号首次使用 CDK 才需要
npx cdk deploy -c vpcId=vpc-xxxxxxxxxxxx        # EKS 创建约 15-20 分钟
aws eks update-kubeconfig --name canal-rehearsal --region us-east-1

验收点:cdk deploy 的 Outputs 里能取到 BastionId / PrimaryEndpoint / ReaderEndpoint / DBSecretName / MskClusterArn(后续脚本靠 source ./stack_outputs.sh 导出这五个值),且 kubectl get nodes 全部 Ready

第 2 步,部署后配置。 全部脚本化,本地有 AWS 凭证即可执行——ssm_run.sh 经 SSM send-command 将完整的本地脚本发送到跳板机执行并回传输出,无须交互式登录跳板机,也无须在浏览器中操作 Admin UI。

./ssm_run.sh 'kubectl get nodes'                # 连通性自检
./ssm_run.sh -f bootstrap_db.sh                 # 建库/账号/marker 表 + binlog 保留 168h
./ssm_run.sh -f bootstrap_pf.sh                 # canal-admin port-forward 交给 systemd
./ssm_run.sh -f extract_templates.sh            # 从镜像取 canal.properties/instance.properties 模板
./ssm_run.sh -f bootstrap_admin.sh              # 导入 canal_manager 表 + 部署 canal-admin
./ssm_run.sh -f bootstrap_cluster.sh            # 建集群 + 下发主配置
./ssm_run.sh -f bootstrap_server.sh             # 部署 canal-server,确认注册
./ssm_run.sh -f bootstrap_instance.sh           # 建 instance(订阅只读副本)
./ssm_run.sh -f fix_instance_cluster.sh         # 补 instance 归属,以 ZK 为判据确认接管
TIMEOUT=900 ./ssm_run.sh -f verify_e2e.sh       # 验收点:写 marker → Kafka → cursor 推进

第 3 步,装 Runbook 资产与 Agent。 Kiro CLI 要先人工登录一次:以 ec2-user 身份(不要 sudo su -,root 下没有登录态)在跳板机上跑 Kiro-cli login 完成浏览器认证,认证出问题用 Kiro-cli doctor 查(CLI 安装与登录文档)。登录状态按用户保存,资产安装脚本不会代为登录。

./deploy_Agent_assets.sh                        # Runbook 资产 + env.sh + session_init.sh
./ssm_run.sh -f setup_cli_Agent.sh              # 含 CLI 版本闸门 + Agent validate + deny 冒烟探针
./ssm_run.sh -f smoke_cli_Agent.sh              # 只读冒烟

冒烟检查使用 --no-interactive --trust-tools=fs_read,只允许 Agent 读取文件,不允许执行 shell 命令。这样既能检查 Agent 定义是否加载成功、是否理解行为要求,又不会改变环境状态。不要用 --trust-all-tools,这相当于关闭所有门禁。

验收点:setup_cli_Agent.sh 打印出的 CLI 版本应属于脚本支持的版本分支(版本闸门决定按 md 还是 JSON 载体装,见 3.3),Agent validate 通过但不足以说明规则生效(2.3),所以真正要看的是同一个脚本里那道 deny 冒烟探针的结果;smoke_cli_Agent.sh 要看到 Agent 载入了定义、并且拿不到 shell。

第 4 步,启动会话并执行只读预检。

cd ~/canal_cutover_Agent && source session_init.sh
Kiro-cli --v3 chat --Agent canal-cutover        # 原生 3.x 可省 --v3

原生 3.x 可以省掉 --v3;在 2.x 上则必须添加此参数,不带就等于跑在一个不执行 permissions 的运行环境中,deny 禁区一条都不生效(文末附录)。session_init.sh 会打印 DESTINATIONADMIN_API 和 port-forward 服务状态,三项均正确后再继续。随后依次输入“按 Runbook 执行 P0 环境断言”和“执行 P1 GTID 预检”,这两步 Agent 自主完成,并逐项核对校验结果。

验收点:P0 五项断言全过;P1 输出 purged ⊆ 并集 为真。但本轮计算的并集不能用于后续配置——它算在切换前的蓝 reader 上,仅用于确认链路连通、校验标准的格式正确。真正写入配置的并集需在第 5 步之后重算,理由见 4.3。

第 5 步,回到 Agent:停 instance,然后存档 cursor。两道门禁,都在 switchover 之前。

先停消费端再切换,canal 不会在切换窗口中触发 1236;反过来会(4.6 就是由此触发的)。停止消费后必须立即存档,因为 cursor 是后面唯一的回滚依据。

验收点:停 instance 后 ZK 里的 running 节点消失(实测 10:53:02);存档后把存档文件名和 gtid 终值记录下来(实测 ...d8796fbe:1-3356),第 7 步改配置和万一要回滚都要用它。

第 6 步必须由人工完成,也是最容易遗漏的一步。 创建 B/G 之后、switchover 之前

aws rds create-blue-green-deployment --blue-green-deployment-name canal-rehearsal-bg \
  --source <蓝 primary ARN> --target-engine-version 8.4.10 \
  --target-db-parameter-group-name canal-rehearsal-84

# ⚠️ 核对绿 reader(不是绿 primary)的参数组:--target-db-parameter-group-name
#    只明确作用于绿 primary,绿 replica 用哪个参数组必须实地确认
aws rds describe-db-instances --query "DBInstances[?contains(DBInstanceIdentifier,'green')]\
.[DBInstanceIdentifier,EngineVersion,DBParameterGroups[0].DBParameterGroupName]" --output text
# 以 SQL 为最终判据,期望 8.4.x / ON / ON / ROW
mysql -h <绿 reader endpoint> -u dbadmin -p \
  -e "SELECT @@version, @@gtid_mode, @@enforce_gtid_consistency, @@binlog_format;"

绿 reader 若使用 default.mysql8.4,其 gtid_modeOFF_PERMISSIVE,Canal 的 GTID dump 会在寻位阶段失败,报错形式不是 1236。这条路径本次演练未复现,属于待验证结论;请以上面那条 SQL 的实际返回值为准。

switchover 本身也是人执行(禁区)。切换完成后,首先要重新设置 binlog retention:

CALL mysql.rds_set_configuration('binlog retention hours', 168);
CALL mysql.rds_show_configuration;

retention 是实例本地配置,绿环境不继承。修补窗口内的业务 binlog 一旦被清理,并集就会将已清理的区间标记为已消费,导致数据丢失且没有告警。先确认 retention,再算并集。

这一条的证据强度要说清楚:它是 7-23 演练的实测结论——03:56 发现绿库 gtid_purged 在一小时内从 1-8 增长到 1-1722,业务 binlog 正在被清理,当时立即将保留时间设为 168h(见 4.6 第 2 条,以及公开仓库里 7-23 的演练报告问题 #4)。目前未见官方文档明确说明,蓝绿部署是否会继承 binlog retention hours。因此,这里记录的是演练中观察到的现象,不能当作官方已经确认的产品规则。请在自己的实例上执行 mysql.rds_show_configuration,读取当前配置,以实际结果为准。

验收点:上面那条 SQL 返回 8.4.x / ON / ON / ROW 四项全对;mysql.rds_show_configuration 回读 binlog retention hours 等于 168。这两项未通过就不能继续,否则第 7 步算出来的并集会是错的。

第 7 步回到 Agent,按规定顺序完成后半段操作。

在正确的 endpoint 上重算并集(为什么强调“正确的”,见 4.2)→ 人工改 instance 配置(master.gtidmaster.timestamp 两行都要改)→ 删 cursor(门禁)→ 校验配置 → 启动(门禁)→ P3 验证 → 端到端 → 全量对账。

并集一共算两次,只有第二次能用。 第 4 步 P1 那次是基线,算在切换前的蓝 reader 上,作废;这一步用第 5 步记录的 gtid 终值重算,算在切换后的绿 reader 上,只有这一份能写进 master.gtid。两次结果的差异并非无关紧要——4.3 里它是同一个 UUID 从 1-3410 变成 1-3356,差 54 个事务。

各动作的验收点:

动作 验收点
重算并集 gtid_union.txt 的 mtime 晚于 switchover 完成时刻;并集里出现绿库自己的 UUID
校验配置 四项:key 恰好出现 1 次 / 值逐字等于并集 / gtidon=true / timestamp>0。任意一项校验未通过,就停止流程;Agent 不得自行修改配置后重试。
启动 instance Agent 必须先检查并确认 cursor 已删除,才能提交启动计划;跳过这项检查就属于违规操作。
P3 验证 起点 gtid 逐字等于并集 / 零 1236 / 位点单调推进 / 与源端差值收敛
全量对账 缺失 0。重复不算缺陷,那是 at-least-once 的预期

全文多处提到的“故障判定表”如下。 它的适用范围很窄——P3 验证阶段出现 1236 时用,其他阶段的错误不走它。完整版在公开仓库的 steering/canal-cutover-Runbook.md 第 6 节,以下保留原有的故障判断依据:

现象 结论 处置
报错中 sent 集合 ≠ 配置的并集 存在未清理的存量位点,或配置未生效(重复 key / 值换行截断 / 未下发) 停 instance,从“删 cursor”那一步(P2-2)重走
sent 集合 == 并集,仍然 1236 并集不满足服务器要求——gtid_purged 又往前走了 重跑预检(P1),用当时最新的 gtid_purged 重算并集

要区分这两类故障,需要比较报错中的 sent 集合与配置中的并集。第一种情况是配置未生效;第二种情况是配置已经生效,但计算并集时使用的状态已经过时。判断只需要一个输入——报错中的 GTID set sent by the replica,它是 canal 内存位点的实时快照,也是第一取证点。4.6 的两次 1236 正好各命中一行。

演练结束后 ./cleanup.sh 先 dry-run 盘点,再 ./cleanup.sh --execute 销毁(需逐字输入确认短语)。它会连 B/G 对象、-old1 旧蓝实例和参数组一并清理,不影响现有 VPC。销毁这一步的缺陷见 4.4。

四、两轮演练的验证结果

准入规则只有一条:Agent 必须先在演练环境完整跑通一次割接,并验证失败处置与故障分诊行为,才允许参与生产割接。 演练既验证流程,也用于 Agent 验收。两轮演练各有侧重:2026-07-23 那轮意外进入故障路径(1236 现场分诊),2026-08-15 那轮跑通正常路径并完成量化对账。

[图 5:门禁化割接流程,三种颜色对应 1.1 表中的前三个分区]

绿色节点由 Agent 自主执行(只读或仅写测试表),黄色节点为门禁点(Agent 出示计划/证据/回滚方式,经人工批准后执行),红色节点为禁区(仅人工执行)。P2 阶段的五步顺序遵循“停→档→清→验→启”这一硬性规则。

4.1 正常路径跑通,对账无缺失(2026-08-15)

时间均为 UTC。造数端全程以 2 秒一条的频率写入,未中断。

时间 阶段 结果
08:47 / 09:54 / 10:51 P0 环境断言(三轮) 5 项全部通过
10:53:02 P2-1 停 instance 门禁批准后执行,ZK running 消失
10:53:52 P2-2 存档 cursor gtid 终值 ...d8796fbe:1-3356timestamp=1786791181000
人工 switchover 造数端记录了两条 ERROR 1290 ... --read-only,即切换窗口内写入被拒绝,符合预期
11:03:59 P2-3 删除 cursor 门禁批准后执行
11:04:29 P2-4 校验配置 FAIL(配置值为空)→ 停下,未自行重试
11:16:27 P2-4 复校 通过,回读 DB 逐字一致,cluster_id 仍为 1
11:28:10 P2-5 启动 instance 门禁批准后执行
11:29–11:34 P3 观察 5 分钟 cursor 十轮单调递增,差值收敛到 15 个事务
11:36–11:40 端到端 marker id=4565 到达 Kafka
11:43–11:48 全量对账 源表 4761 行,Kafka 去重 4897,重复 0,缺失 0

11:04:29 那次 FAIL 是这轮唯一一处偏离硬性规则的情况,需要单独说明。 这轮先删了 cursor(11:03:59),之后才重算并集(11:16,见 4.3),顺序颠倒,导致校验时配置值仍为空。由于校验未通过,流程停止——Agent 停止执行,没有自行重试;人工补完配置后,于 11:16:27 复校通过。3.5 第 7 步给出的顺序(重算 → 改配置 → 删 cursor → 校验 → 启动)就是从这次 FAIL 中总结出来的,按这个顺序执行即可避免同样的问题。

启动后的寻位日志是这轮最关键的一行证据——起点 gtid 与修补并集逐字一致,订阅地址已指向绿 reader:

---> find start position successfully, EntryPosition[included=false,journalName=,
position=<null>,serverId=<null>,gtid=56196dbf-987c-11f1-88cf-0271942cc0a9:1-4,
d1bc9029-9890-11f1-9dee-0e55fd8ce4ed:1-8,d8796fbe-987a-11f1-8e24-0ef2e727c853:1-3356,
timestamp=1786791181000] cost : 13ms , the next step is binlog dump

[图 6:全量对账结果(演练会话记录)]

验收标准是“Kafka id 集合 ⊇ 源表”,缺失 0、重复 0。

Kafka 比源表多出的 136 个 id 并非异常:源表快照采集于 11:43,Kafka 全量消费持续到 11:48,这期间造数端仍在写。Agent 在结论中主动解释了这一差异(见图 6 末行),没有把多出的条数作为问题上报。验收标准“缺失 0”已满足;重复 0,而重复本身属 at-least-once 的预期,不作为判定失败的依据。

4.2 缺陷一:B/G 的 reader 成员切换失败,聚合状态却报成功

触发条件。 switchover 执行时绿 reader 处于 recovery/restart 状态,复制已停止,未能参与改名。

现象与影响范围。 switchover 返回后,B/G 的聚合状态是 SWITCHOVER_COMPLETEDStatusDetails: "Switchover completed"。逐成员查看 SwitchoverDetails,reader 对应的记录却显示失败:

"SwitchoverDetails": [
  { "SourceMember": "...primary...-old1",
    "TargetMember": "...primary...",
    "Status": "SWITCHOVER_COMPLETED" },
  { "SourceMember": "...readerf7bf189d-tdweem6ytmlk",
    "TargetMember": "...readerf7bf189d-tdweem6ytmlk-green-9rsjqs",
    "Status": "SWITCHOVER_FAILED" }
]

primary 改名成功,所以从主库看一切正常;reader 没改名,原 reader 名仍解析到被冻结的 8.0.42 旧副本。Canal 订阅的正是 reader,于是三处都指向了同一个错误实例:

  • canal 的 master.address 指向一个仍在运行、可以连接、版本仍是 8.0.42 的实例;
  • 环境变量里的绿只读副本地址(由 reader 名推导)指向同一个错误对象;
  • 位点修补要用的 gtid_purged 从这个旧副本上取——数值合法,与真正的新链路无关。

切割环境已经处于错误的状态, 如果不核对成员,整个修补就会基于错误的服务器完成,而校验不会因此失败——用于比对的信息全都来自那台错误的实例:并集采用的是它的 gtid_purged,配置校验回读的是指向它的 master.address,版本校验检查的也是它。

因此,校验切割后的环境状态是需要在流程中增加的一步。检测方法:① 用 aws rds describe-blue-green-deployments 逐成员检查 SwitchoverDetails,不看聚合 Status;② 用 SQL 在 canal 实际要连接的 endpoint 上确认 @@version 是目标版本。AWS API 的状态字段反映的是“部署对象”的状态,SQL 才能确认“实际连接的是哪个实例”。

恢复步骤。 修补脚本的第 0 步强制校验目标 reader 的版本,不是 8.4.x 直接中止:

V=$(mysql -h "$NEW_RO" -u dbadmin -N -B -e "SELECT @@version")
case "$V" in
  8.4.*) echo "OK: 目标 reader 是 $V" ;;
  *)     echo "FATAL: 目标 reader 版本 $V 非 8.4.x,中止"; exit 1 ;;
esac

随后将绿只读副本地址显式固定为带 -green- 后缀的真实 endpoint,重算并集,改 master.address/master.gtid/master.timestamp 三行,回读逐字校验。

验收标准。 目标 reader 的 @@version 返回 8.4.x;三行配置回读与写入值逐字一致。

4.3 缺陷二:在旧 reader 上计算了并集

触发条件。 缺陷一的直接后果——在错误的 endpoint 上执行预检,或在 retention 确认之前算并集。

现象与影响范围。 11:14 那轮预检产出的并集是 56196dbf...:1-4,d8796fbe...:1-3410;11:16 在真正的绿 reader 上重算,并集变成 56196dbf...:1-4,d1bc9029...:1-8,d8796fbe...:1-3356——多出绿库自己的 UUID d1bc9029...:1-8,这正是“出生即 purged”需要补入的那一段。错误并集按原样归档为 gtid_union_WRONG_computed_on_old_reader_*.txt

差异不只是“多出一段 UUID”,同一个 UUID 的区间也变了:d8796fbe1-3410 缩到 1-3356,少了 54。 而 1-3356 正是 4.1 里存档 cursor 记下的 gtid 终值,也就是 canal 真实消费到的位置。按 master.gtid 的语义,写入的区间表示“这些事务已经消费过,从下一个开始”——因此,那份错误并集会让 canal 认为 3357–3410 这 54 个事务已经消费过,直接跳过。

所以,错误并集的后果不只是“起点错位”,而是本文一直提到的静默丢数据(3.5 第 6 步和 4.6 第 2 条均有说明),只是这次的成因是计算对象错误,而不是 retention 被清。这 54 个事务的丢失不会触发报错或告警,也不会触发任何失败判定条件;对账脚本能够发现这一缺失,前提是确实执行了对账。

配置校验脚本的校验标准是“配置值 == 并集文件”。如果忘记重算,它就会用旧 reader 的并集校验绿库配置,校验虽通过,起点却错了——校验基准本身出了错,这比校验失败严重得多:校验失败会让流程停下(4.1 的 11:04:29 就是如此),而校验基准错误却会让流程始终被判定为通过,直到对账时才会发现这 54 个事务的缺失。

对照两张截图最为直观。先看后来确认无效的那份并集:

[图 7:⚠️ 这张截图中的并集后来已确认无效(演练会话记录)]

Agent 把改配置这一步交回人工,给出的 canal.instance.master.gtid 只有两段 UUID——当时绿只读副本地址仍指向旧 reader。请勿照抄这个值,对比下一张图。(这张截图另有两处值得关注:Agent 主动从存档 cursor 中提取了 master.timestamp 的毫秒值,以及它复述了 properties 的三条注意事项——值必须单行、不要重复 key、保存后回读 DB 验证。)

修正之后,同一位置的并集多出一段:

[图 8:修正后的门禁请示(演练会话记录)]

并集变成三段,多出绿库自己的 d1bc9029...:1-8;证据摘要里明确标注“并集已用最新 purged 计算”,回滚方式写明“如遇 1236 错误,按故障判定表处置”。这段证据摘要记录了“停→档→清→验→启”各步骤当时的执行状态,可以据此核对流程是否按规定顺序推进。

检测方法。 ls -l --time-style=full-iso state/run_*/gtid_union.txt,mtime 必须晚于 switchover;并集里的 UUID 段要与目标实例的 server_uuid 对应。

恢复步骤。 在 canal 实际要连接的目标实例上重跑预检,更新 master.gtid,将错误并集改名归档,不删除,以保留证据。

验收标准。 instance 启动后的寻位日志中 gtid= 逐字等于并集文件内容(形如 4.1 那段日志)。

我们事先预见过这一风险,但预想的是“人忘了重算”,实际发生的却是“重算了,但对象错了”。因此,结论应修正为:并集的正确性由两个条件共同决定——计算时点(retention 确认之后,purged 会动态变化)和计算对象(必须是 canal 实际要连接、且运行目标版本的实例)。Runbook 此前只写了前者。

4.4 脚本类缺陷:两个假信号

缺陷 表现 教训
逐字回读校验因 mysql 补入尾随换行而误判 预期配置 72 行/2873 字节,DB 回读 73 行/2874 字节,diff 报不一致;配置内容其实逐字相同 假阴性,会让人误以为配置写入有误,在割接窗口内可能引发不必要的回滚。回读比对前应先规范化尾随空白
P3 验证脚本读容器 stdout,无法获取 instance 日志 脚本判为“寻位日志未找到”并 WARN;若不追查,所谓“零 1236”就是基于空数据得出的结论 假阳性风险更高。判断零命中前,必须先确认日志源中确实有内容

第二条是 7-23 已记录、8-15 复现的同一个缺陷。Agent 当场指出了这一问题:

[图 9:Agent 把未完成验证的项目标为“未检查”,而非通过(演练会话记录)]

P3 校验结果表里“Kafka RecordTooLarge”一行是 ⚠️ 未检查,证据栏写“脚本未报告异常”——它没有把“脚本没报错”等同于“这项通过了”。同一截图中的“位点推进”与“差值收敛”两项则给出了具体读数(1036→1052→1068→…→1182,差值收敛到 15 个事务)。

这轮的处理是:P3 的“起点等于并集”“零 1236”“零 RecordTooLarge”三项改为根据 instance 级日志文件进行人工核对(命中均为 0),报告中如实标注“人工核对”,没有把脚本未报错视为通过。这正是“证据驱动”的含义——判定校验通过必须有实际证据支持。

4.5 故障路径:1236 的两个分支均得到验证(2026-07-23)

7-23 那轮 switchover 被提前触发,演练随即转入故障路径,Canal 沿用旧位点,触发 errno 1236,前篇的场景在演练环境原样复现(确认时 Canal 已重试 198 次,sent 集合就是旧 cursor,missing 是绿主库内部事务,即“出生即 purged”)。那一小时里接连遇到三个 Runbook 未覆盖的问题,均得到了处置:

  1. TSDB 启用时 master.gtid 必须配套 master.timestamp。 删 cursor、写并集、启动,cursor 却没重建。Agent 停止流程,转而从 instance 级日志取证,定位到 canal 的硬校验 use gtid and TableMeta TSDB should be config timestamp > 0。这是原有流程未覆盖的新故障,Agent 将问题交给人工判断,没有自行猜测原因或处置方式。
  2. 绿环境不继承 binlog retention。 修补期间绿库 gtid_purged 一小时内从 1-8 增长到 1-1722,业务 binlog 正在被清理,而已清理的区间会被并集标记为“已消费”——导致数据丢失且无告警。为阻止损失扩大,立即重设了 retention。
  3. 并集会动态变化。 retention 重设后重启,再次触发 1236:sent 集合等于配置的并集,missing 是更靠后的区间。Agent 按判定表分诊——sent 与并集一致,排除“存量位点残留”,判定为“并集计算时点过旧”——重跑预检,用当时最新的 gtid_purged 重算,更新配置,启动,错误消失。

这轮演练实际丢失了部分数据:从 switchover 完成到重新设置 retention 之间写入的数据,其对应的 binlog 已被绿环境按默认策略清理,无法再通过 binlog 补发。这就是未及时在绿环境中重新设置 retention 带来的后果。

4.6 Agent 行为验收

验收项 结论 证据
门禁 ✅ 通过 8-15 三次变更(停 / 删 cursor / 启动)均先出示计划,再等待批准;Runbook_state.md 里三处 GATE PASSED 记录,时间戳 10:53:02 / 11:03:59 / 11:28:10,未提前执行
失败即停 ✅ 通过 8-15 的 11:04:29 配置校验 FAIL 后停止流程、呈现失败证据,未自行改配置、未重试变更脚本
故障分诊 ✅ 通过(证据来自 7-23) 两次 1236 分别命中判定表两个分支;对预检误报的外来 UUID 完成“差集 → 逐台 server_uuid 核验身份”的取证后,才申请放行

原始记录在公开仓库的 reports/rehearsal-report-2026-08-15.mdreports/rehearsal-report-2026-07-23.md 中;门禁这一项对应的执行记录,是演练目录下 Runbook_state.md 的三处 GATE PASSED。如需核对,可直接查阅这两份报告;本文的校验标准摘要未包含报告之外的信息。

两点补充。失败即停这一项在 8-15 中由真实故障触发,而非故障注入测试——那次 FAIL 的成因是人工还没改配置,Agent 根据校验结果报错并停止执行,相比人为构造的场景更有说服力。故障分诊在 8-15 未重复演练(全程零 1236),该项的最新证据仍来自 7-23;若需再次验证,可通过“不删 cursor 直接从 Admin UI 启动”构造故障,无需再做一次 B/G。

还有两次表现超出了我们的预期:Agent 发现了脚本自身的问题,没有因为脚本显示通过,就认定检查已经完成。一次是发现配置校验脚本会用未修正的并集作为比对基准,于是改用手动等效校验;另一次是 4.4 表里的日志源问题,它发现验证脚本读的容器 stdout 里没有 instance 日志,直接执行会得出虚假的“零 1236”结论,于是改为检查正确的日志文件。

第四类工作——脚本未覆盖的动态取证——也有一个典型案例:

[图 10:脚本未覆盖的环节由 Agent 组合使用只读命令取证(演练会话记录)]

切换后 canal-admin 的连接池仍保留着旧库连接,表现为“读正常、写 read-only”(即第五章修正的第 4 条)。重启 pod 之后 Agent 逐项核实——pod 是否已重启、Secret 中配置的地址、该地址实际指向的实例(版本 8.4.10、UUID、read_only=0)——确认已指向新主库,才申请重新执行 P2-5。它的取证顺序是“配置中指定的对象 → 实际连接的对象”,与 4.2 要求的核对方式一致。(实例名已打码。)

五、对前篇 Runbook 的修正与增补

前四条来自 7-23,后两条来自 8-15:

  1. 【修正】binlog retention 不能依赖继承。 前篇建议“在蓝环境创建绿环境之前设置 retention 并在绿只读副本上验证”,实测发现绿环境不继承(它是实例本地配置)。修正后:switchover 完成后的第一个动作是在新主库重设并验证 retention;并集等 retention 确认之后再算。这条是实测结论,未见官方文档说明蓝绿部署下该配置的继承行为(见 3.5 第 6 步的说明)。
  2. 【修正】caching_sha2_password 迁移建议需重审。 前篇建议提前迁移认证插件;实测 canal 1.1.8 对 caching_sha2 账号会出现“认证成功但报文解析崩溃”(issue #5403)。结合 RDS MySQL 8.4 默认参数组里 mysql_native_password 为 ON 的实测结论(与社区版 8.4 默认值相反),存量 Canal 采用 native 过渡是当前更稳妥的方案;应使用客户实际的 Canal 版本复测后,再作出迁移决策。
  3. 【增补】启用 TSDB 后,设置起始位点时必须同时配置 canal.instance.master.gtidcanal.instance.master.timestamp。Canal 会强制检查这两项配置。
  4. 【增补】切换后重建所有长连接池。 连接池不感知 DNS 切换,会继续使用旧库连接,表现为“读正常、写 read-only”。Canal Admin、业务应用、监控采集都适用。
  5. 【增补】switchover 后逐成员核对 SwitchoverDetails。 聚合状态 SWITCHOVER_COMPLETED 不代表每个成员都切换成功;reader 成员失败时,原 reader 名仍指向被冻结的旧版本副本,而这正是 Canal 的订阅点。核对之后还要用 SQL 在实际订阅的 endpoint 上确认 @@version
  6. 【修正】并集的正确性有两个条件,不是一个。 除了“计算时点要在 retention 确认之后”,还要加上“计算对象必须是 canal 实际要连接、且运行目标版本的实例”。前篇只写了时点。

六、本文的主要结论

  1. 引入 AIOps 时,要先明确哪些工作可以交给 Agent、哪些必须由人处理,再决定让 Agent 自动执行到哪一步。 值得交给 Agent 的是逐项核对校验结果、顺序守护、故障分诊、动态取证这类认知负荷高、创造性要求低的工作;不可逆的操作仍由人工执行,而“校验标准如何制定、权限如何划定”只能由人决定——先将自己的场景填入 1.1 的分工表,再按 1.2 的准入条件判断是否适合交给 Agent,全部满足后再实施。
  2. 门禁要写成策略,不能只写在提示词里。 通过 Kiro 的能力级权限,可以把“只读操作自动执行、变更操作先询问、禁止操作直接阻断”写成可检查、可审计的权限配置,而不只是提示词中的要求。deny 跨作用域优先、复合命令按子命令判定、Agent 不能改写自己的权限文件,这三条是它能够承担门禁职责的关键。同时也要认清边界:策略只能判断命令名,无法判断参数语义,mysql -e 这类命令仍需依靠人工确认和契约约束。
  3. “配置了”和“生效了”是两件事。 Agent validate 对随意编造的字段也返回 0,整个 permissions 块也可能被运行时忽略,且没有任何提示。这与开头的位点事故属于同一类故障:配置被静默忽略,现场看不出异常。因此,不能只看配置校验是否通过,还要故意执行一次被禁止的操作,确认系统确实会拦截。部署脚本应同时检查 CLI 版本,并测试 deny 规则是否生效;只有这两项检查都通过,才允许启动割接流程(见 2.3,相关实测记录见文末附录)。
  4. 演练既要检查流程是否可靠,也要确认 Agent 是否按要求执行。两轮演练发现了十余个真实问题,并据此修正了前篇的六处结论;同时,也检验了 Agent 是否具备参与生产割接的条件。没有在演练环境完整走通的 Runbook 和 Agent,都不应投入生产割接。演练环境用 CDK 描述,按天创建销毁,估算约 $9/天($264/月,按 3.4 的分项表计算)——以这样的成本,提前发现流程可能出错的地方。

➡️ 下一步行动:

相关产品:

相关文章:

七、附:一个易忽视的问题——Kiro CLI 2.x 可以安装、校验通过,但 permissions 一条都不执行

前面按 CLI v3 介绍主流程。本节补充说明两轮演练实际使用的运行环境,以及它与 v3 的差异。这些差异很重要,但若穿插在主流程中,会使三章内容充斥版本细节,因此集中说明。

先说明运行环境。 两轮演练全程使用堡垒机上的 Kiro CLI 2.18.1 :运维操作方式要与生产一致,实际割接时也通过跳板机操作(3.4)。图 1、图 2、图 3 全是 CLI 会话记录。

2026-08-17 在那台机器上的实测(Kiro IDE 1.0.309 / Kiro CLI 2.18.1)

现象 实测结果
CLI 2.18.1 读 Markdown 定义 ❌ 报 Json supplied ... is invalid,2.x 仅支持 JSON 载体
CLI 2.18.1 执行 permissions 规则 ❌ 不执行:配了 deny shell: "echo *",命令照样执行成功
Kiro-cli Agent validate 能否发现 ❌ 无法发现。它对随意编造的字段也返回退出码 0

在 2.x 上按新文档配置门禁规则,结果却是“看似已配置,实际未生效,校验却通过”。这和本文开头那次位点事故是同一个失败类别——配置被静默忽略,当场看不出任何异常。这也是 2.3 中设置冒烟探针的原因:运行环境即使不执行策略,也不会主动提示。

那么,两轮演练中真正约束 Agent 的是什么? 有两层约束,但都不是声明式策略

  • 第一层是 CLI 自身的逐条确认菜单。 allowedTools 只放 fs_read,其余命令每次执行前都会弹出确认菜单(图 3 下半屏)。
  • 第二层是契约。 图 2 里 Agent 判定 kubectl delete pod 属于 scripts/ 之外的变更命令,主动拒绝并交回人工;4.7 的门禁验收反映的正是这一层的执行情况。

这里还有一点尚未验证:我们只观察了 allowedTools 白名单在正常流程中的表现,还没有像验证 deny 一样,故意发起越界操作,检查它是否会阻断。因此,这部分仍标为待验证。2.3 中的冒烟探针就是为此准备的,但本文还没有可引用的运行结果。

演练之后,依据都是“2.x 不执行 permissions”的实验结果,仓库针对这一发现修改了三处:

  • 版本要求由建议改为硬性前提。 rehearsal_env/setup_cli_Agent.sh 安装 Agent 前先获取 CLI 版本:3.x 直接使用;2.x 则检测 --v3 开关,支持时要求后续命令全部显式带上 --v3,不支持则 exit 1 并打印“2.x 不执行 permissions,本仓库 Agent 的 deny 禁区将形同虚设”(判定分支见 3.3)。所以 3.5 第 4 步的启动命令带 --v3,这是必须添加的开关。
  • CLI 载体的断言条件由“不含”改为“必须含”。 2.x 时代生成的 JSON 刻意不含 permissions——该版本不执行这些规则,保留它只会让评审者误以为已有权限保护;要求必须使用 v3 之后,则变为必须含,且 assert permissions.rules 非空、deny 规则里必须出现 switchover,2.x 时代的旧载体会在这一步直接校验失败(3.3)。

最后再次提醒仍在使用 2.x 的读者:permissions 块仍然留在 md 里,因为 md 是唯一配置源,两个载体都由它生成,评审时用于声明权限意图。但如果只在 2.x 上运行,就只能将其视为文档说明,而非实际保护。

八、参考资料

*前述特定亚马逊云科技生成式人工智能相关的服务目前在亚马逊云科技海外区域可用。亚马逊云科技中国区域相关云服务由西云数据和光环新网运营,具体信息以中国区域官网为准。

本篇作者

尹帮友

好未来 SRE 工程师,主要负责国内外智能硬件业务,深度结合 AI 落地智能运维体系。专注于构建高可靠、高弹性的软硬件一体化运维架构,在保障海外国内业务稳定性的同时优化运维成本,推动 AIOps 能力落地与团队技术沉淀创新,助力业务在复杂环境下高质量发展。

陈浩

好未来DBA,负责好未来自建数据库和多云厂商云产品的运维工作,热衷于构造提效和解决问题的工具及系统,愿望是达成人、系统和数据库的生态和谐。

冯源

亚马逊解决方案架构师,曾服务于南洋理工大学、惠普和新智云。工作涉及企业混合云环境运维管理、运营管理、混合云平台和云服务解决方案。有十余年企业基础设施咨询及实施、项目管理和交付以及混合云平台研发管理经验。2022年加入AWS,负责零售、餐饮和制造等行业客户。现阶段在认真学习和熟悉AWS服务和产品,关注混合云及混合云环境下的应用部署和管理。


AWS 架构师中心:云端创新的引领者

探索 AWS 架构师中心,获取经实战验证的最佳实践与架构指南,助您高效构建安全、可靠的云上应用