阶段一 · Kubernetes 核心

配置管理

前置回顾

前面五篇解决了"应用怎么部署,怎么访问"的问题.但还有一个基本问题没有涉及: 应用怎么拿到自己的配置?
数据库地址,API Key,功能开关,日志级别...这些不应该硬编码在镜像里(否则改个配置就要重新构建镜像).
本篇讲解 K8s 原生的配置管理方案:ConfigMap(非敏感)和 Secret(敏感),以及它们与实践中更常用的 Nacos,Apollo,Rainbow等远程配置平台的异同.

配置外置的动机

反模式:配置写死在镜像中
Dockerfile → COPY config.yaml /app/config.yaml → 构建镜像
问题:
1. 改个数据库地址就要重新 build + push + 重新部署
2. 同一个镜像无法在 dev / staging / prod 环境复用
3. 密码,API Key 写进镜像 → 镜像泄露 = 密钥泄露
4. 无法在运行时动态修改(如临时开 debug 日志)

正确做法:配置与镜像分离
镜像只包含代码 → 配置在部署时注入(环境变量 / 挂载文件)
→ 同一个镜像 + 不同配置 = 不同环境
→ 改配置不需要重新构建镜像

这正是十二要素应用(12-Factor App) 的第三条原则:"在环境中存储配置".K8s 的 ConfigMap 和 Secret 就是这个原则的落地实现.

ConfigMap - 非敏感配置

ConfigMap 存储非敏感的键值对或文件内容.它本质上就是一个 map[string]string,存在 etcd 中.

创建方式

# 方式一:命令行直接传键值
$ kubectl create configmap app-config \
--from-literal=LOG_LEVEL=info \
--from-literal=DB_HOST=mysql-svc \
--from-literal=CACHE_TTL=300

# 方式二:从文件创建(文件名 = key,文件内容 = value)
$ kubectl create configmap nginx-conf \
--from-file=nginx.conf \
--from-file=mime.types

# 方式三:YAML 声明式(推荐,可纳入 Git 版本管理)
$ kubectl apply -f configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data: # 键值对形式
LOG_LEVEL: "info"
DB_HOST: "mysql-svc"
CACHE_TTL: "300"

---

apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-conf
data: # 多行文件内容
nginx.conf: |
server {
listen 80;
location / {
proxy_pass http://backend-svc:8080;
}
}

使用方式

ConfigMap 有两种注入 Pod 的方式:环境变量和文件挂载.

# 方式一:注入为环境变量
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
image: my-app:v1
env:
- name: LOG_LEVEL # Pod 内的环境变量名
valueFrom:
configMapKeyRef:
name: app-config # ConfigMap 名称
key: LOG_LEVEL # ConfigMap 中的 key

# 或者一次性注入整个 ConfigMap 的所有键值
envFrom:
- configMapRef:
name: app-config # LOG_LEVEL,DB_HOST,CACHE_TTL 全部变成环境变量
# 方式二:挂载为文件(适合配置文件场景)
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:1.25
volumeMounts:
- name: config-volume
mountPath: /etc/nginx/nginx.conf # 挂载到容器的哪个路径
subPath: nginx.conf # 只挂载 ConfigMap 中的某个 key(避免覆盖整个目录)
volumes:
- name: config-volume
configMap:
name: nginx-conf # 引用的 ConfigMap

subPath 的陷阱:不支持热更新

使用 subPath 挂载单个文件时,ConfigMap 更新后容器内的文件不会自动更新(因为 subPath 用的是绑定挂载,不走 kubelet 的 symlink 机制).
如果需要热更新,应该挂载整个目录而不是单个文件.

环境变量 vs 文件挂载:怎么选

维度 环境变量 文件挂载
适合 简单键值对(DB_HOST,LOG_LEVEL) 结构化配置文件(nginx.conf,application.yaml)
热更新 不支持(Pod 启动时注入,之后不变) 支持(kubelet 定期同步,延迟约 30-60s)
可见性 env 命令可查看 文件系统中可读
大小 适合短值 支持整个配置文件(ConfigMap 上限 1MB)

Secret - 敏感配置

Secret 的 API 和 ConfigMap 几乎一模一样,核心区别是语义上的:Secret 用于存储密码,Token,证书等敏感数据.

创建方式

# 命令行创建
$ kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password='S3cr3t!@#'

# TLS 证书
$ kubectl create secret tls api-tls \
--cert=tls.crt \
--key=tls.key
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque # 通用类型(还有 tls / dockerconfigjson 等)
data: # 注意:值必须是 Base64 编码
username: YWRtaW4= # echo -n "admin" | base64
password: UzNjcjN0IUAj # echo -n "S3cr3t!@#" | base64

---

