目录

Kubernetes-07 配置管理:ConfigMap 与 Secret

Kubernetes 配置管理:ConfigMap 与 Secret

系列第七篇。将配置从代码和镜像中分离是云原生应用的核心原则之一。本篇讲解 K8s 配置管理的最佳实践。


目录

  1. 为什么要分离配置
  2. ConfigMap
  3. Secret
  4. 使用方式详解
  5. 动态配置更新
  6. Go 应用配置实战
  7. 配置管理最佳实践
  8. 小结

1. 为什么要分离配置

十二因素应用(12-Factor App)的第三条:在环境中存储配置

# 不好的做法:配置写死在代码或镜像里
const dbDSN = "mysql://user:password@prod-db:3306/myapp"

# 好的做法:从环境变量或配置文件读取
dsn := os.Getenv("DATABASE_DSN")

分离配置的好处:

  • 同一个镜像可以用于 dev/staging/prod,只改配置
  • 密钥不会泄露到代码仓库
  • 修改配置不需要重新构建镜像

K8s 提供两种配置对象:

  • ConfigMap:非敏感配置(数据库地址、功能开关、超时时间)
  • Secret:敏感配置(密码、API Key、TLS 证书)

2. ConfigMap

2.1 创建 ConfigMap

方式一:YAML

apiVersion: v1
kind: ConfigMap
metadata:
  name: go-app-config
  namespace: production
