ConfigMap 与 Secret:应用配置管理

深入理解 ConfigMap 和 Secret 的使用方式、安全最佳实践,以及配置热更新的实现方法。

概述

配置管理是应用运维的核心能力。Kubernetes 提供了 ConfigMap 和 Secret 两种资源来管理配置数据。本文将深入探讨:

学习目标

  • 理解 ConfigMap 和 Secret 的使用场景
  • 掌握多种配置注入方式
  • 学会 Secret 的安全配置与加密存储
  • 实现配置热更新与回滚
  • 了解配置管理最佳实践

ConfigMap:非敏感配置

ConfigMap 概述

┌─────────────────────────────────────────────────────────────────┐
│                    ConfigMap 定位                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   ConfigMap 用于存储:                                          │
│   ✓ 配置文件(如 nginx.conf, application.yaml)                │
│   ✓ 环境变量(DB_HOST, LOG_LEVEL 等)                         │
│   ✓ 命令行参数(启动脚本参数)                                  │
│   ✓ DNS 映射(hosts 文件等)                                   │
│                                                                 │
│   ConfigMap 不能存储:                                          │
│   ✗ 敏感信息(密码、Token、证书)→ 使用 Secret                 │
│                                                                 │
│   特点:                                                       │
│   - 明文存储,任何能访问 API Server 的人可见                    │
│   - 适合非敏感配置                                             │
│   - 支持多种格式(文件、键值对、JSON)                         │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

ConfigMap 创建方式

# 方式1:键值对
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  DATABASE_HOST: "localhost"
  DATABASE_PORT: "5432"
  LOG_LEVEL: "info"

---
# 方式2:多行文本(config-file)
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-config
data:
  default.conf: |
    server {
      listen 80;
      server_name localhost;
      location / {
        proxy_pass http://backend:8080;
      }
    }

---
# 方式3:从文件创建
# kubectl create configmap app-config --from-file=config.yaml

ConfigMap 使用方式

# 方式1:环境变量
apiVersion: v1
kind: Pod
metadata:
  name: config-env-pod
spec:
  containers:
  - name: app
    image: myapp:1.0
    env:
    # 方式1.1:单个值
    - name: DB_HOST
      valueFrom:
        configMapKeyRef:
          name: app-config
          key: DATABASE_HOST

    # 方式1.2:引用整个 ConfigMap
    envFrom:
    - configMapRef:
        name: app-config

    # 方式1.3:可选引用(不会因为 key 不存在而失败)
    - name: OPTIONAL_VAR
      valueFrom:
        configMapKeyRef:
          name: optional-config
          key: SOME_KEY
          optional: true
# 方式2:命令行参数
spec:
  containers:
  - name: app
    image: myapp:1.0
    command:
    - /app/start.sh
    - --config
    - /etc/config/app.conf
    env:
    - name: CONFIG_PATH
      value: /etc/config
    volumeMounts:
    - name: config-volume
      mountPath: /etc/config

---
# 方式3:挂载为文件
apiVersion: v1
kind: Pod
spec:
  containers:
  - name: app
    image: myapp:1.0
    volumeMounts:
    - name: config-volume
      mountPath: /etc/config
      readOnly: true

  volumes:
  - name: config-volume
    configMap:
      name: app-config
      # 可选:只挂载特定 keys
      items:
      - key: database.conf
        path: db.conf
      - key: app.yaml
        path: application.yaml
      # 可选:文件权限
      defaultMode: 0644

ConfigMap 挂载对比