# 或者用 stringData 直接写明文(K8s 会自动 Base64)
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
stringData: # 明文写入,apply 后 K8s 自动转为 Base64 存储
username: admin
password: "S3cr3t!@#"

Base64 不是加密

Secret 的 data 字段用 Base64 编码,但这不是加密: 任何人拿到 Secret 的 YAML 都能直接 echo YWRtaW4= | base64 -d 解出明文.
Base64 只是为了安全地传输二进制数据(如证书),不提供保密性.
Secret 的真正安全性依赖 RBAC 权限控制 + etcd 加密(后文详述).

使用方式

和 ConfigMap 完全一致--环境变量或文件挂载:

spec:
containers:
- name: app
image: my-app:v1
env:
- name: DB_USERNAME
valueFrom:
secretKeyRef: # 注意是 secretKeyRef,不是 configMapKeyRef
name: db-credentials
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password

# 或挂载为文件
volumeMounts:
- name: secret-volume
mountPath: /etc/secrets
readOnly: true # 建议只读
volumes:
- name: secret-volume
secret:
secretName: db-credentials
# 挂载后:/etc/secrets/username 和 /etc/secrets/password 各一个文件

Secret 类型

类型 用途 data 中的键
Opaque 通用(默认) 任意
kubernetes.io/tls TLS 证书 tls.crt + tls.key
kubernetes.io/dockerconfigjson 镜像拉取凭证 .dockerconfigjson
kubernetes.io/basic-auth HTTP Basic Auth username + password
kubernetes.io/ssh-auth SSH 私钥 ssh-privatekey

热更新机制与限制

ConfigMap / Secret 更新后,Pod 内看到的值什么时候变?

注入方式不同,热更新行为不同:

┌────────────────────┬────────────────────────────────────────────┐
│ 环境变量注入 │ 永不更新 │
│ │ Pod 启动时读取一次,之后不再同步 │
│ │ 要生效必须重启 Pod(rollout restart) │
├────────────────────┼────────────────────────────────────────────┤
│ Volume 挂载 │ 自动更新(延迟 30s~2min) │
│ (不用 subPath) │ kubelet 定期检查 → 发现变化 → 更新 symlink │
│ │ 应用需要自己 watch 文件变化或定期重读 │
├────────────────────┼────────────────────────────────────────────┤
│ Volume + subPath │ 永不更新 │
│ │ 绕过了 symlink 机制,和环境变量一样 │
└────────────────────┴────────────────────────────────────────────┘

文件更新了 ≠ 应用感知到了

kubelet 更新挂载文件后,应用进程不会收到任何信号.应用需要自己实现 watch 机制(如 fsnotify)或定期重读配置文件.
很多框架(如 Spring Cloud,Viper)内置了文件热加载能力,但默认可能未开启.如果应用不支持动态重载,最简单的方式还是 kubectl rollout restart deployment/xxx 触发 Pod 重建.

不可变 ConfigMap / Secret

对于确定不需要修改的配置(如版本信息,固定参数),可以标记为不可变:

apiVersion: v1
kind: ConfigMap
metadata:
name: app-version-info
data:
VERSION: "2.1.0"
BUILD_DATE: "2024-03-15"
immutable: true # ← 设置后无法再修改 data,只能删除重建

好处:kubelet 不再定期同步该 ConfigMap 的变化 → 减少 API Server 压力(大规模集群有意义).

etcd 静态加密

默认情况下,Secret 在 etcd 中是明文存储的(只做了 Base64 编码).任何能直接读 etcd 的人都能拿到所有密钥.

安全层级(由低到高):

1. 默认状态:
Secret 以 Base64 存在 etcd → 直接读 etcd 可解码

2. RBAC 控制:
限制谁能 kubectl get secret → 应用层面安全
但 etcd 磁盘被拿走仍然可以读出明文

3. etcd 静态加密(Encryption at Rest):
API Server 写入 etcd 前加密,读取时解密
→ 即使 etcd 磁盘泄露,数据也是密文

4. 外部密钥管理(KMS):
加密密钥本身也不存在本地 → 由外部 KMS(如 AWS KMS,Vault)管理
→ 最高安全级别
# API Server 的加密配置(/etc/kubernetes/encryption-config.yaml)
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets # 只加密 Secret(ConfigMap 通常不需要)
providers:
- aescbc: # 加密算法:AES-CBC
keys:
- name: key1
secret: base64编码的32字节密钥
- identity: {} # 兜底:未加密的旧数据仍可读

托管集群通常已开启

如果你使用云厂商的托管 K8s(GKE,EKS,TKE),etcd 加密通常已经默认开启,密钥由云平台的 KMS 管理, 你不需要手动配置.自建集群(kubeadm)则需要自己搞定.

