业务故事站
P3 Apollo

配置管理:Base+Overlay 与 Sealed-Secret

"大部分配置通用、少部分配置环境相关"是配置管理的核心矛盾。Apollo 用 Base+Overlay 配置中心解决它:模板一次登记、按环境 overlay 深度合并渲染出差异化配置;敏感信息用 Sealed-Secret(AES-256-GCM)加密落库,明文不落库。看完这 4 个故事,你就能搭出"一次定义、多环境覆写 + 密钥加密托管"的配置底座。注意:渲染结果尚未下发 Agent(config_update 任务未打通),密钥加密存储已可用。

应用开发 平台运维 / SRE 配置管理 Base+Overlay Sealed-Secret 密钥轮换 共 4 个故事

能 / 不能速览

✅ 这个主题能做
  • 配置模板登记(raw_yaml / kustomize / helm),git_path 指向仓库内 base 配置
  • Base+Overlay 深度合并渲染:map 递归合并、标量覆盖、list 替换
  • 变量替换 ${KEY} / $KEY + 内置环境变量(ENVIRONMENT / PROJECT_ID)
  • Schema required 校验(含嵌套对象,阶段 E),漏配直接渲染失败
  • 两环境渲染结果路径级 diff(add / remove / change)
  • Sealed-Secret 加密落库、单条/批量轮换、sealed-secret.yaml 导出、远程解密(全程审计)
⛔ 这个主题做不了
  • 渲染结果下发 Agent(config_update 任务)未打通,仅定义常量
  • Sealed-Secret 与 bundle 下发接线未实现,Agent 端解密注入无链路
  • 配置维度漂移自动修复未实现(Agent 实际配置上报对比未接)
  • Vault / 云 KMS 外部托管未接(KMSProvider 接口已预留)
  • SM4 国密加密未实现,当前 AES-256-GCM 绕行(有回迁计划)

适用角色

本主题面向三个角色:

  • 应用开发(王工):登记配置模板、写 base / overlay、渲染与 diff,是配置内容的生产者。
  • 平台运维 / SRE(陈工):维护 Git 仓库、绑定环境、管理密钥生命周期,是配置中心的运维者。
  • 安全合规(刘经理):要求敏感信息加密存储、季度轮换、解密留痕,是密钥治理的监督者。

能力速览(能做什么)

配置模板登记

POST /projects/:id/config-templates 登记模板:type(raw_yaml/kustomize/helm)、git_repo_id、git_path、schema。base 配置从 Git 仓库本地目录读取(gitops.LocalRepo 快照)。

Base+Overlay 渲染

POST /config-templates/:id/render?environment_id=:base + 环境 overlay → deepMerge 深度合并 → 变量替换 → Schema 校验 → YAML 输出并写缓存。

两环境 diff

POST /config-templates/:id/diff?env_a=&env_b=:对同一模板两个环境的渲染结果做路径级对比,输出 add/remove/change 差异行——"环境配置到底差在哪"一目了然。

Sealed-Secret 加密

POST /projects/:id/secrets 加密落库(AES-256-GCM,密文 AES256:<iv>:<ct>,明文不落库);列表 / 详情只返回元数据,不泄露明文与密文。

密钥轮换

单条 rotate 与批量 rotate-batch(旧 key 全量解密 → 新主密钥统一重加密 → 全部成功才切 key,单条失败不中断)。

导出与解密

sealed-secret.yaml 导出(可安全提交 Git);远程解密 POST /secrets/:id/decrypt(config:execute + 项目成员校验 + 审计 CONFIG_SECRET_DECRYPT)。

调整指南(怎么调整)

  • 改环境差异:同一模板为 dev/test/prod 各建 environment_configs 绑定,overlay_path 指向各自覆盖 YAML;渲染结果按环境缓存。
  • 改变量注入:base 里写 ${DB_HOST},渲染时可用内置变量(ENVIRONMENT / PROJECT_ID);想注入自定义变量需扩展渲染接口的 vars 参数。
  • 改校验严格度:模板 schema.required 声明必填字段,漏配渲染直接失败不写缓存;支持嵌套对象 properties(阶段 E)。
  • 改密钥管理:单条 rotate 只重加密目标密文,多密文请用 rotate-batch;轮换走"先写密文后切 key",任一步失败旧 key 与旧密文完整保留。
  • 改审计口径:解密端点每次调用落 CONFIG_SECRET_DECRYPT 审计(成功 / denied / failed 均留痕),越权探测也会留痕。

