Files
cma-management/docs/db-migration-runbook.md
T
Hermes CI Fix 443a779ece
ci/woodpecker/push/main Pipeline failed
fix(cma): DDL 与启动剥离 —— 关闭启动 create_all + 部署链特权迁移步 (cma-create-all-minpriv-rootfix-20260916)
根治 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 留痕)
2026-09-16 06:53:28 +08:00

80 lines
4.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`)
```bash
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)
1. 在 `backend/app/models/` 加模型并确保被 `app/models/__init__.py` 导入。
2. 本地 `--check` 应能列出该表缺失(证明 ORM 已注册)。
3. 提交推送 → 部署链 `[4b]` 用特权账号建表 → 重启 → 冒烟 `schema_check` 通过。
4. 若模型**改列**(新增/改名/改类型):`create_all` 不处理 → 必须写独立迁移脚本/`ALTER`(参考
`backend/scripts/migrate_ext_input_ledgers_20260915.py`、`treasury_migrate.py` 的 `up()/down()` 模式)。
5. **不要**让运行库账号重新获得 CREATE 来「绕过」:那正是事故根因。
## 5. `cma_api` 的 CREATE 授权回收(项目Bot 执行)
2026-09-16 事故应急时授予了 `GRANT CREATE ON cma.* TO 'cma_api'@'localhost'/'127.0.0.1'`(保留至今)。
本单上线后(`[4b]` 已生效、应用启动不再建表)可安全回收:
```sql
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
bash backend/scripts/tests/db-migrate-sim-e2e.sh # 需 root;scratch 库 cma_migrate_sim,零生产触碰
```
覆盖 17 项断言:负控制复现(`CREATE command denied` → 启动失败)、迁移幂等(76→0)、修复后启动零 DDL、
「新增模型未迁移」不中断 + 迁移补齐、回滚(含 dump 与有数据拒绝)。