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

22 KiB
Raw Blame History

tags, create time
tags create time
k8s
configmap
secret
devops
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-file vs --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 的挂载目录中。这个过程的延迟取决于:

  1. kubelet sync 周期:默认每 1 分钟(--sync-frequency 参数控制)。
  2. 传播延迟:从 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

延伸阅读