data:
  # 简单键值对
  APP_ENV: "production"
  LOG_LEVEL: "info"
  SERVER_PORT: "8080"
  DB_HOST: "mysql-service.production.svc.cluster.local"
  DB_PORT: "3306"
  DB_NAME: "myapp"
  MAX_CONNECTIONS: "100"
  ENABLE_TRACING: "true"

  # 多行配置文件(YAML/JSON/TOML 等)
  app.yaml: |
    server:
      port: 8080
      timeout: 30s
      maxHeaderSize: 1MB
    database:
      host: mysql-service
      port: 3306
      name: myapp
      maxOpenConns: 100
      maxIdleConns: 10
    redis:
      addr: redis-service:6379
      db: 0
    feature:
      enableNewUI: true
      enableBetaAPI: false    

  nginx.conf: |
    server {
        listen 80;
        server_name _;
        location / {
            proxy_pass http://localhost:8080;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }
    }    

方式二:kubectl 命令

# 从字面量创建
kubectl create configmap go-app-config \
  --from-literal=APP_ENV=production \
  --from-literal=LOG_LEVEL=info

# 从文件创建(文件名作为 key)
kubectl create configmap go-app-config \
  --from-file=app.yaml \
  --from-file=nginx.conf=./config/nginx.conf

# 从目录创建(目录下所有文件名作为 key)
kubectl create configmap go-app-config \
  --from-file=./config/

2.2 查看 ConfigMap

kubectl get configmap
kubectl get cm go-app-config -o yaml

# 查看具体的 key
kubectl get cm go-app-config -o jsonpath='{.data.app\.yaml}'

3. Secret

Secret 与 ConfigMap 类似,但 value 经过 base64 编码(注意:不是加密!)。

3.1 Secret 类型

类型 用途
Opaque 通用(默认),存储任意数据
kubernetes.io/dockerconfigjson 镜像仓库认证
kubernetes.io/tls TLS 证书和私钥
kubernetes.io/service-account-token ServiceAccount Token
kubernetes.io/basic-auth 用户名/密码
kubernetes.io/ssh-auth SSH 私钥

3.2 创建 Secret

方式一:YAML(值必须 base64 编码)

# 生成 base64 编码
echo -n "my-password" | base64
# bXktcGFzc3dvcmQ=

echo -n "myuser" | base64
# bXl1c2Vy
apiVersion: v1
kind: Secret
metadata:
  name: db-secret
  namespace: production
type: Opaque
data:
  # base64 编码的值
  username: bXl1c2Vy          # myuser
  password: bXktcGFzc3dvcmQ=  # my-password
  dsn: bXl1c2VyOm15LXBhc3N3...  # base64 of "myuser:my-password@tcp(mysql:3306)/myapp"

或使用 stringData(自动 base64 编码):

apiVersion: v1
kind: Secret
metadata:
  name: db-secret
  namespace: production
type: Opaque
stringData:
  # 直接写明文,K8s 自动 base64
  username: "myuser"
  password: "my-password"
  dsn: "myuser:my-password@tcp(mysql-service:3306)/myapp?parseTime=true"

方式二:kubectl 命令

# 从字面量创建
kubectl create secret generic db-secret \
  --from-literal=username=myuser \
  --from-literal=password=my-password

# TLS 证书
kubectl create secret tls myapp-tls \
  --cert=tls.crt \
  --key=tls.key

# 镜像仓库认证
kubectl create secret docker-registry registry-secret \
  --docker-server=myregistry.io \
  --docker-username=myuser \
  --docker-password=mypassword \
  --docker-email=myuser@company.com

3.3 Secret 安全注意事项

base64 不是加密! 任何能访问 Secret 的人都可以解码:

echo "bXktcGFzc3dvcmQ=" | base64 -d
# my-password

生产环境的安全增强:

  1. 使用 RBAC 严格限制对 Secret 的访问
  2. etcd 加密(EncryptionConfiguration)
  3. 外部 Secret 管理系统

External Secrets Operator 示例:

# 从 AWS Secrets Manager 同步到 K8s Secret
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: db-secret
  namespace: production
spec:
  refreshInterval: 1h         # 每小时同步一次
  secretStoreRef:
    name: aws-secretsmanager
    kind: ClusterSecretStore
  target:
    name: db-secret           # 创建的 K8s Secret 名称
    creationPolicy: Owner
  data:
  - secretKey: password       # K8s Secret 中的 key
    remoteRef:
      key: prod/myapp/db      # AWS Secrets Manager 中的路径
      property: password      # JSON 中的字段

4. 使用方式详解

ConfigMap 和 Secret 有三种使用方式:

4.1 方式一:环境变量(单个 key)

spec:
  containers:
  - name: go-app
    env:
    # 从 ConfigMap 注入
    - name: APP_ENV
      valueFrom:
        configMapKeyRef:
          name: go-app-config
          key: APP_ENV

    - name: LOG_LEVEL
      valueFrom:
        configMapKeyRef:
          name: go-app-config
          key: LOG_LEVEL
          optional: true     # 不存在时不报错

    # 从 Secret 注入
    - name: DB_PASSWORD
      valueFrom:
        secretKeyRef:
          name: db-secret
          key: password

    - name: DB_DSN
      valueFrom:
        secretKeyRef:
          name: db-secret
          key: dsn

4.2 方式二:环境变量(全量注入)

spec:
  containers:
  - name: go-app
    envFrom:
    # ConfigMap 所有 key 注入为环境变量
    - configMapRef:
        name: go-app-config
      prefix: "APP_"         # 可选:加前缀避免冲突

    # Secret 所有 key 注入为环境变量
    - secretRef:
        name: db-secret

注意: 全量注入时,ConfigMap 的 key 会直接成为环境变量名,要注意命名冲突。

4.3 方式三:挂载为文件

spec:
  containers:
  - name: go-app
    volumeMounts:
    - name: config-files
      mountPath: /etc/app          # 挂载目录
      readOnly: true
    - name: secret-files
      mountPath: /etc/secrets
      readOnly: true

    # 只挂载单个文件
    - name: tls-certs
      mountPath: /etc/ssl/tls.crt
      subPath: tls.crt             # 只挂载 secret 中的 tls.crt 这个 key

  volumes:
  - name: config-files
    configMap:
      name: go-app-config
      # 可以选择只挂载部分 key
      items:
      - key: app.yaml
        path: app.yaml             # 文件名(相对于 mountPath)
        mode: 0444                 # 文件权限
      - key: nginx.conf
        path: nginx/nginx.conf     # 可以包含子目录

  - name: secret-files
    secret:
      secretName: db-secret
      defaultMode: 0400            # 默认权限(比较严格)

  - name: tls-certs
    secret:
      secretName: myapp-tls

5. 动态配置更新

5.1 Volume 挂载:自动热更新

当 ConfigMap 或 Secret 更新后,已挂载为 Volume 的文件会在约 60 秒内syncPeriod + kubelet 同步周期)自动更新。