┌─────────────────────────────────────────────────────────────────┐
│               环境变量 vs 文件挂载 对比                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌─────────────────┐         ┌─────────────────┐             │
│  │    环境变量       │         │    文件挂载      │             │
│  ├─────────────────┤         ├─────────────────┤             │
│  │ 启动时注入        │         │ 运行时可更新     │             │
│  ├─────────────────┤         ├─────────────────┤             │
│  │ 不可动态更新     │         │ 支持热更新(需配置)│           │
│  ├─────────────────┤         ├─────────────────┤             │
│  │ 适合固定配置     │         │ 适合配置文件     │             │
│  ├─────────────────┤         ├─────────────────┤             │
│  │ 容器启动后不可变  │         │ 可作为配置中心   │             │
│  └─────────────────┤         └─────────────────┘             │
│                                                                 │
│  选择建议:                                                     │
│  - 不频繁变化的配置 → 环境变量                                  │
│  - 需要动态更新的配置 → 文件挂载                                │
│  - 敏感配置 → Secret(环境变量或文件)                          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Secret:敏感数据存储

Secret 类型

┌─────────────────────────────────────────────────────────────────┐
│                    Secret 类型                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────┐    │
│  │   Opaque        │  │  kubernetes.io/ │  │ tls         │    │
│  │   (通用)        │  │  dockerconfigjson │ │ (证书)      │    │
│  │               │  │  (镜像仓库)     │  │             │    │
│  │  键值对         │  │                 │  │  TLS 证书   │    │
│  │  任意数据       │  │ Harbor/Docker   │  │             │    │
│  │               │  │   Hub           │  │  私有仓库   │    │
│  └─────────────────┘  │                 │  │  认证       │    │
│                        └─────────────────┘  └─────────────┘    │
│                                                                 │
│  ┌─────────────────┐  ┌─────────────────┐                     │
│  │  basic-auth    │  │  ssh-auth      │                     │
│  │  (用户密码)     │  │  (SSH 密钥)     │                     │
│  │               │  │               │                     │
│  │  用户名/密码   │  │  SSH 公钥/私钥  │                     │
│  └─────────────────┘  └─────────────────┘                     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Secret 创建方式

# 方式1:手动定义
apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
type: Opaque
data:
  # 值必须是 base64 编码
  username: YWRtaW4=         # admin
  password: cGFzc3dvcmQ=    # password

# 注意:base64 不是加密,只是编码
# 任何能访问 API 的人都能解码

---
# 方式2:从文件创建
# kubectl create secret generic db-credentials \
#   --from-literal=username=admin \
#   --from-literal=password=password

# 从文件
# kubectl create secret generic tls-cert \
#   --from-file=tls.crt=server.crt \
#   --from-file=tls.key=server.key

TLS Secret

# TLS Secret
apiVersion: v1
kind: Secret
metadata:
  name: myapp-tls
type: kubernetes.io/tls
data:
  # base64 编码的证书和私钥
  tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t...
  tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0t...

# 或使用 kubectl 创建
# kubectl create secret tls myapp-tls \
#   --cert=server.crt \
#   --key=server.key

Docker Registry Secret

# 镜像仓库认证
apiVersion: v1
kind: Secret
metadata:
  name: docker-registry-secret
type: kubernetes.io/dockerconfigjson
data:
  # 格式:{"auths":{"registry.example.com":{"username":"user","password":"pwd","email":"","auth":"..."}}}
  .dockerconfigjson: eyJhdXRocyI6eyJyZWdpc3RyeS5leGFtcGxlLmNvbSI6eyJ1c2VybmFtZSI6InVzZXIiLCJwYXNzd29yZCI6InBhc3MifX19

# 或使用 kubectl
# kubectl create secret docker-registry my-registry \
#   --docker-server=registry.example.com \
#   --docker-username=user \
#   --docker-password=password

Secret 使用方式

# 方式1:环境变量
spec:
  containers:
  - name: app
    image: myapp:1.0
    env:
    - name: DB_PASSWORD
      valueFrom:
        secretKeyRef:
          name: db-credentials
          key: password

    envFrom:
    - secretRef:
        name: db-credentials

