Skip to content

私有化部署 — 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.yamlservices 段。

加一个新服务 = 在 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 —— 与 ShardingConfigconnectionInitSqlsSET 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 的发布/监听方向。业务直觉在这件事上不可靠 —— 下面三条都是核代码时才发现填错的。

服务requiresrecommends依据
gateway只经 Nacos 做服务发现,不直连任何服务
public唯一的 Feign(FeignCloudApplication)指向自己
admin-ui纯静态站
userpublicFeignCloudCaptcha / FeignCloudConfig / FeignCloudRegion / FeignSender
financepublic, usertask-workerFeignCloudPortFeignUser;MQ ← public 的 MERCHANT_REGISTER
vempublic, finance, socketFeignCloudConfig / FeignMerchant / FeignSenderFeignPayment;MQ → hiapi-socket-exchange
socket只消费 MQ
sypublic只有 FeignSender
task-worker无出向调用,被 finance 调
shoppublic, userFeignCloudRegionFeignUser / FeignUserAccount / FeignUserAddress
storepublic, userFeignCloudRegionFeignUser

只声明直接依赖。 校验是逐个已开启的服务各跑一遍的,传递闭包自动成立 —— 开 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 建 namespaceSpring 里填的 namespaceID 不是名字,ID 不存在时配置读到空且不报错
给每个已开启的 Java 服务播一份占位配置user/finance/vem/syspring.config.import强制项,对应 dataId 不存在会直接启动失败

第三件事最容易被低估。全新交付的环境里 Nacos 是空的,这四个服务根本起不来 —— 而早期验证之所以没暴露,是因为当时连的是已有配置的开发环境 Nacos。

补充:spring.config.importapplication.yamlapplication-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 -f

Nacos 控制台(默认不经 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 报 403services.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 resetbitnami 迁到 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>

两条不能松的红线

  1. 模板只生成 Role,不生成 ClusterRole,这是结构性的而非"暂时先这样"。让跑在集群里的业务服务有权改 Deployment,等于把提权路径焊死进产品 —— 任何能在 public 里执行代码的漏洞都直接升级成集群沦陷。要跨命名空间/集群级操作,由运维手工授权。
  2. 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 的时候注解自动跟着换,不用记得手改。