Go 应用实现配置热重载:

package config

import (
    "log"
    "os"
    "sync"
    "time"

    "github.com/fsnotify/fsnotify"
    "gopkg.in/yaml.v3"
)

type Config struct {
    Server   ServerConfig   `yaml:"server"`
    Database DatabaseConfig `yaml:"database"`
    Feature  FeatureConfig  `yaml:"feature"`
    mu       sync.RWMutex
}

type ServerConfig struct {
    Port    int           `yaml:"port"`
    Timeout time.Duration `yaml:"timeout"`
}

type DatabaseConfig struct {
    Host         string `yaml:"host"`
    Port         int    `yaml:"port"`
    Name         string `yaml:"name"`
    MaxOpenConns int    `yaml:"maxOpenConns"`
}

type FeatureConfig struct {
    EnableNewUI    bool `yaml:"enableNewUI"`
    EnableBetaAPI  bool `yaml:"enableBetaAPI"`
}

var globalConfig = &Config{}

func Load(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, err
    }

    cfg := &Config{}
    if err := yaml.Unmarshal(data, cfg); err != nil {
        return nil, err
    }
    return cfg, nil
}

// WatchConfig 监听配置文件变化并热重载
func WatchConfig(path string, onChange func(*Config)) error {
    watcher, err := fsnotify.NewWatcher()
    if err != nil {
        return err
    }

    // K8s ConfigMap 挂载使用软链接,需要监听目录而非文件
    dir := filepath.Dir(path)
    if err := watcher.Add(dir); err != nil {
        return err
    }

    go func() {
        defer watcher.Close()
        for {
            select {
            case event, ok := <-watcher.Events:
                if !ok {
                    return
                }
                // K8s ConfigMap 更新时会触发 CREATE 事件(软链接替换)
                if event.Op&fsnotify.Create == fsnotify.Create {
                    time.Sleep(100 * time.Millisecond) // 等待写入完成
                    cfg, err := Load(path)
                    if err != nil {
                        log.Printf("Failed to reload config: %v", err)
                        continue
                    }
                    log.Printf("Config reloaded from %s", path)
                    onChange(cfg)
                }
            case err, ok := <-watcher.Errors:
                if !ok {
                    return
                }
                log.Printf("Watcher error: %v", err)
            }
        }
    }()

    return nil
}

5.2 环境变量:需要重启 Pod

环境变量方式注入的配置不会自动更新,需要重启 Pod:

# 方法 1:重启 Deployment(所有 Pod 滚动重启)
kubectl rollout restart deployment/go-app

# 方法 2:删除 Pod(Deployment 会自动重建)
kubectl delete pods -l app=go-app

# 方法 3:修改 Deployment 的某个 annotation 触发更新
kubectl patch deployment go-app -p \
  '{"spec":{"template":{"metadata":{"annotations":{"kubectl.kubernetes.io/restartedAt":"'$(date -u +"%Y-%m-%dT%H:%M:%SZ")'"}}}}}'

使用 Reloader(自动化热重启):

# 安装 Reloader(Stakater)
kubectl apply -f https://raw.githubusercontent.com/stakater/Reloader/master/deployments/kubernetes/reloader.yaml
# 在 Deployment 上添加 annotation,ConfigMap 或 Secret 变更时自动滚动更新
metadata:
  annotations:
    reloader.stakater.com/auto: "true"
    # 或指定监听哪些 ConfigMap/Secret
    configmap.reloader.stakater.com/reload: "go-app-config"
    secret.reloader.stakater.com/reload: "db-secret"

