Files
Qiniu/technical/k8s/k8s-06-configmap-and-secret.md
T

704 lines
22 KiB
Markdown
Raw Normal View History

2026-07-07 15:55:47 +08:00
---
tags: [k8s, configmap, secret, devops]
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 内部:
```mermaid
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)
```bash
# 方式一:从字面值创建
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-file` vs `--from-file`
> `--from-env-file` 解析 `KEY=VALUE` 格式,每个键值对成为一个独立的 key;而 `--from-file` 将整个文件内容作为单个 key 的 value。二者用途不同,不要混淆。
#### 2. 声明式创建(YAML)
```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
}
```
查看创建结果:
```bash
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`)**
```yaml
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`)**
```yaml
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 挂载
```yaml
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 的挂载目录中。这个过程的延迟取决于:
1. **kubelet sync 周期**:默认每 **1 分钟**(`--sync-frequency` 参数控制)。
2. **传播延迟**:从 API Server 到 kubelet 的 watch 通知延迟。
```mermaid
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),这是一个重要的性能优化手段。
```yaml
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 示例
```yaml
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 示例
```bash
# 通过 kubectl 创建 TLS Secret
kubectl create secret tls my-tls-secret \
--cert=tls.crt \
--key=tls.key
```
```yaml
# 等价的 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
```bash
# 创建 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
```
```yaml
# 在 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 进行真正的加密存储:
```yaml
# /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) |
#### 环境变量注入
```yaml
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 挂载
```yaml
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:
```mermaid
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 权限控制
```yaml
# 限制特定 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 的读写操作:
```yaml
# /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](https://github.com/stakater/Reloader) 是一个 Kubernetes Controller,当它检测到关联的 ConfigMap/Secret 发生变更时,会自动触发 Deployment 的滚动更新(rollout restart),从而实现配置刷新。
```yaml
# 部署 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
// 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 触发重启):
```yaml
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,后导入的会覆盖先前的。
```yaml
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 用户运行,可能无法读取。
```yaml
# 解决方案:调整 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 |
---
## 延伸阅读
- [[k8s-03-pod]] — Pod 生命周期与资源管理
- [[k8s-04-deployment-and-replicaset]] — Deployment 滚动更新与回滚
- [Kubernetes 官方文档 - ConfigMap](https://kubernetes.io/docs/concepts/configuration/configmap/)
- [Kubernetes 官方文档 - Secret](https://kubernetes.io/docs/concepts/configuration/secret/)
- [stakater/Reloader - ConfigMap/Secret 自动触发重启](https://github.com/stakater/Reloader)
- [Secrets Store CSI Driver](https://secrets-store-csi-driver.sigs.k8s.io/)
- [External Secrets Operator](https://external-secrets.io/)