#DevOps

Kubernetes ConfigMap 与 Secrets:配置与密钥管理

配置不该硬编码进镜像——ConfigMap 存非敏感配置,Secret 存密钥。本文讲清两者的创建/挂载(env vs volume)、Secret 的 base64 与加密(etcd 加密/KMS/外部 secrets operator)、ConfigMap 热更新(subPath 不更新陷阱),以及生产级密钥管理(External Secrets Operator)。

本文要点

  • 主题聚焦:DevOps
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年1月23日·更新于 2026年7月23日

Kubernetes ConfigMap 与 Secrets:配置与密钥管理

讲一个我亲历的事故。某团队的应用连不上数据库,排查发现是 DB 密码改了但应用还在用老的——因为密码被 ENV DB_PASSWORD=xxx 写死进了 Dockerfile,重新 build 镜像要走一整套 CI/CD,半小时才上线。期间业务全线 502。复盘时另一个组的同学举手:"我们密码没写镜像里,写在 K8s Secret 的 YAML 里,但那个 YAML 提交进了 Git 仓库明文存着。"

两种姿势,两种翻车。前者是配置和镜像耦合——改个配置要重打镜像、重走流水线;后者是密钥和代码耦合——仓库一泄露,所有环境所有密钥同时泄露。

K8s 给的解药是两个 API:ConfigMap 存非敏感配置(日志级别、特性开关、nginx.conf),Secret 存敏感数据(DB 密码、API key、TLS 证书)。但这两个 API 本身有一堆坑:ConfigMap 的 env 注入不会热更新、subPath 挂载永远不更新、Secret 的 base64 根本不是加密、默认 etcd 里明文裸奔。本文把这一整条链路拆透——从基础的"为什么配置要外置",到生产级的 External Secrets Operator + Vault,给你一套能直接抄的姿势。

一、第一原则:配置和密钥都不该进镜像

先确立心智模型。一个镜像应当是环境无关的制品——同一个 app:1.2.3 镜像,应当能原封不动地跑在 dev、staging、prod 三个环境,区别只在它启动时注入的配置和密钥。

这条原则有几层理由,每一层都和血泪事故挂钩:

  • 不可变性 vs 可变性。镜像走 registry、被缓存、被签名、被分发——它天生适合"一次构建多处部署"。把环境相关的值(DB host、密钥、特性开关)打进去,等于让一个可变的东西寄生在一个不可变的载体里,每次配置变更都要重打镜像、重推 registry、重启所有 Pod。把配置外置,改一行 ConfigMap 一秒生效(env 注入除外,后面讲),这是云原生"配置即代码"的前提。
  • 安全性。镜像会被开发者本地 docker pull、会被推到第三方 registry、会被打进灾难恢复的备份。任何一处泄露,写死在镜像里的密钥就跟着泄露。密钥进镜像 = 定时炸弹。
  • 审计与轮换。密钥是有寿命的,必须定期轮换。如果密钥在镜像里,轮换一次 = 重打镜像 + 全量滚动重启;如果在 Secret 里,改一个 Secret 对象,滚动重启一波即可;如果在外部 Vault 里 + 动态密钥,连 Secret 都不用动。

K8s 的 ConfigMap 和 Secret 就是承接这条原则的两个 API。它们俩的 API 形态几乎一模一样(都是 key-value 的字典),区别只有两个:Secret 的 value 是 base64 编码的(存敏感数据),Secret 有类型约束(Opaque / kubernetes.io/tls / kubernetes.io/dockerconfigjson 等)。下面分别拆。

二、ConfigMap:非敏感配置的载体

ConfigMap 就是一组 key-value,给 Pod 提供配置。明确一点:它不加密,任何能 kubectl get cm 的人都能看到明文,所以只放非敏感的东西。把 DB 密码塞 ConfigMap 是新手最常见的错误之一。

创建 ConfigMap 的三种姿势

最直接的命令式创建,适合快速验证:

bash
# 从字面量创建
kubectl create configmap app-config \
  --from-literal=LOG_LEVEL=info \
  --from-literal=FEATURE_NEW_ONBOARDING=true \
  --from-literal=MAX_CONNECTIONS=100

# 从文件创建(key = 文件名,value = 文件内容)
kubectl create configmap nginx-config --from-file=nginx.conf

# 从目录创建(目录下每个文件都变成一个 key)
kubectl create configmap app-config --from-file=./config/