---
# 方式2:文件挂载
spec:
  containers:
  - name: app
    image: myapp:1.0
    volumeMounts:
    - name: secret-volume
      mountPath: /etc/secrets
      readOnly: true

  volumes:
  - name: secret-volume
    secret:
      secretName: db-credentials
      # 每个 key 生成一个文件
      items:
      - key: username
        path: DB_USER
      - key: password
        path: DB_PASS

Secret 安全最佳实践

静态加密

# K8s 1.29+ 默认启用 ETCD 加密
# 或者手动配置

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
    - secrets
    providers:
    - aescbc:
        keys:
        - name: key1
          secret: <base64-encoded-key>
    - identity: {}  # 不加密的后备方案
# 创建加密配置文件
cat > /etc/kubernetes/encryption-config.yaml << EOF
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
    - secrets
    providers:
    - aescbc:
        keys:
        - name: encryption-key
          secret: $(head -c 32 /dev/urandom | base64)
    - identity: {}
EOF

# 修改 kube-apiserver 启动参数
# --encryption-provider-config=/etc/kubernetes/encryption-config.yaml

RBAC 访问控制

# 只允许特定 ServiceAccount 读取 Secret
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: secret-reader
rules:
- apiGroups: [""]
  resources: ["secrets"]
  verbs: ["get", "list", "watch"]
  # 限制命名空间
  resourceNames: ["db-credentials"]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: app-secret-reader
subjects:
- kind: ServiceAccount
  name: app-sa
  namespace: production
roleRef:
  kind: Role
  name: secret-reader

外部密钥管理

┌─────────────────────────────────────────────────────────────────┐
│              集成外部密钥管理(最佳实践)                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   推荐方案:                                                    │
│                                                                 │
│   ┌───────────┐     ┌───────────┐     ┌───────────┐           │
│   │  HashiCorp│     │   AWS     │     │   GCP     │           │
│   │   Vault   │     │ Secrets  │     │  Secret   │           │
│   │           │     │  Manager │     │  Manager  │           │
│   └───────────┘     └───────────┘     └───────────┘           │
│        │                 │                 │                   │
│        └─────────────────┼─────────────────┘                   │
│                          │                                       │
│          ┌───────────────┴───────────────┐                     │
│          │     External Secrets Operator │                     │
│          └───────────────────────────────┘                     │
│                          │                                       │
│          ┌───────────────┴───────────────┐                     │
│          │        Kubernetes             │                     │
│          │         Secret                │                     │
│          └───────────────────────────────┘                     │
│                                                                 │
│   优点:                                                        │
│   ✓ Secret 不存储在 K8s 中                                      │
│   ✓ 集中审计和轮换                                             │
│   ✓ 自动同步更新                                               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

External Secrets Operator 示例

# 安装 ESO
# helm install external-secrets external-secrets

---
# 创建 SecretStore
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: vault-backend
spec:
  provider:
    vault:
      server: "https://vault.example.com:8200"
      path: "secret"
      version: "v2"
      auth:
        token:
          secretRef:
            name: vault-token
            key: token

---
# ExternalSecret
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: database-creds
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: vault-backend
    kind: ClusterSecretStore
  target:
    name: db-credentials
    creationPolicy: Owner
  data:
  - secretKey: username
    remoteRef:
      key: database/credentials
      property: username
  - secretKey: password
    remoteRef:
      key: database/credentials
      property: password

配置热更新

挂载卷的热更新

# 热更新实验
# 1. 挂载 ConfigMap 为文件
apiVersion: v1
kind: Pod
metadata:
  name: config-watch
spec:
  containers:
  - name: app
    image: myapp:1.0
    volumeMounts:
    - name: config
      mountPath: /etc/config

  volumes:
  - name: config
    configMap:
      name: app-config

# 2. 更新 ConfigMap
kubectl apply -f updated-configmap.yaml

# 3. 观察挂载内容变化
# K8s 会在 ConfigMap 更新时自动更新挂载的文件
# 但应用程序需要能够检测/监听文件变化