做得好的场景

"一次定义、多环境覆写 + 密钥加密托管"特别适合以下场景:
  • 多环境差异化:同一份 base 配置,dev/test/prod 只改 db host / 端口等环境字段,不再人肉复制粘贴、diff 靠眼睛。
  • 渲染即校验:Schema 必填校验把"漏配字段"挡在渲染阶段,配置错不至于等到部署才发现。
  • 敏感信息不落地:密钥明文只出现一次(创建请求),密文 AES256 格式自带 IV 可自描述,列表永不回传明文。
  • 密钥轮换可审计:rotate / rotate-batch 全流程留痕,解密端点审计覆盖失败路径,安全团队随时可查"谁解过什么密钥"。

限制与不足

以下是明确的边界,使用前先知道:
  • 配置未下发 Agent:渲染结果与 Sealed-Secret 密文不会随 /agent/pull 下发,config_update 任务仅定义常量,Agent 端解密注入无链路。
  • 配置漂移未闭环:diff 引擎(含可复用的 hub.ComputeDrift)已就绪,但 Agent 实际配置上报对比未接,配置维度漂移无法自动修复。
  • 外部 KMS / SM4 未接:主密钥由本地 encryption_configs 自管(AES-256-GCM),Vault / 云 KMS 与国密 SM4 底座规划中。
  • 覆写策略单一:deepMerge 只支持 Merge(深度合并),Replace / Delete 与 JSON Patch(RFC 6902)未实现。
  • Git Webhook Secret 明文:git_repositories.webhook_secret 目前明文存储(已提议纳入 Sealed-Secret 管理)。

场景故事

故事 1 一套 base 配置渲染出 dev / test 差异:不再人肉复制粘贴
背景
王工维护订单服务,dev 和 test 环境的差异只有数据库地址与日志级别,配置内容九成相同。他在本地建了配置仓库目录(含 base.yaml),通过 /apollo/git-repos 登记 Git 仓库,再登记配置模板 order-base(git_path=configs/order),为 dev(环境 1)和 test(环境 2)各绑定一个 overlay,然后分别渲染——"一套 base、两份 overlay、两个环境的完整配置"。
传统做法对比
以前每个环境一份配置文件,改一处要同步改三处,漏改一次就出现"dev 正常、test 连错库";现在 base 只维护一份,环境差异收敛在 overlay 里。
角色
王工(应用开发,写 base / overlay 并渲染验证);陈工(平台运维 / SRE,维护 Git 仓库与环境的绑定)。
操作步骤
  1. POST /projects/1/git-repos 登记本地配置仓库(url 指向含 base.yaml 的目录)
  2. POST /projects/1/config-templates 登记模板 order-base(git_repo_id、git_path=configs/order)
  3. 为 dev / test 各登记 environment_configs 绑定(overlay_path 指向各自覆盖 YAML)
  4. POST /config-templates/:id/render?environment_id=1 与 ?environment_id=2 分别渲染