生产里更推荐声明式 YAML,进 Git 走 GitOps。注意 ConfigMap 用的是明文 data(值要 base64)或 stringData(值是明文字符串,API 会自动编码):

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: prod
data:
  LOG_LEVEL: "info"
  FEATURE_NEW_ONBOARDING: "true"
  MAX_CONNECTIONS: "100"
  # 也可以放整段配置文件
  application.yml: |
    server:
      port: 8080
    spring:
      datasource:
        hikari:
          maximum-pool-size: 20

我的建议:字面量值用 data,整段配置文件也用 data + 多行字符串,少用 stringData——后者虽然在 YAML 里看着像明文方便,但容易让人误以为这是"未加密的 Secret",混淆了 ConfigMap 和 Secret 的边界。

挂载姿势一:环境变量注入(env / envFrom)

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
spec:
  template:
    spec:
      containers:
        - name: app
          image: app:1.2.3
          # 单个 key 注入单个环境变量
          env:
            - name: LOG_LEVEL
              valueFrom:
                configMapKeyRef:
                  name: app-config
                  key: LOG_LEVEL
          # 整个 ConfigMap 一次性注入成多个环境变量
          envFrom:
            - configMapRef:
                name: app-config

envFrom 一行把 ConfigMap 里所有 key 都变成环境变量,省事。但有个坑:key 名必须符合环境变量命名规则(字母数字下划线,不能以数字开头),不然 K8s 会静默跳过这些 key(在事件里能看到报错,但 Pod 照常启动),你排查半天才发现 feature-flag.new 这种 key 根本没注入进来。

挂载姿势二:Volume 挂载(每个 key 一个文件)

yaml
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
        - name: app
          image: app:1.2.3
          volumeMounts:
            - name: config-vol
              mountPath: /etc/app
              readOnly: true
      volumes:
        - name: config-vol
          configMap:
            name: app-config

这样挂载后,ConfigMap 里每个 key 都变成 /etc/app/ 下的一个文件——/etc/app/LOG_LEVEL 内容是 info,/etc/app/application.yml 是那段 YAML。应用用文件读取 API 拿配置。

Volume 挂载比 env 注入有几个明显优势:

  • 整段配置文件(nginx.conf、application.yml)只能用 volume,env 塞不下多行
  • 支持热更新(下一节详谈)
  • 配置和代码解耦得更彻底——很多框架(Spring Boot、Next.js)天然支持从文件读配置

三、ConfigMap 的热更新:env 不更新,volume 更新,subPath 不更新

这是 ConfigMap 第一大翻车点。改了 ConfigMap,应用拿到的还是旧值——为什么?要看 K8s 的更新机制。

env 注入:永不更新。环境变量是容器启动时由 kubelet 写进进程环境的,进程一旦启动就不会变。改了 ConfigMap,已经运行的 Pod 拿到的还是老值,必须重启 Pod 才能生效。这是 env 注入的硬伤。

Volume 挂载:自动更新(有延迟)。kubelet 会定期(默认 1 分钟,由 kubelet 的 --sync-frequency 控制)把 ConfigMap 的最新内容同步到 Pod 挂载的 volume 里。文件内容会变,但你的应用代码得主动 watch 文件变化或定期重新读取才能感知——不然文件变了应用还在用内存里的旧值。

text
ConfigMap 变更时间线:
  T+0s    : kubectl apply 新 ConfigMap
  T+0~60s : kubelet 把新内容写进 volume(..data 符号链接指向新版本目录)
  T+?     : 应用重新读文件,看到新值(取决于应用代码)

注意中间这个 T+? 是不确定的。如果你的应用是启动时读一次配置进内存,那即使 volume 文件变了,应用拿到的还是启动时的旧值。热更新的完整链路是 ConfigMap 更新 → kubelet 同步 volume → 应用感知变化 → 应用 reload,最后一步得应用配合。很多框架(如 Nginx)需要配个 inotify watcher + reload 信号,或者干脆用 Reloader 这种 controller 监听 ConfigMap/Secret 变化,自动触发 Deployment 滚动重启。

subPath 挂载:永不更新(这是最大的陷阱)

如果你只想挂 ConfigMap 里的某一个文件到指定路径,很自然地会这样写:

yaml
volumeMounts:
  - name: config-vol
    mountPath: /etc/nginx/nginx.conf    # 想把单个文件挂到这里
    subPath: nginx.conf                 # 只取 ConfigMap 里的 nginx.conf 这个 key