# 策略:
# - 应用内监听文件变化
# - 使用 inotify / fsnotify 等机制
# - 或使用配置中心 SDK

应用内热更新实现

// 示例:Go 应用监听配置文件变化
package main

import (
    "log"
    "os"
    "path/filepath"
    "sync"
    "time"
)

type Config struct {
    mu sync.RWMutex
    data map[string]string
}

func (c *Config) Reload(path string) error {
    c.mu.Lock()
    defer c.mu.Unlock()
    // 重新读取配置
    // ...
    log.Println("Config reloaded")
    return nil
}

func watchConfig(path string, reloadFunc func() error) {
    lastMod := time.Now()
    for {
        filepath.Walk(path, func(fpath string, info os.FileInfo, err error) error {
            if info.ModTime().After(lastMod) {
                lastMod = info.ModTime()
                reloadFunc()
            }
            return nil
        })
        time.Sleep(5 * time.Second)
    }
}

ConfigMap 滚动更新策略

# 同步更新策略
# 当 ConfigMap 更新时,Pod 内的挂载内容会更新
# 但有以下注意事项:

# 1. subPath 挂载的文件不会自动更新
volumeMounts:
- name: config
  mountPath: /etc/config/database.yaml
  subPath: database.yaml  # 使用 subPath 不会热更新

# 2. 环境变量不会自动更新
# 环境变量在容器启动时注入,ConfigMap 更新后需要重启 Pod

# 解决方案:
# - 不使用 subPath
# - 使用 sidecar 重新加载配置
# - 或者完全避免热更新,滚动更新 Pod

应用层配置刷新

# Spring Boot Actuator 配置
# application.yaml
spring:
  application:
    name: myapp
  config:
    import: optional:file:./config/

management:
  endpoints:
    web:
      exposure:
        include: refresh,health,info
  endpoint:
    refresh:
      enabled: true

# 当 ConfigMap 更新后,调用
# POST /actuator/refresh
# 会重新加载 @RefreshScope 注解的 Bean

配置管理最佳实践

配置分离策略

┌─────────────────────────────────────────────────────────────────┐
│                    配置管理分层                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  代码层(不变)                                                 │
│  └── 默认配置、打包在镜像中                                      │
│       │                                                         │
│       ▼                                                         │
│  ConfigMap(环境相关)                                          │
│  └── 环境变量、非敏感配置                                        │
│       │                                                         │
│       ▼                                                         │
│  Secret(敏感数据)                                             │
│  └── 密码、Token、证书                                          │
│       │                                                         │
│       ▼                                                         │
│  外部配置中心(如 Consul/Zookeeper)                           │
│  └── 动态配置、集中管理                                         │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

多环境配置

# base-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  APP_NAME: "myapp"
  LOG_LEVEL: "info"
  MAX_CONNECTIONS: "100"

---
# staging-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config-staging
data:
  LOG_LEVEL: "debug"
  MAX_CONNECTIONS: "50"

---
# production-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config-prod
data:
  LOG_LEVEL: "warn"
  MAX_CONNECTIONS: "200"

---
# Deployment 引用
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  template:
    spec:
      containers:
      - name: app
        envFrom:
        - configMapRef:
            name: app-config          # 基础配置
        - configMapRef:
            name: app-config-${ENV}   # 环境配置覆盖
          # 需要通过 helm 或 kustomize 注入 ENV

Kustomize 配置管理

# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- deployment.yaml
- configmap.yaml

---
# overlays/staging/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- ../../base

patches:
- path: staging-patch.yaml

configMapGenerator:
- name: app-config
  behavior: overlay
  literals:
  - LOG_LEVEL=debug

---
# overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- ../../base

configMapGenerator:
- name: app-config
  behavior: overlay
  literals:
  - LOG_LEVEL=warn

Helm 配置管理