Secret 安全最佳实践

实践 做法
RBAC 最小权限 只允许需要的 ServiceAccount 读取特定 Secret;禁止 list/watch 全部 Secret
不提交到 Git Secret YAML 不纳入版本库;使用 SealedSecrets 或 External Secrets Operator 加密后才提交
开启 etcd 加密 自建集群必须配置 Encryption at Rest
限制 Secret 挂载 Pod 的 ServiceAccount 只能访问明确绑定的 Secret;开启 BoundServiceAccountTokenVolume
定期轮换 数据库密码,API Key 定期更新;配合 External Secrets Operator 自动同步
审计日志 开启 K8s Audit Log,追踪谁读取了哪个 Secret

外部密钥管理:Vault 集成

K8s 原生 Secret 的根本问题:密钥存在 etcd 里,而 etcd 由 K8s 集群管理.集群被攻破,密钥全部泄露.
外部密钥管理的思路是:密钥不存在 K8s 内部,运行时从外部系统动态获取.

HashiCorp Vault 模式

传统方式(Secret 存在 etcd):
管理员 → kubectl create secret → etcd 存储
Pod 启动 → kubelet 从 etcd 读 Secret → 注入 Pod
问题:密钥在 etcd 中,集群被攻破 = 密钥泄露

Vault 方式(密钥在外部系统):
管理员 → Vault Web UI / CLI → 密钥存在 Vault 加密后端
Pod 启动 → Vault Agent Sidecar 向 Vault 认证 → 获取密钥 → 注入 Pod
优势:etcd 中不存在任何密钥;Vault 有独立的访问控制,审计,自动轮换

两种常见的集成方式:

方式 原理 适用
Vault Agent Sidecar 每个 Pod 注入一个 Vault Agent 容器,启动时向 Vault 认证并获取密钥写入共享卷 需要动态轮换,细粒度控制
External Secrets Operator 一个集群级 Controller,从 Vault / AWS Secrets Manager 等外部源同步到 K8s Secret 对象 想保持 K8s 原生用法(Pod 仍然读 Secret),但密钥源头在外部
# External Secrets Operator 示例:
# 在 K8s 中声明"从 Vault 的哪个路径取什么 key"
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
spec:
refreshInterval: 1h # 每小时同步一次
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: db-credentials # 生成的 K8s Secret 名称
data:
- secretKey: username # K8s Secret 中的 key
remoteRef:
key: secret/data/prod/db # Vault 中的路径
property: username # Vault 中的字段
- secretKey: password
remoteRef:
key: secret/data/prod/db
property: password

# 效果:Controller 自动创建并维护一个 K8s Secret "db-credentials"
# Pod 照常用 secretKeyRef 读取,完全不知道密钥来自 Vault

与远程配置平台的对比

企业实际生产中,配置管理通常不止 K8s ConfigMap 一种方案.Nacos,Apollo,七彩石(Rainbow)等远程配置中心也是常见选择.它们和 K8s ConfigMap/Secret 解决的问题有重叠但不完全相同.

能力对比

维度 K8s ConfigMap/Secret 远程配置平台(Nacos / Apollo / 七彩石)
定位 容器编排层面的配置注入 应用层面的配置管理中心
热更新 Volume 挂载延迟 30s~2min;应用需自行 watch 文件变化 实时推送(长轮询/WebSocket),SDK 内置监听回调,秒级生效
版本管理 无内置版本(需要 Git + GitOps 实现) 内置版本历史,对比,回滚
灰度发布 不支持(ConfigMap 更新立即影响所有引用的 Pod) 支持按 IP / 集群 / 环境 / 百分比灰度推送
权限控制 K8s RBAC(namespace 级别) 独立的权限体系(项目/环境/审批流)
可视化 kubectl 或 Dashboard 完整 Web UI(编辑,搜索,批量操作)
多环境 每个环境一个 namespace + 独立 ConfigMap 内置环境隔离(dev / staging / prod)
监听通知 无原生推送机制 SDK 提供 listener/watcher 回调
依赖 仅依赖 K8s 集群本身 需要额外部署和维护配置中心集群
适用范围 容器化应用 任何应用(容器/VM/物理机),不依赖 K8s

什么时候用哪个

选择决策树:

配置是部署时确定的,运行期间基本不变?
(如数据库地址,端口,镜像版本)

└── 是 → ConfigMap / Secret 足够
简单,零额外依赖,和 K8s 原生集成

配置需要运行时动态修改并实时生效?
(如功能开关,限流阈值,灰度规则,A/B 实验参数)

└── 是 → 远程配置平台更合适
实时推送 + 灰度 + 版本管理 + Web UI

