
最近折腾了一个叫 SwiftDeploy 的部署工具,踩了不少坑,这篇把核心设计说清楚。这个工具解决的是一个老问题:部署流程里的配置、策略、监控三个东西本来应该紧耦合,结果在大多数项目里都是散的——Compose 文件一套、Env 文件一套、CI 里再一套,改一个阈值要改好几个地方。SwiftDeploy 用一个 manifest.yaml 把这三件事闭环了,底层靠 OPA(Open Policy Agent)做策略判断、Prometheus 格式暴露指标、CLI 本身不参与任何决策逻辑。
所有环境相关的变量集中在一个文件里:
services: image: swiftdeploy-hng14-api:latest port: 3000 mode: stable # swiftdeploy promote 会原地改这个字段 nginx: image: nginx:1.27-alpine port: 8080 proxy_timeout: 60s network: name: swiftdeploy-net driver_type: bridge metadata: version: "1.0.0" service_name: swiftdeploy-api contact: "you@example.com" deployed_by: "swiftdeploy" compose_project: swiftdeploy policy: thresholds: # 这些值会传给 OPA 的 input.thresholds,绝不硬编码在 .rego 里 min_disk_free_gb: 10 min_mem_available_gb: 1 max_cpu_load: 2.0 max_error_rate_percent: 1 max_p99_latency_ms: 500 metrics_window_seconds: 30 opa: image: openpolicyagent/opa:0.69.0 host_port: 9182
CLI 读取这个文件,用 Jinja2 渲染出 nginx.conf 和 docker-compose.yml,从不要求你手动去改任何生成出来的文件。
| 属性 | 实现方式 |
|---|---|
| OPA 无法通过 Nginx 访问 | OPA 只绑定 127.0.0.1:9182;Nginx 只代理到 api 服务 |
| API 端口不对外暴露 | 只设置 expose:,不在 api 服务上做 ports: 映射 |
| CLI 里没有决策逻辑 | CLI 发送上下文、读取 allowed + checks[],所有逻辑都在 Rego 里 |
| 阈值不写在 Rego 里 | .rego 文件只引用 input.thresholds.*,值全部来自 manifest.yaml |
swiftdeploy init 解析 manifest.yaml 后喂给两个 Jinja2 模板:
METRICS_WINDOW_SECONDS 这个环境变量从 policy.thresholds.metrics_window_seconds 写入——也就是 OPA 用来做 SLO 窗口的同一个值,这样 API 的滑动窗口指标和 Rego 规则永远保持同步。
swiftdeploy validate 在任何容器启动前跑五个预检:
FastAPI 应用以 Prometheus 文本格式暴露 GET /metrics,中间件分两层:
request in │ ▼
[chaos middleware] <- 在 canary 模式下注入慢响应/错误(POST /chaos 时跳过) │ ▼
[prometheus middleware] <- 计时包含 chaos 延迟的完整栈 │ ▼
route handler
标准指标:
http_requests_total{method, path, status_code}
http_request_duration_seconds{method, path} <- histogram,标准桶
app_uptime_seconds
app_mode <- 0=stable, 1=canary
chaos_active <- 0=none, 1=slow, 2=error
滑动窗口 gauges(OPA 查询 canary SLO 时用的):
swiftdeploy_window_requests_total <- 最近 N 秒内的请求总数
swiftdeploy_window_errors_total <- 窗口内 5xx 数量
swiftdeploy_window_p99_latency_seconds <- 同窗口内进程内 P99
窗口用 collections.deque 实现。每个请求追加 (timestamp, duration, is_error) 三元组,剔除过期条目,重新计算三个 gauges——P99 通过排序索引算出。不需要外部 TSDB,被抓取时 gauge 值始终是最新的。
核心约束:CLI 本身不能做任何允许/拒绝的决策。用 Python 的 if 语句写逻辑,阈值和业务逻辑跟运维工具绑在一起了。用 OPA 的话:
每个策略领域只管一个问题和一种数据形状:
| 领域 | 问题 | 输入结构 |
|---|---|---|
| swiftdeploy.infrastructure | 宿主机健康度是否足以部署? | {phase, host: {disk_free_gb, cpu_load_1m, mem_available_gb}, thresholds} |
| swiftdeploy.canary | canary 是否足够安全可以升级? | {phase, promotion_target, metrics: {error_rate_percent, p99_latency_ms, window_seconds}, thresholds} |
改基础设施规则绝不会碰到 canary/policy.rego,反之亦然。
每个 decision 文档都带着每条规则的 checks:
decision := { "allowed": count(reasons) == 0, "domain": "infrastructure", "phase": input.phase, "reasons": sort([r | reasons[r]]), "checks": [ {"rule_id": "infra_disk_free_minimum", "passed": disk_ok, "detail": disk_detail}, {"rule_id": "infra_cpu_load_maximum", "passed": cpu_ok, "detail": cpu_detail}, {"rule_id": "infra_memory_available_minimum", "passed": mem_ok, "detail": mem_detail}, ], ...
}
CLI 直接遍历 checks[] 来展示实时状态,自己不推断通过/失败。
每种失败场景都有唯一的 failure_kind 和人类可读的消息:
| 情况 | failure_kind | 给运维的消息 |
|---|---|---|
| OPA 容器未启动 | opa_connection_refused | "Start with: docker compose up -d opa" |
| OPA 响应慢 | opa_timeout | "OPA request timed out (read)" |
| OPA 返回非 JSON | opa_bad_json | 包含原始片段 |
| OPA 返回没有 result 键 | opa_no_result | 包含原始片段 |
| psutil 未安装 | host_stats_unavailable | 安装指引 |
这些路径都不会导致 CLI 崩溃或挂起。
init (渲染 nginx.conf + docker-compose.yml) │ v
docker compose up -d opa │ v
wait_opa_ready (轮询 /health,最多 75 秒) │ v
collect_host_stats --> POST /v1/data/swiftdeploy/infrastructure/decision │ +---------+-----------+ │ │ allowed: false allowed: true │ │ 打印 FAIL checks docker compose up --build -d exit(1) │ (栈不启动) v 轮询 GET /healthz(通过 nginx)
实际运行时如果 CPU 突然飙升,输出长这样:
Policy compliance (infrastructure (pre-deploy)): [PASS] infra_disk_free_minimum: PASS: disk free 66.57 GB meets minimum 10.00 GB. [FAIL] infra_cpu_load_maximum: FAIL: CPU load 2.52 exceeds maximum 2.00. [PASS] infra_memory_available_minimum: PASS: memory available 8.10 GB meets minimum 1.00 GB.
[swiftdeploy] POLICY VIOLATION - deploy blocked (infrastructure). - Policy violation: CPU load (2.52) exceeds maximum allowed (2.00).
栈根本没起来。docker compose up 根本没跑。这会儿唯一存在的容器就是 OPA 旁车。
改写 manifest.yaml 之前,CLI 会:
升级到 stable 走不同的 Rego 分支,完全跳过 SLO 评估(从 canary 迁出时没有"canary 指标"可以检查)。
python swiftdeploy status --interval 2 -n 5
每次采样独立抓取 /healthz、/metrics 和两个 OPA 领域,然后打印:
=== 2026-05-06T18:16:17Z mode='stable' req/s~=3.2100 === window(30s): errors=2/41 err_rate=4.8780% p99=312.45ms chaos_active: 2 (error) Policy compliance (infrastructure (pre-deploy)): [PASS] infra_disk_free_minimum: PASS: disk free 66.62 GB meets minimum 10.00 GB. [PASS] infra_cpu_load_maximum: PASS: CPU load 0.89 is within maximum 2.00. [PASS] infra_memory_available_minimum: PASS: memory available 11.38 GB meets minimum 1.00 GB. OPA [infrastructure (pre-deploy)] aggregate: ALLOW Policy compliance (canary (hypothetical promote->canary)): [FAIL] canary_error_rate_window: FAIL: error rate 4.8780% exceeds maximum 1.0000% over 30 s window. [PASS] canary_p99_latency_window: PASS: P99 latency 312.45 ms within maximum 500.00 ms over 30 s window. OPA [canary (hypothetical promote->canary)] aggregate: DENY
每次采样追加一条 JSON 行到 history.jsonl,包含 chaos_active、窗口指标和两个 OPA 快照及其 checks[]。
python swiftdeploy audit
从 history.jsonl 生成 audit_report.md,分四个部分:
时间线事件表示例:
| 时间 (UTC) | 事件 | 详情 |
|---|---|---|
| 2026-05-06T18:17:02Z | chaos_change | none -> error |
| 2026-05-06T18:20:14Z | mode_change | stable -> canary |
| 2026-05-06T18:23:41Z | chaos_change | error -> none |
在 canary 模式下,POST /chaos 会设置进程级别的混沌状态:
# 开启 40% 错误率
curl -s -X POST http://127.0.0.1:8080/chaos \ -H "Content-Type: application/json" \ -d '{"mode": "error", "rate": 0.40}' # 开启每个请求 2 秒延迟
curl -s -X POST http://127.0.0.1:8080/chaos \ -H "Content-Type: application/json" \ -d '{"mode": "slow", "duration": 2.0}' # 恢复
curl -s -X POST http://127.0.0.1:8080/chaos \ -H "Content-Type: application/json" \ -d '{"mode": "recover"}'
开 40% 错误率、流量持续的情况下,status 面板在一个 30 秒窗口内就会显示 canary_error_rate_window FAIL。此时执行 swiftdeploy promote canary 会得到:
Policy compliance (canary (pre-promote)): [FAIL] canary_error_rate_window: FAIL: error rate 50.8772% exceeds maximum 1.0000% over 30 s window. [PASS] canary_p99_latency_window: PASS: P99 latency 1.96 ms within maximum 500.00 ms over 30 s window.
[swiftdeploy] POLICY VIOLATION - promote blocked (canary safety policy). - Policy violation: error rate (50.8772%) exceeds maximum (1.0000%) over last 30 seconds.
manifest.yaml 不会被修改。恢复之后等窗口数据清空,同样的命令就会成功。
1. 单一配置源是强制函数,不是便利工具。
当阈值只在 manifest.yaml 里、别的地方都没有,你不可能在 Rego 文件里不小心写了一个比运行手册更严格的限制。manifest 就是运行手册本身。
2. OPA 的价值在于分离,不在于语言本身。
Rego 有学习曲线。真正的好处是改策略就是往 .rego 文件提一个 PR,有清晰的审计记录,而不是把逻辑分散在部署工具的代码里。
3. 滑动窗口 gauges 比查 TSDB 更适合 CLI 门控。
替代方案——为了在部署时算一条 PromQL 表达式专门跑个 Prometheus Server——为了 CLI 里一个判断多加了一套基础设施。应用自己用 deque 在进程内算就行了。CLI 抓的是 gauge 值,不是原始计数器桶。
4. 失败模式才是真正的 API。
这个项目最有价值的工作不是happy path,而是给每种 OPA 传输失败都赋予独特的 failure_kind 和消息,让凌晨两点值班的工程师一眼就知道 OPA 是挂了、慢了、返回了乱码,还是策略本身说不行。
5. Windows 上的 CPU 近似值不等于 Linux 的 load average。
基础设施策略用的是 Linux 的 1 分钟 load average。在 Windows 上,psutil.cpu_percent x logical_cpus 在容器启动时会剧烈飙升。第一次真正触发门控的那一刻,既是最爽的也是最烦的——爽是因为门控正常工作了,烦是因为 Windows 的表现跟预期完全不一样。