Skip to content

私有化部署 — nuwa 交付中心

2026-07-28 定调。这份文档描述目标形态与开发顺序,是私有化部署这条线往下走的主文档。

开发时的任务清单、本地环境、验收命令在工作区根目录的 DEVELOPMENT.md(本地文件,无版本历史)—— 那里放会变的进度,这里放不该变的设计。

背景与配置外部化看 总览与架构;k8s 清单模板看 Helm Chart 指南;机器规格看 购机清单。方向变更的决策记录见 ADR-0008


一、为什么换方向

原方案(git 仓库存客户配置 + SOPS 加密 + Argo 拉模式)是按 3 个独立部署客户设计的。

2026-07-28 规模重估:独立部署客户约占总客户三成,每年 100~200 个 —— 约每周 4 个新客户上线。按这个量级重算,原方案里好几处"手工也还行"的地方全部不成立:

原做法3 客户100~200/年
new-customer.sh 建客户目录、跑 SOPS 加密一年 3 次每周 4 次
登客户机器跑 deploy.sh 升级每月约 10 次每天约 5 次(按每客户年升 6 次算,1200 次/年)
age 主私钥人工保管一把钥匙一把钥匙锁着 200 个客户的生产库密码
交付基线 checklist 人工执行半天/客户每周 2 人日纯点鼠标

所以客户配置从 git 挪进 nuwa,交付与升级都从 nuwa 发起。

二、目标形态

      流水线                          nuwa(中心)                      客户集群
  ┌────────────┐                ┌──────────────────────┐        ┌──────────────┐
  │ 打 tag     │ ──镜像版本上报→ │ 版本库               │        │              │
  │ build.sh   │                │ 客户 + 部署配置      │        │  k3s         │
  └────────────┘                │ 依赖校验             │        │   ├ gateway  │
                                │ **服务端渲染 YAML**  │        │   ├ public   │
   运维在后台:                  │ 分批/熔断/版本看板   │        │   ├ admin-ui │
   建客户→填中间件参数          └──────────────────────┘        │   └ nacos    │
   →勾选应用→下载脚本                 ↑         ↓                └──────────────┘
                                     │         │                       ↑
                                心跳上报   下发目标版本            脚本/agent
                              (通道已有) (出参新增字段)

关键:"点击升级"不需要新通道。 License V2 的心跳(POST /license/heartbeat)已经在上报 InstallID / InstanceUUID / EgressIP,响应里也已经能下发指令(Revoked / Status / Message)。入参加 currentVersion、出参加 targetVersion,链路就通了。

三、保留什么,废弃什么

处置说明
配置外部化(prod profile)✅ 保留最大的一块,任何模型下都是前提 —— 没有它就不存在"一个镜像跑任意客户"
hiapi-chart✅ 保留换个人拿着而已。k8s 清单模板出自这里,不管由 helm 本地渲染还是 nuwa 服务端渲染
统一 CI(build.sh + tag 触发)✅ 保留上面再加一层"镜像版本上报 nuwa"
依赖矩阵(requires/recommends)✅ 保留渲染期的硬校验 + 后台勾选时的缺口提示
购机清单✅ 保留采购由专职同事执行
建库 Job / Nacos 初始化 Job✅ 保留踩过坑才有的东西,换包装照样需要
hiapi-customers + SOPS/age废弃客户配置改由 nuwa 承载。age 主私钥备份那个高危待办随之消失
Argo 拉模式❌ 废弃改为 nuwa 中心编排 + 客户侧轻量 agent,理由见 §四第 2 条
交付操作手册❌ 已删除整篇是 SOPS 流程。其中仍成立的部分并入 Chart 指南 §十二、§十三

四、已定的设计决策

1. YAML 由 nuwa 服务端渲染,脚本只负责 kubectl apply

客户机器要装改模板后
脚本内嵌 helm + charthelm客户要重新下载
nuwa 渲染完整 YAML只要 kubectl客户什么都不用做
脚本带 token 从 nuwa 拉 charthelm自动

选服务端渲染。最符合"nuwa 是中心",客户机器依赖最少,而且渲染出错发生在我们这边,能看见 —— 不是等客户截图发过来。

实现上 nuwa 容器里带一份 chart + helm 二进制,shell out 渲染即可。

2. 不用 Argo,但 Argo 的能力在 200 集群下是刚需

3 个客户时可以说"这些能力用不上",200 个集群时全是刚需:

