Kubernetes-07 配置管理:ConfigMap 与 Secret
目录
Kubernetes 配置管理:ConfigMap 与 Secret
系列第七篇。将配置从代码和镜像中分离是云原生应用的核心原则之一。本篇讲解 K8s 配置管理的最佳实践。
目录
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
生产环境的安全增强:
- 使用 RBAC 严格限制对 Secret 的访问
- etcd 加密(EncryptionConfiguration)
- 外部 Secret 管理系统:
- External Secrets Operator + AWS Secrets Manager / Vault
- Sealed Secrets(加密存储在 Git)
- HashiCorp Vault Agent Injector
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
xingliuhua