Kubernetes 基础体系 · 第 22/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

ConfigMap 与 Secret:注入、更新、不可变、轮换和安全边界

ConfigMap 和 Secret 都是 Kubernetes API 中用于保存配置数据的对象。它们可以被 Pod 以环境变量、文件或命令行参数的形式使用,但二者的语义并不相同:

  • ConfigMap 用于非机密配置,例如日志级别、功能开关、配置文件片段。
  • Secret 用于需要限制访问范围的数据,例如密码、令牌、私钥、证书。
  • Secret 的“机密性”不是由对象名称或 kind: Secret 自动提供的。默认情况下,Secret 数据通常只是经过 Base64 编码;真正的保护还依赖 API 访问控制、etcd 静态加密、节点安全和应用自身的泄漏控制。

本文基于 Kubernetes 当前稳定的 v1 API,重点解释数据注入、更新传播、不可变对象、凭据轮换和安全边界之间的因果关系。

1. 先建立对象和 Pod 之间的关系

一个 ConfigMap 或 Secret 是 Namespace 范围内的 API 对象。Pod 引用它时,引用中只写对象名称:

apiVersion: v1
kind: ConfigMap
metadata:
  name: payment-config
  namespace: production
data:
  LOG_LEVEL: "info"
  config.yaml: |
    timeoutSeconds: 3
apiVersion: v1
kind: Secret
metadata:
  name: payment-credentials
  namespace: production
type: Opaque
stringData:
  username: payment
  password: change-me

Pod 也必须位于 production Namespace:

apiVersion: v1
kind: Pod
metadata:
  name: payment
  namespace: production
spec:
  containers:
    - name: app
      image: example/payment:1.0
      envFrom:
        - configMapRef:
            name: payment-config
        - secretRef:
            name: payment-credentials

Pod 不能直接引用另一个 Namespace 中的 ConfigMap 或 Secret。这个限制不是语法偶然,而是安全边界的一部分:如果任意 Namespace 的 Pod 都能直接引用其他 Namespace 的 Secret,Namespace 隔离和 RBAC 的意义会被显著削弱。

Pod 内的多个容器共享同一个网络 Namespace 和 Pod 生命周期;它们也可以通过 Pod volume 共享由 ConfigMap 或 Secret 提供的文件。因此,一个 Pod 中拥有访问该 volume 的容器,通常都能读取其中的数据。容器之间不是 Secret 的安全隔离边界。

Pod 还具有一个由 kubelet 管理的 sandbox 和容器生命周期。ConfigMap、Secret 的 volume 文件由 kubelet 在节点上准备,应用容器读取的是节点上挂载后的文件,而不是每次读取都访问 API Server。

2. ConfigMap 和 Secret 的数据模型

2.1 ConfigMap 的 databinaryData

ConfigMap 的文本数据位于 data

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  LOG_LEVEL: "debug"
  app.yaml: |
    server:
      port: 8080

不能把 ConfigMap 当作无限大小的配置数据库使用。ConfigMap 和 Secret 都受 Kubernetes 对象大小及 API Server 请求限制影响,通常应将约 1 MiB 视为单个对象的上限量级;实际可用大小还会受到序列化、etcd 和 API Server 配置影响。大型配置应放在专门的配置系统或对象存储中,由应用按需加载。

binaryData 用于二进制内容,值也以 Base64 表示:

apiVersion: v1
kind: ConfigMap
metadata:
  name: binary-config
binaryData:
  logo.bin: AAEC

databinaryData 是两个字段,不应把同一个键同时放入二者。

2.2 Secret 的 datastringData 和 Base64

Secret 的 data 字段要求每个值是 Base64 编码:

apiVersion: v1
kind: Secret
metadata:
  name: db-secret
type: Opaque
data:
  username: cGF5bWVudA==
  password: Y2hhbmdlLW1l

解码后分别是:

username = payment
password = change-me

Base64 只是编码,不是加密。任何能够读取 Secret 对象的人都可以直接解码:

kubectl -n production get secret db-secret \
  -o jsonpath='{.data.password}' | base64 --decode

stringData 便于手写清单:

apiVersion: v1
kind: Secret
metadata:
  name: db-secret
type: Opaque
stringData:
  username: payment
  password: change-me

