文件
hyb-oa/.trellis/tasks/08-23-isolated-backup-restore-drill/implement.md
T

148 行
15 KiB
Markdown

# 隔离备份恢复演练实施计划
## 执行前门禁
- [x] 确认任务已在用户审阅最终规划摘要后由 `task.py start` 进入 `in_progress`。
- [x] 只读确认本地 `main`、当前生产构建事实、来源/当前 migration 集合和现有恢复脚本;现场两构建均为 65 且目录无变化,但后续执行不硬编码数量。
- [x] 只读确认生产公网 `/ready` 正常;不读取生产数据库或执行生产写入。
- [ ] 连接 ARM 后只读核对 receiver 配置有效、最新正式日批次身份、dead-man 状态、Docker 可用性和磁盘余量;不列出完整目录或原始 manifest。
- [ ] 磁盘安全预算至少满足“批次总字节数 5 倍 + 2 GiB”;不足则 `PAUSE`。
- [ ] 若当前账号没有对正式归档只读和本轮临时区/Docker 的最小权限,停止并给用户一条窄权限操作步骤;不得申请通用 root shell 或扩大既有健康监控 sudo 入口。
## 有序执行清单
1. 生成不含主机信息的本轮 run id,记录 UTC 开始时间和生产前置 `/ready` 状态。
2. 在 ARM 正式日归档中选择 manifest 时间最新的 complete 批次,验证三文件、字节数、SHA-256 与 24 小时 RPO 门禁;在本地仓库确认来源提交是当前构建祖先或相同,并单独记录是否严格相等。
3. 创建权限 `0700` 的本轮临时工作区;启动 RTO 计时,复制整个批次并对临时副本重新执行同一校验。
4. 创建无宿主端口、无业务网络、无持久 volume 的一次性 PostgreSQL 16 容器;数据库名含 `drill`,凭据随机且仅存在本轮进程环境。
5. 使用 `scripts/restore-db.sh` 的 shadow 合同恢复数据库;任何警告、确认不匹配或 restore 非零立即停止后续验证并进入清理。
6. 从来源提交动态冻结 migration 名称集合;通过错误即停的只读 SQL 验证恢复库成功且未回滚的 migration 集合精确匹配来源、无失败/未知项,并静态记录来源与当前构建的 migration 差异;随后验证固定 12 类关键表并记录行数。
7. 使用 `scripts/restore-storage.sh` 恢复附件到本轮空目录;比较归档与解包普通文件数。
8. 在影子库中聚合本地附件引用,以不输出路径的校验程序验证路径安全、文件存在及适用的大小/SHA-256;记录引用总数和不一致数。
9. 所有数据检查通过时停止 RTO 计时;按 manifest 时间计算 RPO,记录当前发布到首个同源恢复点的可用滞后,形成区分“原始数据可恢复性”和“当前 schema 升级状态”的 `GO/PAUSE/NO_GO` 脱敏结论。
10. 清理本轮容器、临时网络(若有)、临时附件目录、临时批次副本和临时凭据;确认无本轮残留。
11. 再次只读检查生产公网 `/ready` 和 ARM dead-man/健康 timer 状态,确认演练没有影响在线生产与现有监控。
12. 将证据写入当前 task,并在既有事实源补充当前状态;不提交任何真实备份、日志或业务内容。
## Attempt 1 执行状态
- 最新 ARM 候选在 `select_latest_batch` 阶段因 manifest `build_mismatch` 失败关闭;复制、容器创建、数据库恢复和附件恢复均未开始。
- 失败后的只读复核确认:公网 `/ready` 与生产自动备份 unit 的构建短标识一致,两个生产侧事实均为 `7b86ffb3`;因此当前不是计划事实源过期或备份 unit 冻结构建漂移。
- ARM 一次性 root 入口和本轮 runtime 均为零残留,两个监控 timer 仍为 active;本轮 5 个普通用户暂存文件已按 SHA-256 精确匹配并删除,未宽泛删除目录。
- Attempt 1 保持 `NO_GO`。在当时取得最新 ARM manifest 的脱敏构建短标识、run id 配对关系和 manifest 摘要一致性前,不进入第 3 步及之后的恢复流程。
## Attempt 1 后只读调查清单
1. 生成固定无参数的 root-owned ARM 只读身份探针和最小 sudoers,只允许读取 receiver 路径配置及最新正式日归档。
2. 本地执行 `bash -n`、候选 SHA-256、sudoers candidate `visudo`(环境可用时)和禁止写入/恢复关键字人工复核。
3. 上传到 ARM 普通用户私有暂存目录;由用户输入 sudo 密码安装固定入口,安装前再次核对全部候选 SHA-256 和系统 sudoers 语法。
4. 运行一次固定入口,记录构建短标识、批次年龄、manifest 配对、产物聚合完整性和入口自清理状态。
5. 对照公网 `/ready` 与生产自动备份 unit 的当前短标识:相等时记录同源恢复点已形成;不相等时在本地仓库核对是否为当前构建祖先,祖先且完整/新鲜仍可形成原始恢复重试方案,未知或非祖先才 `NO_GO`。
6. 按 SHA-256 精确删除普通用户暂存文件;不得删除 ARM 正式归档或宽泛清理目录。
### 只读调查执行结果
- [x] 探针候选通过本地/ARM SHA-256、Bash 语法和 ARM sudoers candidate 校验。
- [x] 固定入口只读读取最新正式批次并自删除;未复制、解包、启动容器或恢复数据。
- [x] 最新批次 `ec682565` 在 24 小时内,run id/目录配对及 DB/附件大小、SHA-256 均通过,但与当前 `7b86ffb3` 不匹配。
- [x] 只读对时确认自动备份合同刷新晚于最新批次生成;finding 收敛为“发布后尚未产生新构建恢复点”。
- [x] root 入口残留为 0,3 个普通用户候选及暂存目录已按精确身份清理,两个 ARM 监控 timer 仍 active。
- [x] 08-24 最新自然批次只读探针通过:来源与当前构建相同、年龄不超过 24 小时、run id 配对及 DB/附件 SHA-256 均通过;临时入口和暂存零残留。当前可提交恢复重试审批,尚未获得恢复授权。
### Attempt 2 恢复重试结果
- [x] 用户批准后安装并执行动态 migration 集合版固定恢复入口。
- [x] ARM 正式归档最新批次首次门禁通过,并开始复制到本轮私有临时区。
- [x] 临时副本复检因 runner 错把正式源目录 basename 身份规则应用到固定名 `batch` 而触发 `batch_identity`;失败阶段为 `create_isolated_runtime`,数据库容器和恢复均未开始。
- [x] runtime、root 入口、6 个普通用户候选和暂存目录均精确清理为零残留,两个 ARM 监控 timer 保持 active。
- [x] 修订 runner:正式源目录继续强制 basename/run id 配对;临时副本 manifest run id 重绑到源侧已验证 run id,并继续校验完整内容合同及源/副本元数据精确一致。补齐 2 正例 + 3 负例后重新执行只读身份探针,实际恢复另行批准。
### Attempt 3 准备批准边界
- [x] 用户已批准修复 runner,并重新执行 08-25 最新正式批次只读身份探针。
- [x] 回归已覆盖:正式源通过、同内容改名副本通过、内容篡改副本失败、源 basename/run id 错配失败、副本 run id 与源侧已验证 run id 不同失败。
- [x] Attempt 3 未复用 Attempt 2 新鲜度:08-25 最新批次年龄约 6 小时 11 分,来源与当前构建相同,run id 配对、大小与 SHA-256 均通过。
- [x] 用户复核探针后明确批准 Attempt 3 实际恢复,范围仅限 ARM 隔离影子恢复。
### Attempt 3 恢复结果与停止点
- [x] 修复后的候选在本地和 ARM 通过 SHA-256、Bash 与 sudoers 语法校验后安装;正式归档门禁和临时副本源侧身份重绑通过。
- [x] runner 在 `restore_database` 阶段因 `restore-db.sh` 返回非零而以 `database_restore_failed` 失败关闭;migration、12 类关键表、附件恢复与关联验证均未执行。
- [x] 一次性容器、本轮 runtime、临时 root 入口和普通用户候选均精确清理为零残留;ARM 两个监控 timer 保持 active,生产演练后 `/ready` 仍为相同构建的 `PASS`。
- [x] 在任何 Attempt 4 前修复失败诊断合同:分别记录 dump 列表校验与实际 restore 的脱敏阶段码,并确保失败清理前只保留安全聚合诊断;本地回归已通过,远端探针和实际恢复仍需重新批准。
### Attempt 4 前的修复清单
1. runner 在调用 `restore-db.sh` 前,以同一 PostgreSQL 16 容器对临时副本执行独立 `pg_restore --list`,记录 `dump_list=PASS|FAIL|NOT_RUN`。
2. 独立确认影子目标数据库名与连接状态,记录 `target_confirmation=PASS|FAIL|NOT_RUN`;不输出数据库名或连接信息。
3. 仅在前两项通过后执行实际 restore,记录 `restore_apply=PASS|FAIL|NOT_RUN`;失败类别区分为 `dump_list_failed`、`restore_target_confirmation_failed` 或 `restore_apply_failed_after_list_pass`。
4. 原始 stderr 只进入本轮 `0600` 私有日志并随 runtime 删除;最终输出只含 allowlist 阶段状态和既有脱敏聚合。
5. 补三条阶段负例、敏感输出负例和每条失败路径零残留回归;通过后重新执行最新批次身份探针,再请求 Attempt 4 实际恢复批准。
### Attempt 3 后本地修复结果
- [x] `dump_list`、`target_confirmation`、`restore_apply` 固定输出 `PASS/FAIL/NOT_RUN`,并增加执行路径、脚本/pg_restore 退出码、对象错误计数与稳定类别。
- [x] 诊断在清理前输出;注入 apply 退出码 `23` 后,调用方在 runtime/access 清理完成后仍可读取阶段状态和退出码。
- [x] partial-success 规则保持失败关闭:任何 pg_restore 非零均为 `FAIL/NO_GO`,不设置可忽略对象错误白名单。
- [x] PostgreSQL 16 custom dump 直接驱动真实 `scripts/restore-db.sh` 的 Docker stdin 与 host-file 两条路径,均恢复成功。
- [x] dump-list 失败、目标确认失败、apply 失败及敏感输出负例通过;既有成对备份/恢复回归和自动异机备份合同通过。
- [x] 用户另行批准并完成 Attempt 4 前最新批次只读探针:构建 `SAME`、年龄约 7 小时 24 分、run id/产物完整性与 migration 集合门禁通过,入口及暂存零残留;尚未执行实际恢复。
### Attempt 4 前的持久化门禁
1. 将已验证的 runner 和附件校验器从个人 workspace 收敛为 `scripts/qa/` 下的跟踪文件,不改变恢复边界或生产脚本。
2. 将八类 runner 回归收敛为可独立执行的 `run-offhost-shadow-restore-runner-contract.sh`,直接驱动真实 `restore-db.sh` 双路径。
3. 合同脚本必须机械验证三阶段诊断、执行路径、两个退出码、非零失败关闭、源/副本身份、dynamic migration、敏感输出和清理顺序;对候选生成稳定 SHA-256。
4. 运行新合同、既有成对备份/恢复、自动异机备份 74 项、Trellis 治理与敏感扫描;全部通过后才允许把跟踪文件打包为 Attempt 4 候选。
5. 本阶段仅本地修改与测试;新批次探针结果和持久化门禁均通过后,再单独申请 Attempt 4 实际恢复。
### Attempt 4 前持久化门禁结果
- [x] runner 已收敛为 `scripts/qa/offhost-shadow-restore-runner.template.sh`;现场路径、账号、容器/数据库命名、timer、构建和 migration 指纹均为经严格校验的私有渲染字段。
- [x] 附件关联校验器已收敛为 `scripts/qa/validate-offhost-shadow-restore-attachments.py`。
- [x] `scripts/qa/run-offhost-shadow-restore-runner-contract.sh` 可独立复跑并提供 `render` 模式;相同私有配置生成确定性候选 SHA-256,shell 注入在输出候选前失败关闭。
- [x] 源/副本身份、内容篡改、dynamic migration、附件安全、三阶段诊断、真实 restore 双路径、NOT_RUN、退出码、清理顺序和敏感输出合同全部通过。
- [x] 新增三个文件由 `check-sensitive-content.mjs` 定向扫描,`3/3` 文件、finding `0`;本地测试容器残留 `0`。
- [x] `.gitignore` 增加 `/.restore-drill-private/` 专用目录;合同分别证明仓库内未忽略的私有 JSON、未忽略的现场候选均在创建输出前失败,ignored 输入/输出正例通过。
- [x] 私有原始日志保留唯一初始化截断,五个固定阶段均以标记分隔并追加写;静态合同拒绝后续阶段再次截断。
- [x] 完整复跑 runner 合同、既有隔离备份/恢复演练和自动异机备份 74 项;task、Trellis、process、默认/定向敏感扫描及空白门禁通过,测试容器和本地私有路径残留均为 `0`。
- [ ] Attempt 4 候选尚未渲染或上传;实际恢复前须再次选择最新批次并取得明确批准。
## 验证命令类别
- 本地/仓库:`bash -n` 覆盖备份、DB 恢复、附件恢复和 receiver 脚本;只读统计 migration 目录。
- ARM 前置:receiver `validate-config`、systemd 公共状态、Docker 版本/容量、候选 manifest 脱敏校验。
- 恢复输入:`sha256sum`、manifest schema/文件集/字节数校验、`pg_restore --list`、`tar -tzf`/`tar -tvzf`。
- DB 验证:`psql -v ON_ERROR_STOP=1` 的 migration 名称/状态集合和固定表计数查询;本地用 Git 比较来源/当前 migration 目录,不执行 migrate。
- 附件验证:`find` 普通文件计数及不输出路径的 Python 只读校验;只输出引用数和 mismatch 数。
- 任务门禁:
- `python3 ./.trellis/scripts/task.py validate .trellis/tasks/08-23-isolated-backup-restore-drill`
- `node scripts/qa/check-trellis-governance.mjs`
- `node scripts/qa/check-process-contract.mjs --baseline --json`
- `node scripts/qa/audit-docs.mjs --check-registry`
- `node scripts/qa/check-sensitive-content.mjs`
- `git diff --check`
具体远端命令在执行阶段依据 ARM 已安装的 root-owned receiver 配置生成,不把真实地址、账号、目录或凭据写入本计划。
## 停止条件
- 最新批次不存在、超过 24 小时、manifest 或 SHA-256 不一致,或来源提交未知/不是当前构建祖先或相同。仅“不严格相等”不再单独停止。
- 权限、Docker、PostgreSQL 16 或磁盘预算不满足。
- 命令目标可能指向腾讯云生产、ARM 正式归档、receiver 管理目录或既有容器/网络。
- DB restore、migration、固定表、附件安全、文件引用、哈希/大小任一检查失败。
- 生产 `/ready` 在演练期间异常,或现有 ARM dead-man/健康监控状态改变。
- 清理对象身份不明确。此时保留现场并报告,不使用宽泛删除命令。
## 变更与证据文件边界
- 允许写入:当前 Trellis task、既有 OPS7 Runbook/人工试运行报告中的脱敏状态摘要、本轮 ARM 私有临时区和一次性 Docker 对象。
- 禁止写入:产品代码、migration、生产/ARM 正式配置、生产 DB/附件、ARM 正式归档、企业微信。
- 计划新增的 task 内证据:`research/restore-drill-evidence.md`;只在实施实际发生后创建。
## 审查与收口
- 本任务涉及真实备份数据和远端运维边界。执行完成后做一次只读复核,重点确认:生产零变更、归档未修改、RTO/RPO 口径正确、附件验证没有漏项、敏感内容未进入证据、临时对象已清理。
- 若现有脚本需要修改,回到规划阶段补充设计和新的批准;不得在远端临时放宽 shadow/路径/确认门禁。
- 门禁通过后用中文 commit 引用 `08-23-isolated-backup-restore-drill`,是否推送仍按用户当时授权执行。