Appearance
子应用交付 — 发布 / 部署 / 安装 / 升级 全流程
2026-08-03 定调。这份文档回答一个完整问题:一个子应用从开发者发版,到出现在某个客户的集群里、 在客户后台被点"安装"并可用,中间到底发生了什么,以及哪些环节还不存在。
决策记录见 ADR-0009。 私有化交付的整体方向与里程碑在 nuwa 交付中心(那里是主文档,本篇是它的下游细化)。 H5 合并构建的细节在 H5 发布流水线规划。
一、两个平面,先分清
整件事之所以容易乱,是因为"安装"这个词在两个平面上各有一个含义,而代码里只实现了第二个:
| 平面 | 做什么 | 谁执行 | 现状 |
|---|---|---|---|
| 部署面 | 在客户 k8s 里把这个服务的 Deployment/Service/Ingress 跑起来 | hiapi-agent | 缺 |
| 业务面 | 把子应用的菜单 / 组件 / 页面 / API 元数据装进 public 的库 | public AppStoreLogic | ✅ 已有 |
现在客户后台点"安装",只发生第二件事,而且它的前提是第一件事已经由人工完成了 (AppInstallTask 每 60 秒 Feign 探活,探得到就把 CloudApplication.deploy 置 1)。
二、现状:四条链路各自到哪了
1. 发布(开发者 → 应用市场)
| 制品 | 产出 | 落点 | 消费方 |
|---|---|---|---|
| 前端 zip / components.json / pages.json / menus.json / admin.json / umd.js | 子应用 pnpm build:h5 + UploadPlugin | nuwa OSS hiapi-cloud/<project>/<version>/ | public AppStoreLogic.loadApp() |
| 后端镜像 | jib → ACR | ACR + dep_image_builds(CI 上报) | chart 渲染拼 image.registry/<image>:<tag> |
| 应用元数据 | 开发者门户建 App | apps / app_versions | 市场接口 + nuwa 勾选清单 |
⚠️ 版本真源有三处且互不认识:app_versions(市场说有什么版本)、dep_image_builds(ACR 里真有什么镜像)、 chart 的 appVersion / TargetVersion(这次装什么)。现在没有任何校验保证三者一致。
2. 建客户 + 勾选(nuwa)
lic_customers + lic_licenses + dep_customer_deployments(一客户一条,selected_services 存人工勾的应用)。 勾选清单 = apps 表全部 ONLINE 应用(见 nuwa 交付中心 §三-3)。
⚠️ "可用应用"与"本次部署"合成了一个列表,见 §三。
3. 一键部署(nuwa → 客户机器)✅ 代码完成
POST /v1/deploy/deployments/:id/render → helm SDK 服务端渲染 → 打包 manifests.yaml + deploy.sh + install-k3s.sh + README.txt → MASTER_KEY 加密落库 → 一次性下载券 (下载即作废、包被取走时 DRAFT→ACTIVE)。
⚠️ 从未在真机跑过(卡在 M0:ACR 里还没有可拉取的镜像);单向,无回流 —— nuwa 不知道客户装成功没有、现在跑什么版本。
4. 客户后台安装 / 升级 / 卸载(public)
install → Feign 调子服务 /install → 拉 OSS 的 menus/admin/components/pages.json → 入库
uninstall→ Feign 调子服务 /uninstall → 删菜单
upgrade → Feign 调子服务 /upgrade → 重新拉一遍全程不碰 k8s。CloudApplication 的两个状态字段的真实含义:
| 字段 | 含义 | 谁改 |
|---|---|---|
deploy 0/1 | Pod 在不在集群里跑(Feign 探活推断) | AppInstallTask 定时任务 |
install 0~4 | 菜单/组件/页面装没装 | 人点按钮 |
三、目标架构
1. 三层数据,各管一件事
lic_licenses(已有:配额 / 到期 / 环境 / enforce)
└── lic_license_apps 【商务面】这把授权码可用哪些应用
dep_customer_deployments
selected_services 【交付面】首次一键部署装哪些(⊆ entitlement)
desired_services 【运行面】集群里现在该跑哪些 = 期望状态为什么挂 License 不挂 Customer:License 已是配额/到期/环境的载体,PROD 与 DEV 可用应用不同是常态, "到期即停用"直接复用现有闸门。
2. 执行链路
nuwa(期望状态唯一真源)
↑ 回流 ↓ 拉取 desired state
客户集群 ┌── hiapi-agent ── apply / rollout / 健康判定 / 自动回滚 ──→ workload
└── public ── "我要装 vem" ──→ nuwa 提交意图(不碰 k8s)- 客户后台的按钮 = 修改期望状态的请求,不是执行动作。
- nuwa 收到意图后校验:授权 ✓ 配额 ✓ 未到期 ✓ 硬依赖已开齐 ✓ 该版本镜像在台账里真实存在 ✓。
- agent 是客户集群里唯一有 workload 写权限的组件,必须自带健康判定与自动回滚 (回滚后立刻上报并停止再试,不能陷入升级-回滚循环)。
- 交付包保留为首次装机 + 救援通道,日常增删改全走 agent。
理由与被否掉的备选见 ADR-0009。
3. 客户后台的应用是三态
| 状态 | 含义 | 来源 |
|---|---|---|
| 已授权 · 未部署 | 买了,集群里没跑 | nuwa entitlement |
| 已部署 · 未安装 | Pod 在跑,菜单/组件未装 | agent 回流(不再靠 Feign 探活) |
| 已安装 | 可用 | CloudApplication.install |
四、端到端流程(目标)
新客户
- nuwa 建客户 → 建 License(PROD)→ 勾可用应用(entitlement)
- 勾首装子集(底座 + 勾选项)→ 填中间件 / 域名 / imagePullSecret
- 渲染下发 → 下载一次性交付包
- 客户机器:
install-k3s.sh→deploy.sh(顺带装 agent,用包里的 install_id + license 向 nuwa 注册) - agent 上线 → 回流 → nuwa 后台看得见"这客户跑着什么、健不健康"
后期加应用(两条路,同一个终点)
- 我们推:nuwa 勾上 → 期望状态变更 → agent apply → public 收到"新服务就绪" → 逻辑安装
- 客户自助:客户 platform 后台看到「已授权 · 未部署」→ 点安装 → public 提交意图 → nuwa 校验 → 同上
升级
分级(见 nuwa 交付中心 §M6):safe 客户自助 / schema 客户可点+强提示 / breaking 只能我们排期推。 后端自助升级必须等 Flyway —— ddl-auto: update 不可预演、不可回滚。
卸载
定义为:停 workload + 逻辑卸载(删菜单/组件),PVC 与数据库保留,列入"待清理"由人工确认后删。 私有化环境没有回收站,删库不可逆。
五、admin-ui:打静态产物镜像,不做运行时构建
结论:必须打成静态产物镜像(这事 hiapi-cloud-admin-ts/deploy/README.md 开头已声明废弃运行时构建)。 私有化下还要再加两条:
- 客户内网没有 git、没有 npm registry,运行时构建直接不可用;
- 运行时构建 = 每个客户集群里都躺着一份 admin-ui 完整源码。admin-ui 里没有 JNI 保护模块, 源码就是全部。旧方案还把 deploy key 打进了镜像层。
"每次升级都要重新 build"这个顾虑不成立:admin-ui 不需要跟着子应用发版。 子应用后台界面走 qiankun 微应用,菜单/图标/i18n 随子应用自己发布(admin.json), 主应用运行时动态注册(src/utils/micro-apps.ts 已实现)。装新子应用不需要重新 build admin-ui; admin-ui 只在它自己代码改动时发版。CI 打镜像 + agent 换 tag 滚动更新是 20 秒的事,还能一键回滚。
⚠️ admin-ui 在私有化下真正的阻塞点不是打不打镜像,是子应用资源全指向我们的公网 OSS: micro-apps.ts 的 FALLBACK 硬编码 hiapi-cloud.oss-cn-shenzhen.aliyuncs.com, 后端 admin.json 里存的也是 OSS 绝对地址。内网客户打开后台,子应用界面白屏。 要做的是:子应用制品随交付落到客户自己的静态站,admin.json 的地址改成相对/可配置前缀。 这与 web-ui 的产物托管是同一个问题,一起解决。
六、web-ui(H5)与多端
H5 产物落点:RWO PVC + nodeAffinity
第 1 层(子应用 → OSS)已通;第 2 层(合并 + build:h5)有 CloudH5BuildService + K8s Job, 但产物落 hiapi-h5-dist PVC,要求 RWX 且写死 aliyun-nas,k3s 没有 —— 这是 web-ui 至今进不了 chart、底座缺一块的原因。
定:单节点私有化下直接用 RWO + local-path,构建 Job 与 web-ui Pod 用 nodeAffinity 钉在同一节点(单节点 k3s 本来就同节点)。多节点客户再换 NFS。 这是把 web-ui 送进 chart 的最小改动路径,不为一个还没出现的多节点场景先引入对象存储依赖。
同一个卷兼作 admin-ui 子应用制品的托管点,顺带解掉 §五 的白屏问题。
小程序 / APP:本期不做
本期只做 H5 闭环。小程序与 APP 的阻塞点不是技术能力,是商务资产在客户手里:
| 需要什么 | 谁持有 | 能自动到哪 | |
|---|---|---|---|
| H5 | 无 | — | 全自动 |
| 小程序 | appid + 上传私钥 + 人工提交审核 | 客户 | 最多到"上传体验版"(miniprogram-ci) |
| APP | 签名证书 + Apple/Google 账号 + 审核 | 客户 | 原生壳无法自动;子应用更新可走 uniapp wgt 热更新 |
等 H5 闭环真机验证通过后再单独规划。不要把三端做成同一个"发布"按钮 —— 三端的失败模式完全不同(H5 秒回滚、小程序卡审核、APP 要重新上架), 同一个按钮会让客户以为点完三端就都好了。
七、开发顺序
2026-08-03 夜间批次:P1~P4 代码全部落地(各仓库已分别提交,提交号与部署顺序见 工作区根
DEVELOPMENT.md「2026-08-03 夜间批次」节)。真机验证仍卡 P0/M0。
| 内容 | 状态 | |
|---|---|---|
| P0 | M0:ACR 推镜像 + 真机跑通一次 install-k3s.sh → deploy.sh | ❌ 硬阻塞,控制台操作 |
| P1 | entitlement 拆分:lic_license_apps + 市场接口鉴权/过滤 + nuwa 后台两步勾选 | ✅ 完成待部署 |
| P2 | hiapi-agent(新仓库)+ nuwa 期望状态/意图/回流(dep_install_status 前移进来) | ✅ 完成待部署(agent 需建远端 + CI) |
| P3 | public 改造:AppStoreClient 收口域名+凭据、/deploy 提交意图、新增 /intent | ✅ 完成待部署 |
| P4 | agent + web-ui 进 chart 底座(RWO PVC),ingress 加 h5Host;顺带修网关零路由 bug | ✅ 完成待部署(子应用制品本地化未做,归 B-13) |
| P5 | Flyway + 版本分级 → 客户自助升级后端 | 未动 |
| P6 | 小程序体验版 / APP wgt 热更 | 未动(本期不做) |
P1 一并收敛两件事:
- 版本真源定一处:
dep_image_builds是"有什么版本可装"的真源,chart 只管拓扑,app_versions只管前端制品。 - 客户 ↔ nuwa 通道收敛:现有 license 心跳 / agent token 上报镜像 / 一次性下载券三条, 加上 agent 就是第四条。统一成
install_id + license的双向鉴权,否则每加一个能力就加一条通道。
八、二次评审:已识别缺口与边界(2026-08-03)
定案后对着现有代码(chart hook Job、deliver.go、secrets.yaml、license 加密设施)复查一遍, 按"不先定就会返工"的程度分三档。
A. P2(agent)设计前必须定
- agent 的 apply 单元 = 全量渲染清单,不做单服务增量。 增量装应用时新服务的库不存在、Nacos 占位配置不存在,Pod 必然 CrashLoop; 而全量清单里的 db-init / nacos-bootstrap 两个 hook Job 是幂等的,重跑即补齐。 agent 复用交付包 deploy.sh 的既有逻辑:先删 hook Job 再 apply(Job spec 不可变)。
- 卸载必须有 prune。
kubectl apply不删除"从清单里消失"的资源 —— agent 必须保存上一份清单做 diff 删除,否则"卸载"根本删不掉 workload。 连带要求:agent 本地要有 last-applied 状态,且重启后能恢复。 - 期望状态通道里有明文生产密码。 全量清单含
hiapi-credentialsSecret(中间件四密码), 日常通道等于持续的密码分发通道。必须端到端加密:按 install 派生密钥加密 payload (可复用 license 的 per-customer ModuleKey 模式),agent 解密即 apply、不落盘。 - 防 nuwa 被打穿后的供应链投毒。 nuwa 沦陷 = 能向全部客户集群推任意镜像。 期望状态用 Ed25519 签名(license 已有同款设施),agent 验签; 镜像仓库前缀白名单写死在 agent 侧,不来自期望状态本身。
- agent 不能自升级(与 public 自杀是同一个问题)。策略:agent 保持极简、低频变更; 升级 agent 本身走交付包救援通道(重跑 deploy.sh),不走 agent 自己。
- 救援包与 agent 的共存规则。 人工 apply 救援包后集群偏离期望状态, agent 下一轮 reconcile 会覆盖回去。救援前必须在 nuwa 侧把该客户置为维护模式 (agent 暂停 reconcile),修完再恢复;否则救援动作会被静默撤销。
B. P1 / P3 开工前必须定
授权到期的收敛行为已定并落地(2026-08-03 业主拍板:宽限 7 天)。 自然到期后 7 天内授权照常生效(license.EntitlementGrace);超宽限由SweepEntitlementExpiry每小时自动提交 UNINSTALL 意图摘除 workload(source=system-expiry, 审计可查,数据永远保留)。SUSPENDED 是管理员手动冻结,立即生效无宽限。 遗留:客户后台的"即将到期"横幅(public UI)未做。- 版本兼容矩阵。 客户自助装"最新 stable",但没有任何数据说明"vem 1.3 需要 public ≥ 1.2"。
app_versions要加最低依赖版本字段,意图校验时比对客户现跑版本;缺这个,自助安装就是撞运气。 - 我们自己的 SaaS 现网也调同一批市场接口。 P1 给
/v1/market/*加鉴权时, 给自己的 SaaS 平台也发一张 license(吃自己的狗粮),不留匿名后门。 - 配额联动。 装新应用会增加 JVM 实例数,EnforceQuota 客户可能"装成功即被授权拒绝心跳"。 意图校验要预检 maxInstances,不够时明确报"需扩配额",不是装完才炸。
网关路由生效方式待核实已核实并修复(2026-08-03)。 路由不是 discovery locator, 是 Nacos 配置中心里的路由表(hiapi-cloud-gateway-<profile>.yaml,NacosDynamicRouteRepository读取,改配置热生效、无需重启)。核实中挖出真 bug: 全新私有化交付时网关是零路由 —— prod 的config.import是 optional, bootstrap Job 播种的占位配置只有注释,网关起来后没有任何路由,客户装完连登录都不通。 已修:基础路由表(user/public/finance/vem/shop/store 六条)静态化进 gateway 的application-prod.yaml(随镜像走,装完即有;也是治理规划 T6"路由收回代码仓"第一步); Nacos 配置与静态表合并生效,第三方/新增子应用的路由继续在 Nacos 热添加。 路由到未部署服务无害(调用时 503),所以静态表不随勾选裁剪。 另:CloudApplication.gateway字段只写不读,且 nuwa 市场接口不返回该键(写入的是 null), 是死字段 —— 第三方应用的路由前缀登记将来放 App 元数据时要新设计,别复用这个字段。 11.5 对外标识口径已定(2026-08-03 业主拍板,读法 A):App.project=serverId= Nacos 服务名 = 镜像名(全名,如hiapi-cloud-vem),App 表不加字段; chart 的 services 短键(vem)降为 chart 内部细节,由 nuwa Catalog 的 alias 层 (全名 ↔services.<键>.image)桥接,意图/授权/到期收敛全部归一化比较。 应用注册规范:project 必须填全名,且与 chart 的 image 字段一致,否则渲染进 Unknown 被拒。 顺带修掉真 bug:镜像台账查询原用短键、而 build.sh 上报 IMAGE_NAME 全名,必 miss。客户自助安装是否需要审批已定(2026-08-03 业主拍板:不审批)。 三道硬校验(授权/依赖/镜像)即时裁决,意图全量留审计;将来要审批的话ChangeIntent已留 status 字段,加 PENDING 态即可。完全无外网客户的降级方案已定(2026-08-03 业主拍板:客户必须有外网,不做离线方案)。 交付前置条件写进合同/装机说明即可。- 升级粒度已定(2026-08-03 业主拍板):支持单应用升级,各应用版本不要求一致。
Deployment.ServiceVersions(服务→tag)渲染成services.<x>.tag;UPGRADE 意图带 service 即单应用、不带即整体;镜像台账按服务生效版本逐个校验。已落地。 - 镜像全部私仓(2026-08-03 业主拍板):客户使用我们发的仓库账号拉取。 agent 侧镜像前缀白名单已落地(默认 = ACR + chart 引用的公共镜像,
AGENT_ALLOWED_IMAGE_PREFIXES可覆盖);render 时 imagePullSecret 缺失亮警告。 每客户仓库账号的生成与"到期停发"闸门归 M0 一并设计。 - h5Host 已进部署配置(2026-08-03):可选域名字段,渲进
ingress.h5Host。
C. 边界写清楚,防止验收标准过度承诺
- Flyway 之前,agent 的"自动回滚"对 Java 服务只部分成立。 新 Pod 启动时
ddl-auto: update已经改了 schema,回滚旧镜像靠"加列不删列"的惯例兜底, 不是保证。P2 验收只承诺:workload 退回旧镜像并恢复流量;schema 级回退在 Flyway 之前不存在。 - web-ui 首次部署是空站。 产物 PVC 初始为空,底座装完必须触发一次 H5 构建才有站点。 触发方定义待选:agent 首次回流"web-ui 就绪"后 nuwa 提醒运营,或 public 检测空产物自动触发一次。
- 单节点即单点。 RWO + nodeAffinity 是单节点交付基线的选择,节点挂 = 全挂, 与购机清单定位一致;多节点客户换 NFS 时再解。
其余未决
- agent 的形态已倾向独立进程(A-1/A-5 的要求排除了"塞进 license 心跳"的做法),P2 设计稿定稿。