这看起来很合理——但用 subPath 挂的文件永远不会热更新。原因:subPath 挂载本质上是把单个文件 bind mount 到目标路径,绕过了 ConfigMap volume 的符号链接更新机制。kubelet 同步的是整个 volume 目录,但 subPath 指向的那个固定 inode 不会跟着变。

这个坑坑过无数团队:改了 nginx.conf,apply 完看 kubectl describe 显示 ConfigMap 是新的,kubectl exec 进去看文件也是旧的(或者干脆还是老的),以为是缓存、是 kubelet 卡了、是 API server 出问题,折腾大半天。结论记死:

ConfigMap 热更新的三档:volume 挂载(会更新,1 分钟延迟)> env 注入(不更新,要重启 Pod)= subPath 挂载(不更新,要重启 Pod)。

如果你确实需要单文件挂载又想要热更新,要么挂整个目录让应用读对应文件,要么用 symlink 技巧,要么干脆配 Reloader 触发滚动重启——别在 subPath 上死磕。

四、Secret:敏感数据的载体

Secret 的 API 形态和 ConfigMap 几乎一样,但有三个关键差异必须先讲透。

第一,base64 编码,不是加密。 这是 K8s 新手第二大认知误区(第一大是"namespace 是网络边界")。Secret 的 data 字段值是 base64 编码的,base64 是编码不是加密——echo 'cGFzc3dvcmQxMjM=' | base64 -d 一行命令就还原。任何人只要 kubectl get secret xxx -o yaml 拿到内容,就能秒解。base64 的存在只是为了让二进制数据(TLS 证书、私钥)能安全地塞进 JSON/YAML,不是为了保密。

yaml
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
data:
  # 值是 base64 编码(echo -n 'password123' | base64 => cGFzc3dvcmQxMjM=)
  DB_PASSWORD: cGFzc3dvcmQxMjM=
stringData:
  # 也可以用 stringData 写明文,API server 会自动编码后存进 data
  # 方便人写,但 get -o yaml 看到的还是 base64
  API_KEY: sk_live_abc123

实战建议:写 YAML 用 stringData(明文方便 review),但永远不要把含 stringData 的 YAML 提交进 Git——这种文件一旦进仓库就等于明文密钥进仓库。生产环境的 Secret 应当由下一节讲的 External Secrets Operator 动态生成,根本不进 Git。

第二,类型(type)字段。 Secret 有若干预定义类型,影响校验和某些控制器的行为:

type用途
Opaque任意键值,默认类型,存 DB 密码 / API key 用这个
kubernetes.io/tls存 TLS 证书,必须含 tls.crt 和 tls.key 两个 key,Ingress 用它配 HTTPS
kubernetes.io/dockerconfigjson存私有 registry 的认证信息(auths JSON),配 imagePullSecrets
kubernetes.io/basic-auth用户名密码对(username / password)
kubernetes.io/service-account-tokenSA token,K8s 自动管理,别手动创建

绝大多数业务场景用 Opaque 就够了。私有镜像拉取用 dockerconfigjson,HTTPS 入口用 tls。

第三,挂载姿势和 ConfigMap 一致,分 env 注入和 volume 挂载,坑也一样(env 不热更新、subPath 不热更新)。但 Secret 多一个细节:volume 挂载时每个文件默认权限是 0644,任何同 Pod 的容器都能读。安全敏感场景要加 defaultMode:

yaml
volumes:
  - name: secret-vol
    secret:
      secretName: app-secrets
      defaultMode: 0400    # 只有 owner (root) 能读,group/other 无权限
      items:
        - key: DB_PASSWORD
          path: db_password   # 挂载成 /etc/secrets/db_password

另外,Secret 的 volume 挂载是 tmpfs(内存文件系统)——密钥永远不会落到节点的磁盘上,Pod 一停 tmpfs 就消失。这是 Secret 比 ConfigMap 在安全性上的一个隐含优势。

五、Secret 的安全:默认是裸奔的

K8s 默认安装下,Secret 存在 etcd 里明文不加密(只有 base64 编码),任何能 kubectl get secret 的用户、任何能访问 etcd 的人,都能直接看到所有密钥。这是默认状态——对生产环境是不及格的。把它加固到生产可用,至少做这三件事。

加固一:开启 etcd 静态加密(Encryption at Rest)

从 K8s 1.13 起,可以配置 API server 对写入 etcd 的 Secret 数据做真正加密。配置文件 /etc/kubernetes/encryption-config.yaml:

yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets              # 对 secrets 资源加密
      - configmaps           # 可选,也可加密 configmaps
    providers:
      - secretbox:           # 用 XChaCha20-Poly1305,纯软件实现,性能好推荐
          keys:
            - name: key1
              secret: <32字节 base64 随机数>
      - aesgcm:              # 备选,AES-GCM,性能好但需要定期轮换 key
          keys:
            - name: key1
              secret: <base64>
      - identity: {}         # 必须放最后,作为"不加密"的兜底(用于读取旧明文数据)

然后在 kube-apiserver 启动参数加 --encryption-provider-config,重启 API server。注意:改完配置后,老数据还是明文存在 etcd 里,需要重写一遍才生效:

bash
# 触发所有 secret 重写,让它们按新配置加密落盘
kubectl get secrets --all-namespaces -o json \
  | kubectl replace -f -

验证:直接 etcdctl get /registry/secrets/prod/app-secrets 看到的应该是密文不是明文。

云厂商托管 K8s(GKE / EKS / AKS)一般有一键开关开启 etcd 加密,用云厂商的 KMS 做 key provider,省去自己管 key 的麻烦。生产环境这个开关务必打开。

加固二:用 RBAC 收紧读权限

默认情况下,namespace 里的任何 ServiceAccount 都能读该 namespace 的 Secret(通过默认绑定的 role)。这是过宽的。生产环境应当:

  • 给每个应用建独立的 ServiceAccount
  • 用最小权限 Role,只允许读它真正需要的 Secret
  • 删除或收紧默认的 system:service-accounts 组绑定
yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: app-secret-reader
  namespace: prod
rules:
  - apiGroups: [""]
    resources: ["secrets"]
    resourceNames: ["app-db-secret"]   # 精确到只允许读这一个 Secret
    verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: app-secret-reader
  namespace: prod
subjects:
  - kind: ServiceAccount
    name: app            # 只绑定给 app 这个 SA
roleRef:
  kind: Role
  name: app-secret-reader
  apiGroup: rbac.authorization.k8s.io

用 resourceNames 精确到具体 Secret 是关键——别图省事写 resources: ["secrets"] 不限名字,那等于把整个 namespace 的密钥都给出去。

加固三:永远别把 Secret YAML 提交进 Git

这条前面已经强调过。即使开了 etcd 加密,YAML 文件里的 stringData 或 data 还是 base64(秒解)。一旦进 Git,历史永不忘记。Git 仓库该提交的是 External Secrets Operator 的 ExternalSecret 资源(下一节讲)——它只声明"密钥在外部 Vault 的哪个路径",不包含密钥值本身。

补充一条:审计。开启 audit log,对 Secret 的 get/list/watch 操作都记下来,定期 review 谁在拉密钥。这能在密钥泄露时第一时间定位来源。

六、External Secrets Operator:生产级密钥管理的正确姿势

到这里你会问:既然 Secret 不能进 Git,那 Secret 对象从哪来?答案是用 controller 动态生成。External Secrets Operator(ESO) 是目前事实标准,开源、跨云、跨后端。

它的工作模型是这样的:你把真实密钥存在外部密钥管理服务(HashiCorp Vault、AWS Secrets Manager、GCP Secret Manager、Azure Key Vault、1Password、Doppler……),ESO 监听 K8s 里的 ExternalSecret 资源,根据它声明的"去哪里取哪些 key",从外部服务拉密钥,自动同步成一个原生 K8s Secret 对象。

text
┌──────────────────┐   pull    ┌─────────────────────┐
│  Vault / AWS SM  │ ◄──────── │ External Secrets    │
│  (single source  │           │ Operator (ESO)      │
│   of truth)      │           │  - 监听 ExternalSecret
│                  │ ────────► │  - 定期 reconcile   │
└──────────────────┘  write    └──────────┬──────────┘
                                          │ create/update
                                          ▼
                                ┌─────────────────────┐
                                │ K8s Secret (原生)   │
                                │ app-db-secret      │
                                └──────────┬──────────┘
                                           │ mount / env
                                           ▼
                                       ┌────────┐
                                       │  Pod   │
                                       └────────┘

这个架构解决了所有前面的问题:

  • 密钥不进 Git:仓库里只有 ExternalSecret(声明引用,不含值),没有 Secret 本身
  • 单一可信源:密钥在 Vault/SM 里集中管理、轮换、审计,K8s 只是消费方
  • 自动同步:外部密钥变了,ESO 下次 reconcile 时自动更新 K8s Secret,无需人工
  • 跨环境一致:dev/staging/prod 用同一套 ExternalSecret 模板,引用不同路径