API Server 接收对象时会把 stringData 合并到 data 的语义结果中;读取 Secret 时通常看到的是 data,而不是原始 stringData。如果同一个键同时存在,stringData 的值会覆盖对应的 data 值。

stringData 适合交互式创建,但在使用 Server-Side Apply 时要特别谨慎:官方文档明确指出,stringData 与 Server-Side Apply 的字段管理存在不良交互。需要声明式持续管理时,应考虑在安全的流水线中生成 data,或使用专门的 Secret 管理方案,避免把明文写入 Git。

Secret 的 type 是用途标识和部分工具的行为提示,例如:

  • Opaque:通用 Secret。
  • kubernetes.io/tls:通常包含 tls.crttls.key
  • kubernetes.io/dockerconfigjson:镜像仓库认证配置。
  • kubernetes.io/basic-auth:通常包含用户名和密码。

type 本身不会让数据更安全,也不等同于 Kubernetes 自动验证了内容的密码学有效性。

3. 三种注入方式及其状态语义

ConfigMap 和 Secret 常见的注入方式有三种:

  1. 单个键映射为环境变量;
  2. 多个键通过 envFrom 导入环境变量;
  3. 作为 volume 挂载为文件。

此外,还可以把引用值写入容器启动命令参数。不同方式的更新行为不同,这是生产中最容易误判的地方。

3.1 单个环境变量

apiVersion: v1
kind: Pod
metadata:
  name: env-example
  namespace: production
spec:
  containers:
    - name: app
      image: busybox:1.36
      command: ["sh", "-c"]
      args: ["echo LOG_LEVEL=$LOG_LEVEL; sleep 3600"]
      env:
        - name: LOG_LEVEL
          valueFrom:
            configMapKeyRef:
              name: app-config
              key: LOG_LEVEL
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: password

这里的关键过程是:

  1. Pod 被调度到节点;
  2. kubelet 在创建容器时解析 ConfigMap 和 Secret 引用;
  3. 得到的值被写入容器的初始环境;
  4. 容器进程启动;
  5. 后续对象更新不会修改已经存在的进程环境。

因此,更新 app-configdb-secret 后,容器内已有进程看到的环境变量不会自动变化。若应用需要新值,必须重新创建容器,通常通过滚动更新 Deployment 完成。

这也意味着环境变量适合“启动时读取一次”的配置,不适合要求在线变化的配置或频繁轮换的凭据。

3.2 envFrom

env:
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: db-secret
        key: password

envFrom:
  - configMapRef:
      name: app-config
  - secretRef:
      name: db-secret

envFrom 会把对象中的键批量转换为环境变量名。键名必须满足环境变量命名约束;非法或不适合作为环境变量名的键可能被跳过并产生事件,而不是以文件名形式保留。

批量导入还带来命名冲突问题。多个来源可能提供同名键,显式 envenvFrom 的来源顺序和容器运行时最终环境构造规则会影响结果。生产配置应尽量使用明确的单键映射,或者使用唯一、带前缀的键名,避免把整个 Secret 无差别导入环境。

3.3 作为 volume 挂载

apiVersion: v1
kind: Pod
metadata:
  name: volume-example
  namespace: production
spec:
  containers:
    - name: app
      image: example/payment:1.0
      volumeMounts:
        - name: app-config
          mountPath: /etc/payment
          readOnly: true
        - name: db-credentials
          mountPath: /var/run/secrets/payment
          readOnly: true
  volumes:
    - name: app-config
      configMap:
        name: app-config
    - name: db-credentials
      secret:
        secretName: db-secret

容器中通常会看到:

/etc/payment/LOG_LEVEL
/etc/payment/app.yaml
/var/run/secrets/payment/username
/var/run/secrets/payment/password

文件名来自 ConfigMap 或 Secret 的键,文件内容是对应值。volume 方式与环境变量最大的不同是:对象更新后,kubelet 通常会把新的内容投影到挂载目录中。这不是应用一定能感知的保证,而是文件内容更新机制;应用仍然需要重新读取文件或监听文件变化。

