Appearance
私有化部署 — Helm Chart 指南
范围:
hiapi-chart这一个 umbrella chart —— 它是全部 k8s 清单的模板源,不管由谁渲染(正常路径是 nuwa 服务端渲染下发,见 nuwa 教程 §6.5;本机helm template用来调 chart 本身)。背景与分层看 总览与架构;机器规格看 购机清单。
一、为什么是一个 chart,不是每服务一个
服务形态高度同质:Spring Boot + 8080 + Nacos + 一个库。差异只有镜像名、库名、副本数、依赖谁。
11 份几乎相同的 chart 只会慢慢漂移 —— 改了 A 的探针忘了改 B,半年后没人说得清它们为什么不一样。所以全部由 templates/services.yaml 一个 range 渲染,差异写在 values.yaml 的 services 段。
加一个新服务 = 在 values 里加 6 行,不碰模板。
yaml
services:
newsvc:
enabled: false
type: java # java(Spring Boot) | web(nginx 静态站) | go
image: hiapi-cloud-newsvc
db: hiapi_cloud_newsvc # 注入 MYSQL_DATABASE;不写则不注入
replicas: 1
requires: [user] # 依赖的其它服务,安装时前置校验二、目录
Chart.yaml 中间件 dependency 声明(全部带 condition)
values.yaml 默认值;客户差异由 nuwa 渲染时叠加,不要改这里
files/
nacos-mysql-schema.sql nacos 建表脚本,随 chart 分发(客户可能没公网)
templates/
_helpers.tpl 中间件地址计算 —— "双模"的全部实现在这里
services.yaml 通用 Deployment + Service,range 渲染所有服务
nacos.yaml 自带 Nacos:建库/导表 initContainer + Deployment + Service
nacos-bootstrap-job.yaml 改口令 / 建 namespace / 播占位配置(post-install hook)
db-init-job.yaml 创建各服务的业务库(post-install hook)
rbac.yaml 需要调 k8s API 的服务的 SA/Role/RoleBinding(只有 public)
secrets.yaml 凭据 Secret + 缺失校验
ingress.yaml /cloud-* → 网关,/ → 管理后台
NOTES.txt 安装后提示三、中间件双模
deploy: true 用集群内实例,false 用客户自己的云托管:
yaml
mysql:
deploy: false
external:
host: rm-xxxxxx.mysql.rds.aliyuncs.com
port: 3306
user: root服务侧零改动。 它永远只读 MYSQL_HOST/MYSQL_PORT/… 这批环境变量,地址由 _helpers.tpl 算出来,服务不知道自己连的是 RDS 还是集群内 Pod。客户从集群内升级到 RDS = 改两行 values + 一次数据迁移。
四组开关:mysql / redis / rabbitmq / nacos,每组都是 deploy + external。
deploy: false 却没填 external.host 时,helm 直接拒绝:
Error: mysql.deploy=false 时必须填 mysql.external.host实现细节:bitnami 的 redis 单主模式 Service 名带
-master后缀(<release>-redis-master),mysql/rabbitmq 不带。写 helper 时踩过。
四、凭据
credentials.* 是唯一的密码来源
yaml
credentials:
mysqlPassword: ...
redisPassword: ...
rabbitmqPassword: ...
nacosPassword: ...不要在 mysql.auth / redis.auth / rabbitmq.auth 里另填密码。 三个 bitnami 子 chart 已经配成 existingSecret: hiapi-credentials,跟业务服务读同一个 Secret。
这是一个修过的坑,值得知道原委。 早期这里是断的:
mysql.auth.rootPassword留空,注释写着"由 secrets.yaml 填充",但根本没有这个机制 —— 子 chart 的值只能在values.yaml里静态给定,模板算出来的名字传不进去。实际结果是三个中间件各自随机生成密码,业务服务读的却是credentials.*,两边对不上。故障形态极难定位:中间件全部 Running、业务全部连不上、日志里只有认证失败,不会有人第一时间想到是 helm 生成了两套密码。
修法是三个子 chart 统一走
existingSecret。为此 Secret 名固定成hiapi-credentials(不带 release 前缀)。代价是同 namespace 装两个 release 会撞名 —— 私有化一客户一集群,可以接受。
缺密码时拒绝安装
Error: 缺少中间件密码:credentials.mysqlPassword、credentials.nacosPassword
请在客户 values 里填写(建议 SOPS 加密),或预建 Secret 后指定 credentials.existingSecret不生成随机密码,也不让 Pod 反复 CrashLoop 之后你去翻日志 —— 装不上要在 helm 阶段就说清楚缺什么。
Secret 变更通过 checksum/credentials 注解触发滚动,不必手动重启。
自带 Secret 时必须齐备 9 个 key
用 credentials.existingSecret 时,除了把 mysql.auth.existingSecret 等三处也改成同一个名字,Secret 里必须有这 9 个 key(key 名由 bitnami 和 Nacos 写死,不能改):
mysql-password mysql-root-password mysql-replication-password
redis-password rabbitmq-password rabbitmq-erlang-cookie
nacos-password nacos-auth-token nacos-identity-value不填 existingSecret 时,后三个由 chart 从 nacosPassword/rabbitmqPassword 确定性派生,不用管。
五、业务库由 chart 创建
各服务是 ddl-auto: update —— Hibernate 会建表、加列,但不会建库。全新交付的环境里业务库当然不存在,于是每个服务都在 Unknown database 'hiapi_cloud_xxx' 上 CrashLoop。
这和 Nacos 占位配置是同一类问题:早期验证连的都是已有库、已有配置的开发环境,只有全新交付才暴露。这类"开发环境替我们兜住了的前置条件"值得专门找一遍。
templates/db-init-job.yaml(post-install/post-upgrade hook)遍历 enabled 且声明了 db 字段的服务,逐个建库。没有 db 的(gateway / admin-ui / socket)自然不参与。
先 SHOW DATABASES 探再建,而不是直接 CREATE DATABASE IF NOT EXISTS —— 后者对一个没有 CREATE 权限、但库已由客户 DBA 建好的账号仍然会报 access denied,白白让整个交付卡住。探过再建的话,这种环境下 Job 直接通过。
字符集固定 utf8mb4,排序规则默认 utf8mb4_unicode_ci —— 与 ShardingConfig 里 connectionInitSqls 的 SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci 对齐,别让表和会话用两种排序规则。
客户 DBA 坚持自己建库时设 dbInit.enabled=false,手工建库清单见购机清单 §五。
⚠️ 数据库账号必须有 DDL 权限。 除了
ddl-auto: update,分表逻辑还会在运行时执行CREATE TABLE ... LIKE。只给增删改查权限的账号,服务起不来 —— 这条常与客户 DBA 的安全规范冲突,要在采购阶段就谈。
六、依赖校验
两个层级:
requires硬依赖 —— 少了核心功能就是断的,没开齐直接拒绝安装recommends软依赖 —— 少了只是某个功能降级,安装照常,NOTES.txt 里提示一句
Error: 服务 vem 依赖 socket,请一并开启:--set services.socket.enabled=true比装完之后在日志里发现"连不上 socket-server"要快得多。
依赖矩阵(照代码核过,2026-07-28)
每条边都有出处:Feign 接口的实际使用点(不是 pom 依赖)+ MQ 的发布/监听方向。业务直觉在这件事上不可靠 —— 下面三条都是核代码时才发现填错的。
| 服务 | requires | recommends | 依据 |
|---|---|---|---|
| gateway | — | — | 只经 Nacos 做服务发现,不直连任何服务 |
| public | — | — | 唯一的 Feign(FeignCloudApplication)指向自己 |
| admin-ui | — | — | 纯静态站 |
| user | public | — | FeignCloudCaptcha / FeignCloudConfig / FeignCloudRegion / FeignSender |
| finance | public, user | task-worker | FeignCloudPort、FeignUser;MQ ← public 的 MERCHANT_REGISTER |
| vem | public, finance, socket | — | FeignCloudConfig / FeignMerchant / FeignSender、FeignPayment;MQ → hiapi-socket-exchange |
| socket | — | — | 只消费 MQ |
| sy | public | — | 只有 FeignSender |
| task-worker | — | — | 无出向调用,被 finance 调 |
| shop | public, user | — | FeignCloudRegion、FeignUser / FeignUserAccount / FeignUserAddress |
| store | public, user | — | FeignCloudRegion、FeignUser |
只声明直接依赖。 校验是逐个已开启的服务各跑一遍的,传递闭包自动成立 —— 开 vem 会强制开 finance,finance 又会强制开 user。所以 vem 不必写 user,写了反而让人以为 vem 直连 user。
核出来的三处错
- finance → task-worker 漏了,而且它是软的。 只在 webhook 通道(
NotifyDispatcher.sendHttp)用,缺了不影响启动,但商户配的 HTTP 回调会一直投递失败重试,且不会有明显报错。这种"不致命但静默坏掉"的依赖正是recommends存在的理由:硬拦会逼所有 finance 客户多跑一个服务,不管又查不出来。 - vem 不直连 user。 原先写在
requires里,实际是经 finance 传递的。 - shop / store 代码里没有任何 finance 调用。 "支付复用 finance" 目前只是规划,原先
requires里的 finance 是照业务直觉填的,已移除。
顺带发现一个悬空监听:
FINANCE_ASSETS_CHANGE在 finance 里有EventFinanceAssetsReceipt监听,但全仓库没有任何地方发布它。不影响部署,记在 TODO.md 里待确认。
七、探针
Java 服务统一走 actuator 分组:
| 探针 | 路径 |
|---|---|
| startup / readiness | /actuator/health/readiness |
| liveness | /actuator/health/liveness |
不要改成 /actuator/health —— 原因见总览。
type: web / go 的服务用 healthPath(默认 /healthz)。
八、Nacos
社区没有能用的 chart(nacos-group/nacos-k8s 停在 1.x,对 2.x 的 gRPC 端口和鉴权改动都没跟进),所以自己写模板,不走 dependency。
存储用 MySQL 不用内嵌 derby
- 复用双模设计:
mysql.deploy=false时 Nacos 自动跟着连 RDS - 客户备份 MySQL 时顺带把 Nacos 配置也备份了,不必另交代一套 PVC 备份流程
- 将来扩集群不用做数据迁移
建库和导表由 Deployment 的 initContainer 负责,幂等(先探 config_info 表存不存在)。
初始化 Job 做的三件事
templates/nacos-bootstrap-job.yaml 是 post-install hook,在 Nacos 起来之后:
| 做什么 | 不做会怎样 |
|---|---|
改掉 schema 里写死的默认口令 nacos/nacos | 任何能连到 8848 的 Pod 都能改配置、摘服务实例 |
用 customNamespaceId 建 namespace | Spring 里填的 namespace 是 ID 不是名字,ID 不存在时配置读到空且不报错 |
| 给每个已开启的 Java 服务播一份占位配置 | user/finance/vem/sy 的 spring.config.import 是强制项,对应 dataId 不存在会直接启动失败 |
第三件事最容易被低估。全新交付的环境里 Nacos 是空的,这四个服务根本起不来 —— 而早期验证之所以没暴露,是因为当时连的是已有配置的开发环境 Nacos。
补充:
spring.config.import在application.yaml与application-prod.yaml中是叠加而非覆盖,所以在 prod 里改写成optional:未必真的生效。播种方案对两种语义都成立,不依赖这个判断。
dataId 规则是 <spring.application.name>-prod.yaml,而各服务的 spring.application.name 恰好等于镜像名。task-worker 没有 spring.config.import,用 services.task-worker.nacosConfig: false 排除。
几个必须记住的点
- 9848 必须开。Nacos 2.x 客户端走 gRPC,端口固定是主端口 +1000。只开 8848 的话注册看着成功,但订阅和配置监听会一直重连 —— 这种"半通"最难查。
- PVC 是 RWO,所以 Deployment 用
Recreate。默认的滚动更新会让新 Pod 抢不到卷、老 Pod 又不退出,直接死锁。 - 升级
nacos.image版本时,files/nacos-mysql-schema.sql要一并换成同版本的。 建表脚本是裸CREATE TABLE,没有IF NOT EXISTS。 - JWT 签名密钥(
NACOS_AUTH_TOKEN)不留硬编码默认值 —— 那等于把伪造管理员 token 的能力公开在 chart 里。改成由 release 名 +nacosPassword确定性派生。 dbInit.image(mysql 客户端)和nacos.bootstrapImage(curl)是两个外部镜像,无公网的客户环境要先镜像到自己的仓库。
全新安装头一两分钟业务 Pod 会重启几次
口令修改、namespace 创建、占位配置播种都得等 Nacos 自己先跑起来,只能放在 post-install hook 里,排在业务 Pod 之后。这段时间业务 Pod 登录 Nacos 会失败,表现为 CrashLoopBackOff。
Job 跑完就自愈,startupProbe 给了 300 秒足够扛过去。看到头一分钟的重启不要急着排查。
九、Ingress
/cloud-api /cloud-user /cloud-finance /cloud-vem /cloud-shop /cloud-store → gateway
/ → admin-ui/ 是 catch-all,必须排在最后。
模板里
admin-ui带连字符,Go template 会把.Values.services.admin-ui解析成减法,必须写(index .Values.services "admin-ui")。
十、bitnami 依赖已 vendored 进仓库(2026-07-30)
bitnami 把 chart 迁到了 Docker Hub 的 OCI registry(oci://registry-1.docker.io/bitnamicharts/…),国内 helm dependency update 直接 connection reset。这不只是开发时的麻烦 —— 客户环境同样拉不到,私有化部署的机器经常连外网都没有。
所以三个依赖包已经提交进仓库:
hiapi-chart/charts/mysql-12.3.5.tgz
hiapi-chart/charts/redis-20.13.4.tgz
hiapi-chart/charts/rabbitmq-15.5.3.tgz
hiapi-chart/Chart.lock ← 版本锁,和上面三个包对得上一共 248KB,.gitignore 里特意写了注释说明它们是故意提交的。开箱即可 helm template / helm install,不需要 helm repo add,也不需要 helm dependency update。
一个容易误判的点:condition: mysql.deploy 为 false 只是「不渲染」,不代表可以不下载。helm 在渲染前会校验所有声明的依赖都存在,缺了就报
Error: found in Chart.yaml, but missing in charts/ directory: mysql, redis, rabbitmq连 helm template 都跑不起来。所以哪怕客户四个中间件全用云托管,这三个包也必须在。
升级依赖版本
只在能连 Docker Hub 的网络下做(国内挂代理):
bash
helm repo add bitnami https://charts.bitnami.com/bitnami
HTTPS_PROXY=http://127.0.0.1:10809 helm dependency update ./hiapi-chart注意 helm 不读 macOS 的系统代理,必须显式给 HTTPS_PROXY 环境变量。 拉完把 charts/*.tgz + Chart.lock 一起提交,再跑一次 nuwa 的 scripts/sync-chart.sh。
global.imageRegistry 是雷,别用
我们自己服务的镜像仓库前缀在 image.registry,不在 global.imageRegistry。
原因:global.* 在 helm 里会下传给所有子 chart,而 imageRegistry 正是 bitnami 的保留键。填在 global 下等于告诉三个子 chart「你们的镜像也在我们的 ACR 里」(并没有),新版 bitnami chart 检测到镜像被换过会拒绝渲染:
⚠ ERROR: Original containers have been substituted for unrecognized ones.
Unrecognized images:
- registry.cn-shenzhen.aliyuncs.com/hiapi-cloud/bitnami/redis:7.4.3-debian-12-r0
...报错出在 redis 子 chart 的 NOTES.txt 里,信息里也不会提 global.imageRegistry,不知道这条规则基本查不出来。
global.imagePullSecrets 相反,共享是对的:客户内网仓库同时放我们和 bitnami 的镜像时,一个 pull secret 覆盖两边。
完全无外网的客户
bitnami 的镜像仍然指向 docker.io/bitnami/*,这批客户要把镜像也同步进内网仓库,按子 chart 各自指定,不要图省事回头用 global:
yaml
global:
security:
allowInsecureImages: true # 换过镜像必须显式声明,否则子 chart 拒绝渲染
mysql:
image: {registry: harbor.内网, repository: bitnami/mysql, tag: 8.4.5-debian-12-r0}十一、本地校验
没有集群也能验证模板正确性,依赖已 vendored,直接跑:
bash
helm template hiapi ./hiapi-chart -n hiapi -f 客户values.yaml建议至少覆盖这几条边界:依赖没开齐、密码缺失、deploy=false 没填外部地址、外部 RDS、完整依赖链。这几条都应该给出明确的中文报错而不是渲染出错误的 YAML。比如:
Error: 服务 vem 依赖 socket,请一并开启:--set services.socket.enabled=true渲染时会有一条
warning: destination for rabbitmq.ingress.tls is a table—— 那是 bitnami rabbitmq 子 chart 自身的值类型不一致,和我们无关,rabbitmq.ingress.enabled默认 false 也不会渲染出东西。可以忽略。
十二、装完之后看什么
bash
kubectl -n hiapi get pods -w头一两分钟业务 Pod 会重启几次,这是正常的。 业务库、Nacos 的口令/namespace/占位配置都由 post-install Job 在中间件起来之后才初始化,在那之前业务 Pod 连不上。Job 跑完即自愈,startupProbe 给了 300 秒足够扛过去。
看初始化进度(两个 Job,建库的先跑):
bash
kubectl -n hiapi logs job/hiapi-db-init -f
kubectl -n hiapi logs job/hiapi-nacos-bootstrap -fNacos 控制台(默认不经 Ingress 暴露):
bash
kubectl -n hiapi port-forward svc/hiapi-nacos 8848:8848
# http://127.0.0.1:8848/nacos 用户 nacos最小闭环验收
- [ ] 一条命令在空 namespace 拉起底座(gateway + public + admin-ui + nacos)
- [ ] 浏览器能打开管理后台并登录
- [ ] 增量开一个 L2 应用能装上,关掉能移除且不删数据库
- [ ] 中间件在集群内/外部之间切换,服务镜像无需改动
- [ ] 删掉 Nacos 数据后重装,配置能自动恢复
十三、常见故障
| 现象 | 多半是 |
|---|---|
| 中间件全部 Running,业务全部连不上,日志只有认证失败 | 密码写了两套。唯一来源是 credentials.*,别在 mysql.auth 之类地方另填 |
| 业务 Pod 头一两分钟 CrashLoop | 正常,等两个 post-install Job 跑完 |
| 服务注册成功但配置读不到、日志一直重连 | Nacos 的 9848(gRPC)没通 |
user/finance/vem/sy 起不来,报找不到配置 | Nacos 里缺 <服务名>-prod.yaml,看 bootstrap Job 有没有跑成功 |
服务起不来,报 Unknown database 'hiapi_cloud_xxx' | hiapi-db-init Job 没跑成功,或关了 dbInit.enabled 又没手工建库 |
| 建库/建表报 access denied | 数据库账号没有 DDL 权限 |
| public 打包 web-ui 报 403 | services.public.serviceAccount.create 被关掉、或 create: false 时没填 name,见 §十四 |
| Ingress 不生效 | class 对不上。chart 默认已改成 traefik(2026-07-30),与 k3s 自带的一致,不要再 --disable=traefik。客户已有 ingress-nginx 的,在 nuwa 的部署配置里把「Ingress class」改成 nginx(注解会自动跟着换,见下) |
helm dependency update connection reset | bitnami 迁到 OCI registry 了。正解是 vendored charts/*.tgz,不是换源。中间件全用外部实例时不受影响 |
十四、ServiceAccount 与 RBAC
public 里的 K8sJobClient 会在集群内用 Pod 挂载的 ServiceAccount token 直连 k8s API 建 Job(用于打包 web-ui / H5 构建),命名空间取自 /var/run/secrets/.../namespace,即 release 所在的 namespace。用 default SA 会 403。
由 templates/rbac.yaml 生成,开关是逐服务的:
yaml
services:
public:
serviceAccount:
create: true
rules:
- apiGroups: ["batch"]
resources: ["jobs"]
verbs: ["create", "get", "list", "watch", "delete"]
- apiGroups: [""]
resources: ["pods", "pods/log"] # 构建日志回显后台,不给 exec
verbs: ["get", "list"]生成 <release>-<服务名> 的 SA + Role + RoleBinding,services.yaml 自动绑上 serviceAccountName。
没写 serviceAccount 的服务会显式 automountServiceAccountToken: false —— 默认行为是每个 Pod 都挂一份 default SA 的 token,权限虽小,但没必要给攻击者留这个起点。
客户集群不允许 chart 创建 RBAC 时,改成 create: false + name: <运维预建的 SA>。
两条不能松的红线
- 模板只生成
Role,不生成ClusterRole,这是结构性的而非"暂时先这样"。让跑在集群里的业务服务有权改 Deployment,等于把提权路径焊死进产品 —— 任何能在public里执行代码的漏洞都直接升级成集群沦陷。要跨命名空间/集群级操作,由运维手工授权。 - rules 里出现
*会fail掉安装。 values 是 nuwa 按客户渲染的,渲染逻辑一旦出岔子,通配符等同 cluster-admin。宁可装不上。
遗留
h5-prereqs.hiapi-cloud.yaml 里构建 Job 引用的 ConfigMap / Secret / 两个 PVC 还没收进 chart。PVC 写死 storageClassName: aliyun-nas 且需要 RWX(构建 Job 写、nginx 读,不同 Pod 共享),k3s 私有化环境没有这个存储类。收进 chart 前得先定:私有化环境下 H5 构建产物是走 NFS,还是构建完直接推 OSS 不落盘。
已装 chart 的环境不要再 apply
h5-prereqs.hiapi-cloud.yaml的第 1~3 段,会多出一套没人用的同义 SA/Role。
十四、Ingress:class 名和注解
默认 className: traefik(2026-07-30 改)。 交付基线是 k3s,它自带 traefik、servicelb 和 local-path 默认 StorageClass —— 三样都是我们需要的,没有理由 --disable=traefik 再自己装一套。
class 填错是最难查的一类故障:Ingress 建得出来,kubectl get ing 一切正常, 但没有任何控制器认领它,域名一直 404,而且没有任何一条日志指向这个原因。 所以 nuwa 的部署配置里有「Ingress class」这个字段,渲染时显式写进 values,不吃 chart 默认值。 装完对一眼:
bash
kubectl get ingressclass
kubectl -n hiapi get ing -o jsonpath='{.items[*].spec.ingressClassName}'注解按 class 分开给:
yaml
ingress:
className: traefik
annotationsByClass:
nginx:
# 少了这条,上传会被 ingress-nginx 卡在默认的 1MB 上(表现是 413)
nginx.ingress.kubernetes.io/proxy-body-size: 100m
traefik: {} # traefik 默认不限请求体大小
annotations: {} # 额外的(cert-manager、IP 白名单),总是合并进去同一条注解在别的控制器上是废话,而少了它又会出真问题,所以不能只留一份"通用注解"。 换 class 的时候注解自动跟着换,不用记得手改。