安装 ESO(Helm):

bash
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets \
  -n external-secrets --create-namespace

第一步:配置 SecretStore(连外部密钥服务)

SecretStore 定义"去哪里取"。以 AWS Secrets Manager 为例:

yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: aws-secrets-manager
  namespace: prod
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-east-1
      auth:
        jwt:
          serviceAccountRef:
            name: external-secrets-sa   # 用 IRSA (IAM Roles for Service Accounts)

这里用 IRSA(基于 ServiceAccount 的 IAM 角色)认证,比把 AWS access key 塞 Secret 里安全得多。GCP 用 Workload Identity,Vault 用 K8s auth backend——都是"用 Pod 身份换外部凭证",零长期密钥。

以 Vault 为例的 SecretStore:

yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: vault-backend
  namespace: prod
spec:
  provider:
    vault:
      server: "https://vault.internal:8200"
      path: "kv"                # KV v2 挂载路径
      version: "v2"
      auth:
        kubernetes:
          mountPath: "kubernetes"
          role: "app-role"      # Vault 里配的 role,绑定 K8s ServiceAccount
          serviceAccountRef:
            name: app

第二步:声明 ExternalSecret(要哪些 key,映射成什么)

yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: app-db-external-secret
  namespace: prod
spec:
  refreshInterval: 1h           # 每 1h 同步一次,外部密钥变更后最迟 1h 生效
  secretStoreRef:
    name: vault-backend
    kind: SecretStore
  target:
    name: app-db-secret         # 生成的原生 K8s Secret 叫这个名字
    creationPolicy: Owner       # ESO 拥有这个 Secret,删 ExternalSecret 时一并清理
  data:
    - secretKey: DB_PASSWORD    # K8s Secret 里的 key
      remoteRef:
        key: kv/prod/app        # Vault 路径
        property: db_password   # 该路径下 JSON 里的字段
    - secretKey: DB_USER
      remoteRef:
        key: kv/prod/app
        property: db_user

apply 这个 ExternalSecret 后,ESO 会去 Vault 的 kv/prod/app 路径读取 {db_password: xxx, db_user: yyy},生成一个名为 app-db-secret 的原生 K8s Secret。之后每 1 小时自动 reconcile,Vault 那边改了,这边 1 小时内自动同步。

应用 Pod 像消费普通 Secret 一样消费它(完全无感):

yaml
spec:
  template:
    spec:
      containers:
        - name: app
          image: app:1.2.3
          env:
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: app-db-secret
                  key: DB_PASSWORD
            - name: DB_USER
              valueFrom:
                secretKeyRef:
                  name: app-db-secret
                  key: DB_USER

到这里整条链路就闭合了:密钥在 Vault 里管 → ESO 同步成 K8s Secret → Pod 通过 env/volume 读取 → Git 仓库和镜像里都没有任何明文密钥。

refreshInterval 选多长是个权衡:越短越实时但外部 API 调用越多(AWS SM 按调用计费),越长越省钱但密钥轮换后生效慢。常见取值 1h~24h。如果是 Vault 动态密钥(用完即焚那种),可以配到分钟级。

七、实战:一条完整的"DB 密码零明文"链路

把前面所有东西串成一个端到端的生产案例。需求:prod namespace 的 app 要连 PostgreSQL,密码不能出现在 Git 仓库、镜像、etcd 明文。完整方案 = Vault 存密码 + ESO 同步 + etcd 加密 + RBAC 收紧 + 应用只读 Secret。

Step 1:在 Vault 写入 DB 密码

bash
# 在 Vault 服务器上(或通过 vault CLI)
vault kv put kv/prod/app \
  db_user=app_user \
  db_password=$(openssl rand -base64 24)

用 openssl rand 生成强密码,从命令行参数传入而不是写进脚本文件,避免密码进 shell history。配 Vault policy 只允许 prod/app 这个 K8s ServiceAccount 读这个路径:

hcl
path "kv/data/prod/app" {
  capabilities = ["read"]
}

Step 2:配 SecretStore + ExternalSecret(第六节的 YAML,apply 进集群)

bash
kubectl apply -f secretstore.yaml -f external-secret.yaml
# 等 ESO 同步完
kubectl get externalsecret app-db-external-secret -n prod -w
# READY 列变成 True 即同步成功
kubectl get secret app-db-secret -n prod        # 生成的原生 Secret 出现了

Step 3:确认集群已开 etcd 加密(第五节)

