为什么要将配置与代码分离
"同一个镜像,跑在 dev/staging/prod 不同环境"是容器化的核心优势。但不同环境的数据库地址、日志级别、第三方 API Key 各不相同。这些配置不能硬编码在镜像里——否则就丧失了可移植性。
K8s 通过两种资源分离配置:
ConfigMap
存储非敏感配置,如应用参数、配置文件、日志级别。数据以明文存储在 etcd 中。
Secret
存储敏感信息,如密码、Token、TLS 证书。数据以 Base64 编码存储(注意:不是加密!)。
ConfigMap
创建 ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: default
data: # 键值对数据
LOG_LEVEL: info # 简单键值
APP_PORT: "8080"
DATABASE_HOST: mysql-service
nginx.conf: | # 多行文件内容(| 保留换行)
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://backend;
}
}
app.properties: |
spring.datasource.url=jdbc:mysql://mysql-service:3306/mydb
spring.datasource.max-pool-size=10
# 从文件创建 ConfigMap
kubectl create configmap nginx-conf --from-file=nginx.conf
# 从目录创建(目录下所有文件)
kubectl create configmap app-config --from-file=config/
# 从字面值创建
kubectl create configmap env-config \
--from-literal=LOG_LEVEL=info \
--from-literal=APP_PORT=8080
挂载方式一:作为环境变量
spec:
containers:
- name: app
image: my-app:v1
env: # 注入单个键
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config # ConfigMap 名称
key: LOG_LEVEL # ConfigMap 中的键
envFrom: # 注入全部键(作为环境变量)
- configMapRef:
name: app-config
挂载方式二:作为 Volume 文件
spec:
containers:
- name: nginx
image: nginx:1.25
volumeMounts:
- name: config-vol
mountPath: /etc/nginx/conf.d # ConfigMap 所有 key 成为该目录下的文件
- name: config-vol
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf # subPath:只挂载指定键为单个文件
volumes:
- name: config-vol
configMap:
name: app-config
defaultMode: 0644 # 文件权限(八进制)
items: # 只挂载指定 key
- key: nginx.conf
path: default.conf # 重命名文件
以 Volume 方式挂载的 ConfigMap,K8s 会在 ConfigMap 更新后(约 1 分钟内)自动更新挂载的文件,无需重启 Pod。但环境变量方式注入的配置不会热更新,需要重启 Pod 才生效。应用还需要自行监听文件变更并重新加载配置(如 nginx 的 -s reload)。
Secret
Secret 类型
创建 Opaque Secret
apiVersion: v1
kind: Secret
metadata:
name: db-secret
type: Opaque
data: # base64 编码的值
username: YWRtaW4= # echo -n 'admin' | base64
password: cGFzc3dvcmQxMjM= # echo -n 'password123' | base64
# 也可以用 stringData(明文,K8s 自动编码,不会写回 YAML)
stringData:
dsn: "mysql://admin:password123@mysql:3306/mydb"
# 命令行创建(推荐,避免 base64 手动编码出错)
kubectl create secret generic db-secret \
--from-literal=username=admin \
--from-literal=password=password123
# 创建 TLS Secret
kubectl create secret tls api-tls \
--cert=tls.crt \
--key=tls.key
# 创建 Docker 镜像拉取 Secret
kubectl create secret docker-registry registry-secret \
--docker-server=registry.example.com \
--docker-username=user \
--docker-password=passwd
在 Pod 中使用 Secret
spec:
imagePullSecrets: # 拉取私有镜像
- name: registry-secret
containers:
- name: app
image: my-app:v1
env:
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-secret
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
volumeMounts:
- name: tls-certs
mountPath: /etc/ssl/certs
readOnly: true
volumes:
- name: tls-certs
secret:
secretName: api-tls
defaultMode: 0400 # 私钥文件权限要严格
K8s Secret 的 data 字段只是 Base64 编码,不是加密!任何能读取 Secret 的人都可以解码。必须做到:
1. 开启 etcd 静态加密(EncryptionConfiguration)
2. 严格控制 RBAC,最小化 Secret 读取权限
3. 生产环境推荐使用 Vault 或 External Secrets Operator 管理敏感配置
4. 永远不要把 Secret YAML 提交到 Git 仓库!
外部 Secret 管理
External Secrets Operator
External Secrets Operator(ESO)将外部 Secret 管理系统(Vault、AWS Secrets Manager、GCP Secret Manager 等)的 Secret 同步为 K8s Secret。
# ExternalSecret:定义从哪个外部系统同步什么数据
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-external-secret
spec:
refreshInterval: 1h # 每小时同步一次
secretStoreRef: # 引用 SecretStore(外部系统连接配置)
name: vault-backend
kind: SecretStore
target:
name: db-secret # 生成的 K8s Secret 名称
data:
- secretKey: password # K8s Secret 中的键名
remoteRef:
key: production/db # Vault 中的路径
property: password # Vault 中的字段
如果你无法使用 Vault 等外部系统,可以考虑 Sealed Secrets(Bitnami)。它允许将 Secret 加密后安全地提交到 Git(只有集群内的控制器能解密),实现 GitOps 安全管理 Secret。