典型实现会在挂载目录中维护版本化内容,并通过符号链接或原子切换让容器看到新版本。应用不应依赖某个具体的宿主机目录结构,而应只读取挂载路径下的公开文件。更新传播存在延迟,延迟受到 kubelet 同步周期、缓存策略和对象变更通知方式影响,不能把它当作严格实时推送。

如果应用只在启动时读取 /etc/payment/app.yaml,文件后来更新也不会改变应用内存中的配置。正确的因果链是:

API 对象更新
  → kubelet 发现新版本
  → 挂载目录切换到新内容
  → 应用重新读取或检测到文件变化
  → 应用内部状态更新

前三步不代表最后一步自动发生。

3.4 subPath 是一个重要反例

下面的挂载方式看似只挂载一个文件:

volumeMounts:
  - name: app-config
    mountPath: /etc/payment/app.yaml
    subPath: app.yaml

使用 ConfigMap 或 Secret volume 的 subPath 挂载时,后续 ConfigMap 或 Secret 更新不会自动反映到这个 subPath 文件中。原因是它不是持续暴露整个投影目录,而是在容器启动时绑定一个具体路径。

如果需要动态更新,应挂载整个目录:

volumeMounts:
  - name: app-config
    mountPath: /etc/payment

然后让应用读取 /etc/payment/app.yaml。如果应用必须使用单文件路径,常见做法是由应用或 sidecar 负责把目录中的当前文件复制到目标位置,但这会引入额外同步、权限和故障处理逻辑。

4. 引用不存在或更新失败时会发生什么

引用 ConfigMap 或 Secret 时,可以选择是否要求它必须存在:

env:
  - name: OPTIONAL_MODE
    valueFrom:
      configMapKeyRef:
        name: optional-config
        key: mode
        optional: true

optional: true 时,缺失对象或缺失键通常会被当作没有该值处理;当为 false 或未设置时,缺少必需数据会阻止容器正常创建,Pod 可能处于 CreateContainerConfigError 状态。

诊断时应同时检查 Pod 事件和对象本身:

kubectl -n production describe pod payment

kubectl -n production get configmap app-config -o yaml
kubectl -n production get secret db-secret -o yaml

describe pod 中的事件通常比只查看 Pod 状态更有用,例如可以区分:

  • 引用对象不存在;
  • 引用的键不存在;
  • Secret 类型或内容不符合应用预期;
  • volume 挂载失败;
  • 容器启动后自行退出。

应用已经运行后修改对象,不会修复一个已经注入到环境变量中的错误值;即使对象内容正确,容器仍可能持有旧值。

5. 更新传播与 Deployment 重启

5.1 为什么修改 ConfigMap 不一定重启 Pod

Kubernetes 不会因为 ConfigMap 或 Secret 变化就自动创建新的 Pod。Deployment 的 Pod 模板没有变化,因此 ReplicaSet 的期望状态也没有变化。

例如:

kubectl -n production edit configmap app-config

这可能让 volume 文件最终变化,但不会自动滚动重启 Deployment,也不会更新环境变量。

如果应用只能在启动时读取配置,可以显式重启:

kubectl -n production rollout restart deployment/payment
kubectl -n production rollout status deployment/payment

这会创建新的 ReplicaSet Pod,而不是修改原 Pod 的进程环境。

5.2 用 Pod 模板哈希触发滚动更新

声明式部署常把配置内容的哈希写入 Pod 模板注解:

spec:
  template:
    metadata:
      annotations:
        config-hash: "由发布系统根据 app-config 内容计算"

当配置内容变化时,发布系统更新 config-hash,Deployment 认为 Pod 模板变化,于是创建新的 ReplicaSet。

这个方案的关键不是注解名称,而是条件:

新 ReplicaSetPodTemplateSpecnewPodTemplateSpecold\text{新 ReplicaSet} \Leftarrow \text{PodTemplateSpec}_{new} \ne \text{PodTemplateSpec}_{old}

ConfigMap 内容本身不属于 Deployment 的 Pod 模板;只有把内容变化反映到模板字段,才能触发 Deployment 的滚动更新。哈希不能放成固定字符串,否则每次发布模板都相同,不会产生滚动更新。

5.3 直接 kubectl set env 的边界

可以通过命令修改 Deployment 的环境变量:

kubectl -n production set env deployment/payment CONFIG_VERSION=2025-01-01