bash
# 看 kube-apiserver 是否配了 encryption-provider-config
ps aux | grep encryption-provider-config
# 实测:直接读 etcd
ETCDCTL_API=3 etcdctl get /registry/secrets/prod/app-db-secret
# 输出应当是密文(带 k8s:enc:secretbox: 前缀),不是明文

Step 4:用 RBAC 限定只有 app SA 能读

apply 第五节那个精确到 app-db-secret 的 Role + RoleBinding。验证:

bash
# 用另一个 SA 起 Pod,尝试读,应当被拒
kubectl auth can-i get secret app-db-secret \
  --as=system:serviceaccount:prod:other-app
# no

Step 5:应用 Deployment

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
  namespace: prod
spec:
  replicas: 3
  selector:
    matchLabels: { app: app }
  template:
    metadata:
      labels: { app: app }
    spec:
      serviceAccountName: app           # 用专属 SA,不调默认的
      containers:
        - name: app
          image: registry.example.com/app:1.2.3   # 镜像里不含任何密钥
          env:
            - name: DB_HOST
              valueFrom:
                configMapKeyRef:        # 非敏感配置走 ConfigMap
                  name: app-config
                  key: DB_HOST
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:           # 敏感配置走 ESO 生成的 Secret
                  name: app-db-secret
                  key: DB_PASSWORD
            - name: DB_USER
              valueFrom:
                secretKeyRef:
                  name: app-db-secret
                  key: DB_USER

Step 6:轮换演练

模拟密钥轮换——在 Vault 改密码:

bash
vault kv patch kv/prod/app db_password=$(openssl rand -base64 24)

之后 ESO 的 refreshInterval 到点(或手动 kubectl annotate externalsecret app-db-external-secret -n prod force-sync=$(date +%s) --overwrite 立即触发),Secret 更新。然后要么 app 自己 reload(volume 挂载 + 应用 watch 文件),要么用 Reloader 触发 Deployment 滚动重启(env 注入场景必备):

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
  annotations:
    reloader.stakater.com/search: "true"   # 监听关联的 Secret/ConfigMap 变化,自动滚动重启

整条链路验证一遍:

检查项结果
Git 仓库里搜 db_password 明文无(只有 ExternalSecret 引用)
Docker 镜像 docker history 看环境变量无密钥(只有 ENV DB_HOST=...)
etcdctl 直接读 /registry/secrets/prod/app-db-secret密文(etcd 加密生效)
kubectl auth can-i 测其他 SAdenied(RBAC 收紧生效)
Vault 里改密码,1h 内 K8s Secret 自动更新是(ESO 同步生效)

这套姿势我视为生产环境的及格线。它的本质是把"配置变更"和"制品构建"彻底解耦,把"密钥"和"代码"彻底解耦——镜像一年不重新 build 也能跟上配置迭代,仓库被 fork 一万次也不会泄露半个密钥。

小结

  • 第一原则是配置和密钥都不进镜像。镜像应当环境无关,配置外置让"改配置"和"重打镜像"彻底解耦。ConfigMap 存非敏感配置,Secret 存敏感数据——但 base64 不是加密,能 get -o yaml 就能解码。
  • ConfigMap 的热更新分三档:volume 挂载会更新(1 分钟延迟,应用要自己感知)、env 注入不更新(要重启 Pod)、subPath 挂载不更新(最大的坑,挂单文件别指望热更新)。
  • Secret 默认裸奔,生产至少要开 etcd 加密(EncryptionConfiguration + 重写)、RBAC 精确到 resourceName 限制读权限、永远别把含密钥值的 YAML 提交进 Git。
  • 生产级方案是 External Secrets Operator:Vault / AWS SM / GCP SM 当单一可信源,ESO 监听 ExternalSecret 自动同步成原生 K8s Secret,配合 IRSA / Workload Identity / K8s auth backend 做零长期密钥认证。Git 里只有引用,没有值。
  • 完整链路 = Vault 存 → ESO 同步 → etcd 加密 → RBAC 收紧 → Pod 用 SA 读 Secret,任何一环缺位都前功尽弃。轮换演练要定期做,没有"出事再换"的余地。

金句收尾:ConfigMap 和 Secret 解决的是"配置去哪了"的问题,而不是"配置安全了"的问题——前者 API 已经替你做了,后者必须你自己用 etcd 加密、RBAC、外部密钥服务一层层补上。把 K8s Secret 当保险柜,它其实只是个带锁的展示柜,钥匙还在 etcd 那里。真正的保险柜,永远是 Vault。

Related

相关推荐