--- 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: tls.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/)