跳到主要内容

1 篇博文 含有标签「Secret」

Secret 敏感信息管理

查看所有标签

Kubernetes 全景解析 (3):存储体系与配置管理深度剖析

· 阅读需 31 分钟
Rainy
雨落无声,代码成诗 —— 致力于技术与艺术的极致平衡

前言

在 Kubernetes 的核心能力矩阵中,存储与配置管理是决定应用能否在生产环境稳定运行的关键支柱。容器天生是"用完即弃"的——Pod 被删除后,其文件系统上的所有数据都会随之消失。然而,数据库需要持久化数据文件,日志系统需要保留历史记录,微服务需要在不重新构建镜像的前提下修改配置参数。这些需求共同构成了 K8s 存储体系与配置管理的设计动机。

本文将从存储体系的分层架构出发,逐层深入 Volume、PV/PVC、StorageClass 的设计原理与使用方法,再到 ConfigMap 与 Secret 的配置外部化实践,帮助你建立一套完整的 K8s 存储与配置认知框架。


一、K8s 存储体系概览

1.1 为什么容器需要持久化存储

容器的文件系统由一层层只读的镜像层(Image Layer)和一个可写的容器层(Container Layer)组成。当容器被删除时,可写层也会被一并回收。这意味着:

  • 数据库数据会丢失:MySQL、PostgreSQL 等有状态服务将无法正常工作
  • 日志会丢失:应用产生的日志随容器消亡而消失
  • 文件上传会丢失:用户上传的附件、图片等临时存储在容器内会不可恢复

为了解决这些问题,Kubernetes 引入了 Volume 机制,将外部存储挂载到容器内部的指定路径,使数据独立于容器的生命周期而存在。

1.2 存储体系的分层设计

Kubernetes 的存储体系采用了经典的分层抽象设计,从底层到上层依次为:

层级概念作用域核心职责
L1VolumePod定义 Pod 内部存储的挂载方式
L2PV / PVC集群 / 命名空间解耦存储的供给与消费
L3StorageClass集群实现存储的动态供给与分类

这种分层设计的核心思想是关注点分离:开发人员只需声明"我需要多少存储"(PVC),运维人员负责"提供什么样的存储"(PV/StorageClass),两者通过标准的接口进行匹配。

设计哲学

K8s 存储体系的设计遵循"供需分离"原则。就像云计算中的 IaaS 层将计算资源虚拟化一样,K8s 将存储资源抽象为标准化的 API 对象,使上层应用无需关心底层存储的具体实现(NFS、Ceph、AWS EBS、GCE PD 等)。


二、Volume:Pod 级存储抽象

2.1 Volume 的基本概念

Volume 是 Kubernetes 中最基础的存储抽象,它定义在 Pod 规格中,与 Pod 的生命周期紧密绑定。当 Pod 被创建时,Kubelet 会负责将 Volume 挂载到容器内指定的路径;当 Pod 被删除时,Volume 的处理方式取决于其类型——临时 Volume 会被清除,而持久化 Volume 中的数据则会被保留。

Volume 的几个关键特性:

  • Volume 定义在 Pod 级别:同一个 Pod 中的多个容器可以共享同一个 Volume
  • Volume 的生命周期与 Pod 绑定:Pod 不存在时,Volume 也不存在(临时类型)
  • Volume 支持多种后端:K8s 内置了数十种 Volume 类型,同时通过 CSI(Container Storage Interface)支持第三方存储

2.2 常见 Volume 类型

Volume 类型持久性适用场景说明
emptyDir临时Pod 内容器间共享数据、缓存Pod 删除后数据丢失,可选用内存作为介质
hostPath节点级开发调试、访问节点文件直接挂载宿主机路径,存在安全风险
nfs持久多 Pod 共享文件存储需要外部 NFS 服务器
configMap配置注入配置数据以文件或环境变量形式挂载
secret敏感配置注入密钥、证书Base64 编码存储,支持加密
persistentVolumeClaim持久有状态应用的数据持久化引用 PV,生命周期独立于 Pod
csi取决于后端接入第三方存储通过 CSI 驱动对接各种存储系统

2.3 Volume 生命周期与 Pod 的绑定关系

# Volume 生命周期与 Pod 的关系
# 1. Pod 被调度到某个 Node
# 2. Kubelet 调用 Volume 插件挂载存储到宿主机
# 3. Kubelet 将宿主机上的挂载点绑定到容器内
# 4. Pod 运行期间,容器读写 Volume 中的数据
# 5. Pod 被删除时,Kubelet 执行卸载操作
# 6. 临时 Volume 数据被清除,持久化 Volume 数据被保留
注意