系统响应
渲染返回最终 YAML(并写 environment_configs.rendered_config 缓存):
{
  "template_id": 1, "environment_id": 1,
  "rendered": "server:\n  host: 0.0.0.0\n  port: 8080\ndb:\n  host: db-dev.internal\n  port: 5432\nlogging:\n  level: debug\n"
}
test 环境渲染结果中 db.host=db-test.internal、logging.level=info——只有 overlay 覆盖的字段不同。
结果洞察
deepMerge 的语义是"map 递归合并、标量覆盖、list 替换":base 里没写的字段 overlay 补上,overlay 写了的环境字段覆盖 base。渲染缓存自动写入,下次 GET 模板详情即可复用;仓库缺失时按空 base 渲染并附提示,不会崩。
调整建议
把"环境专属字段"(db host / 端口 / 日志级别)全部收敛到 overlay,base 只放通用内容;用 ENVIRONMENT 内置变量在 base 里做轻度差异化;给模板加 schema.required(如 db.host / db.port)让渲染兜底校验。
动手试一试
登录:admin / admin1。页面路径:/apollo/configs 配置管理 → 配置模板。输入内容:先建 Git 仓库(本地目录含 base.yaml),再建模板 order-base,渲染到环境 1。预期结果:返回渲染后 YAML 并写缓存;仓库不存在时返回"已使用空 base 渲染"提示。
限制提示
demo 默认只有环境 1,多环境渲染 diff 需先到 /apollo/environments 建第二个环境并登记同一模板的 overlay;渲染结果目前只写缓存,不会下发 Agent(config_update 任务未打通)。
故事 2 两环境渲染 diff 揪出"test 悄悄改了口径" + Schema 校验拦住漏配
背景
王工发现 test 环境的配置"总感觉哪里不对",却说不清差在哪。他给模板配了 schema(required=["server","db"],db 里再嵌套 required=["host","port"]),然后对 dev(环境 1)和 test(环境 2)跑一次 diff——用路径级差异行把"test 的 db 端口被改成 5433"这种事直接摆出来。
传统做法对比
以前两份配置 diff 靠眼睛 + Beyond Compare,配置一多就漏;现在 POST /config-templates/:id/diff 返回 add/remove/change 三类的路径级差异,环境差在哪一目了然。
角色
王工(应用开发,跑 diff 揪差异);陈工(平台运维 / SRE,确认环境差异是否符合预期)。
操作步骤
  1. 在模板 schema 里声明 required(含嵌套 properties)
  2. POST /config-templates/:id/diff?env_a=1&env_b=2 对比两环境渲染结果
  3. 看返回的 add / remove / change 差异行
  4. 故意在 overlay 漏配 db 字段,再次渲染验证 Schema 拦截
系统响应
diff 返回路径级差异行:
{
  "template_id": 1, "env_a": 1, "env_b": 2,
  "diff": [
    {"type": "change", "path": "db.port", "old": 5432, "new": 5433},
    {"type": "add", "path": "db.pool_size", "new": 20}
  ]
}
漏配必填字段时渲染直接失败:{"code":40002,"message":"配置缺少必填字段: db.host, db.port",...}
结果洞察
diff 输出的是点分路径(db.port),比"整个文件对比"精准得多——change 告诉你是哪个键值变了、add 告诉你是哪个环境多了什么。Schema 校验把"漏配字段"挡在渲染阶段(失败不写缓存),嵌套 properties 能深入校验 db 对象的内部字段。
调整建议
把 diff 纳入"环境升级前"的例行检查:渲染后跑一次 diff,确认差异只有预期字段;Schema 的 required 配在"必填但容易漏"的字段上(如 db.host / db.port),别把可选项也设成必填。
动手试一试
登录:admin / admin1。页面路径:/apollo/configs → 配置模板。输入内容:模板 schema={"required":["db"]},diff?env_a=1&env_b=2;再删掉 overlay 的 db 字段渲染。预期结果:diff 返回路径级 change/add 行;漏配 db 时渲染报 40002"配置缺少必填字段"。
限制提示
diff 是"两环境渲染结果"的静态对比,不是"期望 vs Agent 实际配置"的漂移检测——配置维度漂移自动修复未实现;渲染接口的变量注入当前只能走内置变量,自定义 vars 需扩展接口。
故事 3 数据库口令用 Sealed-Secret 加密落库,sealed-secret.yaml 放心提交 Git
背景
订单库口令一直是"明文写在配置文件里"——刘经理(安全合规)要求必须整改。王工在 /apollo/configs 的"密钥"Tab 建了一条 Sealed-Secret db-password,明文只在创建请求里传一次,落库的是 AES-256-GCM 密文;再把导出的 sealed-secret.yaml 提交到 Git,随配置一起交付。
传统做法对比
以前口令明文躺在配置文件里,Git 仓库泄漏 = 口令泄漏,改口令要改 N 个环境;现在密文入 Git(AES256:<iv>:<ct>),没有主密钥解不开,泄漏风险大幅下降。
角色
王工(应用开发,创建密钥并导出 sealed-yaml);刘经理(安全合规,要求加密存储并检查"无明文落库");陈工(平台运维 / SRE,保管主密钥生命周期)。
操作步骤
  1. POST /projects/1/secrets 创建密钥:name=db-password、plaintext=口令明文
  2. GET /secrets/:id/sealed-yaml 导出 sealed-secret.yaml(kind: EncryptedSecret)
  3. 把 sealed-secret.yaml 提交到 Git 仓库
  4. GET /projects/1/secrets 确认列表只返回元数据(无明文、无密文)
