2.6. 配置与密钥

把配置从镜像里拆出来,是十二要素应用的第一条。但「拆出来」不等于「管好了」:环境变量不会自动更新,Secret 默认只是 base64,挂载卷还有 subPath 的坑。

层级:

L2 · 核心

难度:

基础

预计:

40 分钟

前置:

Pod 与容器

2.6.1. 你将学到

  • ConfigMap 与 Secret 的创建方式与消费方式

  • 环境变量、envFrom、卷挂载、subPath 的行为差异

  • 配置热更新的真实边界,以及应用如何感知变更

  • Downward API 与 Projected Volume 的组合用法

  • Secret 的安全边界:base64 不是加密

2.6.2. ConfigMap 与 Secret 的区别

维度

ConfigMap

Secret

用途

非敏感配置

密码、Token、证书、密钥

存储

etcd 明文

etcd 中 base64(可配置静态加密)

大小限制

1 MiB

1 MiB

挂载

文件 / 环境变量

文件 / 环境变量

RBAC

可读

应严格限制 get/list/watch

版本化

手工

可用 SealedSecret / External Secrets

警告

base64 不是加密

data 字段只是 base64 编码,任何有权限读 Secret 的人都能解码。生产环境应开启 etcd 静态加密、限制 RBAC,并考虑 External Secrets Operator / Sealed Secrets。

2.6.3. 创建 ConfigMap

2.6.3.1. 从字面量

kubectl create configmap app-config \
  --from-literal=APP_MODE=prod \
  --from-literal=LOG_LEVEL=info \
  -n demo

2.6.3.2. 从文件

kubectl create configmap nginx-conf \
  --from-file=myserver.conf=./nginx-conf.d/myserver.conf \
  --from-file=myserver-status.cfg=./nginx-conf.d/myserver-status.cfg \
  -n demo

2.6.3.3. 从 YAML

configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: demo
data:
  APP_MODE: "prod"
  LOG_LEVEL: "info"
  application.yaml: |
    server:
      port: 8080
    logging:
      level: info
kubectl apply -f configmap.yaml
kubectl get configmap app-config -n demo -o yaml

2.6.4. 消费 ConfigMap

2.6.4.1. 1. 单个环境变量

env:
  - name: APP_MODE
    valueFrom:
      configMapKeyRef:
        name: app-config
        key: APP_MODE
        optional: false

2.6.4.2. 2. 批量注入

envFrom:
  - configMapRef:
      name: app-config
  - secretRef:
      name: mysql-secret
  - prefix: APP_
    configMapRef:
      name: app-config

envFrom 会把所有 key 变成环境变量。key 必须是合法的环境变量名;非法 key 会被跳过,并在事件中记录。

2.6.4.3. 3. 挂载为文件

volumes:
  - name: config
    configMap:
      name: nginx-conf
      items:
        - key: myserver.conf
          path: conf.d/myserver.conf
containers:
  - name: nginx
    image: nginx:1.27
    volumeMounts:
      - name: config
        mountPath: /etc/nginx/conf.d
        readOnly: true

挂载后目录结构:

/etc/nginx/conf.d/
└── myserver.conf

2.6.4.4. 4. subPath 的单文件挂载

volumeMounts:
  - name: config
    mountPath: /etc/nginx/nginx.conf
    subPath: nginx.conf

危险

subPath 不会热更新

使用 subPath 挂载的文件,在 ConfigMap 更新后不会同步到容器。这是最常见的热更新坑。需要热更新时,挂载目录而不是单文件,或者接受重启 Pod。

2.6.5. 消费 Secret

2.6.5.1. 创建

kubectl create secret generic mysql-secret \
  --from-literal=MYSQL_ROOT_PASSWORD='S3cure!Pass' \
  --from-literal=MYSQL_PASSWORD='appPass' \
  -n demo

kubectl create secret tls demo-tls \
  --cert=./tls/tls.crt \
  --key=./tls/tls.key \
  -n demo

2.6.5.2. YAML 形式

secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: mysql-secret
  namespace: demo
type: Opaque
data:
  MYSQL_ROOT_PASSWORD: UzNjdXJlIVBhc3M=
  MYSQL_PASSWORD: YXBwUGFzcw==

生成 base64:

echo -n 'S3cure!Pass' | base64

解码验证:

kubectl get secret mysql-secret -n demo -o jsonpath='{.data.MYSQL_ROOT_PASSWORD}' | base64 -d

2.6.5.3. 常见 Secret 类型

类型

用途

必填 key

Opaque

通用

自定义

kubernetes.io/tls

TLS 证书

tls.crt、tls.key

kubernetes.io/dockerconfigjson

镜像仓库凭据

.dockerconfigjson

kubernetes.io/basic-auth

用户名密码

username、password

kubernetes.io/ssh-auth

SSH 私钥

ssh-privatekey

2.6.5.4. 镜像拉取凭据

kubectl create secret docker-registry regcred \
  --docker-server=registry.example.com \
  --docker-username=robot \
  --docker-password='***' \
  --docker-email=[email protected] \
  -n demo
spec:
  imagePullSecrets:
    - name: regcred

2.6.6. 热更新:到底谁会变

消费方式

更新后是否生效

说明

环境变量(env / envFrom)

否

进程启动时读取,必须重启 Pod

卷挂载(目录)

是(有延迟)