emptyDir 的生命周期严格绑定到 Pod。即使 Pod 被驱逐(Eviction)或节点维护导致 Pod 被重新调度到其他节点,原节点上的 emptyDir 数据也会丢失。

2.4 完整 YAML 示例

以下示例展示了如何在 Pod 中使用多种 Volume:

apiVersion: v1
kind: Pod
metadata:
name: volume-demo
labels:
app: volume-demo
spec:
containers:
# 容器 1:写入数据到共享 Volume
- name: writer
image: busybox:1.36
command: ["/bin/sh", "-c"]
args:
- |
while true; do
date >> /data/log.txt
echo "Writer: data written" >> /data/log.txt
sleep 5
done
volumeMounts:
- name: shared-data
mountPath: /data

# 容器 2:从共享 Volume 读取数据
- name: reader
image: busybox:1.36
command: ["/bin/sh", "-c"]
args: ["tail -f /data/log.txt"]
volumeMounts:
- name: shared-data
mountPath: /data

# 容器 3:使用 hostPath 访问宿主机时间
- name: time-checker
image: busybox:1.36
command: ["/bin/sh", "-c"]
args: ["while true; do cat /host-time/hostname; sleep 10; done"]
volumeMounts:
- name: host-time
mountPath: /host-time

volumes:
# emptyDir:Pod 内容器间共享的临时存储
- name: shared-data
emptyDir:
sizeLimit: 256Mi # 可选:限制存储大小

# hostPath:挂载宿主机目录(仅用于开发/调试)
- name: host-time
hostPath:
path: /etc
type: Directory # Directory/DirectoryOrCreate/File/FileOrCreate/Socket/CharDevice/BlockDevice

三、PV 与 PVC:持久化存储的供需模型

3.1 PV(PersistentVolume):集群级存储资源

PersistentVolume 是集群级别的一块存储资源,由集群管理员创建,或者由 StorageClass 动态供给。PV 独立于 Pod 的生命周期存在,即使使用它的 Pod 被删除,PV 中的数据仍然保留。

PV 的核心属性包括:

  • 容量(Capacity):存储大小,如 10Gi
  • 访问模式(Access Modes):定义如何访问存储
  • 回收策略(Reclaim Policy):定义 PVC 释放后 PV 的处理方式
  • 存储类(StorageClass):定义存储的类型和参数
  • 挂载选项(Mount Options):传递给挂载命令的额外参数

3.2 PVC(PersistentVolumeClaim):命名空间级存储请求

PersistentVolumeClaim 是用户对存储资源的"申请单"。PVC 在命名空间内创建,用户通过 PVC 声明所需的存储大小、访问模式等需求,K8s 控制平面会自动将满足条件的 PV 绑定到该 PVC。

PV 与 PVC 的关系

可以将 PV 理解为"机房里的一块硬盘",PVC 理解为"申请一块硬盘的工单"。管理员提前准备硬盘(静态供给),或者设置自动采购流程(动态供给),用户只需提交工单即可。

3.3 供给模式:静态供给 vs 动态供给

静态供给(Static Provisioning)

管理员预先创建 PV,用户创建 PVC 后,K8s 控制平面根据容量和访问模式进行匹配绑定。这种方式适合存储资源有限、需要精确控制的场景。

动态供给(Dynamic Provisioning)

管理员预先创建 StorageClass,用户在 PVC 中指定 StorageClass,K8s 自动调用对应的 Provisioner 创建 PV 并绑定到 PVC。这种方式是生产环境的主流选择,极大简化了存储管理。

3.4 访问模式

访问模式缩写说明
ReadWriteOnceRWO存储可被单个节点以读写模式挂载
ReadOnlyManyROX存储可被多个节点以只读模式挂载
ReadWriteManyRWX存储可被多个节点以读写模式挂载
ReadWriteOncePodRWOP存储可被单个 Pod 以读写模式挂载(K8s 1.27+ 稳定)
访问模式与存储后端

并非所有存储后端都支持所有访问模式。例如,AWS EBS、GCE PD、Azure Disk 等块存储通常只支持 RWO;而 NFS、CephFS、GlusterFS 等文件存储支持 RWX。在选择存储后端时,务必确认其支持的访问模式是否满足你的需求。

3.5 回收策略

回收策略说明适用场景
RetainPVC 释放后,PV 被保留但标记为 Released,数据不丢失需要手动处理数据的场景
DeletePVC 释放后,PV 和底层存储资源被自动删除动态供给的云盘等临时存储
Recycle执行 rm -rf 清空数据后重新可用已废弃,不建议使用
Recycle 回收策略已废弃