系统响应
创建返回元数据(无明文):
{
  "id": 1, "name": "db-password", "encryption_method": "aes-256-gcm",
  "key_id": "proj-1", "description": "订单库口令",
  "created_by": "admin", "created_at": "2026-08-10T10:00:00Z"
}
导出的 sealed-secret.yaml(节选):
# Sealed-Secret(可安全提交到 Git 仓库)
apiVersion: apollo.zytech.io/v1
kind: EncryptedSecret
metadata: {name: db-password, projectId: 1}
spec:
  encryptedData: "AES256:<iv hex>:<ciphertext base64>"
  encryptionMethod: aes-256-gcm
  keyId: proj-1
结果洞察
密文格式 AES256:<iv>:<ct> 自带 IV 前缀可自描述,主密钥由服务端加密存储于 encryption_configs 表(一项目一主密钥);sealed-secret.yaml 不含明文、可安全提交 Git——CI / 部署端用同一主密钥解密即可(当前 Agent 端解密注入未实现,属规划项)。
调整建议
创建请求的 plaintext 字段是明文唯一一次出现,别写进脚本历史 / 日志;把 description 写清楚(归属系统 + 用途)便于密钥清单审计;刘经理可按"secrets 表密文 + encryption_configs 主密钥分离"的口径验收整改。
动手试一试
登录:admin / admin1。页面路径:/apollo/configs → 密钥 Tab。输入内容:新建密钥 db-password(明文如 ZycTech@2026),再点"查看 sealed-secret YAML"。预期结果:列表返回元数据(无明文),sealed-yaml 是 kind: EncryptedSecret 的 AES256 密文。
限制提示
当前加密底座是 AES-256-GCM(设计要求 SM4 国密,属 F14 记录的有意偏离,有回迁计划);Vault / 云 KMS 外部托管未接;主密钥存在本地 encryption_configs 表,备份库时主密钥与密文同库存放,物理安全需额外保障。
故事 4 季度密钥轮换:rotate-batch 一次轮完,远程解密全程审计留痕
背景
刘经理定的整改项:口令每季度轮换一次,且解密取用必须留痕。项目里已有 db-password、redis-password 两条 Sealed-Secret。陈工演示季度轮换:先跑 rotate-batch 批量轮换(旧 key 全量解密 → 新主密钥统一重加密 → 全部成功才切 key),再对单条演示 decrypt 远程取明文(config:execute + 项目成员校验 + 审计)。
传统做法对比
以前改口令要逐环境登录服务器改配置再重启,一次轮换折腾半天;现在一条 rotate-batch 批量重加密所有密文,主密钥"最后才切换",失败自动回滚不丢密文。
角色
陈工(平台运维 / SRE,执行批量轮换与解密);刘经理(安全合规,检查轮换结果与审计记录 CONFIG_SECRET_ROTATE_BATCH / CONFIG_SECRET_DECRYPT)。
操作步骤
  1. POST /projects/1/secrets/rotate-batch 批量轮换项目全部密文
  2. 查看返回的 total / rotated / failed 与失败清单
  3. POST /secrets/:id/decrypt 远程取一次明文(验证解密可用)
  4. 到 /apollo/admin/audit-logs 查 CONFIG_SECRET_ROTATE_BATCH 与 CONFIG_SECRET_DECRYPT 审计
