《Go 语言编程实战》15.2 ConfigMap/Secret 与 HPA

把 TaskHub 的配置与镜像解耦:用 ConfigMap 注入非敏感配置、用 Secret 管密钥(并说清 base64 不等于加密),用 HPA 让副本数跟着 CPU 与自定义指标自动伸缩;实测环境变量与文件挂载两种注入方式都能被进程读到。

本节把 TaskHub 的配置从镜像里拆出来:数据库地址、日志级别进 ConfigMap,密码进 Secret,副本数交给 HPA 按负载自动伸缩。

上一节的清单里有个隐患:配置和密钥都藏在 env 与 secretKeyRef 背后,还没讲清楚它们到底怎么注入、改了会不会生效、安不安全。这一节补齐三块:ConfigMap(非敏感配置)、Secret(敏感数据)、HPA(自动伸缩)。

同样声明:本机无 Kubernetes 集群,本节所有清单均未 kubectl apply 验证。 ConfigMap/Secret 的注入语义(环境变量 vs 文件挂载)我用 docker run 的 -e 与 -v 在本机等价复现并给出真实输出;HPA 的扩缩容行为必须在有 metrics-server 的真实集群里才能验证,本机未实测。

15.2.1 配置必须与镜像分离

把配置写死在镜像里的代价是:改一个日志级别就要重新构建、推送、拉取整个镜像。配置分离的目标是让「同一份镜像,在不同环境跑出不同行为」:

内容放哪例子
非敏感配置ConfigMap日志级别、超时、功能开关
敏感数据Secret数据库密码、JWT 密钥、API token
环境标识构建参数 / 环境变量版本号(第 14 章的 -X main.version)

这个分层和第 3 章讲的「默认值 < 配置文件 < 环境变量 < 命令行」是同一个思路——越靠后的来源优先级越高,也越贴近部署环境。

15.2.2 ConfigMap:两种注入方式

ConfigMap 有两种消费方式,语义完全不同:

apiVersion: v1
kind: ConfigMap
metadata:
  name: taskhub-config
  namespace: taskhub
data:
  TASKHUB_LOG_LEVEL: "info"
  TASKHUB_DB_HOST: "taskhub-db.taskhub.svc.cluster.local"
  config.yaml: |
    server:
      port: 8080
      read_timeout: 5s
    cache:
      ttl: 60s

方式一:环境变量注入(在 Pod spec 里):

envFrom:
  - configMapRef:
      name: taskhub-config

方式二:挂载成文件:

volumeMounts:
  - name: config
    mountPath: /etc/taskhub
    readOnly: true
volumes:
  - name: config
    configMap:
      name: taskhub-config
维度环境变量文件挂载
读取方式os.Getenvos.ReadFile
能否放多行内容否(YAML 里有换行会麻烦)是
改 ConfigMap 后不会更新到运行中的容器会自动更新(约 1 分钟内)
适合简单标量(级别、地址)结构化配置(YAML/JSON 文件)

最后一行是关键区别:环境变量在容器启动时一次性注入,之后改 ConfigMap,运行中的 Pod 看不到新值;文件挂载则是符号链接指向一个会被原子替换的文件,Kubernetes 更新 ConfigMap 后,挂载的文件内容会在约 1 分钟内同步。

所以有个常见组合:简单的开关用 env,复杂的配置用文件。TaskHub 就是这么分的——日志级别走 env,config.yaml 走文件。

15.2.3 Secret:base64 不是加密

Secret 的 data 字段是 base64 编码,不是加密:

apiVersion: v1
kind: Secret
metadata:
  name: taskhub-secrets
  namespace: taskhub
type: Opaque
data:
  db-password: czNjcjN0LXBAc3M=
stringData:                # 写明文也行,K8s 会自动 base64
  jwt-secret: "change-me-in-production"

czNjcjN0LXBAc3M= 解出来就是 s3cr3t-p@ss——任何人都能 echo czNjcjN0LXBAc3M= | base64 -d 还原。base64 只是为了让二进制数据能放进 YAML,不提供任何机密性。

