Kubernetes ConfigMap 与 Secrets:配置与密钥管理
配置不该硬编码进镜像——ConfigMap 存非敏感配置,Secret 存密钥。本文讲清两者的创建/挂载(env vs volume)、Secret 的 base64 与加密(etcd 加密/KMS/外部 secrets operator)、ConfigMap 热更新(subPath 不更新陷阱),以及生产级密钥管理(External Secrets Operator)。
本文要点
- 主题聚焦:DevOps
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
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 的三种姿势
最直接的命令式创建,适合快速验证:
# 从字面量创建
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 会自动编码):
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)
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 一个文件)
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 文件变化或定期重新读取才能感知——不然文件变了应用还在用内存里的旧值。
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 里的某一个文件到指定路径,很自然地会这样写:
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,不是为了保密。
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-token | SA token,K8s 自动管理,别手动创建 |
绝大多数业务场景用 Opaque 就够了。私有镜像拉取用 dockerconfigjson,HTTPS 入口用 tls。
第三,挂载姿势和 ConfigMap 一致,分 env 注入和 volume 挂载,坑也一样(env 不热更新、subPath 不热更新)。但 Secret 多一个细节:volume 挂载时每个文件默认权限是 0644,任何同 Pod 的容器都能读。安全敏感场景要加 defaultMode:
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:
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 里,需要重写一遍才生效:
# 触发所有 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组绑定
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 对象。
┌──────────────────┐ 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):
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 为例:
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:
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,映射成什么)
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 一样消费它(完全无感):
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 密码
# 在 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 读这个路径:
path "kv/data/prod/app" {
capabilities = ["read"]
}
Step 2:配 SecretStore + ExternalSecret(第六节的 YAML,apply 进集群)
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 加密(第五节)
# 看 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。验证:
# 用另一个 SA 起 Pod,尝试读,应当被拒
kubectl auth can-i get secret app-db-secret \
--as=system:serviceaccount:prod:other-app
# no
Step 5:应用 Deployment
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 改密码:
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 注入场景必备):
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 测其他 SA | denied(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
相关推荐
文章
Docker 部署 Next.js 社区项目的注意事项
围绕 DevOps、Next.js 的技术趋势、工程实践和独立开发视角。
文章
OpenTelemetry 如何帮助你观察 AI 应用?
围绕 DevOps、AI 的技术趋势、工程实践和独立开发视角。
文章
长运行工具调用:Operation、Poll 与 Cancel 的终态协议
初始请求超时后,工具可能仍在服务端运行;发出取消也不等于副作用已停止。本文为单次长运行工具调用建立 operation 身份、轮询水位、取消竞态和唯一终态接受点。
文章
Agent 记忆删除传播:从用户遗忘请求到可验证清除
删除主表一行不会清掉向量、摘要、缓存和恢复后的备份。本文用删除状态机、代际墓碑、派生血缘与端到端 canary 验证真正遗忘。