kubelet 周期同步,通常几十秒

卷挂载(subPath)

否

单文件绑定,不更新

immutable: true

否

禁止修改,只能删了重建

即使文件更新了,应用也未必会重新加载。常见做法:

  1. 应用支持 SIGHUP / 文件监听:最理想;

  2. 滚动重启:kubectl rollout restart deploy/app;

  3. Checksum 注解:Helm 中把 ConfigMap 的 hash 写进 Pod template,配置变则 Pod 重建;

  4. 配置热加载 Sidecar:如 reloader,监听 ConfigMap 变化并触发重启。

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

2.6.7. Downward API

Downward API 把 Pod / 容器的元数据注入容器,不需要额外对象。

downward-api.yaml
apiVersion: v1
kind: Pod
metadata:
  name: downward-demo
  labels:
    app: demo
spec:
  containers:
    - name: app
      image: busybox:1.36
      command: ["sh", "-c", "env; sleep 3600"]
      env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        - name: NODE_NAME
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName
        - name: CPU_LIMIT
          valueFrom:
            resourceFieldRef:
              containerName: app
              resource: limits.cpu
      volumeMounts:
        - name: podinfo
          mountPath: /etc/podinfo
  volumes:
    - name: podinfo
      downwardAPI:
        items:
          - path: labels
            fieldRef:
              fieldPath: metadata.labels
          - path: annotations
            fieldRef:
              fieldPath: metadata.annotations

2.6.8. Projected Volume

Projected Volume 把多个来源(Secret、ConfigMap、Downward API、ServiceAccount Token)投影到同一个目录,适合需要把证书、配置、Token 放在一起的场景。

projected.yaml
apiVersion: v1
kind: Pod
metadata:
  name: projected-demo
spec:
  containers:
    - name: app
      image: nginx:1.27
      volumeMounts:
        - name: all-in-one
          mountPath: /etc/app
          readOnly: true
  volumes:
    - name: all-in-one
      projected:
        sources:
          - configMap:
              name: app-config
          - secret:
              name: demo-tls
          - downwardAPI:
              items:
                - path: pod-name
                  fieldRef:
                    fieldPath: metadata.name
          - serviceAccountToken:
              path: token
              expirationSeconds: 3600
              audience: api

2.6.9. 实验:配置热更新验证

2.6.9.1. 1. 创建 ConfigMap 并挂载

kubectl create namespace demo --dry-run=client -o yaml | kubectl apply -f -
kubectl create configmap web-config \
  --from-literal=index.html='version 1' \
  -n demo
web.yaml
apiVersion: v1
kind: Pod
metadata:
  name: web
  namespace: demo
spec:
  containers:
    - name: nginx
      image: nginx:1.27
      volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html
  volumes:
    - name: html
      configMap:
        name: web-config
kubectl apply -f web.yaml
kubectl exec -n demo web -- cat /usr/share/nginx/html/index.html

2.6.9.2. 2. 更新 ConfigMap

kubectl create configmap web-config \
  --from-literal=index.html='version 2' \
  -n demo --dry-run=client -o yaml | kubectl apply -f -

等待 30–60 秒,再查看:

kubectl exec -n demo web -- cat /usr/share/nginx/html/index.html

预期变成 version 2。如果用的是 subPath,这里仍然是 version 1。

2.6.9.3. 3. 环境变量对比

给 Pod 加一个 env 引用 web-config,更新 ConfigMap 后检查环境变量,确认不会变化。

2.6.9.4. 4. 不可变 ConfigMap

apiVersion: v1
kind: ConfigMap
metadata:
  name: immutable-config
data:
  key: value
immutable: true

修改会被拒绝:

The ConfigMap "immutable-config" is invalid: data: Forbidden: field is immutable

只能删除重建。

2.6.10. 排错表

现象

原因

处理

CreateContainerConfigError

引用的 ConfigMap/Secret 不存在或 key 缺失

describe pod 看具体 key

环境变量为空

key 名不匹配 / envFrom 非法 key

kubectl get cm -o yaml 对照

挂载目录为空

items 路径写错

检查 path 与 mountPath

更新不生效

用了 subPath / 环境变量

改为目录挂载或重启

Secret 解码乱码

编码时带了换行

用 echo -n 或 printf

TLS Secret 不被 Ingress 识别

缺少 tls.crt / tls.key

检查类型与 key

镜像拉取 401

没配 imagePullSecrets

创建 docker-registry Secret

2.6.11. 进阶挑战

  1. 用 immutable: true 保护一个生产配置,设计一次安全的配置变更流程。

  2. 用 Projected Volume 同时挂载 ConfigMap、TLS Secret 和 ServiceAccount Token。

  3. 写一个脚本,比较 ConfigMap 更新前后 Pod 内文件的内容与 mtime。

  4. 给 Deployment 加 checksum 注解,验证配置变更会触发滚动更新。

  5. 用 kubectl auth can-i get secrets 检查当前用户是否有权读取 Secret。

2.6.12. 小结

  • ConfigMap 管普通配置,Secret 管敏感数据;Secret 的 base64 不是加密。

  • 环境变量不会热更新,目录挂载会,subPath 不会。

  • Downward API 注入元数据,Projected Volume 把多个来源合并到一个目录。

  • 生产环境要限制 Secret 的 RBAC,并考虑外部密钥管理。

2.6.13. 延伸阅读