这会改变 Pod 模板,进而触发滚动更新。但它只适合明确管理的环境变量,不会自动把 ConfigMap 的全部内容同步到 Pod 模板,也可能与 Git 中的声明式配置产生漂移。

6. 不可变 ConfigMap 和 Secret

immutable: true 表示对象创建后,其数据不能再被修改:

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config-v1
immutable: true
data:
  LOG_LEVEL: "info"
apiVersion: v1
kind: Secret
metadata:
  name: db-credentials-v1
immutable: true
type: Opaque
stringData:
  username: payment
  password: initial-password

不可变对象的状态转移是:

创建
  → 可读取、可引用
  → data 允许读取但不允许修改
  → 需要新内容时创建新对象

尝试修改数据会被 API Server 拒绝,而不是静默接受:

kubectl -n production patch configmap app-config-v1 \
  --type merge \
  -p '{"data":{"LOG_LEVEL":"debug"}}'

预期结果是类似“对象不可变”的错误。

不可变的主要机制收益是减少意外修改和 kubelet 对对象变更的监听、缓存压力;它不是加密机制,也不阻止有权限的用户读取对象。不可变 Secret 仍然可能被 get secret 的主体读取,也仍然可能通过环境变量、文件、日志或调试接口泄漏。

不可变对象改变了更新策略:不能原地修改,而要使用新名称或新版本:

volumes:
  - name: db-credentials
    secret:
      secretName: db-credentials-v2

然后更新 Deployment 的 Pod 模板,让新 Pod 引用 v2。推荐顺序是:

  1. 创建 db-credentials-v2
  2. 更新 Deployment,使 Pod 模板引用 v2
  3. 等待新 Pod 就绪;
  4. 验证应用已使用新凭据;
  5. 删除不再需要的 v1,同时确认没有其他工作负载引用它。

直接删除并重新创建同名对象不是等价的安全替换方案。删除期间可能导致新 Pod 无法启动,旧 Pod 的挂载行为也不应被当作可靠的原子更新协议。使用新名称并让 Deployment 通过滚动更新切换,状态更容易观察和回滚。

immutable 字段本身一旦设为 true 也不能再改回 false;若必须恢复可变对象,需要创建新对象。

7. Secret 轮换:数据更新不是凭据切换

轮换是生成或取得新凭据、让消费者切换到新凭据、验证新凭据有效后撤销旧凭据的完整过程。仅仅修改 Secret 对象,不等于轮换完成。

设旧凭据为 C0C_0,新凭据为 C1C_1。安全切换至少需要满足:

  1. 身份提供方接受 C1C_1
  2. 应用能够读取并使用 C1C_1
  3. 在所有必要消费者完成切换前,C0C_0 仍可用;
  4. 所有消费者完成切换后,才撤销 C0C_0

因此典型流程是:

生成 C1
  → 在数据库或外部身份系统中启用 C1
  → 创建 Secret v2
  → 更新工作负载并滚动发布
  → 验证新 Pod 使用 C1 成功
  → 撤销 C0
  → 清理 Secret v1

如果数据库只允许一个密码,直接把数据库密码改成 C1C_1,再更新 Kubernetes Secret,会产生一个时间窗口:

数据库已经要求 C1
Pod 环境变量仍然是 C0
应用连接失败

更稳妥的方案是双凭据或双用户轮换:

  1. 创建新数据库用户或新密码 C1C_1
  2. 同时保留 C0C_0
  3. 发布使用 C1C_1 的新 Pod;
  4. 通过健康检查、连接指标和业务验证确认成功;
  5. 撤销 C0C_0

如果凭据通过 Secret volume 提供,应用可以设计为重新读取文件并重建连接池,从而避免 Pod 重启;但这必须是应用明确实现的行为。若使用环境变量,通常需要重启或滚动更新,因为环境变量对运行中的进程是固定的。

轮换还必须处理失败路径:

  • 新凭据创建成功,但发布失败:保留旧凭据,不要立即撤销;
  • 部分 Pod 已切换,部分 Pod 未切换:在双凭据窗口中允许两者;
  • 新凭据格式错误:新 Pod 启动探针或业务探针应失败,Deployment 保留旧 Pod;
  • 旧凭据撤销后发现仍有旧 Pod:撤销动作可能造成线上错误,需先确认所有消费者;
  • Secret 泄漏:不能只更新 Kubernetes 对象,还必须在真实凭据提供方撤销泄漏凭据。

