site logo

Marico's space

SwiftDeploy:使用 OPA、Prometheus 和单一 YAML 文件构建自管理部署工具

服务器技术 2026-05-07 11:36:36 4

最近折腾了一个叫 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,从不要求你手动去改任何生成出来的文件。

架构概览

Architecture Diagram

关键隔离属性

属性 实现方式
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 模板:

  • templates/nginx.conf.j2 — upstream 块、代理超时、错误页、访问日志格式、X-Deployed-By 响应头,临时文件路径放在 /tmp 下让 nginx 用户有写权限
  • templates/docker-compose.yml.j2 — 三个服务(api、nginx、opa),api 服务做了安全加固(cap_drop: ALL、no-new-privileges、user: 1000:1000)、健康检查、命名卷

METRICS_WINDOW_SECONDS 这个环境变量从 policy.thresholds.metrics_window_seconds 写入——也就是 OPA 用来做 SLO 窗口的同一个值,这样 API 的滑动窗口指标和 Rego 规则永远保持同步。

swiftdeploy validate 在任何容器启动前跑五个预检:

  1. manifest.yaml 存在且能解析
  2. 所有必填字段非空(包括完整的 policy 块)
  3. docker image inspect <services.image> 成功
  4. nginx.port 在宿主机上未被占用
  5. 渲染出来的 nginx.conf 在临时容器里通过 nginx -t

眼睛:Prometheus 监控埋点

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 值始终是最新的。

大脑:OPA 策略旁车

为什么用 OPA 而不是 CLI 里的 if 语句?

核心约束:CLI 本身不能做任何允许/拒绝的决策。用 Python 的 if 语句写逻辑,阈值和业务逻辑跟运维工具绑在一起了。用 OPA 的话:

  • 阈值只存在 manifest.yaml 里(改一个地方,所有环境都生效)
  • 策略逻辑只存在 .rego 里(可审计、可通过 opa test 测试)
  • CLI 是个"傻传话员"——组装上下文、发请求、读取决策对象

领域隔离

每个策略领域只管一个问题和一种数据形状:

领域 问题 输入结构
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 崩溃或挂起。

带门的生命周期:部署和升级

swiftdeploy deploy

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 旁车。

swiftdeploy promote canary

改写 manifest.yaml 之前,CLI 会:

  1. 通过 Nginx 抓取 GET /metrics
  2. 从滑动窗口 gauges 算出 error_rate_percent 和 p99_latency_ms
  3. 带 promotion_target: "canary" 发送到 swiftdeploy/canary/decision
  4. 收到 allowed: false 就直接退出,不碰 manifest.yaml

升级到 stable 走不同的 Rego 分支,完全跳过 SLO 评估(从 canary 迁出时没有"canary 指标"可以检查)。

实时面板:swiftdeploy status

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[]。

记忆:swiftdeploy audit

python swiftdeploy audit

从 history.jsonl 生成 audit_report.md,分四个部分:

  • 摘要 — 采样数、拒绝数、传输错误数
  • 时间线事件 — 通过比对相邻记录检测到的模式切换和混沌切换
  • 违规记录 — 每个领域的 allowed: false 及原因
  • 最近时间线 — 最后 25 条采样,表格形式,含 Chaos 列和各领域 OPA 状态

时间线事件表示例:

时间 (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

注入混沌,看门 fires

在 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 会得到:

Swiftdeploy promote canary blocked by OPA canary safety policy
 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 的表现跟预期完全不一样。

原文链接:https://dev.to/trojanhorse7/swiftdeploy-building-a-self-governing-deployment-tool-with-opa-prometheus-and-a-single-yaml-file-2g6k