# values.yaml
app:
  name: myapp
  replicaCount: 3
  image:
    repository: myregistry/myapp
    tag: "1.0.0"

config:
  logLevel: info
  maxConnections: 100

---
# values-prod.yaml
app:
  replicaCount: 5
  image:
    tag: "1.2.0"

config:
  logLevel: warn
  maxConnections: 200

---
# 部署
# helm install myapp ./chart -f values-prod.yaml

常见问题与避坑指南

Q1:Secret 能否加密存储?

# 方法1:ETCD 加密(K8s 原生)
# 需要 kube-apiserver 开启 encryption-provider-config

# 方法2:第三方插件
# - Sealed Secrets:加密后存储,公钥加密
# - Vault + ESO:外部存储

# 方法3:Kubernetes Secrets 加密-at-rest
# K8s 1.29+ 默认启用

Q2:ConfigMap 更新后 Pod 不生效?

# 原因1:使用了 subPath
# subPath 挂载的文件不会自动更新

# 原因2:使用了环境变量
# 环境变量在 Pod 创建时注入,不会自动更新

# 解决方案:
# 1. 不使用 subPath
# 2. 重启 Pod
kubectl rollout restart deployment/myapp

# 3. 应用内监听文件变化

Q3:如何验证配置正确性?

# 方式1:使用 pre-sync hook(ArgoCD/GitOps)
# 在部署前验证配置

# 方式2:配置校验工具
# - kubeval
# - conftest

# 方式3:应用层验证
# 启动时检查必要配置,缺失则 panic

Q4:Secret 如何轮换?

# 轮换流程:
# 1. 创建新 Secret
kubectl create secret generic db-creds-v2 --from-literal=password=newpassword

# 2. 更新 Deployment 引用
kubectl patch deployment myapp -p '{"spec":{"template":{"spec":{"containers":[{"name":"app","env":[{"name":"DB_PASSWORD","valueFrom":{"secretKeyRef":{"name":"db-creds-v2","key":"password"}}}]}]}}}}'

# 3. 滚动更新 Pod
kubectl rollout restart deployment myapp

# 4. 验证新密码生效
# 5. 删除旧 Secret
kubectl delete secret db-creds-v1

总结

┌─────────────────────────────────────────────────────────────────┐
│                    核心要点回顾                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ConfigMap                                                      │
│  ├── 非敏感配置存储                                             │
│  ├── 支持键值对和文件                                           │
│  ├── 环境变量/文件挂载两种方式                                  │
│  └── 挂载支持热更新,环境变量需要重启                           │
│                                                                 │
│  Secret                                                         │
│  ├── 敏感数据存储                                               │
│  ├── 类型:Opaque/TLS/DockerConfigJSON                         │
│  ├── base64 编码(非加密)                                       │
│  └── 推荐集成外部密钥管理                                       │
│                                                                 │
│  安全最佳实践                                                   │
│  ├── ETCD 加密存储                                             │
│  ├── RBAC 最小权限                                             │
│  └── 外部密钥管理(Vault/AWS SM/GCP SM)                       │
│                                                                 │
│  配置管理                                                       │
│  ├── 多环境分离(Kustomize/Helm)                              │
│  ├── 配置热更新(文件挂载+应用监听)                             │
│  └── 配置验证与回滚                                             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

思考题

  1. 为什么 Secret 的 data 是 base64 编码而不是加密?这有什么安全风险?
  2. 如果应用不支持热更新,如何实现零停机配置切换?
  3. 如何设计一个配置管理系统,支持配置变更审计和回滚?

引用与参考

  1. ConfigMaps
  2. Secrets
  3. Encrypting Secret Data at Rest
  4. External Secrets Operator

下篇预告

下一篇文章我们将探讨 存储卷与持久化存储,包括:

  • EmptyDir/HostPath 临时存储
  • PV/PVC 持久化存储
  • StorageClass 动态供给
  • CSI 与存储扩展

敬请期待!