ci/woodpecker/push/main Pipeline failed
根治 2026-09-16 发布中断(新增模型触发启动 create_all,cma_api 无 CREATE → 崩溃循环): - backend/app/database.py: 新增 CMA_AUTO_CREATE 开关(**默认关**),生产启动不再执行 DDL; 运行库账号因此无需 CREATE(最小权限可保持) - backend/scripts/db_migrate.py: 特权迁移步(--check/--apply/--rollback/--status); 幂等(checkfirst)+ journal 留痕 + 可回滚(dump 失败/表有数据即拒绝,fail-closed); 显式拒绝用应用运行账号 cma_api 执行 - deploy.sh: 新增 [4b] 结构迁移(**重启前**执行;失败即中止部署,后端旧码继续服务) - backend/scripts/tests/db-migrate-sim-e2e.sh: 17 项 scratch 复现/验收(负控制复现 CREATE command denied) - docs/db-migration-runbook.md: 运行手册(含 cma_api CREATE 回收步骤) 验收:scratch E2E 17/17;pytest 1113 passed/1 xfailed;生产 --check 缺失表 0 / 列级差异 0; --apply 幂等空跑 created_new=0(journal 留痕)
4.6 KiB
4.6 KiB
CMA 数据库结构变更运行手册(DDL 与应用启动剥离)
- 任务:
cma-create-all-minpriv-rootfix-20260916(P2) - 事故档案:
wecom-project/_verification/2026-09-16-cma-release-incident-verification.md - 结论一句话:DDL 只在部署链的 [4b] 特权迁移步发生,应用启动不再建表;运行库账号
cma_api无需 CREATE。
1. 三条不变式
| # | 不变式 | 落地位置 |
|---|---|---|
| 1 | 应用启动不执行 DDL(create_all 默认关,CMA_AUTO_CREATE=1 仅限开发/测试) |
backend/app/database.py::auto_create_enabled/init_db |
| 2 | 结构变更由有 DDL 权限的账号在应用重启前执行(独立、幂等、可回滚、留痕) | backend/scripts/db_migrate.py + deploy.sh [4b] |
| 3 | 运行库账号保持最小权限(DML 表级白名单,无 CREATE/ALTER/DROP) | DB 授权侧(由 ops/项目Bot 回收,见 §5) |
2. 部署链顺序(deploy.sh)
[0] 提交纪律检查(忽略未跟踪) ← 勿回退
[1] git pull origin main
[1b] 鉴权面审计闸门(B 类缺口即中止) ← 勿回退
[2] 前端构建 pnpm build
[3] 前端重铺 /var/www/cma
[4] 后端依赖
[4b] 数据库结构迁移(特权;重启前;幂等;失败即中止部署) ← 本单新增
[5] 重启 cma-backend + reload nginx
冒烟:就绪轮询 → BOT桥接 → schema_check → 登录/KPI
[4b] 失败时不重启后端 → 旧版本继续服务(不再出现「新模型建不出 + 服务被重启」的崩溃循环)。
3. 迁移步用法(backend/scripts/db_migrate.py)
cd /root/cma-management/backend
./venv/bin/python3 scripts/db_migrate.py --check # 只读预检(默认动作)
./venv/bin/python3 scripts/db_migrate.py --apply # 幂等建表(缺什么建什么)+ journal
./venv/bin/python3 scripts/db_migrate.py --status # 历史 journal
./venv/bin/python3 scripts/db_migrate.py --rollback latest # 回滚最近一次新建的表(先 dump 后删)
- 连接:优先
CMA_MIGRATE_DB_USER/CMA_MIGRATE_DB_PASS(专用迁移账号,TCP);未设则走本机unix socket + root(socket 认证,无口令落盘)。显式拒绝用应用运行账号(cma_api)执行本步。 - 幂等:
create_all(checkfirst=True)—— 已存在的表零动作;二跑created_new=0。 - journal:
backend/scripts/backups/db-migrate-journal/<ts>-<status>.json(含 db/git head/执行者/新建表清单)。 - 回滚:只删该次 journal 记录为「新建」的表;表内有数据则拒绝(exit 3),
--force才放行;回滚前默认mysqldump备份(dump 失败即拒绝回滚,fail-closed)。 - 列级差异:
create_all不补列。--check/--apply会报告「ORM 有列、库里没有」并提示需要独立ALTER迁移;部署链以此告警(不阻断)。
4. 新增模型的正确姿势(发布 checklist)
- 在
backend/app/models/加模型并确保被app/models/__init__.py导入。 - 本地
--check应能列出该表缺失(证明 ORM 已注册)。 - 提交推送 → 部署链
[4b]用特权账号建表 → 重启 → 冒烟schema_check通过。 - 若模型改列(新增/改名/改类型):
create_all不处理 → 必须写独立迁移脚本/ALTER(参考backend/scripts/migrate_ext_input_ledgers_20260915.py、treasury_migrate.py的up()/down()模式)。 - 不要让运行库账号重新获得 CREATE 来「绕过」:那正是事故根因。
5. cma_api 的 CREATE 授权回收(项目Bot 执行)
2026-09-16 事故应急时授予了 GRANT CREATE ON cma.* TO 'cma_api'@'localhost'/'127.0.0.1'(保留至今)。
本单上线后([4b] 已生效、应用启动不再建表)可安全回收:
REVOKE CREATE ON cma.* FROM 'cma_api'@'localhost';
REVOKE CREATE ON cma.* FROM 'cma_api'@'127.0.0.1';
SHOW GRANTS FOR 'cma_api'@'127.0.0.1'; -- 期望:仅 SELECT + 表级 DML,无 CREATE
回收后再发布一次(含任意新模型的版本)验证不中断即可闭环(证据:[4b] journal created_new>=0 +
冒烟通过 + 无 CREATE command denied)。
6. 自验(可重复)
bash backend/scripts/tests/db-migrate-sim-e2e.sh # 需 root;scratch 库 cma_migrate_sim,零生产触碰
覆盖 17 项断言:负控制复现(CREATE command denied → 启动失败)、迁移幂等(76→0)、修复后启动零 DDL、
「新增模型未迁移」不中断 + 迁移补齐、回滚(含 dump 与有数据拒绝)。