AI 网关部署脚本里那段 90 秒的 sleep,藏着最致命的合规红线
AI 网关部署脚本里那段 90 秒的 sleep,藏着最致命的合规红线
不要照搬资源拓扑与网段划分,去拆解它的环境状态机与合规约束。
当团队决定「我们也搞一套类似 ChatGPT 的内部网关」时,最直觉的动作往往是找到开源标杆或前序团队的仓库,把里面的基础设施即代码(IaC,比如 Bicep 或 Terraform)完整克隆下来。接着,批量替换掉里面的资源前缀,看着 what-if 或 plan 输出一片绿,就以为大功告成。
这是部署复杂合规系统时最昂贵的抄法。
前序团队的架构建立在他们的「世」上:特定的云区域、已有的专有网络、用户对话可能随时成为合规审查记录的法务定性,以及必须精确到部门层级的财务结算要求。你的上下文只要有一条不同,抄来的代码就会在上线第一周从技术捷径演变成合规事故。
读完本文,你将能带着一份明确的「抄什么、改什么、扔掉什么」的清单走进下一场架构评审会,而不是拿着一份毫无意义的资源名对照表。
1. 发版状态机,比资源名更值得抄
如果你去看那些成熟团队的 CONTRIBUTING.md,通常会有一句很容易被跳过的话:Pull Request 不会修改任何实际环境(sandbox、dev、prod)。
这不是一句口号,这是用 GitHub Actions 写死在工作流里的状态机:
# .github/workflows/litellm.yaml(结构示意)
on:
pull_request: { paths: [src/litellm/**] }
push: { paths: [src/litellm/**] }
sandbox:
# PR 阶段仅执行差异对比
operation: whatIf | create
dev:
operation: whatIf | create
dev-test:
# 仅在代码合入后执行真实端到端测试
run: playwright against the new URL
prod:
needs: dev-test
if: push && main
environment: prod # 这里的核心是人工审批闸门
本地验证和云端验证使用的绝不是同一把尺子。make local 跑通 docker compose,只证明容器还能互相通信;make what-if 问的是云平台到底会不会发生变更。把本地运行当成部署,或者把 what-if 当成测试,都是在用错尺子。
在有状态容器的部署脚本里,经常会看到一段容易被误认为是「掩盖并发 bug」的代码:部署前停用当前修订版本,强制 sleep 90,然后再执行新的部署。60 秒留给优雅退出,30 秒作为缓冲。
当你把这套网关搬到 Kubernetes 上时,绝对不要生硬地翻译成「我们也在这里 sleep 90」。你要翻译的是它背后的约束:有状态控制面严格禁止双活。 无论是单修订版本,还是挂载了 Redis 会话,两份实例同时写入都会导致数据脑裂。在 K8s 里,这个约束的实现应该是明确的 drain 策略或限制并发数的 PDB(Pod Disruption Budget)。
数字从来不是约束,禁止双活才是。
同样,模型上架也是一个状态机。不要在生产环境打开大模型的通配符发现(wildcard discovery)功能。关闭它不是 YAML 配置的口味问题,而是为了防止云控制台里的一次误操作,瞬间变成全员可见的模型列表大放送。任何模型变更都应该走 PR,让 sandbox 先看见,测试环境跟进,最后人工批准生效。
what-if 是意见,create 是事实。中间那道人工审批闸门,是你为了控制事实而支付的成本。
2. 身份、密钥与那把不能丢的盐
在云原生架构里,身份验证应该优先使用 OIDC 和托管身份(Managed Identity),而不是长期有效的服务主体(Service Principal)密码。GitHub 只持有 OIDC 令牌和环境配置;容器启动时,应用层再通过托管身份去 Key Vault 拉取真正的运行时密钥。
但这里存在多云环境的不对称性。在使用 Azure OpenAI 时,你可以依靠托管身份彻底消灭静态密钥;但如果你同时接入了 AWS Bedrock,可能依然需要传统的 Access Key。搬到新环境时,第一步是向安全团队确认:能否接受「半边系统没有长期密钥,另半边还有」的现状?如果不能,你就必须在 Bedrock 前面再加一层凭证代理,或者暂缓上线相关模型。
除了常规密钥,这套系统里最不像「配置」的配置,是用来加密虚拟凭证库存的盐(Salt Key)。
如果主密钥(Master Key)丢了,你还可以通过紧急流程重新签发;但如果 Salt 丢了,数据库里所有的历史密钥都会变成一堆永远解不开的乱码。备份 Salt 不是一句「记得把它放进 Key Vault」就能解决的。它需要双人控制、离线存储,并定期进行恢复演练。
这是新团队最容易踩的三种坑:
- 一个托管身份打天下:为了少写几行 RBAC 角色分配,让清理数据的定时任务和聊天 UI 共享同一个身份。结果就是权限爆炸,任何一个组件的漏洞都会导致整仓数据沦陷。
- 把 Secret 写进 IaC 参数文件:为了让本地
what-if跑得顺畅,把机密信息写在参数文件里一并提交,导致敏感信息永远留在了 Git 历史中。 - 直接用 API Key 调用大模型:为了本地调试方便,绕过托管身份。当密钥泄露需要轮换时,你必须去修改每一个消费端的配置,而不是仅仅在云端更新角色关联。
3. 数据住哪、谁能看、灾难时先救谁
不要在架构评审会上用一句「我们都加密了」来结束数据安全的话题。你需要把数据的落点写成一张极其确定的表:
| 数据类型 | 存储位置与介质 | 生命周期 (TTL) | 访问边界 |
|---|---|---|---|
| 聊天正文与历史 | 核心关系型数据库 (Postgres) | 直到用户主动删除,或闲置 90 天后清理 | 应用层 + 合法打破玻璃 (Break-glass) 授权 |
| 上传的原始文件 | 对象存储 | 与聊天记录同步;孤儿文件定期清理 | 同上 |
| RAG 向量特征 | 向量数据库 (如 pgvector) | 同上 | 同上 |
| Prompt 全文遥测 | 专用日志分析工作区 | 约 60 天 | 表级别的 RBAC,严禁授予工作区全局读权限 |
| 计费与用量元数据 | 网关独立数据库 | 长期留存 | 财务与平台团队可见(注意:计费日志中应剥离对话正文) |
| 活跃会话状态 | Redis 缓存 | 仅限活着的会话 | 内部通信,不作任何归档 |
除了数据落点,生产环境的磁盘规格同样是硬约束。前序团队给 Postgres 分配了极高的性能规格和存储空间,如果你只抄了规格,却没有抄配额告警规则,那么上线第一周暴增的并发就会把「磁盘写满」从一个仪表盘预警变成周一早晨的全局宕机。
灾难恢复(DR)脚本协调的绝不是单一组件。它是文件共享快照 + 数据库时间点恢复(PITR) 的精密联动。如果你的新环境只有数据库备份,没有文件快照,发生回滚时,RAG 系统就会指向一堆幽灵向量——数据库里有索引,但文件根本不存在。演练时,必须故意把库和文件的恢复时间点错开,观察应用层是否能正确处理这种不一致。
另外,不要把 Break-glass(紧急破窗) 流程和日常运维混为一谈。Break-glass 是为了法务审计或极端事故准备的:必须双人操作、限时(通常小于 2 小时)、优先给予只读权限,并且工单里必须写明法律依据。如果你为了「排查问题方便」,在生产环境打开了把 Prompt 原文写入计费日志的开关,实际上就是把敏感数据的生命周期从 60 天的严格日志策略,无限期地扩展到了计费数据库的长期存档里。
📌 备份证明你能回到过去,驻留证明你没有去错地方,审计证明你知道谁去过。 这是三件事,不要写进同一张工单。
4. 另一套环境的第一周:抄、改编、扔掉
把别人经过验证的架构拆成三堆。只搬第一堆,你也能顺利开工;但如果你把第三堆当成圣经去遵守,你们团队就会在命名规范和无效组件上白白耗掉一个月。
抄(不可妥协的约束)
- 控制面与 UI 必须分目录、分工作流、分数据库独立部署。
- 建立三套环境,生产环境必须有人工审批闸门,PR 默认只执行差异检测。
- 依赖 OIDC 获取临时凭证,拒绝长期静态密码。
- 用户身份必须通过 Header 传递进代理网关,预算额度要扣在具体的「人」或「部门」上,而不是扣在那个处理请求的聊天容器上。
- 日志必须有明确的 TTL;计费元数据长期保存;数据清理任务必须有确定的执行时钟。
- Break-glass 流程与日常发版(Ops)必须分属两套完全不同的操作手册。
改编(适应你的上下文)
- 区域与数据驻留:如果你的组织在欧盟,可能需要完全不同的双区域策略,甚至明确禁止数据流向某些特定提供商的服务区。
- 网络拓扑:前序团队的 VNet 是他们的既有资产。你可能需要从零规划网段,或者接入你们自己的零信任网络。
- 计算引擎:把原有的 Serverless 容器换成你熟悉的 K8s 也可以,只要保住三个底线——内部 DNS 解析正常、托管身份顺利挂载、能实现等价的单实例排他运行(Drain/PDB)。
- 告警路由:把告警接收人写在 IaC 参数里指向一个群组,而不是绑定在某个英雄工程师的私人手机号上。人员变动应该是一个代码 PR,而不是在微信里交接。
扔掉(别人的叶子,不是你的根)
- 具体的资源命名:直接扔掉他们那套
<env>-<region>-<app>-rg-01的命名法。建立属于你自己的{env}-{region}-{workload}词典,写进类型定义里。 - 非核心扩展:把语音识别、动态会话、联网搜索当成未来的迭代目标。在第一版上线时,这些都应该被无情关闭。
- 强加的企业级组件:如果你连 Token 计费问题都还没通过开源代理解决,就先别急着买昂贵的商业 API 网关(如 APIM Premium)。
升维:约束不可克隆
举一反三:在评估任何开源基础设施仓库时,问自己:这里的配置,哪些是计算逻辑,哪些是组织妥协的结果?
基础设施即代码(IaC)给我们造成了一种危险的错觉:既然代码可以无损复制,那么架构也可以。但 IaC 只是接口,它记录的是上一个团队在他们特定的合规、法务、财务和网络限制下,妥协出来的最终结果,而不是推导过程。
无状态的计算逻辑可以被随意克隆,但有状态的数据合规、身份边界和审计要求不可克隆。当你试图把带有强业务约束的系统直接复制到新环境时,你实际上是在强迫你们的法务和安全团队接受另一家机构的风险偏好。
立刻可以做的事
如果你即将在一周后主导这场 AI 网关的迁移,建议你现在就停下手里的代码,先去确认这三件事:
- 在架构一页纸上,写下你们三个环境对应的 GitHub Environment 名称,并明确写出谁拥有生产环境的审批权。如果写不出具体的人名或群组,先别去写 IaC 脚本。
- 列出一张完全匹配你们实际情况的「用户数据分类表」。只要表里还有一个单元格是空的,就说明系统还不能对内部用户开放。
- 安排一次 30 分钟的桌面推演:假设核心代理镜像损坏,且错误版本已经全量发布。当前的回滚命令是什么?谁来批准?在恢复期间,用户界面上会看到什么?
明天早上,打开你们团队克隆下来的那个 IaC 仓库,找到里面最长的那段 sleep 或最复杂的重试逻辑。问问自己:它是为了掩盖上游的不稳定,还是在执行一条你还不知道的合规红线?