能力Argonuwa agent 路线
状态回流(谁在跑什么版本)部分,且每集群一个 UI心跳通道已有
分批灰度❌ 要自己搭✅ nuwa 服务端逻辑
健康判定✅ actuator readiness 已有
自动回滚⚠️ 要自己写
失败熔断✅ nuwa 服务端

Argo 是 200 份安装、200 个 UI、200 次自身升级,而且天生不向中心汇报。agent 路线更贴合中心化模型且更省 —— 但它不是"30 行 CronJob",必须从第一天就带健康判定和自动回滚。

3. 勾选清单 = App 表,一条不多一条不少

后台"部署哪些应用"列出的就是 apps 表里全部已上线(ONLINE)应用,全部人工勾选。

GET /v1/deploy/catalog 的返回里只有 App 表的字段 —— service(= App.project)、 nameappIdiconUrldescription,没有 chart 的依赖 / 镜像 / 数据库 / 底座标记。

这条要守死。反面教材(2026-07-29 踩过):接口里捎带了 chart 的 requires,前端就拿它画了 一行"缺依赖:public、finance、socket"。客户在应用商店里根本没建过这些应用 (本地测试环境只建了 vem),却在交付页面上看见了这些名字。 只要 chart 的服务名能流到这个接口,前端迟早会把它画出来。