两者结合的常见模式:
┌─────────────────────────────────────────────────┐
│ ConfigMap / Secret:基础设施配置 │
│ 数据库连接串,Redis 地址,TLS 证书, │
│ 配置中心自身的连接地址(bootstrap 配置) │
├─────────────────────────────────────────────────┤
│ Nacos / Apollo / 七彩石:业务配置 │
│ 功能开关,限流规则,灰度比例, │
│ 文案/话术,推荐策略参数 │
└─────────────────────────────────────────────────┘

→ 应用启动时从 ConfigMap 拿到配置中心的地址
→ 运行时从配置中心动态拉取业务配置

各平台特色

平台 特色 典型用法
Nacos 配置管理 + 服务发现一体化;长轮询实时推送;多数据中心 Spring Cloud 生态;Java 微服务
Apollo 强权限管理 + 审批流;namespace 隔离清晰;变更灰度发布 大型团队多环境管理;配置变更需审批
Rainbow 深度集成腾讯内部基础设施;支持配置模板 + 分组灰度推送 腾讯内部服务;结合 trpc 框架使用
etcd / Consul 轻量级 KV 存储 + watch 机制;无 Web UI(需自建) 基础设施级配置;服务发现

ConfigMap 和配置平台不是二选一

生产环境几乎都是两者并用:

  • ConfigMap 负责"让应用能启动起来"(连接地址,证书,环境标识),配置平台负责"让应用在运行时能灵活调整行为".
  • 前者是基础设施层面的配置注入,后者是应用层面的动态配置管理.

生产环境常见模式

GitOps 管理 ConfigMap

配置变更流程:
开发者修改 configmap.yaml → 提交 PR → 团队 Review → 合并

ArgoCD / FluxCD 检测到 Git 变更

自动 kubectl apply → ConfigMap 更新

触发 Deployment rollout restart(通过 annotation hash)

好处:
- 配置变更有审计记录(Git 历史)
- 可回滚(git revert)
- 多环境通过分支/目录隔离

利用 annotation 触发滚动更新

ConfigMap 变了但 Pod 不重启(因为 Deployment 的 template 没变).一个常用技巧:

apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
template:
metadata:
annotations:
# 把 ConfigMap 内容的 hash 写进 annotation
# ConfigMap 内容变 → hash 变 → template 变 → 触发滚动更新
checksum/config: "{{ sha256sum .Values.configData }}" # Helm 模板语法
spec:
containers:
- name: app
envFrom:
- configMapRef:
name: app-config

如果不用 Helm,也可以手动:

# 修改 ConfigMap 后,强制触发 Deployment 滚动更新
$ kubectl rollout restart deployment/app

小结

快速回顾

  • 配置外置原则:配置与镜像分离,同一镜像 + 不同配置 = 不同环境
  • ConfigMap:非敏感键值对/文件,注入方式为环境变量或 Volume 挂载
  • Secret:敏感数据(密码,证书),API 和 ConfigMap 相同;Base64 不是加密
  • 热更新:环境变量和 subPath 挂载不会热更新;Volume 挂载(无 subPath)延迟 30s~2min 自动更新,但应用需自行感知文件变化
  • 安全:RBAC 最小权限 + etcd 加密 + 不提交 Secret 到 Git + 定期轮换
  • 外部密钥管理:Vault / External Secrets Operator 实现密钥不存在 K8s 内部
  • 与配置平台的关系:ConfigMap 负责基础设施配置(启动时确定),Nacos/Apollo/七彩石负责业务配置(运行时动态调整),两者互补不互斥
  • 生产模式:GitOps 管理 ConfigMap 变更 + annotation hash 触发滚动更新

动手练习

  1. 创建一个 ConfigMap(含 LOG_LEVEL=debug 和一个 nginx.conf 文件),分别用 envFrom 和 volumeMount 注入到 Pod 中.exec 进 Pod 验证环境变量和文件内容.
  2. 修改 ConfigMap 的值(如 LOG_LEVEL=info),观察:使用 envFrom 的 Pod 是否变化?使用 Volume 挂载的 Pod 文件是否更新?(等 1-2 分钟后检查)
  3. 创建一个 Secret(用 stringData 写明文),然后 kubectl get secret xxx -o yaml 查看存储格式.尝试用 base64 -d 解码验证.
  4. 尝试使用 subPath 挂载 ConfigMap 中的单个文件,修改 ConfigMap 后验证文件是否更新(预期不会更新).
  5. 将 Deployment 的 annotation 加上 ConfigMap 的 hash,修改 ConfigMap 并重新 apply Deployment,观察是否触发滚动更新.
  6. (进阶)安装 External Secrets Operator,配置一个 SecretStore 指向本地 Vault(dev 模式),创建 ExternalSecret 并验证 K8s Secret 是否自动生成.