Kubernetes 不负责判断某个 Secret 是否已被应用使用,也不负责协调外部数据库凭据的有效期。轮换控制器、发布系统和应用协议必须共同完成这条链路。

8. Secret 的安全边界

8.1 Secret 不是自动加密

默认读取对象时,Secret 的内容以 Base64 形式存储在 API 对象中:

kubectl -n production get secret db-secret -o yaml

能读取该对象的人可以解码;能读取保存该对象的 etcd 数据的人也可能获得原始内容。要保护 etcd 静态数据,需要在 API Server 配置加密提供者,例如使用 EncryptionConfiguration 配置 Secret 的加密存储,并在云环境中结合 KMS 提供密钥保护。

静态加密解决的是:

etcd 磁盘、etcd 备份或存储介质被读取

它不解决:

拥有 Secret get 权限的用户
拥有 Pod exec 权限的用户
运行在目标节点上的高权限进程
应用把密码打印到日志
应用把密码发送到第三方

KMS 通常为 API Server 的数据加密提供外部密钥管理能力。KMS 插件、版本、部署方式和云厂商实现存在差异,不能仅凭“集群启用了 KMS”就推断所有数据已经满足某个合规要求。应验证实际加密配置、密钥权限、轮换、备份恢复和故障时 API Server 的行为。

8.2 RBAC 是主要访问控制边界

一个最小读取权限示例:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: payment-secret-reader
  namespace: production
rules:
  - apiGroups: [""]
    resources: ["secrets"]
    resourceNames: ["db-secret"]
    verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: payment-secret-reader
  namespace: production
subjects:
  - kind: ServiceAccount
    name: payment
    namespace: production
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: payment-secret-reader

这个例子把权限限制为:

  • production Namespace;
  • 仅名为 db-secret 的 Secret;
  • get 操作;
  • 仅绑定给 payment ServiceAccount。

实际还需要检查工作负载是否使用了这个 ServiceAccount:

spec:
  serviceAccountName: payment

如果 Pod 使用默认 ServiceAccount,却把读取权限授予 payment,权限配置不会产生预期效果。

应避免给普通应用授予:

resources: ["secrets"]
verbs: ["list", "watch"]

listwatch 可能让主体获得大量 Secret,而不仅是单个对象。也要避免把 Secret 读取权限授予 Namespace 中所有 ServiceAccount 或整个集群的通用角色。

可用以下命令检查权限:

kubectl auth can-i get secret/db-secret \
  --namespace production \
  --as system:serviceaccount:production:payment

kubectl auth can-i list secrets \
  --namespace production \
  --as system:serviceaccount:production:payment

第一个命令应返回 yes,第二个在最小权限设计中通常应返回 no

8.3 节点和容器权限会改变边界

Secret volume 最终由 kubelet 在节点上准备并挂载给容器。拥有节点级高权限、能够访问 kubelet 或容器运行时数据的攻击者,可能绕过普通 Namespace 视角读取节点上 Pod 的 Secret。

因此:

  • Namespace 不是节点级隔离;
  • Pod 安全上下文和容器特权设置仍然重要;
  • privileged 容器、主机路径挂载、主机 PID/网络等能力会扩大影响范围;
  • 能执行 kubectl exec 的人通常可以在应用进程上下文中读取环境变量或挂载文件;
  • 备份系统、日志系统、调试工具可能复制 Secret 内容。

Secret 的安全性是多层条件的交集,而不是单个 API 字段:

有效保护=API 访问控制etcd 静态加密节点安全应用不泄漏轮换可控\text{有效保护} = \text{API 访问控制} \land \text{etcd 静态加密} \land \text{节点安全} \land \text{应用不泄漏} \land \text{轮换可控}

其中任一项失效,都可能导致机密暴露或无法及时止损。

9. ConfigMap 与 Secret 的选择边界

选择依据不是“配置文件放 ConfigMap、密码放 Secret”这么简单,而是数据是否需要机密保护、消费者如何读取以及更新频率。

适合 ConfigMap 的例子:

