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

704 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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/)