改完 ConfigMap,应用却没反应
这是最常见的困惑:kubectl edit configmap 保存成功,kubectl describe 也显示新值,但容器里读到的还是老配置。于是有人怀疑集群坏了,其实只是没搞清更新的传播路径——ConfigMap 变了,不等于进程里的配置变了,更不等于 Spring 的 @ConfigurationProperties 重新绑定。
一条配置从集群到生效要经过四段:apiserver 里的对象更新、kubelet 把新内容同步到宿主机、容器内的挂载文件被替换、应用重新读取。前面任何一段断了,现象都是“改了没用”。
挂载方式的差别:volume 与 subPath
| 挂载方式 | 文件会更新吗 | 说明 |
|---|---|---|
| 整个 volume 挂载到目录 | 会,通常 1 分钟内 | kubelet 原子替换符号链接,应用需重新打开文件 |
subPath 指定单个文件 |
不会 | 文件被绑定挂载,ConfigMap 更新后永远看不到 |
envFrom / env 注入 |
不会 | 环境变量在容器启动时固化 |
immutable: true 的 ConfigMap |
不会 | 任何修改都必须新建对象 |
subPath 是重灾区,因为它常被用来“只覆盖一个 application.yml”。想保留目录里其他文件、又要能热更新,正确做法是挂整个目录,用 items 挑出需要的 key。
环境的差异也要注意:envFrom 拿到的值在容器生命周期内不会变,只有重启才生效。把数据库密码用环境变量注入,再期望改 Secret 就自动生效,一定会失望。
让应用真正感知变更的三种做法
第一种是滚动重启,最简单也最可靠:
kubectl rollout restart deployment/order-api -n shop-prod
kubectl rollout status deployment/order-api -n shop-prod --timeout=180s
有时候“没生效”其实只是延迟:kubelet 的 syncPeriod 默认 1 分钟,加上卷缓存的 TTL,最慢要接近两分钟才会替换文件。验证时应该进容器看文件和修改时间,而不是只看 ConfigMap 对象:
kubectl exec -n shop-prod deploy/order-api -- ls -l /etc/app-config/
kubectl exec -n shop-prod deploy/order-api -- cat /etc/app-config/application.yml
第二种是给 Pod 模板加校验和注解,让 ConfigMap 内容变化触发一次正常的滚动更新。Helm 里可以这样做:
spec:
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
第三种是让应用自己监听文件变化。Spring Boot 3.x 可以配合 spring.config.import 与 spring-cloud-kubernetes-config 的 reload 能力,在文件变化时重新绑定 @ConfigurationProperties 并刷新 @RefreshScope 的 Bean。但要注意:@Value 注入的字段不会跟着变,只有走 @ConfigurationProperties 和 @RefreshScope 的才会。
spring:
config:
import: optional:configtree:/etc/app-config/
cloud:
kubernetes:
config:
enabled: true
reload:
enabled: true
strategy: refresh
mode: polling
period: 30s
configtree 这种写法把目录当成配置源,每个文件名就是 key,Secret 用 base64 解出来的内容直接成为值,比手工拼 application.yml 更干净。
Secret 不是加密,只是编码
kubectl get secret -o yaml 里那串 base64 任何人都能还原。Base64 的作用是让二进制安全地放进 YAML,不是保护。把 base64 当成“密文”交给审计,是很常见的误判。Secret 的读权限也必须收紧:RBAC 里按命名空间和资源名细分,避免一个 secrets: get,list 的通配权限把全部凭据一次性暴露。真要保护,至少要开启 etcd 的 encryption at rest,更进一步是用 External Secrets Operator 或 Vault 把密钥从集群里拿出去,Pod 只拿短期凭证。
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: order-api-db
namespace: shop-prod
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: order-api-db
data:
- secretKey: password
remoteRef:
key: shop/prod/order-api/db
property: password
这样密钥轮换由外部系统驱动,集群里只有短期副本,泄露面小很多。
常见坑
用 subPath 挂配置文件还指望热更新。 现象是改了 ConfigMap、等十分钟也没变化,重启 Pod 立刻生效。这不是 kubelet 的问题,是绑定挂载的语义如此。
同时改 ConfigMap 和镜像,然后只回滚镜像。 回滚后旧代码配新配置,报错信息通常指向“缺少某个属性”,排查方向完全错。
把大文件塞进 ConfigMap。 单个 ConfigMap 上限 1MiB,超了创建直接失败;用 --from-file 灌日志配置或证书链时很容易踩到。
Secret 里的值带换行。 echo "p@ss" | base64 会把换行也编进去,容器里拿到的密码多一个 \n,认证失败但看不出原因。用 printf '%s' "p@ss" | base64 才是对的。
改完配置不校验就发布。 一个缩进错位的 YAML 会让应用启动即失败,滚动更新卡在 CrashLoopBackOff。发布前先 kubectl apply --dry-run=server -f 过一遍,成本极低。
评论