data:
  LOG_LEVEL: "info"
  FEATURE_X_ENABLED: "true"
  config.yaml: |
    timeoutSeconds: 3

适合 Secret 的例子:

stringData:
  database-url: postgresql://...
  api-token: ...
  private-key.pem: |
    -----BEGIN PRIVATE KEY-----
    ...

但以下误区需要明确:

  • 把 ConfigMap 中的字段命名为 PASSWORD,不会使它变成机密;
  • 把 Secret 放入 Git 后再用 Base64 编码,不会防止泄漏;
  • 使用 Secret 不会阻止应用把值打印到日志;
  • Secret 不是外部密码管理器,不会自动生成、续期或撤销凭据;
  • TLS Secret 只是一种数据约定,证书是否过期需要额外的证书管理流程。

如果应用需要严格的保密性、审计、自动续期或短生命周期凭据,仅使用原生 Secret 往往不够。

10. External Secrets 的位置和风险

External Secrets 一类方案通常通过 CRD 和控制器,把外部密钥系统中的值同步为 Kubernetes Secret。例如外部系统可能是云厂商 Secrets Manager、Vault 或其他企业密码平台。

其数据流大致是:

flowchart LR
    A[外部密钥系统] -->|控制器读取| B[External Secret 控制器]
    B -->|创建或更新| C[Kubernetes Secret]
    C -->|env 或 volume| D[Pod]
    D -->|重新读取或重启| E[应用使用新凭据]

这个方案把“源数据治理”放到了外部系统,但不会自动消除 Kubernetes 中的安全边界问题:

  1. 控制器需要读取外部系统的权限;
  2. 控制器需要写入 Kubernetes Secret 的权限;
  3. 同步后的 Kubernetes Secret 仍可能被 API 读取、存入 etcd、挂载到节点;
  4. 外部系统轮换后,控制器同步存在延迟;
  5. 应用仍需处理文件重读、连接池更新或滚动重启;
  6. 外部系统不可用时,必须定义是保留旧值、阻止发布还是让 Pod 失败。

因此,External Secrets 不是“Secret 不再存在于 Kubernetes”,而是把 Kubernetes Secret 变成外部系统的派生副本。生产设计需要明确源系统、同步方向、刷新周期、权限、失败回退和删除策略。

如果目标是让应用完全不把长期凭据写入 Kubernetes,可能需要使用工作负载身份和运行时直接向外部系统获取短期凭据;这已经超出原生 ConfigMap/Secret 注入机制,并需要相应的 SDK、网络和身份设计。

11. ServiceAccount Token 的特殊情况

ServiceAccount Token 也是 Secret 相关主题中容易混淆的一类数据,但现代 Kubernetes 中不应把长期的自动生成 Token Secret 当作默认方案。

Pod 通常通过 TokenRequest 机制获得短期、绑定 Pod 的 ServiceAccount token,并由 serviceAccountToken projected volume 投影到容器。示例:

volumes:
  - name: api-access
    projected:
      sources:
        - serviceAccountToken:
            path: token
            expirationSeconds: 3600
            audience: https://kubernetes.default.svc

这个 token 与用户创建的普通 Opaque Secret 不同:

  • 它有受众 audience
  • 通常有过期时间;
  • 可以由 kubelet 投影和更新;
  • 应用必须能够重新读取 token 文件;
  • API Server 或外部服务是否接受该 token,取决于其验证配置。

不要把 ServiceAccount token、数据库密码和普通应用 Secret 混为同一类长期凭据。版本较老的集群可能仍存在自动生成的长期 Secret token,但新版本的默认行为已经转向短期 TokenRequest;跨版本和发行版时应检查实际配置,而不是假设所有集群行为相同。

12. 常见失败表现和诊断路径

12.1 修改 Secret 后应用仍使用旧密码

首先判断注入方式:

kubectl -n production get pod payment -o yaml

如果是 env.valueFrom.secretKeyRef,旧值会持续存在,必须让容器重建:

kubectl -n production rollout restart deployment/payment
kubectl -n production rollout status deployment/payment

如果是 Secret volume,则检查挂载目录是否出现新内容:

kubectl -n production exec payment -- \
  sh -c 'stat /var/run/secrets/payment/password'