那 Secret 比 ConfigMap 强在哪?三点实际差别:

  1. 不会随 Pod 打印到日志/describe 里(kubectl describe 不显示 Secret 内容);
  2. 可以限制 RBAC 访问(只有特定 ServiceAccount 能读);
  3. 支持 imagePullSecrets、TLS 证书等专用类型。

真正的机密性要靠外部密钥管理:etcd 加密静态数据(EncryptionConfiguration)、SealedSecrets、External Secrets Operator 接 Vault/AWS Secrets Manager。Secret 只是「不主动泄露」,不是「泄露了也没事」——这个认知差异是很多安全事故的根源。

一个绝对的红线:Secret 绝不能提交进 Git。哪怕 base64 了也不行。用 SealedSecrets 或 CI 注入。

15.2.4 实测:env 与文件挂载的等价性

清单没法 apply,但两种注入方式对进程的可见效果可以完全等价复现。我用 docker run 的 -e(等价环境变量注入)和 -v(等价文件挂载)跑了一个读配置的程序:

docker run --rm --entrypoint /configdemo \
  -e TASKHUB_LOG_LEVEL=debug \
  -e TASKHUB_DB_HOST=taskhub-db.default.svc.cluster.local \
  -v /Users/.../.gbtmp:/etc/taskhub:ro \
  taskhub15:2

真实输出:

LOG_LEVEL (env)      = debug
DB_HOST (env)        = taskhub-db.default.svc.cluster.local
config.yaml (mount)  = "server:\n  port: 8080\n  read_timeout: 5s\n"
db_password (secret) = "s3cr3t-p@ss"

环境变量和挂载文件都被进程正确读到,与 Kubernetes 里 ConfigMap/Secret 的注入效果一致。config.yaml 的多行内容原样保留,db_password 文件内容就是那个「明文密码」。

一个本机踩到的坑值得记:我最初把文件挂到 /tmp 下的目录,容器里读到的却是一个空目录——因为本机 Docker(colima 后端)只共享了 /Users/... 这一个路径,/tmp 不在共享范围里,挂载被静默替换成了空目录。排查「挂载了但读不到」时,先确认宿主目录真的共享给了容器。 Kubernetes 里没有这个问题,但本地用 Docker 模拟时很容易中招。

15.2.5 配置改了,Pod 会更新吗

这是最容易被误解的一点。改 ConfigMap 之后:

注入方式运行中的 Pod需要做什么
环境变量不更新重启 Pod 才会读到新值
文件挂载文件内容约 1 分钟内更新程序要自己 watch 文件并 reload

注意:改 ConfigMap 不会自动触发 Deployment 滚动更新。 Deployment 只在 template 变化时重建 Pod,而 ConfigMap 的内容不在 template 里。要让配置变更触发滚动更新,业界常用一个技巧——把配置的哈希算进 Pod 模板的注解:

spec:
  template:
    metadata:
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}

(这是 Helm 模板写法。)配置一变,checksum/config 就变,template 跟着变,Deployment 就滚动更新。否则你会遇到「配置改了、Pod 没重启、行为没变」的经典困惑。

至于「程序自己 watch 文件热更新」——那是应用层的责任。Go 里通常用 fsnotify 监听文件变化,或用 SIGHUP 信号触发重载。本节的 TaskHub 未实现热更新,配置变更依赖重启(通过 checksum 注解触发滚动更新),这是更简单也更不容易出错的选择。

15.2.6 HPA:让副本数跟着负载走

固定 replicas: 3 在流量低谷时浪费资源、高峰时不够用。HorizontalPodAutoscaler(HPA) 让副本数自动调整:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: taskhub-api
  namespace: taskhub
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: taskhub-api
  minReplicas: 3
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 30
      policies:
        - type: Percent
          value: 100
          periodSeconds: 30
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Percent
          value: 25
          periodSeconds: 60