于是相应地:

  • chart 里有、App 表里没登记的服务不列出来。要让某个服务能被勾,去应用商店登记成应用。
  • 底座不单列一组,mandatory 也不再下发给前端。底座 = chart 里 enabled: true 的服务, 界面上看不见,渲染时默认开启。当前是 nacos + gateway + admin-ui + task-worker (nacos 走 nacos.deploy,不在 services 里)。
    • public 不在底座里(2026-07-29 定),它是普通应用。但网关把 /cloud-api/** 全路由 到它,登录和验证码都在那儿,实际上每个客户都得勾
    • 底座里还差一个 web-ui(H5 构建产物的静态站)—— chart 里还没有这个服务。 那份产物落在 hiapi-h5-dist PVC 上,而该 PVC 要 RWX 且写死 aliyun-nas,k3s 环境没有, 要先解决这个才能加进来。
  • 不做依赖联动,也不显示依赖提示。勾 vem 不会自动勾上 finance/user/socket, 页面上也不提这回事。依赖校验留给渲染期 —— templates/services.yaml 本来就有一道 fail。
  • 列表页显示 selectedServices(人工勾的),不是 enabledServices,后者并进了底座。

chart 仍然是渲染期部署事实的唯一来源,靠 App.project == chart 里的服务名 对上 (project=vemservices.vem),但那是 M2 在服务端用的,不经过这个接口。

对不上的两种情况:

  • App 的 project 在 chart 里没有同名服务 → 照常列出、照常可勾。应用商店比 chart 先一步登记新应用是常态,在这里拦住只会让后台连保存都保存不了。 服务端把它记进 Resolution.Unknown,渲染时默认拒绝下发(可确认后跳过),见 §M2。
  • App 没填 project → 不进清单。没有它对应不到任何服务,勾了也装不上。

4. 中间件密码存 nuwa,列加密

用已有的 MASTER_KEY 环境变量(License V2 已在用)做列加密,不新增配置项。

⚠️ 代价要认清:一个库里躺着所有客户的生产数据库密码,nuwa 自己成了最高价值目标。 另外下载的脚本里含明文密码,它是一次性凭据不是普通文件 —— 要有时效、用完即弃。

5. 装机脚本和部署脚本分开

  • install-k3s.sh —— 一次性:装 k3s、挂数据盘、--disable=traefik 并装 ingress-nginx(chart 用的是 className: nginx,不换会导致 Ingress 不生效)
  • nuwa 下发的 deploy.sh —— 反复跑:装 / 加应用 / 升级

合成一个的话,"我只想升个级"会变成"我得担心它会不会把集群重装了"。

6. Flyway 推后,但"客户自助升级后端"必须等它

业主决定推后(系统未完善、客户少、数据量小)。这个决定本身合理,但要记清它锁住了什么:

ddl-auto: update 下不能把升级按钮交给客户。 前端能放心给客户点,是因为它无状态、可秒退;后端两条都不成立:

前端(已实现)后端
有没有状态数据库 schema + 在途事务
回滚换回旧文件,秒级无损换回旧镜像,但 schema 回不去
失败何时暴露立刻,白屏可能几小时后 —— 某个定时任务、某个支付回调
最坏后果刷新即恢复数据已经写坏

ddl-auto: update 具体缺的是:不知道改了什么、不能预演、不能回滚、大表加列可能锁表。1200 次/年 × 不可预测的 DDL。

所以 M6(客户自助升级)排在 Flyway 之后,不是排期问题,是安全问题。

五、数据模型

新建 internal/deploy 包,不塞进 internal/license license 管"能不能用"(授权/配额/吊销),deploy 管"装什么"(拓扑/版本),两者变更节奏完全不同 —— 混在一张表里,以后每次改部署配置都要动授权代码。

复用已有的 lic_customers,不要新建客户表

dep_customer_deployments        一客户一条(customer_id 唯一索引兜住)
  id
  customer_id        → lic_customers.id
  release_name       默认 hiapi;⚠️ ACTIVE 之后不可修改(PVC/Service 名都带它)
  namespace          默认 hiapi
  ingress_host       必填 —— 没有它 Ingress 渲染不出来
  target_version     产品版本;留空回落 chart 的 appVersion
  middleware         JSON:四组 {deploy, host, port, user, database?, vhost?} —— 地址明文
  credentials_enc    MASTER_KEY 加密:mysql/redis/rabbitmq/nacos 四个密码(blob)
  selected_services  JSON 数组:用户实际勾的业务应用
  enabled_services   JSON 数组:勾选项 + 必装底座(不做依赖闭包)
  status             DRAFT / ACTIVE / SUSPENDED
  created_at / updated_at

dep_deployment_revisions        每次下发存一版,用于回滚和审计
  id, deployment_id, version, rendered_sha256, chart_version, operator, note, created_at

为什么存两份服务列表:selected_services 回填勾选框(必装底座不该出现在勾选框里), enabled_services 是下发时真正要装的集合。只存一份,要么回填时分不清底座,要么下发时 得按当时的 chart 重算 —— 而 chart 的底座集合是会变的。

dep_install_status(回流:install_id / current_version / healthy / last_report_at / message) 推到 M4 再建 —— M1 阶段建了也没人读写。

已有可直接复用的:lic_customers(客户)、lic_installs(部署实例台账 + 心跳 + egress IP + 最后下发版本)、apps/app_versions(Other JSON 里后端存 {docker: ...})、/v1/releases/backend(CI 上报)。

六、开发顺序

按此顺序做,每一阶段都能单独验收。

M0 前置(不在 nuwa 里,但阻塞一切验证)

脚本和配置说明都已就绪,见另一个仓库的 hiapi-chart/ci/README.md(不在本知识库内,故不做链接) (build.sh 统一构建、check-images.sh 做 chart ←→ 源仓库 ←→ ACR 三方对账)。 剩下的是控制台操作 + 一个必须先定的决策,见下。

  1. Codeup 流水线配起来 —— 变量组 acr-credentials + 标签触发,一共 9 条流水线 (7 个 Java 走 jib 不需要 docker;socket-server 和 admin-ui 需要 docker)。 当前最硬的阻塞仍是:ACR 里还没有任何可拉取的镜像,chart 装上去也是一片 ImagePullBackOff。

    先手工推一个镜像,别先配流水线。流水线是为了重复,而现在要先证明这条路本身通: ACR 命名空间存在、凭据能登录、jib 能推、chart 引用的名字对得上。 四件事任一不成立,流水线配得再漂亮也没用。

    ⚠️ ACR 仓库可见性是 M0 剩下的唯一设计决策,未决。 公开仓库谁都能拉, 而 hiapi-cloud-public 绝不能公开(JNI 授权保护模块在里面)。走私有仓就必须解决 pull secret,而 chart 支持 global.imagePullSecretsnuwa 的部署配置里却还没有这个字段。 正经做法是每客户一个 ACR 子账号/授权令牌,由 nuwa 渲染时把 secret 一起渲进去 —— 这跟"license 到期即停发凭据"是同一套闸门,应该一起设计。 底座三个(gateway / task-worker / admin-ui)都不含保护模块,可以先用公开仓把 M0-3 的最小闭环验通,不必等这个决策。

  2. chart 补 ServiceAccount + RBAC —— publicK8sJobClient 要在集群内建 Job 打包 web-ui,清单在 hiapi-cloud-public/deploy/h5-prereqs.hiapi-cloud.yaml 但 chart 里没有,现在装出来会 403。权限就停在"建 Job + 读日志",不要给 ClusterRole

  3. 最小闭环手动跑通一次 —— gateway + public + admin-ui + nacos。验收清单见 Chart 指南 §十二

⚠️ M0-3 不要跳过。 1200 次/年的自动化,得先有一次手动成功过。已经踩到两个"开发环境替我们兜住了的前置条件"(Nacos 空配置、业务库不存在),第三个大概率还在等着,只有真集群才会暴露。

M1 客户部署配置 ✅ 已完成(2026-07-28)

落在 hiapi-cloud-nuwa/internal/deploy/:目录解析(catalog.go)、模型与仓储、服务层校验、 /v1/deploy/* 管理接口(platformOnly),后台页面在 store-web/src/views/deploy/

chart 是内嵌的:scripts/sync-chart.sh 把 hiapi-chart 复制进 internal/deploy/chart/, go:embed 打进二进制,服务目录与依赖矩阵全部解析自它的 values.yaml改完 chart 必须重跑同步脚本,否则 nuwa 用的还是旧矩阵。M2 的渲染复用同一份。

必选底座不是硬编码列表,而是"chart 里 enabled: true 的那些" —— 加底座服务只改 chart。

勾选清单 = App 表(2026-07-29 改):GET /v1/deploy/catalog 返回 apps 表里全部 已上线应用,附上能对应上的 chart 依赖关系,见 §三-3。适配器在 main.go(deployAppLister), deploy 同样不 import internal/app。原来的 POST /v1/deploy/resolve 已删除 —— 不做联动之后 前端不需要每次勾选都往服务端跑一趟,依赖缺口按目录里带下来的 requires 做一次集合差就够了。

⚠️ Resolution.Unknown(勾了但 chart 里没有同名服务的应用)的处理已定: 渲染默认拒绝,确认后可带 allowUnknown 跳过,见 §M2。

deploy 不 import license:凭据加密和客户查询走 Cipher / CustomerLookup 两个接口, 由 main.go 注入。M4 起 license 的心跳要反向查 deploy 的目标版本,直接互相 import 会成环。 密钥复用 license 的 MASTER_KEY,不另引一把"丢了就永久解不开"的东西。

M2 渲染与下发 ✅ 已完成(2026-07-30)

POST /v1/deploy/deployments/:id/render → 渲染 → 打包 → 一次性下载券; GET /v1/deploy/artifacts/:token 免登录下载。后台在部署配置详情页底部两个按钮 (预检渲染 / 渲染下发)。代码在 internal/deploy/{values,render,bundle,artifact,deliver}.go

用 helm 官方 SDK,不是内嵌 helm 二进制。 原计划写的是"内嵌 helm 二进制", 实际改用 helm.sh/helm/v4 库:二进制是平台相关的(要为每个 GOOS/GOARCH 各塞一份), 而 nuwa 本身就是单文件交付。代价是 go.mod 的 go 版本要 ≥1.26、二进制从 20M 涨到 47M。 SDK 大版本必须跟交付同事本机的 helm 对齐,否则"和本地渲染一致"这条验收无从谈起。

验收已做成用例:TestRenderMatchesHelmCLI —— 本机装了 helm 就真的跑一次 helm template 逐字节对比,没装则跳过。目前对 helm v4.2.3 是逐字节一致

几个定下来的决定:

  • Resolution.Unknown 默认拒绝下发,报错点名是哪些应用。静默跳过等于客户买了 A 却没装 A,而且没人会发现。确认后可以带 allowUnknown 跳过,跳过的会写进警告和包里的 README。
  • 硬依赖缺口也是硬错误。chart 里本来就有 fail,但那时错误藏在 helm 的模板报错里; 在 nuwa 里拦住才能给出"vem 需要 socket"这种能直接照做的提示。
  • 启用清单按下发那一刻的内嵌 chart 重算,不照抄库里的 enabledServices。 二进制里的 chart 才是这次要渲的那份;chart 加了底座服务、改了依赖矩阵,用旧列表会漏装。 重算有变化 → 警告 + 更新库里的列表 + 写进 Revision 留档。
  • 密码由服务端解密(和 credentials_enc 同一把 MASTER_KEY),不再要人工带 —— 这是和手工脚本唯一的实质差别。
  • 交付包 = manifests.yaml + deploy.sh + README.txt,不是裸 YAML。因为清单里带着 helm 的两个 post-install hook Job,而 kubectl 完全不认 hook 注解,Job 的 spec 又 不可变 —— 重装时直接 apply 会撞 field is immutable。这个坑必须由脚本兜住 (先 delete job 再 apply),不能指望现场同事记得。
  • 配置在包被取走的那一刻从 DRAFT 变 ACTIVE(不是渲染的那一刻)。包一旦离开 nuwa, 就得当成客户集群里已经有这套资源来对待,release 名/命名空间从此不可改。

下载券的四条安全约束(任一条破了,这张表就成了"所有客户生产密码的公开货架"):

  1. 明文 token 只在渲染响应里出现一次,库里只存 sha256 —— 拖库拿不到下载链接
  2. 包体用 MASTER_KEY 加密落库(里面有四个明文中间件密码)
  3. 下载即作废,并立刻清空密文,只留 sha256 供事后核对
  4. 到期未取的,下一次渲染时顺手清掉密文(没有后台任务 —— 券只在渲染时产生,这个时机够了)

遗留:chart 支持 global.imagePullSecrets,但 nuwa 的部署配置里没有这个字段, 私有镜像仓库的客户还装不了。这条归 M0(镜像发布)一起解决。

M3 装机与安装脚本 ✅ 已完成(2026-07-30)

install-k3s.shdeploy.sh 都在交付包里(internal/deploy/assets/install-k3s.sh, go:embed 进二进制,随每个包分发)。空机器上两条命令:

bash
sudo ./install-k3s.sh    # 一台机器只跑一次
./deploy.sh              # 下发这个客户的清单

install-k3s.sh 与客户无关、不含任何密码,但仍然放进包里 —— 比让现场同事另外找一份靠得住, 少一次"你用的是哪版脚本"的排查。幂等:已经装过 k3s 就只做体检。

为什么是 k3s:单节点私有化下它一次性给了三样我们需要的东西 —— traefik(Ingress)、 servicelb(不用买 SLB)、local-path(默认 StorageClass)。换 kubeadm + ingress-nginx 要多拉一堆镜像、多三个组件要盯,收益是零。

脚本做的体检(都是能让交付白跑一趟的前置):CPU/内存/磁盘对照购机清单、 80/443/6443 占用、NTP 是否同步(授权票据带有效期,时钟偏了会表现成"授权无效", 而没人会往这个方向想)、swap、发行版是否为基线的 Ubuntu 22.04。 装完等到:节点 Ready、traefik rollout 完成、默认 StorageClass 存在 —— 少等 traefik 就会出现"脚本说装完了,但域名还不通"。

⚠️ 顺带修掉一个真会炸的不一致:chart 的 ingress.className 默认是 nginx, 而基线装出来的是 traefik。这种错的表现是 Ingress 建得出来、看着一切正常, 但没有控制器认领它,域名一直 404,没有任何日志指向它。现在: chart 默认改成 traefik,nuwa 部署配置里加了「Ingress class」字段并显式渲进 values (不吃 chart 默认值,免得存量配置随 chart 漂),注解按 class 自动切换 (ingress-nginx 少了 proxy-body-size 会把上传卡在 1MB)。

验收:空机器 → 两个脚本 → 后台可登录。这一步还没在真机器上跑过 —— 等 M0(ACR 里有可拉取的镜像)之后连着 M0-3 一起验,那也是这条路径第一次真正落地。

M4 版本回流

心跳入参加 currentVersion,dep_install_status 落库,后台出版本看板。

验收:后台能看到每个客户在跑什么版本、健康不健康。

M5 自升级 agent

心跳出参加 targetVersion;agent 侧健康判定 + 自动回滚(记住升级前版本,探针 5 分钟不通即回滚,回滚后立刻上报并停止再试 —— 不能陷入升级-回滚循环);nuwa 侧分批 + 失败率熔断。

验收:后台改一个版本号,客户集群自己升上去;人为让新版本起不来,能自动退回。

M6 客户自助升级(需要 Flyway)

⚠️ 前置:Flyway 落地 + 版本分级。

级别内容谁能点
safe纯代码,无 DB 变更客户自助
schema有 Flyway 迁移且向后兼容客户可点,强制提示
breaking不兼容变更只能我们排期推

大部分版本是 safe 级 —— 按钮 80% 的时间可用,剩下 20% 走人工闸门。比全自助安全,比全人工轻松。

配套纪律(是规范不是代码):N ↔ N+1 双向兼容 —— 只加列不删列、不改类型、不加非空约束;删除要跨两个版本(先停用,下一版再删)。滚动更新期间新旧 Pod 同时连同一个库,不守这条会在升级窗口内出错。

七、未决

  • internal/deploy 独立成包(本文档的建议)还是并进 internal/license —— 待定
  • 客户机器采购由专职同事按购机清单执行,暂不做 IaC。200/年按理已过自动化门槛,若采购成为瓶颈需重议
  • hiapi-chart 仓库仍只有本地 commit,未定远端
  • hiapi-customers 目录尚未物理删除(有 commit 无远端,删除不可恢复)