系统响应
批量轮换返回:
{
  "project_id": 1, "total": 2, "rotated": 2, "failed": 0, "failures": []
}
远程解密返回明文(每次调用落审计,含失败 / 越权路径):
{"code":0,"message":"ok","request_id":"req_1753...","data":{"plaintext":"ZycTech@2026"}}
结果洞察
轮换语义是"先写密文后切 key":新主密钥只在内存生成,先逐条重加密落库、全部成功才提交新主密钥覆盖 encryption_config——任一步失败旧 key 与旧密文完整保留,杜绝"切 key 后失败丢密文"。decrypt 端点的 config:execute 权限 + 项目成员校验 + 审计三重约束,刘经理的"解密留痕"要求达成。
调整建议
把 rotate-batch 挂进季度变更日历;单条密文临时轮换用 rotate(只重加密目标,同项目其他密文仍需 batch);解密取用建议限定低权限操作者(operator 有 execute、developer 无),密钥清单与审计记录定期对账。
动手试一试
登录:admin / admin1。页面路径:/apollo/configs → 密钥 Tab → 加密配置。输入内容:POST /projects/1/secrets/rotate-batch,再 POST /secrets/:id/decrypt。预期结果:返回 {total, rotated, failed};decrypt 返回 plaintext;审计日志出现 CONFIG_SECRET_ROTATE_BATCH 与 CONFIG_SECRET_DECRYPT 记录。
限制提示
批量轮换是"顺序保证 + 失败回滚"(手写回滚,非数据库事务),进程在落库与切 key 之间崩溃的极端窗口仍可能留下不可解密密文(已注明可升级为真实事务);rotate 单条只重加密目标密文,同项目其他密文不随之切换。

常见问题

配置模板和环境配置是什么关系?

配置模板(config_templates)定义"配置怎么组织"(类型、git 路径、校验 schema);环境配置(environment_configs)是"模板 × 环境"的绑定,含该环境的 overlay 路径与渲染缓存。渲染 = 模板 base + 环境 overlay 深度合并后按环境生成最终配置。

Sealed-Secret 的密文能不能在列表里看到?

不能。列表和详情只返回元数据(name / encryption_method / key_id / created_at 等),明文与密文都不回传;明文只在创建请求体里出现一次,解密必须走 POST /secrets/:id/decrypt(config:execute + 项目成员校验 + 审计)。

渲染结果会下发给 Agent 吗?

当前不会。渲染结果只写 environment_configs.rendered_config 缓存;agent_tasks 的 config_update 任务类型仅定义常量,没有写入 / 执行路径。渲染结果随 bundle 下发 Agent、Agent 端解密注入均属规划中。

密钥轮换会丢密文吗?

设计上不会。单条 / 批量轮换都遵循"先写密文后切 key":先重加密落库,全部成功才提交新主密钥;任一步失败旧 key 与旧密文完整保留(批量落库中途失败会回滚已落库行)。注意手写回滚非数据库事务,进程在落库与切 key 之间崩溃的极端窗口仍有风险。

为什么用 AES 而不是国密 SM4?

设计(G16)要求敏感配置用国密 SM4 加密,但实现时 SM4 底座缺失(评审 F14 预判成真),绕行为 AES-256-GCM。这是一处对"国密对齐"设计承诺的有意偏离,已记录回迁计划:platform/crypto 补齐 sm4.go 后增加 SM4 provider,存量密文经 rotate 轮换迁移。

主题小结

一句话:配置管理把"一次定义、多环境覆写 + 敏感信息加密托管"落地成可用底座——Base+Overlay 渲染(变量替换 / Schema 校验 / 两环境 diff)与 Sealed-Secret(加密落库 / 单条与批量轮换 / 导出 / 远程解密留痕)均已实现并接线。记住边界:渲染结果不下发 Agent、配置维度漂移未闭环、Vault/KMS 与 SM4 规划中。演示从 /apollo/configs 建模板渲染 + 建密钥轮换,半小时跑通配置中心全流程。