Appearance
私有化部署 — 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 + chart | helm | 客户要重新下载 |
| nuwa 渲染完整 YAML ✅ | 只要 kubectl | 客户什么都不用做 |
| 脚本带 token 从 nuwa 拉 chart | helm | 自动 |
选服务端渲染。最符合"nuwa 是中心",客户机器依赖最少,而且渲染出错发生在我们这边,能看见 —— 不是等客户截图发过来。
实现上 nuwa 容器里带一份 chart + helm 二进制,shell out 渲染即可。
2. 不用 Argo,但 Argo 的能力在 200 集群下是刚需
3 个客户时可以说"这些能力用不上",200 个集群时全是刚需:
| 能力 | Argo | nuwa 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)、 name、appId、iconUrl、description,没有 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-distPVC 上,而该 PVC 要 RWX 且写死aliyun-nas,k3s 环境没有, 要先解决这个才能加进来。
- 不做依赖联动,也不显示依赖提示。勾 vem 不会自动勾上 finance/user/socket, 页面上也不提这回事。依赖校验留给渲染期 ——
templates/services.yaml本来就有一道 fail。 - 列表页显示
selectedServices(人工勾的),不是enabledServices,后者并进了底座。
chart 仍然是渲染期部署事实的唯一来源,靠 App.project == chart 里的服务名 对上 (project=vem ↔ services.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 三方对账)。 剩下的是控制台操作 + 一个必须先定的决策,见下。
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.imagePullSecrets、nuwa 的部署配置里却还没有这个字段。 正经做法是每客户一个 ACR 子账号/授权令牌,由 nuwa 渲染时把 secret 一起渲进去 —— 这跟"license 到期即停发凭据"是同一套闸门,应该一起设计。 底座三个(gateway / task-worker / admin-ui)都不含保护模块,可以先用公开仓把 M0-3 的最小闭环验通,不必等这个决策。chart 补 ServiceAccount + RBAC ——
public的K8sJobClient要在集群内建 Job 打包 web-ui,清单在hiapi-cloud-public/deploy/h5-prereqs.hiapi-cloud.yaml但 chart 里没有,现在装出来会 403。权限就停在"建 Job + 读日志",不要给 ClusterRole最小闭环手动跑通一次 —— 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 名/命名空间从此不可改。
下载券的四条安全约束(任一条破了,这张表就成了"所有客户生产密码的公开货架"):
- 明文 token 只在渲染响应里出现一次,库里只存 sha256 —— 拖库拿不到下载链接
- 包体用 MASTER_KEY 加密落库(里面有四个明文中间件密码)
- 下载即作废,并立刻清空密文,只留 sha256 供事后核对
- 到期未取的,下一次渲染时顺手清掉密文(没有后台任务 —— 券只在渲染时产生,这个时机够了)
遗留:chart 支持 global.imagePullSecrets,但 nuwa 的部署配置里没有这个字段, 私有镜像仓库的客户还装不了。这条归 M0(镜像发布)一起解决。
M3 装机与安装脚本 ✅ 已完成(2026-07-30)
install-k3s.sh 与 deploy.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 无远端,删除不可恢复)