但不要为了诊断把密码直接输出到终端或日志。应用可能仍缓存旧密码,或者连接池还在使用旧连接;此时需要应用级重连或重启。

12.2 Pod 进入 CreateContainerConfigError

检查事件:

kubectl -n production describe pod payment

然后验证对象和键:

kubectl -n production get secret db-secret
kubectl -n production get secret db-secret \
  -o jsonpath='{.data.password}' | base64 --decode

只在受控终端执行解码命令,并注意 shell 历史、终端录屏和审计系统可能保存敏感内容。更安全的验证方式是检查键是否存在,而不是打印值:

kubectl -n production get secret db-secret \
  -o jsonpath='{.data.password}' | test -n "$(cat)"

这类命令仍可能在错误处理或调试环境中暴露内容,生产诊断应优先使用应用健康检查和不包含机密的校验结果。

12.3 Secret volume 没有更新

检查以下条件:

  • Pod 是否实际挂载了整个 volume,而不是 subPath
  • 修改是否成功写入 API 对象;
  • kubelet 是否仍能访问 API Server;
  • 节点是否有 kubelet 事件或权限错误;
  • 应用是否一直持有旧文件内容;
  • 应用是否把文件复制到另一个位置后不再读取。

可以先检查对象的 resourceVersion 是否变化,再观察文件修改时间和应用指标,而不是直接打印 Secret:

kubectl -n production get secret db-secret \
  -o jsonpath='{.metadata.resourceVersion}{"\n"}'

kubectl -n production exec payment -- \
  stat /var/run/secrets/payment/password

具体传播时间不是 Kubernetes API 对用户作出的实时保证,不能把“修改成功”解释为“所有 Pod 已经立即读取新值”。

12.4 发布后旧 Pod 和新 Pod 同时存在

滚动更新的本质是控制器在副本可用性和更新速度之间进行协调。轮换凭据时,旧 Pod 可能在新 Pod 就绪前继续服务;这通常是期望行为,但要求旧凭据在切换窗口内仍有效。

如果先撤销旧凭据,再等待新 Pod 就绪,可能出现:

旧 Pod 使用 C0 → C0 被撤销 → 新 Pod 尚未就绪 → 服务连接失败

因此,轮换必须把外部凭据有效期与 Deployment 的滚动更新策略一起设计,而不能只修改 Secret YAML。

13. 生产风险与取舍

将配置通过环境变量注入很简单,但环境变量可能被调试命令、崩溃转储、诊断接口或应用日志暴露。Secret volume 避免了环境变量复制,但文件仍可被容器内进程读取,且应用需要处理文件更新。

把配置做成不可变对象可以减少意外原地修改,并让版本切换更明确,但会增加对象生命周期和垃圾清理工作。若使用固定名称并删除重建,故障窗口和回滚语义会变差;使用版本化名称则需要发布系统管理引用变更。

把 Secret 加密存入 etcd 可以降低磁盘和备份泄漏风险,但不能替代 RBAC。把 RBAC 收紧到单个 Secret 可以减少读取范围,但无法阻止被授权的应用主动打印内容。使用外部密钥系统可以改善密钥生成、审计和轮换,却会增加控制器、网络、权限和同步失败路径。

最终应根据应用的读取模型选择机制:

应用需求 更匹配的方案
启动时读取一次的非机密配置 ConfigMap 环境变量或文件
需要在线重新读取的配置 ConfigMap volume,避免 subPath,应用实现重读
启动时读取的凭据 Secret 环境变量,但轮换时需要滚动更新
需要文件形式的证书、私钥或凭据 Secret volume
需要自动轮换和审计 外部密钥系统或 External Secrets 类控制器
需要短期身份令牌 TokenRequest 或其他工作负载身份机制
需要强隔离 结合 RBAC、etcd 加密、节点安全、Pod 安全和应用级防泄漏

ConfigMap 和 Secret 解决的是“如何把键值数据交给 Pod”的问题,不是完整的配置治理、凭据生命周期或机密计算方案。正确的设计必须同时回答四个问题:数据存在哪里、谁能读取、更新如何传播、旧值何时失效。只有这四个问题都具有明确的状态转换和失败恢复路径,注入机制才真正可控。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。