7.6 KiB
7.6 KiB
部署前聚合体检与备份合同自动刷新:设计
1. 边界与组件
本任务只改已有部署入口,不改变首次初始化状态机:
- 本地只读聚合体检入口读取受控 unit 六键,并分别通过普通部署 SSH 与专用 forced-command SSH 验证远端事实。
deploy-existing.sh在体检全绿后继续调用既有deploy.sh;deploy.sh的 fail-fast remote preflight 保留。- 应用、公网检查全部通过后,
deploy-existing.sh调用 root forced-command 的refresh。 - root-owned 自包含程序只维护四个固定 service 的 build drop-in,并验证四个既有 timer。
不新增后台服务、数据库对象、业务 API 或业务页面。
2. 聚合体检执行模型
2.1 入口与输入
- 普通部署目标继续使用
DEPLOY_SERVER。 - 独立维护目标使用新的显式
DEPLOY_REFRESH_SERVER;格式使用现有安全 SSH target 字符集,不从主机名、用户名或别名规则猜测。 - 六键仍以 automatic-backup daily unit 的 allowlisted
Environment=为事实源,不回显值。 - 共享只读解析函数可由体检与
deploy-existing.sh复用,避免两份六键 allowlist 漂移;不得把解析结果写入仓库或临时报告。
2.2 不短路聚合器
主脚本保留 set -uo pipefail,明确不启用 set -e。每项使用以下等价模型:
run_check() {
local check_name="$1"
shift
if ("$@") >/dev/null 2>&1; then
record_result "$check_name" PASS
else
record_result "$check_name" FAIL
fi
}
检查函数在子 shell 中运行,不能修改主流程状态。聚合器记录所有结果后统一输出固定顺序的 检查项 PASS/FAIL 和计数,最后按总失败数返回 0/非 0。依赖项无法取得时,依赖检查返回 FAIL;无依赖检查仍继续执行。
2.3 检查分组
- 本地/合同:普通 SSH transport、六键齐全、root SSH transport、forced-command probe。
- 普通部署身份:仓库根标识、目录可写、env 普通文件且可读、PGDATA/附件存在。
- 运行时:唯一 server/db、活动 build、附件 mount、PGDATA mount。
- 备份合同:daily unit build 等于活动 build。
- root 收尾能力:forced-command 程序可执行并返回固定 probe 标识。
所有检查仅把 stdout/stderr 捕获到内存并丢弃;用户可见输出由聚合器根据固定结果生成,不转发底层错误文本。
3. forced-command 安全模型
3.1 密钥边界
- 使用独立公钥。安装器解析 key blob/fingerprint,若 root
authorized_keys已存在同 key blob 的非 managed、非 forced-command 行则拒绝安装。 - managed 行使用固定标记,可原子替换自身,保留其他 root 管理 key;不得覆盖整个
authorized_keys。 - 授权选项固定为
command="<root-owned-target>",no-agent-forwarding,no-port-forwarding,no-X11-forwarding,no-pty,no-user-rc。 - 专用本地 SSH alias 只使用该 key。普通 root 应急管理 key 不属于部署自动化合同。
3.2 自包含程序
- 安装器把审核后的源文件复制到固定
/usr/local/sbin目标,验证 root:root、0755、regular file、非 symlink。 - 程序启动后设置固定
PATH、umask 077,清理调用方可控的路径/loader 类环境变量,仅读取 root-owned systemd unit、Docker inspect 结果和SSH_ORIGINAL_COMMAND。 - 精确命令矩阵:
probe只验证运行前提;refresh执行 build drop-in 更新;空值、空白变体、额外参数和其他命令全部返回固定 unsupported 分类。 - 程序不得 source 或 exec 仓库内容,不接受 argv、stdin 配置、路径、unit、SHA 或 shell 片段。
4. build 刷新合同
4.1 事实读取
- 从 root-owned automatic-backup daily unit 读取固定 allowlist:Compose project 与当前工作目录/env 合同只用于定位唯一活动 server。
- 通过 Docker labels/inspect 定位唯一 running server,读取容器
BUILD_SHA。 - build 必须为完整小写十六进制提交标识;前端标识固定取前 12 位,不接受调用方输入。
4.2 最小写入
- 固定目标仅为 automatic-backup daily/watchdog/drill-reminder 与 WARCH3 四个 service。
- 对每个 service 原子安装 root-owned
20-active-build.confdrop-in,只包含BUILD_SHA和VITE_BUILD_SHA覆盖;不改主 unit、WorkingDirectory、env 路径、ExecStart 或 timer 配置。 - 所有 drop-in 写入完成后执行一次
systemctl daemon-reload。 - 读取 systemd 最终合并后的 Environment,逐个断言两个 SHA;再逐个断言四个 timer
is-enabled=enabled且is-active=active。 - 使用固定 root-owned lock 防止两个 refresh 并发。中途失败不得报告成功;已写 drop-in 不自动回滚到旧 SHA,因为旧 SHA 已与活动应用不一致,人工修复应继续向活动 build 收敛。
5. 调用顺序与失败语义
聚合体检全绿
-> deploy.sh(同步、构建、迁移、/ready、smoke、公网检查)
-> forced-command refresh
-> unit SHA + timer 验证
-> 部署完整完成
- 体检失败:零远端写入,部署退出非零。
deploy.sh失败:保持既有发布错误语义,不调用 refresh。- refresh transport 失败:固定分类
root_identity_unavailable。 - remote command 126/127 或 probe 标识缺失:固定分类
refresh_program_unavailable。 - helper 返回/复核 SHA 不一致:
unit_build_not_refreshed。 - SHA 已一致但任一 timer 非 enabled/active:
timer_not_active。 - 后刷新失败时应用已部署且 smoke 通过,包装脚本输出固定
DEPLOYED_BUT_CONTRACT_REFRESH_FAILED并退出非零;不伪装回滚,也不打印底层路径/响应。
6. 安装与发布边界
- 代码合入不自动修改生产 root 文件或
authorized_keys。 - 首次安装分两步:root 运行安装器;普通身份与 forced-command 身份分别执行 probe。两步均需生产写入批准和脱敏证据。
- 新版
deploy-existing.sh对已有部署直接要求DEPLOY_REFRESH_SERVER,缺失即在写前体检失败,不保留可长期绕过的 optional 模式。生产切换顺序固定为:先用旧运行版本不受影响的方式安装 forced-command 并完成 probe,再执行新版 wrapper;远端入口尚未安装时不得开始同步。 - 回滚代码时可恢复旧
deploy-existing.sh;root drop-in 仍指向当前活动 build,不影响服务启动。删除 forced-command 或 drop-in 属于单独 root 运维动作。
7. 测试设计
- SSH fixture 必须区分 transport 255、远端 126/127、helper 固定业务退出码和成功标识。
- 聚合测试故意让第一个、第三个检查失败,断言最后一个检查仍执行且输出顺序固定。
- 脱敏测试向底层 stderr 注入伪造路径、主机、secret、容器 ID,断言用户输出不包含它们。
- helper fixture 使用隔离的 fake systemctl/docker/install,不触碰真实
/etc;生产程序固定 PATH,测试通过源码级显式 test harness 注入依赖,安装目标不携带测试覆盖开关。 - 静态合同断言 installed helper 不引用
REMOTE_DIR下脚本,不使用eval/bash -c/动态 unit 名,不读取调用方 SHA。
8. 取舍与延期
- 选择 systemd drop-in 而非重新调用 installer:只写每次部署必变的 build 字段,避免执行部署用户可写源码,也避免复制两套完整 unit 生成逻辑。
- 选择 forced-command 专用 key 而非 sudoers:授权面只包含 probe/refresh;代价是需要一次独立 key 和 root authorized_keys 安装。
- 不加每周 timer:当前漂移已完成一次性全量对账,先用真实部署观察是否仍有持续漂移来源。