课程目录(第 9 章 / 共 33 章)
ConfigMap 与 Secret:把配置从镜像里拿出来
用 ConfigMap 存普通配置、Secret 存敏感信息,掌握环境变量与挂载文件两种消费方式,以及更新配置后 Pod 的真实行为。
学完这一章,你将能够
- ✓会用三种方式创建 ConfigMap,并区分环境变量与挂载文件两种消费方式
- ✓说清 Secret 与 ConfigMap 的差别,以及 base64 为什么不是加密
- ✓知道改完配置后哪些会生效、哪些必须重启 Pod
配置写死在镜像里会怎样
假设你把数据库地址、日志级别直接写进了代码,然后打成镜像。接下来会依次遇到这些问题:
- 同一个程序要跑开发、测试、生产三套环境,配置不同,就得构建三个镜像;
- 改一行配置要重新构建、推送、滚动更新,而它其实跟代码没关系;
- 数据库密码跟着镜像走,任何能拉到这个镜像的人都能拿到它。
正确的做法是把「程序」和「配置」分开:镜像只管代码和依赖,配置在部署时注入。Kubernetes 里承担这件事的两个资源就是 ConfigMap(普通配置)和 Secret(敏感信息)。
ConfigMap 的三种创建方式
第一种,从命令行字面值创建(最快,适合实验):
kubectl create namespace demo
kubectl create configmap app-config -n demo \
--from-literal=APP_MODE=prod \
--from-literal=LOG_LEVEL=info \
--from-literal=API_URL=http://api.demo.svc.cluster.local:5678第二种,从文件创建(key 默认是文件名):
printf 'APP_MODE=prod\nLOG_LEVEL=info\n' > app.properties
kubectl create configmap app-config -n demo --from-file=app.properties第三种,写 YAML 再 apply(推荐,因为它是声明式、可进版本库、可 review):
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: demo
data:
APP_MODE: prod
LOG_LEVEL: info
API_URL: http://api.demo.svc.cluster.local:5678kubectl apply -f app-config.yaml
kubectl get configmap app-config -n demodata 里都是明文键值对,所以不要往 ConfigMap 里放密码——那正是 Secret 的活儿。
两种消费方式:环境变量与挂载文件
ConfigMap 和 Secret 都支持同样的两种消费方式,区别在「什么时候读取」:
| 方式 | 写法 | 更新配置后 |
|---|---|---|
| 环境变量 | envFrom(整份导入)或 env.valueFrom(取单个 key) | 不会变,必须重启 Pod |
| 挂载为文件 | volumes + volumeMounts | 文件内容会自动更新(subPath 除外) |
选择建议:程序支持从环境变量读配置(大多数框架都支持)就用环境变量;配置文件很复杂(如 nginx.conf、JSON 配置)或需要「改配置不重启就生效」时用挂载文件,并且程序本身要能感知文件变化。
两条注入路径的差别,一张图就能看清:
ConfigMap / Secret(内容存在 etcd 里,Pod 只是引用它)
│
├─① envFrom / env.valueFrom ──► 容器启动时读一次 ──► 进程环境变量
│ 改完 ConfigMap 后:✗ 完全不变,必须重建 Pod
│
└─② volumes + volumeMounts ──► 每个 key = 挂载目录下的一个文件
改完 ConfigMap 后:✓ 约几十秒后自动更新
✗ 用 subPath 挂单个文件时不更新动手:把 app-config 挂到 web 上
这次我们要建两样东西:ConfigMap app-config、Secret db-secret。先确保 ConfigMap 里有我们需要的三个 key,再创建 Secret(它的细节下一节再讲):
kubectl apply -f app-config.yaml
kubectl create secret generic db-secret -n demo \
--from-literal=DB_USER=kube101 \
--from-literal=DB_PASSWORD='S3cret-Pass!'然后保存这份清单为 web-with-config.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: demo
spec:
replicas: 2
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: nginx:1.27
ports:
- containerPort: 80
envFrom:
- configMapRef:
name: app-config
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: DB_PASSWORD
volumeMounts:
- name: config-volume
mountPath: /etc/app-config
readOnly: true
- name: secret-volume
mountPath: /etc/db-secret
readOnly: true
volumes:
- name: config-volume
configMap:
name: app-config
- name: secret-volume
secret:
secretName: db-secretkubectl apply -f web-with-config.yaml
kubectl rollout status deployment/web -n demo两种方式的效果都验证一下:
kubectl exec -n demo deploy/web -- printenv APP_MODE LOG_LEVEL
kubectl exec -n demo deploy/web -- cat /etc/app-config/API_URL
kubectl exec -n demo deploy/web -- cat /etc/db-secret/DB_PASSWORDprintenv 能打印出 APP_MODE 与 LOG_LEVEL,说明 envFrom 生效;/etc/app-config 下每个 key 变成一个文件,说明挂载生效。最后一条命令直接把密码打印了出来——这恰好说明了下一个问题。
Secret 与 ConfigMap 的差别
Secret 和 ConfigMap 的结构几乎一样,都是键值对,都能用 env 和 volume 消费。区别在于定位:
| 对比项 | ConfigMap | Secret |
|---|---|---|
| 存放内容 | 普通配置(开关、地址、日志级别) | 密码、Token、证书、镜像仓库凭据 |
| 存储形式 | 明文 | base64 编码(不是加密) |
| 典型类型 | 无 | Opaque、kubernetes.io/tls、kubernetes.io/dockerconfigjson |
最关键的一句话:base64 只是编码,不是加密。 任何人只要有 get secret 权限,就能一行命令还原出明文:
kubectl get secret db-secret -n demo -o jsonpath='{.data.DB_PASSWORD}' | base64 -d(macOS 自带的 base64 如果不认 -d,换成 base64 -D。)
所以生产环境要做的不是「相信 base64」,而是:用 RBAC 把 get/list/watch secret 的权限收紧到真正需要的组件;给 etcd 开启静态加密;用外部密钥管理(Vault、云厂商 KMS、SOPS、External Secrets Operator 等)作为真正的秘密源,Secret 只当注入载体;必要时加 immutable: true 防止误改。
关于 stringData
用 YAML 写 Secret 时可以用 stringData 写明文,Kubernetes 保存时会自动转成 data 里的 base64:
apiVersion: v1
kind: Secret
metadata:
name: db-secret
namespace: demo
stringData:
DB_USER: kube101
DB_PASSWORD: S3cret-Pass!如果你手写 data 字段,就必须自己保证 base64 正确,写错会报 illegal base64 data at input byte。
更新配置后 Pod 会变吗
这是本章最重要的一节,因为它经常和直觉相反。
修改挂载为文件的 ConfigMap,文件会变:
kubectl edit configmap app-config -n demo
kubectl exec -n demo deploy/web -- cat /etc/app-config/APP_MODE在编辑器里把 APP_MODE 改成 staging 保存退出。第一次 cat 可能还是 prod,等几十秒到一分钟再试一次就变成 staging。这个延迟来自 kubelet 的同步周期,不是立即生效。
修改通过环境变量注入的 ConfigMap,什么都不会变:
kubectl exec -n demo deploy/web -- printenv APP_MODE无论等多久,它仍然是 Pod 启动那一刻的值。因为环境变量只在容器启动时注入一次,之后 ConfigMap 怎么改都与它无关。要让它生效,只能重建 Pod:
kubectl rollout restart deployment/web -n demo
kubectl rollout status deployment/web -n demo
kubectl exec -n demo deploy/web -- printenv APP_MODESecret 的行为完全一样:挂载成文件会更新,环境变量不会。
三种情况放在一起对比,把这张表记住就够用了:
| 注入方式 | 改完 ConfigMap / Secret 后 | 要重启 Pod 吗 | 为什么 |
|---|---|---|---|
环境变量(envFrom / env.valueFrom) | 完全不变 | 必须 kubectl rollout restart | 环境变量只在容器启动那一刻注入一次 |
挂载为文件(volumeMounts) | 约几十秒后自动更新 | 不用,但程序要重新读文件 | kubelet 周期性同步,用符号链接原子替换 |
挂载单个文件(subPath) | 完全不变 | 必须重建 Pod | 已知行为,不是 bug |
还要注意最后一行的坑:文件更新了,不代表程序重新读取了。nginx 不会自动 reload,需要 nginx -s reload 或配套的 sidecar 触发;数据库连接池也不会因为你改了地址就重连。
重建 Pod 期间容器会短暂 NotReady,Service 会把它从流量列表里摘掉,这背后的判定规则见第 11 章 探针与资源管理。
推荐做法
把配置分成两类:启动时就要用的(如数据库地址)用环境变量,改完老实重启 Pod;运行中可能要调的(如日志级别)用挂载文件,并确保程序支持热加载。不要指望「改 ConfigMap 全集群自动生效」。
常见坑与排错
先记住这张速查表,再看后面的细节:
| 现象 | 原因 | 怎么确认 | 怎么办 |
|---|---|---|---|
Pod 起不来,STATUS 是 CreateContainerConfigError | 引用的 ConfigMap/Secret 不存在,或 key 名字写错 | kubectl describe pod 事件里有 configmap "app-config" not found 或 couldn't find key | 先创建对象;用 kubectl get cm <名字> -o yaml 逐字核对 key |
| 环境变量是空的,或不是 ConfigMap 里的值 | key 拼错;env 覆盖了 envFrom 的同名 key | kubectl exec -n demo deploy/web -- printenv,再和 kubectl get cm app-config -o yaml 对照 | 对齐 key 名;删掉重复的 env 声明后 kubectl rollout restart |
| 容器里的配置文件一直没变 | 用了 subPath 挂单个文件;或程序没有重新读取 | kubectl exec -- cat /etc/app-config/APP_MODE 看文件内容 | 去掉 subPath 改成整目录挂载;给程序加重载逻辑 |
| 挂载后镜像自带的文件不见了 | 挂载遮蔽(shadow)了原目录里的内容 | kubectl exec -- ls /etc/nginx/conf.d 对比挂载前后 | 挂到空目录,或用 items 只挑需要的 key |
illegal base64 data at input byte 6 | 手写 data 字段时 base64 编码不正确 | 报错信息直接指出了字段 | 改用 stringData 或 kubectl create secret --from-literal |
Secret 会出现在哪些地方
kubectl describe pod 不会打印 Secret 的值,它只显示来源,例如 DB_PASSWORD: <set to the key 'DB_PASSWORD' in secret 'db-secret'>。真正泄露的途径是这些:
kubectl get secret db-secret -o yaml直接给出 base64,一条命令就能解码;- 把密码写成 Pod 的
env.value(明文)时,它会出现在kubectl describe pod和kubectl get pod -o yaml里; - 密码进了环境变量后,
kubectl exec -- env、进程崩溃时的环境 dump 都可能带出来。
所以「能 exec 进容器的人」和「能 get secret 的人」都要当特权对待,生产环境用 RBAC 管起来。
base64 编码错误
手写 Secret 的 data 字段时,值必须是标准 base64。写错会直接报:
Secret "db-secret" is invalid: data[DB_PASSWORD]: Invalid value: "S3cret": illegal base64 data at input byte 6用 kubectl create secret generic --from-literal=... 或 YAML 里的 stringData 都能避免自己编码。
env 与 volume 的覆盖与遮蔽
- 同一个 key 既通过
envFrom导入、又在env里显式声明时,显式 `env` 优先,会覆盖envFrom的值。排查「为什么我的环境变量不是 ConfigMap 里那个」时先看这里。 - 把 ConfigMap 挂到
/etc/nginx/conf.d这类已有文件的目录,挂载会遮蔽目录里原有的内容,可能直接把镜像自带的配置顶掉。要么挂到空目录,要么用items只挑需要的 key。 - 挂载后每个 key 变成一个文件,key 里带
.或-会影响用 shell 读取的便利性,建议 key 统一用大写字母加下划线。
动手练习:改配置、看差异
- 用 YAML 创建
app-config,用命令创建db-secret,再 applyweb-with-config.yaml,确认两个 Pod 都 Ready。 kubectl exec -n demo deploy/web -- printenv APP_MODE记下当前值。- 用
kubectl edit configmap app-config -n demo把APP_MODE改成staging,分别观察printenv APP_MODE与cat /etc/app-config/APP_MODE的变化。 kubectl rollout restart deployment/web -n demo后再看printenv APP_MODE,理解为什么只有它需要重启。
自测题
自测:为什么改了 ConfigMap,容器里的环境变量没变?(点击展开答案)
因为环境变量是容器启动时由 kubelet 注入到进程环境里的,注入动作只发生一次。ConfigMap 本身被改掉之后,etcd 里的内容确实变了,但那个已经跑起来的进程的环境变量是一块固定的内存,没有任何机制会去改它。所以「改配置不生效」不是 bug,而是语义:环境变量属于启动快照。要让它生效只能重建 Pod(kubectl rollout restart),让新容器带着新值重新启动。
自测:Secret 用了 base64,为什么不能说它安全?(点击展开答案)
因为 base64 是编码不是加密,它的唯一目的是让二进制内容能放进 YAML 文本里,编码过程没有密钥,任何人拿到字符串都能一行命令还原。所以 Secret 的安全性来自「谁能读到它」,而不是「它长得像乱码」:要靠 RBAC 限制 get/list/watch secret、给 etcd 开静态加密、把真源放到外部密钥管理系统里。默认情况下,有权限读 Secret 的人就等于有权限拿到明文。
自测:挂载的 ConfigMap 文件已经更新了,为什么应用行为还是旧的?(点击展开答案)
这里有两层原因,容易只看到第一层。第一层是文件到底有没有变:整目录挂载会延迟更新,而 subPath 挂单个文件根本不会更新。第二层是程序有没有重新读:文件变了只是磁盘上的内容变了,nginx、Java 进程、数据库连接池都不会自动重新解析,需要显式 reload 或重启。所以排查时先 cat 文件确认内容,再确认程序是否支持热加载。
小结
- 配置和镜像要分离:普通配置进 ConfigMap,密码与证书进 Secret。
- ConfigMap 可以
--from-literal、--from-file或 YAML 创建;推荐 YAML,因为它声明式且可进版本库。 - 两种消费方式:
envFrom/env.valueFrom注入环境变量,volumes+volumeMounts挂载成文件。 - Secret 的 base64 只是编码不是加密,生产要靠 RBAC、etcd 静态加密与外部密钥管理。
- 挂载的 ConfigMap/Secret 会延迟更新,环境变量永远不会变,必须
kubectl rollout restart重建 Pod。
练习
- 用
--from-file把一个nginx.conf做成 ConfigMap,挂到/etc/nginx/conf.d之外的目录,说明为什么不能直接覆盖镜像自带的配置目录。 - 把
db-secret用stringData写成 YAML 并 apply,再用kubectl get secret -o yaml对比stringData与data的差异。 - 想一想:如果 ConfigMap 里存的是数据库地址,而它变了,为什么「Pod 会自动更新文件」仍然救不了你?(提示:环境变量与连接池。)
应用能访问了、配置也能注入了,但容器里的数据一重建就没了。下一章第 10 章 存储卷与 PV/PVC会把数据真正持久化下来。
相关章节:第 12 章 命名空间与 RBAC(把 Secret 的读权限收紧)、第 15 章 排障手册(CreateContainerConfigError 的排查流程)。