几个关键点:

  • averageUtilization 是相对 requests 的百分比,不是绝对量。CPU 目标 70% 意味着「当所有 Pod 的平均 CPU 使用率超过 requests 的 70% 就扩容」。所以 HPA 要求 Pod 必须设 resources.requests,否则无法计算百分比——没设 requests 的 Deployment 配 HPA 会一直报错。
  • scaleUp 快、scaleDown 慢是刻意的。扩容要快(应对流量突增),缩容要慢(避免刚缩完又涨回去造成抖动)。scaleDown.stabilizationWindowSeconds: 300 表示「观察 5 分钟,只有持续低于目标才缩」。
  • maxReplicas: 20 是成本护栏。没有上限的自动伸缩会在流量异常时把你的账单打爆。

15.2.7 HPA 的前提与陷阱

HPA 依赖 metrics-server,没有它 HPA 无法获取 CPU/内存指标,会一直显示 <unknown> 而不扩容。本机未实测 HPA,因为没有集群也没有 metrics-server,无法验证扩缩容行为。要在本地验证,需要 kind + metrics-server,本机均未安装。

即使有 metrics-server,还有几个陷阱:

陷阱现象应对
没设 resources.requestsHPA 报错、不工作必须设 requests
只有 CPU 指标扩容后延迟仍高加自定义指标(QPS、队列长度)
扩容太快副本数剧烈抖动调大 stabilizationWindowSeconds
指标采集延迟扩容滞后 30–60s接受延迟,或提前扩容(定时 HPA)
只扩不缩成本持续偏高检查 scaleDown 策略是否被禁用

CPU 不是万能的伸缩信号。 对于一个「等数据库返回」的 IO 密集服务,CPU 使用率可能一直很低,但请求已经堆积——HPA 看着 CPU 不会扩容,而用户已经在超时。这类服务应该用自定义指标(每秒请求数、P99 延迟、消息队列积压量),通过 Prometheus Adapter 暴露给 HPA。

TaskHub 的做法是:CPU 指标兜底,QPS 指标做主信号。CPU 扩容应对计算密集的突发,QPS 扩容应对 IO 等待导致的堆积——两者互补。

一个务实的建议:上线初期先用固定副本数 + 足够的余量,观察真实的资源曲线,再配 HPA。HPA 的参数(目标利用率、稳定窗口)需要真实数据才能调准,拍脑袋配的 HPA 比固定副本数更糟——它会在流量波动时制造副本抖动,反而降低稳定性。

小结

  • 配置与镜像分离:非敏感进 ConfigMap、敏感进 Secret,同一镜像适配多环境。
  • ConfigMap 两种注入方式:环境变量(启动时一次性)与文件挂载(可热更新)。
  • 环境变量注入改 ConfigMap 后运行中的 Pod 不更新;文件挂载约 1 分钟内更新。
  • 改 ConfigMap 不会自动触发 Deployment 滚动更新,要用 checksum/config 注解把配置哈希进 Pod 模板。
  • Secret 是 base64 编码,不是加密,真正的机密性靠 etcd 静态加密或外部密钥管理。
  • 实测:环境变量与文件挂载两种注入方式都能被进程正确读取,与 K8s 语义一致。
  • HPA 按指标自动伸缩,必须设 resources.requests,且需要 metrics-server。
  • 扩容快、缩容慢是刻意设计;maxReplicas 是成本护栏。
  • CPU 不是万能的伸缩信号,IO 密集服务要用 QPS/队列长度等自定义指标。
  • HPA 本机未实测(无集群、无 metrics-server)。

配置和密钥都管好了,副本也能自动伸缩了。但还有最后一道关:滚动更新时怎么不丢请求。下一节我们把优雅停机、preStop 钩子、terminationGracePeriodSeconds 和 PodDisruptionBudget 拼起来,并用本机 docker stop 的真实数据说明「在途请求为什么必须跑完」。

阅读导航:上一节:15.1 清单与探针 · 下一节:15.3 优雅停机与滚动更新 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「golang」更多文章

  1. 《Go 语言编程实战》目录
  2. 《Go 语言编程实战》18.3 上线、观测与迭代
  3. 《Go 语言编程实战》18.2 故障演练