22 KiB
tags, create time
| tags | create time | ||||
|---|---|---|---|---|---|
|
2026-07-07 15:48 |
ConfigMap 与 Secret — K8s 配置管理
概述
[!info] 十二要素应用原则(12-Factor App) 配置应当与代码严格分离。镜像是不可变的构建产物,而运行时环境(数据库地址、API Key、特性开关)应当通过外部注入,而非硬编码在镜像中。Kubernetes 的 ConfigMap 和 Secret 正是实现这一原则的核心机制——它们将配置数据从 Pod 定义中解耦,使得同一镜像可以部署到不同环境而无需重建。
简而言之:
- ConfigMap:存放非敏感的配置数据(如 Config 文件、命令行参数、环境变量)。
- Secret:存放敏感数据(如密码、Token、TLS 证书),并提供额外的安全保障。
下图展示了配置数据如何从集群外部流入 Pod 内部:
flowchart LR
subgraph Cluster["K8s Cluster"]
CM[ConfigMap]
SEC[Secret]
ETCD[(etcd)]
CM --> ETCD
SEC --> ETCD
end
subgraph Pod["Pod"]
ENV["环境变量注入"]
VOL["Volume 挂载\n(/etc/config, /etc/secret)"]
end
ETCD -->|kubelet sync| VOL
ETCD -->|Pod 启动时注入| ENV
subgraph External["外部管理"]
VAULT["HashiCorp Vault"]
AWS_SM["AWS Secrets Manager"]
end
External -->|CSI Driver / Sync| SEC
ConfigMap 详解
创建方式
1. 命令式创建(kubectl create configmap)
# 方式一:从字面值创建
kubectl create configmap app-config \
--from-literal=APP_ENV=production \
--from-literal=LOG_LEVEL=info \
--from-literal=DB_HOST=mysql.default.svc.cluster.local
# 方式二:从单个文件创建
kubectl create configmap nginx-conf \
--from-file=nginx.conf
# 方式三:从目录创建(目录下所有文件各成为一个 key)
kubectl create configmap app-config-files \
--from-file=config/
# 方式四:指定 key 名称(而非使用文件名作为 key)
kubectl create configmap app-conf \
--from-file=my-config=./app.properties
# 方式五:从 .env 文件创建(key=value 格式)
kubectl create configmap env-config \
--from-env-file=.env
[!tip]
--from-env-filevs--from-file--from-env-file解析KEY=VALUE格式,每个键值对成为一个独立的 key;而--from-file将整个文件内容作为单个 key 的 value。二者用途不同,不要混淆。
2. 声明式创建(YAML)
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: default
labels:
app: my-app
data:
# 简单键值对
APP_ENV: "production"
LOG_LEVEL: "info"
DB_PORT: "3306"
# 多行配置文件作为 value
application.properties: |
server.port=8080
spring.datasource.url=jdbc:mysql://mysql:3306/app
spring.datasource.username=root
logging.level.root=INFO
# JSON 格式的配置
features.json: |
{
"enableDarkMode": true,
"maxRetries": 3,
"timeout": 30
}
查看创建结果:
kubectl describe configmap app-config
kubectl get configmap app-config -o yaml
挂载方式
ConfigMap 中的数据注入到 Pod 有两种主要方式:环境变量注入 和 Volume 挂载。二者在行为上有显著差异。
| 特性 | 环境变量注入 | Volume 挂载 |
|---|---|---|
| 注入时机 | Pod 启动时 | Pod 启动时 + 运行时可更新 |
| 热更新支持 | 不支持,需重启 Pod | 支持(kubelet 定期同步) |
| 适用场景 | 简单键值对、启动参数 | 配置文件(nginx.conf 等) |
| 引用方式 | configMapKeyRef / envFrom |
volumes.configMap |
| 文件权限 | 不涉及 | 可设置 defaultMode |
| 多 key 批量注入 | envFrom 批量导入 |
整个目录挂载 |
环境变量注入
方式一:逐个 key 引用(configMapKeyRef)
apiVersion: v1
kind: Pod
metadata:
name: app-pod
spec:
containers:
- name: app
image: my-app:1.0
env:
# 引用 ConfigMap 中的单个 key
- name: APP_ENV # 容器内的环境变量名
valueFrom:
configMapKeyRef:
name: app-config # ConfigMap 名称
key: APP_ENV # ConfigMap 中的 key 名
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: LOG_LEVEL
optional: true # ConfigMap 或 key 不存在时不报错
方式二:批量导入(envFrom)
apiVersion: v1
kind: Pod
metadata:
name: app-pod
spec:
containers:
- name: app
image: my-app:1.0
envFrom:
- configMapRef:
name: app-config
prefix: "CFG_" # 可选前缀,避免命名冲突
[!warning]
envFrom会跳过无效 key ConfigMap 中如果存在不符合环境变量命名规范的 key(如包含-或以数字开头),这些 key 会被静默跳过,不会报错。使用时需注意 key 的命名规范。
Volume 挂载
apiVersion: v1
kind: Pod
metadata:
name: app-pod
spec:
containers:
- name: app
image: nginx:1.25
volumeMounts:
- name: config-volume
mountPath: /etc/nginx/conf.d # 挂载到容器内目录
readOnly: true
- name: app-properties
mountPath: /app/config # 挂载整个目录
volumes:
# 挂载 ConfigMap 中指定的 key
- name: config-volume
configMap:
name: nginx-conf
items:
- key: default.conf
path: default.conf # 文件名(挂载后的文件名)
mode: 0644 # 文件权限
# 挂载 ConfigMap 全部 key(每个 key 成为一个文件)
- name: app-properties
configMap:
name: app-config
defaultMode: 0644 # 默认文件权限
热更新机制
ConfigMap 通过 Volume 挂载后,当 ConfigMap 的内容在集群中被更新时,kubelet 会自动将新内容同步到 Pod 的挂载目录中。这个过程的延迟取决于:
- kubelet sync 周期:默认每 1 分钟(
--sync-frequency参数控制)。 - 传播延迟:从 API Server 到 kubelet 的 watch 通知延迟。
sequenceDiagram
participant User as 用户/kubectl
participant API as API Server
participant Kubelet as Kubelet (Node)
participant Pod as Pod Container
User->>API: kubectl apply -f configmap.yaml
API->>Kubelet: watch 通知 ConfigMap 变更
Note over Kubelet: 等待 sync 周期(最多 1 分钟)
Kubelet->>Pod: 更新 Volume 挂载文件(原子替换)
Note over Pod: 应用需 reload 配置(如 nginx -s reload)
[!warning] 热更新 ≠ 自动生效 ConfigMap 文件更新后,应用本身必须能够感知文件变化并重新加载配置。例如 Nginx 需要
nginx -s reload,Spring Boot 需要@RefreshScope。否则虽然文件内容已更新,但应用仍使用旧配置。
环境变量方式不可热更新:环境变量在 Pod 启动时由 kubelet 注入到容器的进程环境中,运行期间不会再更新。要更新环境变量,只能删除并重建 Pod。
不可变 ConfigMap
从 Kubernetes 1.21 开始,可以将 ConfigMap 标记为不可变(immutable),这是一个重要的性能优化手段。
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-v1
immutable: true # 标记为不可变
data:
APP_ENV: "production"
LOG_LEVEL: "info"
为什么需要不可变 ConfigMap?
- 减轻 API Server 压力:普通 ConfigMap 会被 kubelet watch,任何变更都会触发全集群的通知广播。对于大规模集群(数千节点),大量 watch 事件会显著增加 API Server 的负担。
- 防止意外修改:标记为 immutable 后,该 ConfigMap 的内容不可更改(只能删除重建)。
- 推荐场景:发布后不再变更的配置(如已上线的版本配置)。
[!tip] 不可变 ConfigMap 的更新策略 由于 immutable ConfigMap 无法修改,更新配置时需要创建一个新的 ConfigMap(如
app-config-v2),然后更新 Deployment 引用新的 ConfigMap 名称。可以结合 Deployment 的滚动更新实现零停机配置切换。
Secret 详解
类型
Kubernetes 内置了多种 Secret 类型,用于不同场景。以下是常用类型的对比:
| 类型 | 用途 | data 字段说明 | 典型场景 |
|---|---|---|---|
Opaque |
通用型(默认) | 用户自定义的任意 key-value | 数据库密码、API Key 等 |
kubernetes.io/tls |
TLS 证书 | 必须含 tls.crt 和 tls.key |
Ingress TLS 终止 |
kubernetes.io/dockerconfigjson |
镜像仓库凭证 | 必须含 .dockerconfigjson |
私有镜像仓库拉取 |
kubernetes.io/basic-auth |
基本认证 | username + password |
Git/HTTP Basic Auth |
kubernetes.io/ssh-auth |
SSH 认证 | 必须含 ssh-privatekey |
Git SSH 拉取 |
kubernetes.io/service-account-token |
SA Token | 自动创建 | Pod 身份认证(已逐步被 Bound SA Token 替代) |
Opaque Secret 示例
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
data:
# 值必须是 Base64 编码
username: cm9vdA== # echo -n 'root' | base64
password: czNjcjN0UEBzc3cwcmQ= # echo -n 's3cr3tP@ssw0rd' | base64
# 也可以用 stringData(自动编码,方便调试,生产慎用)
stringData:
connection-string: "mysql://root:s3cr3tP@ssw0rd@mysql:3306/app"
TLS Secret 示例
# 通过 kubectl 创建 TLS Secret
kubectl create secret tls my-tls-secret \
--cert=tls.crt \
--key=tls.key
# 等价的 YAML 声明
apiVersion: v1
kind: Secret
metadata:
name: my-tls-secret
type: kubernetes.io/tls
data:
tls.crt: <base64-encoded-cert>
tls.key: <base64-encoded-key>
Docker Registry Secret
# 创建 Docker Registry 凭证
kubectl create secret docker-registry regcred \
--docker-server=registry.example.com \
--docker-username=admin \
--docker-password=P@ssw0rd \
--docker-email=admin@example.com
# 在 Pod 中使用
apiVersion: v1
kind: Pod
metadata:
name: private-app
spec:
imagePullSecrets:
- name: regcred # 引用 Docker Registry Secret
containers:
- name: app
image: registry.example.com/my-app:1.0
编码 vs 加密
[!danger] Base64 编码不等于加密 Secret 的
data字段使用 Base64 编码,但这只是编码格式,不是加密。任何拥有集群读权限的人都可以轻松解码。kubectl get secret db-credentials -o jsonpath='{.data.password}' | base64 -d即可还原明文。
etcd 加密(EncryptionConfiguration)
Kubernetes 支持在 etcd 层面对 Secret 进行真正的加密存储:
# /etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
- configmaps # 1.25+ 也支持加密 ConfigMap
providers:
# 第一个 provider 用于解密(兼容)
- aescbc:
keys:
- name: key1
secret: <32-byte-base64-encoded-key> # head -c 32 /dev/urandom | base64
# fallback(不加密,仅读取旧数据)
- identity: {}
[!tip] 加密流程 启用 EncryptionConfiguration 后,新写入的 Secret 会使用配置的 provider 加密存储。已有的旧 Secret 在下次写入(update)时才会重新加密。读取时 API Server 自动解密,对客户端透明。
更安全的加密方案对比
| 方案 | 加密层次 | 密钥管理 | 适用场景 |
|---|---|---|---|
| Base64(默认) | 无加密 | 无 | 仅编码格式 |
| aescbc(EncryptionConfiguration) | etcd 存储层 | 静态密钥(文件) | 基础加密需求 |
| KMS Plugin(AWS KMS、GCP KMS) | etcd 存储层 | 外部 KMS 托管 | 生产环境推荐 |
| 外部密钥管理(Vault) | 应用层 | 专业密钥管理系统 | 高安全要求 |
挂载方式
Secret 的挂载方式与 ConfigMap 基本一致,但有以下关键差异:
| 特性 | ConfigMap | Secret |
|---|---|---|
| 存储介质 | 普通目录(tmpfs 或磁盘) | tmpfs(内存文件系统),不落盘 |
| 默认文件权限 | 0644 |
0400(仅 owner 可读) |
| 适用内容 | 非敏感配置 | 密码、Token、证书等敏感数据 |
| etcd 存储 | 明文 | 可加密(EncryptionConfiguration) |
环境变量注入
apiVersion: v1
kind: Pod
metadata:
name: app-with-secret
spec:
containers:
- name: app
image: my-app:1.0
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-credentials
key: username
# 也可以批量注入所有 Secret key
# envFrom:
# - secretRef:
# name: db-credentials
# prefix: "SECRET_"
Volume 挂载
apiVersion: v1
kind: Pod
metadata:
name: app-with-tls
spec:
containers:
- name: app
image: nginx:1.25
volumeMounts:
- name: tls-certs
mountPath: /etc/tls
readOnly: true
- name: db-secret
mountPath: /etc/secrets
readOnly: true
volumes:
- name: tls-certs
secret:
secretName: my-tls-secret
defaultMode: 0400 # 仅 owner 可读
items:
- key: tls.crt
path: server.crt
- key: tls.key
path: server.key
- name: db-secret
secret:
secretName: db-credentials
defaultMode: 0440 # owner + group 可读
[!info] tmpfs 存储 Secret 通过 Volume 挂载时使用 tmpfs(内存文件系统),数据不会写入节点的磁盘。这避免了敏感数据在磁盘上残留的风险,但会占用少量内存。
安全最佳实践
1. 使用外部密钥管理系统
在生产环境中,推荐使用专业的密钥管理系统而非原生 Secret:
flowchart TD
subgraph External["外部密钥管理"]
VAULT["HashiCorp Vault"]
AWS["AWS Secrets Manager"]
GCP["GCP Secret Manager"]
end
subgraph K8s["Kubernetes"]
CSI["Secrets Store CSI Driver"]
SYNC["External Secrets Operator"]
POD["Pod"]
end
VAULT --> CSI
AWS --> CSI
GCP --> CSI
CSI -->|挂载为 Volume| POD
VAULT --> SYNC
AWS --> SYNC
SYNC -->|同步为 K8s Secret| POD
- Secrets Store CSI Driver:将外部密钥直接挂载为 Volume,不在 etcd 中存储。
- External Secrets Operator:从外部系统同步密钥为 K8s Secret,适合需要原生 Secret 兼容的场景。
2. RBAC 权限控制
# 限制特定 ServiceAccount 只能读取特定 Secret
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: secret-reader
namespace: production
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["db-credentials", "api-keys"] # 仅限特定 Secret
verbs: ["get"] # 仅允许读取
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: app-secret-reader
namespace: production
subjects:
- kind: ServiceAccount
name: my-app-sa
namespace: production
roleRef:
kind: Role
name: secret-reader
apiGroup: rbac.authorization.k8s.io
3. 审计日志
启用 API Server 的审计日志,追踪 Secret 的读写操作:
# /etc/kubernetes/audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
# 记录所有对 Secret 的读写操作
- level: Metadata
resources:
- group: ""
resources: ["secrets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
4. 其他安全建议
- 避免在 Pod Spec 中直接写
stringData:生产环境应使用data+ Base64,或通过 CI/CD 管道注入。 - 定期轮换密钥:结合 External Secrets Operator 实现自动轮换。
- 避免将 Secret 推送到 Git:使用
.gitignore排除 Secret YAML,或使用 Sealed Secrets / SOPS 加密后再提交。 - 最小权限原则:每个 ServiceAccount 只应拥有它所需的最小 Secret 访问权限。
配置热更新实践
在生产环境中,单纯的 ConfigMap Volume 挂载虽然支持热更新,但应用本身还需要 reload 机制。以下是几种常见的无重启配置刷新方案。
方案一:ConfigMap Reloader(Sidecar)
stakater/ConfigMapReloader 是一个 Kubernetes Controller,当它检测到关联的 ConfigMap/Secret 发生变更时,会自动触发 Deployment 的滚动更新(rollout restart),从而实现配置刷新。
# 部署 Reloader 后,在 Deployment 中添加注解
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
annotations:
# Reloader 监听这些 ConfigMap/Secret 的变更
reloader.stakater.com/reload: "app-config,db-credentials"
spec:
template:
spec:
containers:
- name: app
image: my-app:1.0
[!tip] 原理 Reloader 并非真正实现"热更新",而是通过修改 Pod Template 的 annotation 触发 Deployment 的滚动更新。本质上是重启 Pod,但对用户无感知(滚动更新保证零停机)。
方案二:应用内 Watch 机制
一些应用框架原生支持配置文件监听:
// Go 应用使用 fsnotify 监听配置文件变更
import "github.com/fsnotify/fsnotify"
func watchConfig(path string, reload func()) {
watcher, _ := fsnotify.NewWatcher()
defer watcher.Close()
watcher.Add(path)
for {
select {
case event := <-watcher.Events:
if event.Op&fsnotify.Write == fsnotify.Write {
log.Println("Config file modified, reloading...")
reload()
}
case err := <-watcher.Errors:
log.Println("Watch error:", err)
}
}
}
方案三:SubPath 挂载 + 特殊处理
当使用 subPath 挂载时,ConfigMap 的更新不会自动传播到容器。如果必须使用 subPath,需要配合其他方案(如 Reloader 触发重启):
volumeMounts:
- name: config-volume
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf # 只挂载单个文件到指定路径
[!warning] SubPath 无法热更新 使用
subPath挂载的文件不会随 ConfigMap 更新而更新。这是因为 kubelet 为 subPath 创建的是符号链接或独立 bind mount,不在正常的 sync 范围内。如果需要热更新,请避免使用subPath。
常见陷阱与最佳实践
陷阱一:ConfigMap 大小限制(1MB)
Kubernetes 对 ConfigMap 和 Secret 有 1MB 的大小限制(这是 etcd 的 kv 对大小限制决定的)。
# 错误:ConfigMap 超过 1MB 会创建失败
The ConfigMap "xxx" is invalid: data: Too long: must have at most 1048576 bytes
解决方案:
- 拆分为多个 ConfigMap。
- 使用外部配置存储(如 Consul、Nacos)。
- 对于大型配置文件,考虑挂载到 PV。
陷阱二:环境变量命名冲突
当使用 envFrom 批量导入时,多个 ConfigMap/Secret 中可能存在同名 key,后导入的会覆盖先前的。
envFrom:
- configMapRef:
name: base-config # 含 APP_ENV=staging
- configMapRef:
name: override-config # 含 APP_ENV=production(覆盖)
[!tip] 使用 prefix 避免冲突 为每个
envFrom来源设置不同的prefix,如base-config用BASE_,override-config用OVERRIDE_。
陷阱三:Secret 的 watch 机制开销
Pod 通过 Volume 引用 Secret 时,kubelet 会 watch 该 Secret 的变更。在大规模集群中,大量 Secret 的 watch 可能导致 API Server 压力过大。
缓解措施:
- 使用
immutable: true标记不会变更的 Secret。 - 合并小 Secret,减少 watch 对象数量。
- 使用 KMS Provider 加密时,注意解密操作的性能开销。
陷阱四:文件权限与安全上下文
Secret 默认权限为 0400,如果容器进程以非 root 用户运行,可能无法读取。
# 解决方案:调整 Secret 文件权限
volumes:
- name: secret-volume
secret:
secretName: app-secret
defaultMode: 0444 # 所有用户可读
# 或者精确设置
items:
- key: config.yaml
path: config.yaml
mode: 0644
最佳实践总结
| 实践 | 说明 |
|---|---|
| ConfigMap 与 Config 文件分离 | 不同环境的配置使用不同的 ConfigMap |
使用 immutable: true |
已发布配置标记为不可变,减轻 API Server 压力 |
| Secret 使用外部管理系统 | 生产环境使用 Vault/KMS,而非原生 Secret |
避免 envFrom 无差别导入 |
明确列出需要的 key,避免意外覆盖 |
避免 subPath 挂载需要热更新的配置 |
subPath 不支持热更新,优先使用目录挂载 |
| 设置合理的 RBAC | 每个 ServiceAccount 只授予最小必要权限 |
| 配置版本管理 | ConfigMap 名称带版本号(如 app-config-v3),便于回滚 |
| CI/CD 中管理 Secret | ���用 Sealed Secrets、SOPS 或 External Secrets Operator |