从一台空机器,到接上你自己的系统
给两种人写的:把网关装起来再升上去的运维,和用自己的系统给它提单的对接方。命令、参数,以及那几处「像卡住了、其实是正常流转」的地方。
本地跑起来
网关自身的元数据存储是 PostgreSQL 16,开发与生产同一种。ADR 0018 收敛成一种之后,起跑前必须先有一台本机 PostgreSQL,换回来的是 schema 不再有第二个权威源。
下面这几行是从一台空机器到能打开控制台的全部命令。库要自己建,剩下的启动时自己做:跑迁移、回填引用数据、播种演示数据。
# 建库 —— 要跑后端测试再建一个 vela_test
createdb vela_gateway
# 后端 :8080 —— 启动时自己把迁移跑完
cd backend && go run ./cmd/server
# 前端 :5173 —— Vite 把 /api 代理到 :8080
cd frontend && npm install && npm run dev- 默认 DSN 连 127.0.0.1:5432 上的 vela_gateway,而且不写 user= —— libpq 会回落到当前操作系统用户,Homebrew 装的 PostgreSQL 正是这个形态。
- 不想装本机 PostgreSQL 就用仓库根的 docker compose,端口只绑在 127.0.0.1;走这条路必须显式给出 VELA_PG_DSN。
- 空库首次启动只建一个平台管理员,不创建任何实例 —— 登录后到「配置」页新增;dev 构建的登录页会把这个账号预填好,生产构建不预填。没填凭据的实例在 dev 走仿真执行,生产一律回「该实例未配置真实执行凭据」。
生产部署与升级顺序
部署物是一个二进制:后端 API、编译后的前端 SPA、SQL 迁移全在里面。子命令三个 —— version 打印版本串,migrate 只建表与升级表,init 跑迁移加引用数据再建管理员。
迁移是内嵌的,服务启动时自己会跑一遍、失败就直接退出。所以升级里那一步单独的 migrate 是可选的 —— 但留着值得:迁移的成败和耗时单独看得见。
# 版本串:不设 VERSION 就取 git describe —— version 子命令打印的就是它
VERSION="${VERSION:-$(git describe --tags --always --dirty)}"
rm -rf dist && mkdir -p dist/web
# 前端产物放到后端的 web_dir
(cd frontend && npm ci && npm run build) && cp -r frontend/dist/* dist/web/
# 后端交叉编译,纯 Go 无需 C 工具链
(cd backend && GOOS=linux GOARCH=amd64 go build -trimpath \
-ldflags "-s -w -X main.version=$VERSION" -o ../dist/vela-gateway ./cmd/server)出包之前先过一道闸
打包脚本会先跑生产数据自检那几条测试:生产模式下任何已实现的功能都必须是真实现,跑不过就不出包。
单副本:顺序照旧,但理由变了
备份数据库 → 停旧进程 → migrate → 起新进程 → version 确认。照做值得,但它不再是一道安全前提 —— 服务启动时自己会跑迁移,「先起新进程」的后果不是「新二进制对着旧表结构跑」,而是新进程自己把库改了。
多副本共享同一个库:没有「只要不先起新进程就安全」这回事
第一个换上新二进制的副本就会自己抢到迁移锁并把全部迁移跑完,而此刻另外几台旧副本仍在对着已经改过的表结构继续服务。只有三种选择:
- 01
停机窗口
先把全部旧副本停掉,再起新的。
- 02
接受混跑
先确认这一版迁移对旧代码向后兼容,再滚动重启。
- 03
不迁移启动
副本一律带 --no-migrate 起,迁移由人在选定的时刻单独跑一次。
滚动重启还有一个坑:迁移用一把数据库咨询锁串行化,等锁最多 60 秒。同时重启两台、而这一版迁移跑超过 60 秒时,第二台会启动即退出。
--no-migrate 不是「跳过检查」
它仍然会验证 schema 已经是最新的,差一条就拒绝启动,并点名差的是哪几条。如果它只是「什么都不做」,一台表不全的网关就会上线 —— 而那种进程照样让 /healthz 变绿。
- 生产的 DSN 里必须显式写出 sslmode:缺省那档会在服务端不支持 TLS 时静默降级成明文,而凭据和全部审计内容都走这条连接。
- PostgreSQL 15 及以上,建完库要再连到这个库上执行一次 schema 授权 —— 连在别的库上执行只会改错对象。这是新部署最常见的绊脚石。
启动自检:这些审批环节,到底还有没有人能批
这个自检的来由是一次真实的死锁:审批角色里只有一个人,而工单都由他发起。每一层校验都通过了,然后两人控制让它们一张都批不掉。
所以每次启动,网关会先回答一个问题:这些审批环节,到底还有没有人能批。命中下面三种情形之一就打一条 WARN,而且每条都带一句怎么办。
| blocked链上一个能批的人都没有 | 工单根本建不出来。分两种:角色没有成员;或者角色有成员但全部是服务账号 —— 后者最容易看走眼。 |
|---|---|
| deadlock链上恰好一个人,且未开自审批 | 工单建得出来,但由他本人发起的那些永远没有人可以审批。 |
| deadlock启动那一刻已经卡住的存量工单 | 这一条不是预测是现状:那几张单现在就已经没有任何人能处理。 |
自检只报告,不阻止启动
它也不改配置:审批人是谁是组织的决定。级别用 WARN 而不是 Error(这些都不是启动失败),也不用 Info(淹没在启动刷屏里的提示等于没有)。
边界说在明处:它只看得到启动那一刻的快照,之后堆积起来的卡单它不会知道。这是顺手看一眼,不是监控。
真正会拒绝启动的是另外几件事
审批人自检不拦,下面这几条一律直接退出:
- 生产环境的数据库 DSN 为空 —— serve、migrate、init 三条路都拦。libpq 会把空 DSN 读成「本机套接字 + 操作系统用户 + 同名库」而且真的连得上,所以漏填的后果是网关静默连上一个不相干的库。
- 生产环境的 JWT 密钥太弱、太短,或者字符种类太少。
- 反向代理信任列表的值非法 —— 所有环境都拦。那个 Web 框架拿到非法值时会静默回落成信任所有代理,而那会让客户端伪造来源 IP 绕过白名单。
- 迁移失败,或者给了 --no-migrate 而 schema 落后。
- 播种失败。
只打 WARN 不拦的另有几条:生产未启用 TLS、静态加密密钥未设置、生产开了 Webhook 允许内网、环境名不认识、web_dir 里缺 index.html 或 assets。
开放接口对接
一把凭据、一组端点,幂等靠数据库唯一索引。凭据在系统设置里签发,必须显式选一个服务账号。令牌形如 key.secret,库里只存 secret 的 bcrypt 哈希,明文只在签发那次拿得到。
调用时把令牌放进 Authorization: Bearer,或拆成 X-Vela-Key 与 X-Vela-Secret 两个头。通过之后注入的身份就是那个服务账号,下游的能力矩阵、标签范围与审计归属原封不动。
端点
POST /api/v1/open/releases | release:create | 提交 SQL 升级单(JSON 或 multipart) |
|---|---|---|
GET /api/v1/open/releases/{relNo} | release:read | 查这张单的状态与各阶段 |
POST /api/v1/open/releases/{relNo}/abort | release:create | 终止自己提交的单 |
POST /api/v1/open/sql-review | review:check | 不建单的规范审查,给 CI 当合并门禁 |
GET /api/v1/open/instances | release:read | 这把凭据能打到哪些实例(已按服务账号的标签范围过滤) |
GET /api/v1/open/pipelines | release:read | 有哪些发布流程(仅供了解,不能在请求里指定) |
规范审查:给 CI 当合并门禁
方言由目标实例决定 —— 同一段 SQL 在 Oracle 实例和 TiDB 实例上命中的规则不一样,所以实例与连接 id 二选一。
# 不建单,只回一份审查结果
curl -X POST https://<gateway>/api/v1/open/sql-review \
-H "Authorization: Bearer $VELA_TOKEN" \
-F "instance=order-cluster" \
-F "file=@migrations/V12__archive.sql"结果里的 passed 是「没有 error 级命中」,不是「零命中」:一份被每条警告都拦下的发布,是永远发不出去的发布。findings 每条带规则编号、级别、第几条语句与起始行。
提单
标题、目标实例、库、变更类型、SQL 或脚本,再加一个你自己系统里的单号。脚本三种传法:multipart 的 file、内联的 script 或 base64;上限 15MB。
# 提交一张 SQL 升级单
curl -X POST https://<gateway>/api/v1/open/releases \
-H "Authorization: Bearer $VELA_TOKEN" \
-H "Content-Type: application/json" \
-d @body.json# body.json
{
"title": "订单表增加备注字段",
"externalRef": "CHG-2026-0001",
"instance": "order-cluster",
"database": "orders",
"changeType": "ddl",
"sql": "ALTER TABLE tbl_order ADD COLUMN memo VARCHAR(64) NOT NULL DEFAULT ''",
"reason": "需求 #123"
}externalRef 的幂等语义
重复提交同一个 externalRef,拿回的是第一次创建的那张单,不会重复下发变更。这个保证来自数据库唯一索引而不是「先查后插」,所以两个并发的重试也只产生一张单。
执行阶段会停下等确认 —— 这是正常流转,不是卡单
审批回答的是「可不可以做」,执行确认回答的是「现在做」。你的单走到执行阶段会停在等待确认,由网关侧的操作员在控制台点下「确认执行」之后才真正落库。你的系统应当把这个状态呈现为「待执行确认」,而不是超时失败 —— 这是对接方最容易踩的一个坑。
几条得先知道的约定
- 所有响应都是 HTTP 200,成功与否看包体里的 code 是不是 0 —— 唯一的例外是回调端点与 WebSocket 握手。
- 错误码:40100 凭据无效,不要重试;40300 无权;40301 来源 IP 不在这把凭据的白名单里;42800 需要二次验证;40001 参数问题;50000 服务端错误,可以退避重试。
- 发布流程不能在请求里指定:带了它会整单拒绝,而不是忽略 —— 静默忽略会让调用方以为自己指定成功了。
- 轮询 10 到 30 秒一次就够;审批通过之后,网关最多 15 秒把流水线推下去。
- 已经在执行中的单拒绝终止 —— 一个假的「已终止」比没有这个按钮更危险。
- 实例名不唯一时报错,而不是猜一个 —— 猜错就是变更打到了另一个库。
在线契约
取契约文件走 GET /openapi.yaml,Swagger UI 在 GET /docs —— 它打进了二进制,不走 CDN,断网的机器上照常打开。契约覆盖 143 个操作、113 条路径。
外部飞书审批(审批魔方)
把高危命令的审批从站内点单换成:审批魔方推一张飞书交互卡片,人在卡片上批,回调把工单置为通过或驳回。站内审批仍是兜底,两条通道汇聚到同一个原子决策内核。
回调不执行命令。通过之后仍然由有权的人回到审批页自己点执行 —— 审批人按下的是「我同意」,不是「现在就跑」(ADR 0010)。
需要配什么 —— 全部是运行时设置,不是部署期环境变量
在控制台的「设置 › 审批 › 外部飞书审批」里配,值存在库里,改完不用重启。
approval.external.enabled | 总开关。关着的时候回调端点同样拒绝,不靠残留的回调密钥存活。 |
|---|---|
approval.external.baseURL | 审批魔方的服务地址,必须是 https。 |
approval.external.token | 调用令牌,AES-GCM 加密落库,读设置的接口不回传明文。 |
approval.external.aiGroup | AI 分组。 |
approval.external.callbackBaseURL | 网关回调的根地址;完整回调地址是它接上 /api/v1/approvals/lark/callback。 |
approval.external.callbackSecret | 回调密钥,同样加密落库,必填 —— 没配就一律拒绝回调。 |
approval.external.callbackAllowIPs | 回调来源的 IP 或网段白名单,逗号分隔。留空等于不限来源:密钥是主控制,白名单是可选的第二道。 |
「设置 › 通知」里还有一套飞书群通知,容易和它搞混:那一套只推一张卡片,不接收回调、不影响审批结果。
出站:建单之后异步推一张卡片
总开关、地址、令牌、回调根地址都填了,建单成功之后才异步推,推不出去不阻塞建单。关联主键是我方的审批单号,作为 external_task_id 发出去。命令在出门之前先抹掉口令字面量:这个请求把命令交给第三方,它会存下来。
入站:回调怎么被信任
回调走单独一条路径,不带用户令牌。鉴权是共享密钥的常量时间比较加可选的来源 IP 白名单,之后逐条核:这张单外发过吗、卡片对不对得上、是不是已经决策过、批准有没有给审批人。
灰度怎么走
先只在 DEV 与 GLI 上验证闭环,验证点是「通知到了、工单变成待执行」,不是「表没了」;无误之后再放开 STAGING 与 PROD。
接不通时按这张表查
| 卡片没推出来 | 总开关、服务地址、令牌、回调根地址是不是都填了 —— 缺一项就不外发;再看日志里外发失败那一行。 |
|---|---|
| 回调鉴权失败 | 两边的回调密钥是否一致;来源 IP 是否在白名单里。 |
| 回调说审批单不存在 | 确认对方把 external_task_id 原样带了回来。 |
| 点了通过但命令没执行 | 这多半是对的 —— 通过不代执行。另外两种可能:命中了禁止自审批,或者目标连接已经被删掉(会如实记一条 warn 而不执行)。 |
去仓库里读原文
这一页是命令与参数,完整的那几份在仓库里。
README.md功能总览、技术栈、快速开始。DEPLOY.md建库与授权、sslmode 为什么必须显式写、升级那一节,以及上线之后值得盯的那几行启动日志。docs/adr/架构决策记录 —— 每个决定为什么这样做,以及被否掉的替代方案。docs/开放接口对接文档.md开放接口的字段、错误码与对接约定。docs/external-approval-setup.md外部飞书审批的配置步骤、灰度建议与故障排查。backend/docs/openapi.yaml在线契约的源文件 —— 它在后端目录下,不在仓库根的 docs 里。