6. Go 应用配置实战

6.1 推荐的配置加载模式

package main

import (
    "fmt"
    "log"
    "os"
    "strconv"
    "time"

    "gopkg.in/yaml.v3"
)

// AppConfig 应用配置结构
type AppConfig struct {
    Server   ServerConfig   `yaml:"server"`
    Database DatabaseConfig `yaml:"database"`
    Redis    RedisConfig    `yaml:"redis"`
    Log      LogConfig      `yaml:"log"`
}

type ServerConfig struct {
    Port            int           `yaml:"port"`
    ReadTimeout     time.Duration `yaml:"readTimeout"`
    WriteTimeout    time.Duration `yaml:"writeTimeout"`
    ShutdownTimeout time.Duration `yaml:"shutdownTimeout"`
}

type DatabaseConfig struct {
    DSN          string        `yaml:"dsn"`
    MaxOpenConns int           `yaml:"maxOpenConns"`
    MaxIdleConns int           `yaml:"maxIdleConns"`
    ConnMaxLife  time.Duration `yaml:"connMaxLife"`
}

type RedisConfig struct {
    Addr     string `yaml:"addr"`
    Password string `yaml:"password"`
    DB       int    `yaml:"db"`
}

type LogConfig struct {
    Level  string `yaml:"level"`
    Format string `yaml:"format"`  // json | text
}

// LoadConfig 加载配置(优先级:环境变量 > 配置文件 > 默认值)
func LoadConfig() (*AppConfig, error) {
    cfg := defaultConfig()

    // 1. 加载配置文件(ConfigMap 挂载)
    configPath := getEnv("CONFIG_PATH", "/etc/app/app.yaml")
    if data, err := os.ReadFile(configPath); err == nil {
        if err := yaml.Unmarshal(data, cfg); err != nil {
            return nil, fmt.Errorf("parse config file: %w", err)
        }
        log.Printf("Loaded config from %s", configPath)
    } else {
        log.Printf("Config file not found at %s, using defaults/env vars", configPath)
    }

    // 2. 环境变量覆盖(Secret 注入的敏感配置)
    if dsn := os.Getenv("DATABASE_DSN"); dsn != "" {
        cfg.Database.DSN = dsn
    }
    if redisAddr := os.Getenv("REDIS_ADDR"); redisAddr != "" {
        cfg.Redis.Addr = redisAddr
    }
    if redisPass := os.Getenv("REDIS_PASSWORD"); redisPass != "" {
        cfg.Redis.Password = redisPass
    }
    if port := os.Getenv("SERVER_PORT"); port != "" {
        if p, err := strconv.Atoi(port); err == nil {
            cfg.Server.Port = p
        }
    }
    if level := os.Getenv("LOG_LEVEL"); level != "" {
        cfg.Log.Level = level
    }

    return cfg, nil
}

func defaultConfig() *AppConfig {
    return &AppConfig{
        Server: ServerConfig{
            Port:            8080,
            ReadTimeout:     30 * time.Second,
            WriteTimeout:    30 * time.Second,
            ShutdownTimeout: 25 * time.Second,
        },
        Database: DatabaseConfig{
            MaxOpenConns: 100,
            MaxIdleConns: 10,
            ConnMaxLife:  time.Hour,
        },
        Redis: RedisConfig{
            Addr: "redis:6379",
            DB:   0,
        },
        Log: LogConfig{
            Level:  "info",
            Format: "json",
        },
    }
}

func getEnv(key, defaultVal string) string {
    if v := os.Getenv(key); v != "" {
        return v
    }
    return defaultVal
}

6.2 完整的 K8s 配置

# ConfigMap(非敏感配置)
apiVersion: v1
kind: ConfigMap
metadata:
  name: go-app-config
  namespace: production
data:
  app.yaml: |
    server:
      port: 8080
      readTimeout: 30s
      writeTimeout: 30s
      shutdownTimeout: 25s
    database:
      maxOpenConns: 100
      maxIdleConns: 10
      connMaxLife: 1h
    redis:
      addr: redis-service:6379
      db: 0
    log:
      level: info
      format: json    
