Spring Cloud多环境部署:Profiles、ConfigMap、Secrets的最佳实践
Spring Cloud多环境部署:Profiles、ConfigMap、Secrets的最佳实践
适读人群:有微服务实战经验的后端工程师 | 阅读时长:约22分钟 | Spring Boot 3.2 / Kubernetes 1.28
开篇故事
我们刚上K8s那会儿,环境管理一团糟。测试环境和生产环境用的是同一个Docker镜像,通过环境变量区分,但环境变量的管理没有规范:有人直接在Deployment里写明文密码,有人把密钥文件打进镜像,有人把Nacos地址硬编码在代码里……
有次测试同学在测试环境验证完,部署到生产,结果发现服务连的是测试环境的数据库(因为数据库地址配在了镜像里),把测试数据写到了生产,造成了一次数据污染事故。
那次之后我们系统梳理了多环境配置管理的规范:哪些配置用Spring Profiles管理,哪些用K8s ConfigMap,哪些用K8s Secrets,以及怎么保证"相同镜像,不同环境,不同配置"的标准化交付。今天把这套规范完整写出来。
一、核心问题分析
多环境配置管理涉及三类信息,处理方式各不相同:
非敏感的应用配置:比如日志级别、线程池大小、功能开关、超时时间。这类配置可以存在Git仓库里,通过Spring Profiles或K8s ConfigMap管理。
敏感的连接信息:数据库地址、Redis地址、Nacos地址、MQ地址。这类信息不敏感(不是密码),但各环境不同,适合用K8s ConfigMap存储,通过环境变量注入。
高度敏感的密钥:数据库密码、JWT密钥、第三方API密钥。这类信息必须用K8s Secrets管理,不能出现在代码仓库、镜像、或未加密的ConfigMap里。
理解这三类信息的边界,是多环境配置管理的基础。
二、原理深度解析
2.1 多环境配置的层次结构
2.2 配置分类与存储位置
2.3 镜像不变性原则
三、完整代码实现
3.1 Spring Profiles的正确使用方式
# application.yaml(公共配置,所有环境生效)
spring:
application:
name: order-service
jackson:
time-zone: Asia/Shanghai
default-property-inclusion: non_null
server:
shutdown: graceful
# 不同环境的特定配置通过Profile指定,不要在这里写# application-dev.yaml(本地开发专用)
spring:
datasource:
url: jdbc:mysql://localhost:3306/order_dev
username: root
password: root123
data:
redis:
host: localhost
port: 6379
logging:
level:
com.laozhang: DEBUG
org.hibernate.SQL: DEBUG
# 开发环境关闭Nacos,本地直连依赖
spring:
cloud:
nacos:
discovery:
register-enabled: false# application-prod.yaml(生产环境骨架配置)
# 注意:这里不写具体的值!值通过环境变量注入
spring:
datasource:
# 从环境变量读取,环境变量由K8s ConfigMap/Secrets注入
url: ${DB_URL}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: ${DB_POOL_SIZE:20}
minimum-idle: ${DB_MIN_IDLE:5}
data:
redis:
host: ${REDIS_HOST}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
cloud:
nacos:
server-addr: ${NACOS_SERVER_ADDR}
namespace: ${NACOS_NAMESPACE}
username: ${NACOS_USERNAME}
password: ${NACOS_PASSWORD}
logging:
level:
com.laozhang: INFO
root: WARN3.2 K8s ConfigMap配置(非敏感环境差异配置)
# configmap-prod.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: order-service-config
namespace: production
data:
# 数据库连接信息(不含密码)
DB_URL: "jdbc:mysql://mysql-prod:3306/order?useSSL=true&serverTimezone=Asia/Shanghai"
DB_USERNAME: "order_user"
DB_POOL_SIZE: "30"
DB_MIN_IDLE: "10"
# Redis连接信息(不含密码)
REDIS_HOST: "redis-prod-cluster"
REDIS_PORT: "6379"
# Nacos连接信息(不含密码)
NACOS_SERVER_ADDR: "nacos-prod-cluster:8848"
NACOS_NAMESPACE: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
NACOS_USERNAME: "nacos_order_svc"
# 应用配置
SPRING_PROFILES_ACTIVE: "prod"
APP_VERSION: "2.1.0"
---
# configmap-test.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: order-service-config
namespace: testing
data:
DB_URL: "jdbc:mysql://mysql-test:3306/order_test?useSSL=false"
DB_USERNAME: "order_test_user"
REDIS_HOST: "redis-test"
NACOS_SERVER_ADDR: "nacos-test:8848"
NACOS_NAMESPACE: "test-namespace-uuid"
SPRING_PROFILES_ACTIVE: "test"3.3 K8s Secrets配置(高度敏感信息)
# secrets-prod.yaml(不要提交到Git!使用SealedSecrets或Vault管理)
apiVersion: v1
kind: Secret
metadata:
name: order-service-secrets
namespace: production
type: Opaque
stringData:
# 使用stringData而不是data,避免Base64编码的麻烦
# 实际生产中,这些值应该通过密钥管理工具(如Vault、Sealed Secrets)生成
DB_PASSWORD: "${从Vault获取}"
REDIS_PASSWORD: "${从Vault获取}"
NACOS_PASSWORD: "${从Vault获取}"
JWT_ACCESS_SECRET: "${从Vault获取}"
JWT_REFRESH_SECRET: "${从Vault获取}"
ENCRYPTION_KEY: "${从Vault获取}"3.4 Deployment配置(把ConfigMap和Secrets注入为环境变量)
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
namespace: production
spec:
replicas: 3
template:
spec:
containers:
- name: order-service
image: registry.company.com/order-service:2.1.0
ports:
- containerPort: 8080
env:
# 把所有ConfigMap内容作为环境变量注入
- name: SPRING_PROFILES_ACTIVE
valueFrom:
configMapKeyRef:
name: order-service-config
key: SPRING_PROFILES_ACTIVE
- name: DB_URL
valueFrom:
configMapKeyRef:
name: order-service-config
key: DB_URL
- name: DB_USERNAME
valueFrom:
configMapKeyRef:
name: order-service-config
key: DB_USERNAME
# 敏感信息从Secrets注入
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: order-service-secrets
key: DB_PASSWORD
- name: JWT_ACCESS_SECRET
valueFrom:
secretKeyRef:
name: order-service-secrets
key: JWT_ACCESS_SECRET
# K8s downward API:注入Pod信息
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
# 批量注入(适合ConfigMap字段较多时)
envFrom:
- configMapRef:
name: order-service-config
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2000m"
memory: "2Gi"3.5 ConfigMap热更新(不重启服务更新配置)
# 把ConfigMap作为文件挂载,实现热更新
# 注意:Spring Boot默认不监听文件系统变更,需要配合@RefreshScope或Nacos Config使用
spec:
volumes:
- name: app-config
configMap:
name: order-service-config
containers:
- name: order-service
volumeMounts:
- name: app-config
mountPath: /app/config
readOnly: true# application.yaml 中引用挂载的配置文件
spring:
config:
import:
- optional:file:/app/config/application.yaml3.6 多环境的Helm Chart模板
# helm/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "order-service.fullname" . }}-config
namespace: {{ .Release.Namespace }}
data:
DB_URL: {{ .Values.database.url | quote }}
DB_USERNAME: {{ .Values.database.username | quote }}
REDIS_HOST: {{ .Values.redis.host | quote }}
NACOS_SERVER_ADDR: {{ .Values.nacos.serverAddr | quote }}
SPRING_PROFILES_ACTIVE: {{ .Values.spring.profiles | quote }}# helm/values-prod.yaml
spring:
profiles: prod
database:
url: jdbc:mysql://mysql-prod:3306/order
username: order_user
redis:
host: redis-prod
nacos:
serverAddr: nacos-prod:8848四、生产配置与调优
4.1 Secrets安全管理方案
生产环境推荐使用以下方案之一管理Secrets:
方案一:Sealed Secrets(Bitnami开源)
# 安装kubeseal
# 创建加密的SealedSecret(只有集群内部才能解密)
kubectl create secret generic order-secrets --dry-run=client \
--from-literal=DB_PASSWORD=secret123 -o yaml | \
kubeseal --format yaml > sealed-secrets.yaml
# sealed-secrets.yaml可以安全提交到Git方案二:Vault(HashiCorp)
# 通过Vault Agent Sidecar自动注入Secrets
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "order-service"
vault.hashicorp.com/agent-inject-secret-db: "secret/order/db"五、踩坑实录
坑一:密码直接写在ConfigMap里,测试同学看到了生产密码。
ConfigMap里的数据是明文可见的,所有有kubectl权限的人都能kubectl get configmap -o yaml看到内容。密码绝对不能放ConfigMap,必须用Secrets,并且严格控制Secrets的RBAC权限。
坑二:Spring Profile在K8s里通过环境变量设置但没生效,应用还是用的默认配置。
设置SPRING_PROFILES_ACTIVE=prod没有生效,原因是application-prod.yaml文件里的环境变量没有被解析。排查发现是ConfigMap的Key写成了spring.profiles.active(带点号),而不是SPRING_PROFILES_ACTIVE(大写下划线)。Spring Boot支持两种格式,但K8s的ConfigMap Key推荐用大写下划线格式,更标准。
坑三:K8s Secret被Base64编码但没有加密,以为是安全的。
Kubernetes的Secret只做了Base64编码,不是加密。任何有读取Secret权限的人都可以base64 -d解码出明文。Secret的安全依赖于K8s RBAC权限控制和etcd的加密at-rest配置。生产环境必须开启etcd加密,或者使用Vault等外部密钥管理系统。
坑四:ConfigMap更新后,服务没有重启,使用的还是旧配置。
当ConfigMap作为环境变量注入时,Pod必须重启才能读取新值(环境变量在进程启动时加载)。但当ConfigMap作为文件挂载时,K8s会自动同步更新挂载的文件(通常1-2分钟延迟),但应用需要监听文件变更才能热更新。
六、总结
多环境配置管理的核心原则:镜像不变,配置外置;非敏感配置用ConfigMap,敏感信息用Secrets;密码必须用Vault或SealedSecrets管理,不能出现在Git仓库;Spring Profiles只管理应用层配置逻辑,环境差异化的值通过环境变量注入。遵循这套规范,才能实现真正的"Build Once, Deploy Anywhere"。