官方明确说明:The Recycle reclaim policy is deprecated. Instead, the recommended approach is to use dynamic provisioning.

Recycle 策略仅对支持该操作的底层卷插件有效,执行基本的 rm -rf /thevolume/* 清理操作。在生产环境中,推荐使用以下替代方案:

  • 动态供给(Dynamic Provisioning):通过 StorageClass 自动创建和管理 PV,设置 reclaimPolicy: DeletereclaimPolicy: Retain
  • Retain + 手动管理:设置 reclaimPolicy: Retain,由管理员手动清理和复用 PV

3.6 PV/PVC 绑定流程

3.7 Storage Object in Use Protection

Storage Object in Use Protection(存储对象使用中保护)机制确保正在被 Pod 使用的 PVC 和已绑定到 PVC 的 PV 不会被意外删除,从而防止数据丢失。

PVC 保护机制

当 PVC 正在被 Pod 使用时,如果用户删除 PVC,PVC 不会立即被删除。PVC 的删除会被推迟,直到它不再被任何 Pod 使用。

PV 保护机制

当管理员删除已绑定到 PVC 的 PV 时,PV 不会立即被删除。PV 的删除会被推迟,直到它不再绑定到 PVC。

查看保护状态

# 查看 PVC 的保护状态
kubectl describe pvc hostpath

输出示例:

Name: hostpath
Namespace: default
StorageClass: example-hostpath
Status: Terminating
Finalizers: [kubernetes.io/pvc-protection] # PVC 保护 finalizer
# 查看 PV 的保护状态
kubectl describe pv task-pv-volume

输出示例:

Name: task-pv-volume
Labels: type=local
Finalizers: [kubernetes.io/pv-protection] # PV 保护 finalizer
StorageClass: standard
Status: Terminating
Claim: default/hostpath
Reclaim Policy: Delete
Access Modes: RWO
Capacity: 1Gi
Finalizer 的作用

Finalizer 是 Kubernetes 中的一种机制,用于确保资源在被删除前完成必要的清理操作。kubernetes.io/pvc-protectionkubernetes.io/pv-protection 这两个 finalizer 确保存储资源在使用中时不会被意外删除。

3.8 PV 删除保护 Finalizer(v1.33 Stable)

FEATURE STATE: Kubernetes v1.33 [stable](默认启用)

除了基础的 Storage Object in Use Protection,Kubernetes v1.33 引入了更强大的 PV 删除保护 finalizer 机制,确保具有 Delete 回收策略的 PV 仅在底层存储被删除后才从 Kubernetes 中移除。

两种 Finalizer

Finalizer 名称引入版本适用范围说明
external-provisioner.volume.kubernetes.io/finalizerv1.31CSI 卷(动态和静态供给)确保底层存储删除后才移除 PV 对象
kubernetes.io/pv-controllerv1.31In-tree 插件卷(仅动态供给)确保底层存储删除后才移除 PV 对象

CSI 卷示例

kubectl describe pv pvc-2f0bab97-85a8-4552-8044-eb8be45cf48d

输出示例:

Name: pvc-2f0bab97-85a8-4552-8044-eb8be45cf48d
Annotations: pv.kubernetes.io/provisioned-by: csi.vsphere.vmware.com
Finalizers: [kubernetes.io/pv-protection external-provisioner.volume.kubernetes.io/finalizer]
StorageClass: fast
Status: Bound
Claim: demo-app/nginx-logs
Reclaim Policy: Delete
Access Modes: RWO
VolumeMode: Filesystem
Capacity: 200Mi
Source:
Type: CSI
Driver: csi.vsphere.vmware.com

In-tree 插件卷示例

kubectl describe pv pvc-74a498d6-3929-47e8-8c02-078c1ece4d78

输出示例:

Name: pvc-74a498d6-3929-47e8-8c02-078c1ece4d78
Annotations: kubernetes.io/provisioned-by: kubernetes.io/vsphere-volume
Finalizers: [kubernetes.io/pv-protection kubernetes.io/pv-controller]
StorageClass: vcp-sc
Status: Bound
Claim: default/vcp-pvc-1
Reclaim Policy: Delete
Access Modes: RWO
VolumeMode: Filesystem
Capacity: 1Gi
Source:
Type: vSphereVolume
Finalizer 的作用

这些 finalizer 确保:

  1. PV 对象仅在底层存储被删除后才从 Kubernetes 中移除
  2. 无论 PV 和 PVC 的删除顺序如何,都能确保底层存储被正确删除
  3. 当启用 CSIMigration{provider} 特性门控时,kubernetes.io/pv-controller 会被 external-provisioner.volume.kubernetes.io/finalizer 替代

3.9 PVC 扩容(v1.24 Stable)

FEATURE STATE: Kubernetes v1.24 [stable](默认启用)

PVC 扩容功能允许用户在 PVC 创建后增加其存储容量,而无需重新创建 PVC。

支持扩容的卷类型

  • CSI 卷(包括部分迁移的卷类型)
  • flexVolume(已废弃)
  • portworxVolume(已废弃)

前提条件

PVC 的 StorageClass 必须设置 allowVolumeExpansion: true

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: example-vol-default
provisioner: vendor-name.example/magicstorage
parameters:
resturl: "http://192.168.10.100:8080"
allowVolumeExpansion: true # 允许扩容

扩容步骤

  1. 编辑 PVC 对象,增加 spec.resources.requests.storage 的值
  2. Kubernetes 控制平面触发底层卷的扩容操作
  3. 不会创建新的 PV,而是扩展现有卷
直接编辑 PV 容量

不要直接编辑 PV 的容量。如果先编辑 PV 的容量,再编辑 PVC 使其与 PV 匹配,Kubernetes 控制平面会认为卷已被手动扩容,不会触发自动扩容操作。

3.10 CSI 卷扩容(v1.24 Stable)

FEATURE STATE: Kubernetes v1.24 [stable](默认启用)

CSI 卷扩容功能默认启用,但需要特定的 CSI 驱动程序支持卷扩容。请参考具体 CSI 驱动程序的文档以获取更多信息。

3.11 VolumeAttributesClasses(VAC)

VolumeAttributesClasses(VAC)是 Kubernetes 中用于管理存储属性的新 API,允许在不重新创建卷的情况下修改存储的某些属性(如性能级别、IOPS、吞吐量等)。

VAC 的作用

  • 修改存储卷的性能参数(IOPS、吞吐量)
  • 切换存储的备份策略
  • 调整存储的加密级别
  • 其他由存储后端支持的属性

VAC 示例

apiVersion: storage.k8s.io/v1alpha1
kind: VolumeAttributesClass
metadata:
name: fast-ssd
driverName: csi.example.com
parameters:
iops: "5000"
throughput: "500"
encryption: "true"
VAC 的使用

VAC 目前仍处于 Alpha 阶段,具体支持程度取决于 CSI 驱动程序的实现。在生产环境使用前,请确认您的存储后端是否支持 VAC。

3.12 完整 YAML 示例

# ============================================
# 1. 静态供给:先创建 PV,再创建 PVC
# ============================================

# PersistentVolume:集群级存储资源
apiVersion: v1
kind: PersistentVolume
metadata:
name: mysql-pv
spec:
capacity:
storage: 20Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: manual
nfs:
server: 10.0.0.100
path: /data/mysql
# 可选:挂载选项
mountOptions:
- hard
- nfsvers=4.1

---
# PersistentVolumeClaim:用户存储请求
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pvc
namespace: database
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
storageClassName: manual # 必须与 PV 的 storageClassName 一致

---
# 使用 PVC 的 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql
namespace: database
spec:
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: root-password
ports:
- containerPort: 3306
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
volumes:
- name: mysql-data
persistentVolumeClaim:
claimName: mysql-pvc

四、StorageClass:存储的"类"与动态供给

4.1 StorageClass 的作用与参数

StorageClass 为管理员提供了一种描述存储"类别"的方式。不同的 StorageClass 可以映射到不同的存储后端、不同的性能等级、不同的备份策略等。用户在创建 PVC 时只需指定 StorageClass,K8s 就会自动完成存储的创建和绑定。

StorageClass 的核心参数:

参数说明示例
provisioner存储供给插件kubernetes.io/aws-ebs
parameters供给插件的具体参数type: gp3, iopsPerGB: "50"
reclaimPolicy默认回收策略Delete
volumeBindingMode卷绑定模式WaitForFirstConsumer
allowVolumeExpansion是否允许扩容true

4.2 动态供给工作流

动态供给的核心流程如下:

  1. 用户创建 PVC,指定 storageClassName
  2. PVC Controller 发现没有现成的 PV 可匹配
  3. PVC Controller 调用对应 StorageClass 的 Provisioner
  4. Provisioner 调用底层存储 API(如 AWS CreateVolume)创建实际的存储卷
  5. Provisioner 创建 PV 对象并关联到该 PVC
  6. PVC 状态变为 Bound,Pod 可以使用该 PVC
WaitForFirstConsumer

对于拓扑感知的存储(如 AWS EBS、GCE PD),建议设置 volumeBindingMode: WaitForFirstConsumer。这样 PV 不会在 PVC 创建时立即分配,而是等到第一个使用该 PVC 的 Pod 被调度后,再在与 Pod 相同的可用区(AZ)创建存储卷,避免跨可用区挂载的问题。

4.3 常见 Provisioner

Provisioner存储类型支持的访问模式说明
kubernetes.io/aws-ebsAWS EBSRWOAWS 弹性块存储
kubernetes.io/gce-pdGCE Persistent DiskRWO/RWXGoogle Cloud 持久磁盘
kubernetes.io/azure-diskAzure DiskRWOAzure 磁盘
kubernetes.io/azure-fileAzure FileROX/RWXAzure 文件存储
nfs.csi.k8s.ioNFSROX/RWXNFS CSI 驱动
cephfs.csi.ceph.comCephFSROX/RWXCeph 文件系统
rbd.csi.ceph.comCeph RBDRWOCeph 块存储
local-path-provisioner本地路径RWO轻量级本地存储,适合开发测试

4.4 完整 YAML 示例

# ============================================
# StorageClass:定义存储类别
# ============================================

# 高性能 SSD 存储类(AWS EBS gp3)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: kubernetes.io/aws-ebs
parameters:
type: gp3
iopsPerGB: "50"
throughput: "250"
fsType: ext4
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true

---
# 标准 HDD 存储类(AWS EBS st1)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: standard-hdd
provisioner: kubernetes.io/aws-ebs
parameters:
type: st1
fsType: ext4
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: false

---
# 使用动态供给的 PVC(无需预先创建 PV)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data-pvc
namespace: production
spec:
accessModes:
- ReadWriteOnce
storageClassName: fast-ssd # 指定 StorageClass,触发动态供给
resources:
requests:
storage: 50Gi

---
# 本地路径 Provisioner(适合开发/测试环境)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-path
provisioner: rancher.io/local-path
parameters:
archiveOnDelete: "true"
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer

五、ConfigMap:配置外部化

5.1 ConfigMap 的创建方式

ConfigMap 用于将非敏感的配置数据从容器镜像中解耦出来,使配置可以在不重新构建镜像的情况下被修改和更新。ConfigMap 支持以下几种创建方式:

方式一:字面量创建

kubectl create configmap app-config \
--from-literal=LOG_LEVEL=info \
--from-literal=MAX_CONNECTIONS=100 \
--from-literal=FEATURE_FLAGS=dark-mode,experimental-api

方式二:从文件创建

kubectl create configmap nginx-config \
--from-file=nginx.conf=./configs/nginx.conf \
--from-file=default.conf=./configs/default.conf

方式三:从目录创建

# 将目录下所有文件作为 ConfigMap 的键值对
kubectl create configmap app-settings \
--from-file=./configs/

方式四:从 env-file 创建

kubectl create configmap app-env \
--from-env-file=./.env

5.2 在 Pod 中使用 ConfigMap

ConfigMap 可以通过三种方式注入到 Pod 中:

注入方式适用场景热更新说明
环境变量简单配置项不支持Pod 重启后生效
Volume 挂载配置文件支持文件内容实时更新
命令行参数启动参数不支持通过 $(ENV_VAR) 引用

5.3 ConfigMap 热更新机制与限制

当通过 Volume 挂载方式使用 ConfigMap 时,K8s 的 kubelet 会定期(默认每 60 秒)检查 ConfigMap 的更新,并将最新的内容同步到挂载的文件中。应用可以通过监听文件变化来重新加载配置。

热更新的限制
  1. 通过环境变量注入的 ConfigMap 不支持热更新,必须重启 Pod 才能生效
  2. 已挂载的文件内容会延迟更新,更新间隔由 kubelet 的 sync period 决定(默认 60 秒)
  3. 应用需要自行实现配置重载逻辑,K8s 只负责更新文件内容,不会自动通知应用
  4. subPath 挂载不支持热更新,使用 subPath 时,ConfigMap 更新不会反映到容器中

5.4 完整 YAML 示例

# ============================================
# ConfigMap:配置外部化
# ============================================

# 创建 ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: production
data:
# 简单键值对
LOG_LEVEL: "info"
MAX_CONNECTIONS: "100"
CACHE_TTL: "300"

# 完整配置文件
application.yml: |
server:
port: 8080
shutdown: graceful
spring:
datasource:
url: jdbc:postgresql://db:5432/mydb
hikari:
maximum-pool-size: 20
logging:
level:
root: INFO
com.example: DEBUG

# Redis 配置
redis.conf: |
bind 0.0.0.0
port 6379
maxmemory 256mb
maxmemory-policy allkeys-lru
appendonly yes

---
# Pod 中使用 ConfigMap(三种方式)
apiVersion: v1
kind: Pod
metadata:
name: configmap-demo
namespace: production
spec:
containers:
- name: app
image: my-app:latest
ports:
- containerPort: 8080

# 方式 1:作为环境变量
env:
- name: APP_LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: LOG_LEVEL
- name: APP_MAX_CONNECTIONS
valueFrom:
configMapKeyRef:
name: app-config
key: MAX_CONNECTIONS

# 方式 1 补充:一次性导入所有键为环境变量
envFrom:
- configMapRef:
name: app-config
# 可选:前缀
prefix: APP_

# 方式 2:作为 Volume 挂载(支持热更新)
volumeMounts:
- name: config-vol
mountPath: /etc/app/config
readOnly: true
- name: redis-config
mountPath: /etc/redis/redis.conf
subPath: redis.conf # 注意:subPath 不支持热更新

# 方式 3:作为命令行参数
args: ["--log-level", "$(APP_LOG_LEVEL)"]

volumes:
- name: config-vol
configMap:
name: app-config
# 可选:指定要挂载的键
items:
- key: application.yml
path: application.yml
- key: redis.conf
path: redis.conf
# 可选:设置文件权限
defaultMode: 0644
- name: redis-config
configMap:
name: app-config

六、Secret:敏感信息管理

6.1 Secret 的类型

Secret 与 ConfigMap 类似,但专门用于存储敏感信息。Secret 中的数据以 Base64 编码存储(并非加密),在生产环境中应结合加密机制使用。

Secret 类型用途说明
Opaque通用类型默认类型,存储任意键值对
kubernetes.io/dockerconfigjsonDocker 镜像仓库认证用于拉取私有镜像
kubernetes.io/tlsTLS 证书存储 TLS 证书和私钥
kubernetes.io/basic-auth基本认证存储用户名和密码
kubernetes.io/ssh-authSSH 认证存储 SSH 私钥
kubernetes.io/service-account-tokenServiceAccount Token自动为 SA 生成 Token

6.2 与 ConfigMap 的区别

对比维度ConfigMapSecret
数据编码明文存储Base64 编码
大小限制1 MiB1 MiB
etcd 加密通常不加密可配置 EncryptionConfiguration 加密
访问控制标准 RBAC更严格的 RBAC 审计
使用场景应用配置、环境变量密码、密钥、证书、Token
安全警告

Secret 的 Base64 编码不是加密,任何人只要有访问 etcd 的权限就能解码。在生产环境中,务必启用 etcd 静态加密(Encryption at Rest),并严格限制 Secret 的访问权限。

6.3 生产环境 Secret 加密架构与最佳实践

由于 K8s 的 Base64 编码毫无机密性可言(类似于把中文字符翻译成拼音,任何人都能还原),一旦黑客获取了 etcd 数据库的备份文件、或者通过提权直接读取了物理机的磁盘,所有的密码与证书都将彻底暴露。

因此,在生产环境中,必须结合真正的加密机制来保护 Secret 数据。目前主流的三种架构方案如下:

方案一:etcd 静态加密与 KMS (Key Management Service) 插件

Kubernetes 原生支持在存储层进行加密(Encryption at Rest),即 kube-apiserver 在将 Secret 数据写入 etcd 前先将其加密,在客户端读取时再进行解密。

最基础的静态加密是利用本地密钥文件:

# /etc/kubernetes/pki/secrets-encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-32-byte-key> # 本地对称加密密钥
- identity: {} # 兜底:未加密的 Secret 仍可读取

进阶:结合 KMS 插件(生产强烈推荐) 将主密钥放在 apiserver 的本地配置文件中仍有泄漏风险,因此大型生产环境通常会接入云厂商的 KMS(如 AWS KMS、阿里云 KMS)HashiCorp Vault。 在这种架构下,K8s 采用信封加密(Envelope Encryption)

  1. kube-apiserver 随机生成一个 DEK(数据加密密钥)把 Secret 加密。
  2. API Server 通过 Unix Socket 调用 KMS 插件,KMS 插件将 DEK 发给外部密钥管理系统。
  3. 外部 KMS 使用它的 KEK(密钥加密密钥)把 DEK 加密后返回给 K8s。
  4. K8s 将加密后的 DEK加密后的 Secret一并存入 etcd,实现了彻头彻尾的解耦和高安全性。

方案二:GitOps 工作流与 Sealed Secrets

如果在团队中实施 GitOps(如搭配 ArgoCD / FluxCD),所有的 K8s YAML 配置都要提交到 Git 仓库进行版本管理。但如果你将带有 Base64 编码的 Secret YAML 直推到 Github,无异于公开密码。

Bitnami Sealed Secrets 完美解决了这个问题:

  1. 集群中部署一个 Sealed Secrets Controller,它会在集群内部生成一对非对称的公私钥,并严格保护私钥。
  2. 开发者在本地使用 kubeseal 命令行工具,结合前面获取的公钥,将原本的 Secret 加密为一个名为 SealedSecret 的自定义资源(CRD)。
  3. 无论谁拿到这个 SealedSecret 的密文内容都无法解密,因此可以放心地将其公开 Push 到 Git 仓库。
  4. ArgoCD 将其同步进 K8s 集群后,处于集群内的 Controller 识别到该对象,利用极密私钥将其解密,并在集群内存中隐式还原为一个标准的 Kubernetes Secret 对象,供 Pod 挂载消费。

方案三:密码外置化与 External Secrets Operator (ESO)

对于多微服务多集群的企业级架构,将机密信息散落在各个 K8s 集群中维护是非常痛苦的。更现代的做法是引入外置机密治理中心

工作原理

  1. 真正的数据库密码、API Key 等完全不用 kubectl create secret 来建,而是由安全团队统一录入类似 AWS Secrets Manager, Google Secret ManagerHashiCorp Vault 这种专门的极高安全等级保险箱内。
  2. 随后在 K8s 集群部署 External Secrets Operator
  3. 运维人员在 K8s 声明一个 ExternalSecret 对象,指明要去远程保险箱获取哪一个 Key。
  4. 部署的控制器拿着具备精细权限配比的凭证去远程保险箱调接口,读取到数据以后,动态地在 K8s 中生成一个原生 Secret 给应用使用。并在云端密码发生变更时自动回滚和同步(甚至触发目标 Pod 重启)。

这实现了机密数据的"唯一真实数据源 (Single Source of Truth)"理念。

方案四:严格的 RBAC 控制(防线兜底)

不论采用何种加密架构,K8s 内部的 kube-apiserver 依然会自动向有权限的请求方分发解密好的数据。因此,防备越权读取的最后一公里,永远是最小权限原则(Principle of Least Privilege)

# 仅允许特定 ServiceAccount 读取某个指定的 Secret
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: specific-secret-reader
namespace: production
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["db-credentials", "api-keys"] # 切忌使用 * 或者留空
verbs: ["get"]

方案五:实操演练(直接窥探 etcd 证明 Base64 脆弱性)

为了更直观地理解为什么必须搭配 KMS/Sealed Secrets 等加密机制,你可以通过 etcdctl 直接查看集群底层的原生数据:

1. 创建一个普通的 Secret

kubectl create secret generic my-secret \
--from-literal=password=super_secret_pwd

2. 登录到 master 节点上,使用 etcdctl 直接读取数据:

ETCDCTL_API=3 etcdctl \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
get /registry/secrets/default/my-secret

3. 如果未开启静态加密,你会直接看到明文 base64 payload:

/registry/secrets/default/my-secret
... "password": "c3VwZXJfc2VjcmV0X3B3ZA==" ...

任何人只要拿到这串字符,便可用 echo "c3Vw...==" | base64 -d 变为 super_secret_pwd

4. 如果配置了 etcd 静态加密(aescbc),底层返回的将是彻底的密文乱码:

/registry/secrets/default/my-secret
k8s:enc:aescbc:v1:key1:Q@c*a`X&_...

6.4 完整 YAML 示例

# ============================================
# Secret:敏感信息管理
# ============================================

# 1. Opaque Secret:通用密钥
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: production
type: Opaque
data:
# 值必须为 Base64 编码
# echo -n 'my-password' | base64
username: bXktdXNlcm5hbWU= # my-username
password: bXktcGFzc3dvcmQ= # my-password
connection-string: amRiYzpwb3N0Z3Jlc3FsOi8vZGItaG9zdDo1NDMyL215ZGI= # jdbc:postgresql://db-host:5432/mydb
---
# 2. Docker Registry Secret:镜像仓库认证
apiVersion: v1
kind: Secret
metadata:
name: docker-registry-secret
namespace: production
type: kubernetes.io/dockerconfigjson
data:
# 通过命令创建:kubectl create secret docker-registry ...
.dockerconfigjson: eyJhdXRocyI6eyJodHRwczovL2luZGV4LmRvY2tlci5pby92MS8iOnsidXNlcm5hbWUiOiJ1c2VyIiwicGFzc3dvcmQiOiJwYXNzIiwiYXV0aCI6ImRYTmxjanB3WVhOeiJ9fX0=
---
# 3. TLS Secret:HTTPS 证书
apiVersion: v1
kind: Secret
metadata:
name: tls-secret
namespace: production
type: kubernetes.io/tls
data:
tls.crt: <base64-encoded-cert>
tls.key: <base64-encoded-key>
---
# 4. 在 Pod 中使用 Secret
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
spec:
# 拉取私有镜像
imagePullSecrets:
- name: docker-registry-secret

containers:
- name: api
image: my-registry.com/api-server:v2.1.0
ports:
- containerPort: 8080

# 方式 1:作为环境变量
env:
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-credentials
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password

# 方式 2:作为 Volume 挂载
volumeMounts:
- name: db-secret-vol
mountPath: /etc/secrets/db
readOnly: true
- name: tls-vol
mountPath: /etc/tls
readOnly: true

volumes:
- name: db-secret-vol
secret:
secretName: db-credentials
defaultMode: 0400 # 仅所有者可读
- name: tls-vol
secret:
secretName: tls-secret
defaultMode: 0400

七、存储选型决策指南

7.1 存储类型对比

存储类型持久性性能共享能力适用场景推荐度
emptyDir临时高(内存)/ 中(磁盘)Pod 内缓存、临时文件、容器间数据共享
hostPath节点级同节点 Pod开发调试、DaemonSet 日志收集
PV/PVC + NFS持久多节点共享文件存储、内容管理系统
PV/PVC + 云盘持久单节点数据库、高性能单实例应用
PV/PVC + Ceph持久多节点企业级存储、大规模集群
CSI 第三方存储取决于后端取决于后端取决于后端特殊存储需求(对象存储、SAN 等)
ConfigMap配置--应用配置、环境变量、配置文件
Secret敏感配置--密码、密钥、证书、Token

7.2 存储选型决策树

生产环境推荐方案
  • 有状态服务(数据库、消息队列):使用云盘 SSD + StorageClass 动态供给,设置 volumeBindingMode: WaitForFirstConsumer
  • 共享文件(上传文件、静态资源):使用 NFS / CephFS / 对象存储(通过 CSI)
  • 应用配置:ConfigMap + Volume 挂载,配合 Reloader 等工具实现自动滚动更新
  • 敏感信息:Secret + etcd 加密 + External Secrets Operator(对接 Vault 或云厂商密钥管理服务)
  • 日志与监控数据:使用 emptyDir + sidecar 模式收集,或直接写入外部存储

八、本章小结

本文深入剖析了 Kubernetes 存储体系与配置管理的核心机制,让我们回顾一下关键要点:

存储体系的三层架构

  • Volume 是最基础的存储抽象,定义在 Pod 级别,生命周期与 Pod 绑定
  • PV/PVC 实现了存储的供需分离,PV 是集群级资源,PVC 是命名空间级请求
  • StorageClass 实现了存储的动态供给,用户无需预先创建 PV

存储保护与安全机制

  • Storage Object in Use Protection:通过 kubernetes.io/pvc-protectionkubernetes.io/pv-protection finalizer 保护正在使用的存储资源
  • PV 删除保护 Finalizer(v1.33 Stable):external-provisioner.volume.kubernetes.io/finalizerkubernetes.io/pv-controller 确保底层存储被正确删除后才移除 PV 对象
  • Recycle 回收策略已废弃:推荐使用动态供给替代

存储扩容与属性管理

  • PVC 扩容(v1.24 Stable):需要 StorageClass 设置 allowVolumeExpansion: true
  • CSI 卷扩容(v1.24 Stable):需要 CSI 驱动程序支持
  • VolumeAttributesClasses(VAC):新 API,允许在不重建卷的情况下修改存储属性

配置管理的两大工具

  • ConfigMap 用于管理非敏感配置,支持环境变量注入和 Volume 挂载(支持热更新)
  • Secret 用于管理敏感信息,生产环境必须配合 etcd 加密和严格 RBAC 使用

选型的核心原则

  • 根据数据的持久性需求选择临时存储或持久化存储
  • 根据数据的共享需求选择块存储(RWO)或文件存储(RWX)
  • 根据数据的敏感性选择 ConfigMap 或 Secret
  • 优先使用 StorageClass 动态供给,减少运维负担

系列导航

章节主题状态
0架构设计与核心概念✅ 已发布
1工作负载与 Pod 生命周期深度解析✅ 已发布
2网络模型与服务发现全链路解析✅ 已发布
3存储体系与配置管理深度剖析✅ 已发布
4调度器、资源管理与弹性伸缩✅ 已发布
5安全体系与可观测性全景✅ 已发布
6生产级微服务架构实战✅ 已发布
7有状态应用与 Operator 模式实战✅ 已发布

相关阅读