课程目录(第 9 章 / 共 33 章)
课程/让应用可访问、可配置

ConfigMap 与 Secret:把配置从镜像里拿出来

9 章 / 共 33·16 分钟·入门ConfigMapSecret配置

用 ConfigMap 存普通配置、Secret 存敏感信息,掌握环境变量与挂载文件两种消费方式,以及更新配置后 Pod 的真实行为。

学完这一章,你将能够

  • 会用三种方式创建 ConfigMap,并区分环境变量与挂载文件两种消费方式
  • 说清 Secret 与 ConfigMap 的差别,以及 base64 为什么不是加密
  • 知道改完配置后哪些会生效、哪些必须重启 Pod

配置写死在镜像里会怎样

假设你把数据库地址、日志级别直接写进了代码,然后打成镜像。接下来会依次遇到这些问题:

  • 同一个程序要跑开发、测试、生产三套环境,配置不同,就得构建三个镜像;
  • 改一行配置要重新构建、推送、滚动更新,而它其实跟代码没关系;
  • 数据库密码跟着镜像走,任何能拉到这个镜像的人都能拿到它。

正确的做法是把「程序」和「配置」分开:镜像只管代码和依赖,配置在部署时注入。Kubernetes 里承担这件事的两个资源就是 ConfigMap(普通配置)和 Secret(敏感信息)。

ConfigMap 的三种创建方式

第一种,从命令行字面值创建(最快,适合实验):

bash
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 默认是文件名):

bash
printf 'APP_MODE=prod\nLOG_LEVEL=info\n' > app.properties
kubectl create configmap app-config -n demo --from-file=app.properties

第三种,写 YAML 再 apply(推荐,因为它是声明式、可进版本库、可 review):

yaml
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:5678
bash
kubectl apply -f app-config.yaml
kubectl get configmap app-config -n demo

data 里都是明文键值对,所以不要往 ConfigMap 里放密码——那正是 Secret 的活儿。

两种消费方式:环境变量与挂载文件

ConfigMap 和 Secret 都支持同样的两种消费方式,区别在「什么时候读取」:

方式写法更新配置后
环境变量envFrom(整份导入)或 env.valueFrom(取单个 key)不会变,必须重启 Pod
挂载为文件volumes + volumeMounts文件内容会自动更新(subPath 除外)

选择建议:程序支持从环境变量读配置(大多数框架都支持)就用环境变量;配置文件很复杂(如 nginx.conf、JSON 配置)或需要「改配置不重启就生效」时用挂载文件,并且程序本身要能感知文件变化。

两条注入路径的差别,一张图就能看清:

text
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(它的细节下一节再讲):

bash
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

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-secret
bash
kubectl apply -f web-with-config.yaml
kubectl rollout status deployment/web -n demo

两种方式的效果都验证一下:

bash
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_PASSWORD

printenv 能打印出 APP_MODELOG_LEVEL,说明 envFrom 生效;/etc/app-config 下每个 key 变成一个文件,说明挂载生效。最后一条命令直接把密码打印了出来——这恰好说明了下一个问题。

Secret 与 ConfigMap 的差别

Secret 和 ConfigMap 的结构几乎一样,都是键值对,都能用 envvolume 消费。区别在于定位:

对比项ConfigMapSecret
存放内容普通配置(开关、地址、日志级别)密码、Token、证书、镜像仓库凭据
存储形式明文base64 编码(不是加密
典型类型Opaquekubernetes.io/tlskubernetes.io/dockerconfigjson

最关键的一句话:base64 只是编码,不是加密。 任何人只要有 get secret 权限,就能一行命令还原出明文:

bash
kubectl get secret db-secret -n demo -o jsonpath='{.data.DB_PASSWORD}' | base64 -d

(macOS 自带的 base64 如果不认 -d,换成 base64 -D。)

所以生产环境要做的不是「相信 base64」,而是:用 RBACget/list/watch secret 的权限收紧到真正需要的组件;给 etcd 开启静态加密;用外部密钥管理(Vault、云厂商 KMS、SOPS、External Secrets Operator 等)作为真正的秘密源,Secret 只当注入载体;必要时加 immutable: true 防止误改。

关于 stringData

用 YAML 写 Secret 时可以用 stringData 写明文,Kubernetes 保存时会自动转成 data 里的 base64:

yaml
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,文件会变:

bash
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,什么都不会变:

bash
kubectl exec -n demo deploy/web -- printenv APP_MODE

无论等多久,它仍然是 Pod 启动那一刻的值。因为环境变量只在容器启动时注入一次,之后 ConfigMap 怎么改都与它无关。要让它生效,只能重建 Pod:

bash
kubectl rollout restart deployment/web -n demo
kubectl rollout status deployment/web -n demo
kubectl exec -n demo deploy/web -- printenv APP_MODE

Secret 的行为完全一样:挂载成文件会更新,环境变量不会。

三种情况放在一起对比,把这张表记住就够用了:

注入方式改完 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 起不来,STATUSCreateContainerConfigError引用的 ConfigMap/Secret 不存在,或 key 名字写错kubectl describe pod 事件里有 configmap "app-config" not foundcouldn't find key先创建对象;用 kubectl get cm <名字> -o yaml 逐字核对 key
环境变量是空的,或不是 ConfigMap 里的值key 拼错;env 覆盖了 envFrom 的同名 keykubectl 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 编码不正确报错信息直接指出了字段改用 stringDatakubectl 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 podkubectl get pod -o yaml 里;
  • 密码进了环境变量后,kubectl exec -- env、进程崩溃时的环境 dump 都可能带出来。

所以「能 exec 进容器的人」和「能 get secret 的人」都要当特权对待,生产环境用 RBAC 管起来。

base64 编码错误

手写 Secret 的 data 字段时,值必须是标准 base64。写错会直接报:

text
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 统一用大写字母加下划线。

动手练习:改配置、看差异

  1. 用 YAML 创建 app-config,用命令创建 db-secret,再 apply web-with-config.yaml,确认两个 Pod 都 Ready。
  2. kubectl exec -n demo deploy/web -- printenv APP_MODE 记下当前值。
  3. kubectl edit configmap app-config -n demoAPP_MODE 改成 staging,分别观察 printenv APP_MODEcat /etc/app-config/APP_MODE 的变化。
  4. 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。

练习

  1. --from-file 把一个 nginx.conf 做成 ConfigMap,挂到 /etc/nginx/conf.d 之外的目录,说明为什么不能直接覆盖镜像自带的配置目录。
  2. db-secretstringData 写成 YAML 并 apply,再用 kubectl get secret -o yaml 对比 stringDatadata 的差异。
  3. 想一想:如果 ConfigMap 里存的是数据库地址,而它变了,为什么「Pod 会自动更新文件」仍然救不了你?(提示:环境变量与连接池。)

应用能访问了、配置也能注入了,但容器里的数据一重建就没了。下一章第 10 章 存储卷与 PV/PVC会把数据真正持久化下来。

相关章节:第 12 章 命名空间与 RBAC(把 Secret 的读权限收紧)、第 15 章 排障手册CreateContainerConfigError 的排查流程)。