---
# Secret(敏感配置)
apiVersion: v1
kind: Secret
metadata:
  name: go-app-secret
  namespace: production
type: Opaque
stringData:
  DATABASE_DSN: "user:password@tcp(mysql-service:3306)/myapp?parseTime=true&loc=Asia%2FShanghai"
  REDIS_PASSWORD: "redis-secret-password"
---
# Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: go-app
  namespace: production
  annotations:
    configmap.reloader.stakater.com/reload: "go-app-config"
    secret.reloader.stakater.com/reload: "go-app-secret"
spec:
  replicas: 3
  selector:
    matchLabels:
      app: go-app
  template:
    metadata:
      labels:
        app: go-app
    spec:
      containers:
      - name: go-app
        image: myregistry.io/go-app:v1.0.0

        # 环境变量:从 Secret 注入(敏感配置)
        envFrom:
        - secretRef:
            name: go-app-secret

        # 单独注入 Pod 信息(Downward API)
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        - name: NODE_NAME
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName

        # 挂载 ConfigMap 为文件(非敏感配置)
        volumeMounts:
        - name: app-config
          mountPath: /etc/app
          readOnly: true

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

7. 配置管理最佳实践

7.1 分层配置

优先级(高到低):
1. 命令行参数
2. 环境变量(运行时覆盖)
3. 配置文件(ConfigMap 挂载)
4. 默认值(代码中硬编码的合理默认值)

7.2 不同环境管理

# 方法一:不同 Namespace,相同 ConfigMap 名
kubectl apply -f configmap.yaml -n development
kubectl apply -f configmap-prod.yaml -n production  # 同名但内容不同

# 方法二:使用 Kustomize(推荐)
kustomize/
├── base/
│   ├── configmap.yaml
│   └── kustomization.yaml
├── overlays/
│   ├── development/
│   │   ├── configmap-patch.yaml
│   │   └── kustomization.yaml
│   └── production/
│       ├── configmap-patch.yaml
│       └── kustomization.yaml

# 方法三:使用 Helm(values.yaml 控制不同环境)
helm install go-app ./chart -f values-prod.yaml

7.3 Secret 管理建议

❌ 不要:
  - 将 Secret YAML 提交到 Git(即使 base64)
  - 在日志中打印 Secret 值
  - 将同一 Secret 在多个 Namespace 间复制(用 External Secrets Operator 同步)

✅ 要:
  - 使用 External Secrets Operator 或 Sealed Secrets
  - 启用 etcd 加密(EncryptionConfiguration)
  - 使用 RBAC 最小化 Secret 访问权限
  - 定期轮换密钥(数据库密码、API Key 等)
  - 在 CI/CD 中使用 Vault 或云厂商密钥管理服务

7.4 ConfigMap 大小限制

ConfigMap 的大小限制是 1MB(etcd 限制)。

如果配置文件很大,考虑:

  • 精简配置内容
  • 使用对象存储(S3/OSS)+ 启动时下载
  • 拆分成多个 ConfigMap

8. 小结

对比 ConfigMap Secret
用途 非敏感配置 敏感信息
存储方式 明文 base64 编码(不是加密)
注入方式 环境变量 / 文件挂载 同左
热更新 挂载模式自动(~60s) 同左
大小限制 1MB 1MB
访问控制 RBAC RBAC(建议更严格)

常用命令速查

# ConfigMap
kubectl create cm my-config --from-literal=key=val
kubectl create cm my-config --from-file=config.yaml
kubectl get cm my-config -o yaml
kubectl edit cm my-config

# Secret
kubectl create secret generic my-secret --from-literal=password=xxx
kubectl create secret tls my-tls --cert=cert.pem --key=key.pem
kubectl get secret my-secret -o jsonpath='{.data.password}' | base64 -d
kubectl edit secret my-secret

# 触发 Deployment 热重启
kubectl rollout restart deployment/go-app