容器一删数据就没了?K8s 存储卷与配置管理实战:emptyDir/hostPath/NFS/PV/PVC + ConfigMap/Secret 一篇通关
导读 :容器里的数据默认写在容器的可写层,Pod 一旦被删除,数据就跟着"人间蒸发"。本文从最基础的
emptyDir临时卷讲起,一路实操hostPath、NFS、PV/PVC持久化存储,再到配置管理双件套ConfigMap与Secret,全部配有可直接复制的 YAML 和命令,跟着敲一遍就能彻底搞懂 K8s 的数据持久化与配置注入。实验环境 :1 个 master 节点(master30,
10.1.8.30)+ 2 个 worker 节点(worker31/worker32),容器运行时为 containerd,集群内部私有镜像仓库为hub.laoma.cloud。
回顾
昨天内容
- Namespace and Contexts(命名空间与上下文)
- Kubernetes Pod(Pod 基础管理)
练习
实验题 1:Namespace and Contexts 综合实验
- 使用 yaml 方式创建命名空间
dev-ns; - 在
dev-ns命名空间创建 nginx pod,镜像使用nginx:alpine; - 使用原生
kubectl config命令,将当前上下文默认命名空间设置为dev-ns,不使用-n参数查看该 pod; - 安装 kubens 工具,使用 kubens 切换回
default命名空间,查看 dev-ns 下 pod(必须带-n参数); - 编写一个简易 shell 脚本实现命名空间切换功能(参考笔记自定义脚本);
- 删除
dev-ns命名空间。
验证点
kubectl get ns可以看到dev-ns;- 不指定
-n,kubectl get pod 能看到 nginx pod; - kubens 切换 default 后,直接 get pod 看不到 dev-ns 的 pod;
- 删除 ns 后,该 ns 下 pod 自动被清除。
提示:删除命名空间是"级联删除",命名空间下的所有资源(Pod、Service、ConfigMap 等)都会被一并清除,实验环境清理时非常好用。
实验题 2:Kubernetes Pod 基础管理
章节:Kubernetes Pod(pod 基本管理、多容器 pod)
-
创建命名空间
pod-lab,设置上下文默认 ns 为pod-lab; -
编写 yaml 创建多容器 Pod,pod 名称为
two-container-pod:- 容器 1:nginx,name:web,镜像
nginx:alpine; - 容器 2:busybox,name:client,镜像
busybox:1.36,command 执行 sleep 3600 保持运行;
- 容器 1:nginx,name:web,镜像
-
使用
kubectl get pod -o wide查看 pod 状态与 podIP;确认状态为2/2 Running; -
在 client 容器内部,使用
wget 127.0.0.1访问同 pod 内 nginx 服务; -
将 master 节点
/etc/hosts文件复制到 pod 的 web 容器内/tmp/hosts路径; -
查看 nginx 容器日志;进入 client 容器执行 hostname;
-
删除 pod,观察 terminating 状态。
提示 :同一个 Pod 内的多个容器共享网络命名空间(同一 IP、同一 localhost)和存储卷,所以 client 容器访问
127.0.0.1:80就能访问到同 Pod 的 nginx,这也是多容器 Pod 的典型用法。
今天内容
- 静态 pod
- 编写 yaml 文件
- Kubernetes Volume(存储卷)
- Kubernetes Configure(配置管理)
一、Kubernetes Volume
学习参考:卷 Volumes
容器磁盘上的文件生命周期是短暂的,容器重启或 Pod 被删除时数据就会丢失;而且一个 Pod 中多个容器想要共享文件也不方便。Volume(卷) 就是为了解决这两个问题:它独立于容器的文件系统,被 Pod 挂载使用,可以为 Pod 提供临时共享或持久化的数据存储。
1.1 环境准备
先创建一个独立的命名空间 storage,并把当前上下文的默认命名空间切过去,后续所有命令都不用再手动加 -n storage:
bash
# 创建命名空间 storage
root@master30:~# kubectl create ns storage
# 将当前上下文(context)的默认命名空间设置为 storage
root@master30:~# kubectl config set-context --current --namespace storage
说明 :
kubectl config set-context --current --namespace <ns>只改 kubeconfig 里的默认命名空间,不影响其他上下文,是实验前隔离资源的常用操作。
1.2 Volume 类型一览
Kubernetes 支持的 Volume 类型非常丰富,本文重点实操加粗的四种(emptyDir、hostPath、nfs、persistentVolumeClaim):
- emptyDir:临时空目录,随 Pod 生灭
- hostPath:挂载节点本地目录
- gcePersistentDisk
- awsElasticBlockStore
- nfs:网络文件系统共享存储
- iscsi
- fc (fibre channel)
- flocker
- glusterfs
- rbd
- cephfs
- gitRepo
- secret
- persistentVolumeClaim:声明式使用持久卷 PV
- downwardAPI
- projected
- azureFileVolume
- azureDisk
- vsphereVolume
- Quobyte
- PortworxVolume
- ScaleIO
- StorageOS
- local
1.3 emptyDir:Pod 的"临时草稿纸"
默认情况下,当 Pod 被分配到 Node 上时,就会创建一个 emptyDir(初始为空目录)。只要 Pod 在节点上一直运行,Volume 就一直存在;当 Pod 因为任何原因从节点上被删除时,emptyDir 也会被同时删除,里面的数据永久丢失。
注意:容器崩溃(Crash)并不会导致 emptyDir 数据丢失,因为容器崩溃后 Pod 还在、只是容器被 kubelet 重启;只有 Pod 对象本身被删除(手动删除、被控制器驱逐、节点故障等),emptyDir 才会被清空。
典型用途:同一 Pod 内多容器共享文件(如 sidecar 从主容器目录取日志)、临时缓存空间、排序大文件时的临时落盘。
实验:准备一个包含 2 个 busybox 容器的 Pod,两个容器共同挂载同一个 emptyDir 卷,验证数据共享。
bash
root@master30:~# vim pod-with-emptyDir.yaml
yaml
apiVersion: v1
kind: Pod
metadata:
name: busybox
labels:
app: busybox
spec:
# spec.volumes:在 Pod 级别定义卷,emptyDir: {} 表示使用默认配置的临时空目录
volumes:
- name: datavolume
emptyDir: {}
containers:
- name: busybox1
image: hub.laoma.cloud/library/busybox
imagePullPolicy: IfNotPresent # 本地有镜像就不拉取,实验环境常用
command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600']
# spec.containers.volumeMounts:容器引用上面定义的卷,挂载到容器内 /data 目录
volumeMounts:
- mountPath: /data
name: datavolume
- name: busybox2
image: hub.laoma.cloud/library/busybox
imagePullPolicy: IfNotPresent
command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600']
volumeMounts:
- mountPath: /data
name: datavolume
配置说明:
- spec.volumes :定义卷,
emptyDir是默认的临时卷类型; - spec.containers.volumeMounts:在容器中引用已定义的卷,并指定挂载路径。
bash
# 创建 Pod
root@master30:~# kubectl apply -f pod-with-emptyDir.yaml
pod/busybox created
# 获取两个容器的 ID(只取 // 后面前 12 位),后面 crictl 要用到
root@master30:~# kubectl describe pod busybox |egrep -o 'Container ID.*//.{12}'
Container ID: containerd://07d189383321
Container ID: containerd://bb02c680ca8a
# 获取 Pod 被调度到了哪个节点
root@master30:~# kubectl describe pod busybox |grep Node:
Node: worker32.hyc.cloud/10.1.8.32
# 登录到 worker32 节点,用 crictl 查看容器的挂载情况
# 可以看到 emptyDir 实际落在节点的 /var/lib/kubelet/pods/<Pod-UID>/volumes/kubernetes.io~empty-dir/ 目录下
root@worker32:~# crictl inspect 07d189383321|grep datavolume
"hostPath": "/var/lib/kubelet/pods/8417656f-4b6c-4b2a-a2f3-f4a248c4c043/volumes/kubernetes.io~empty-dir/datavolume",
"host_path": "/var/lib/kubelet/pods/8417656f-4b6c-4b2a-a2f3-f4a248c4c043/volumes/kubernetes.io~empty-dir/datavolume"
"source": "/var/lib/kubelet/pods/8417656f-4b6c-4b2a-a2f3-f4a248c4c043/volumes/kubernetes.io~empty-dir/datavolume",
# 验证共享:在 busybox1 容器的 /data 下创建文件
root@master30:~# kubectl exec busybox -c busybox1 -- touch /data/b1-f1
# 在 busybox2 容器中查看 /data,能看到 busybox1 创建的文件,说明两个容器共享同一个卷
root@master30:~# kubectl exec busybox -c busybox2 -- ls /data
b1-f1
# 在节点上直接查看,容器里写入的文件真实落盘在 kubelet 管理的临时目录中
root@worker32:~# ls /var/lib/kubelet/pods/8417656f-4b6c-4b2a-a2f3-f4a248c4c043/volumes/kubernetes.io~empty-dir/datavolume
b1-f1
# 删除 Pod,验证 emptyDir 的生命周期
root@master30:~# kubectl delete pod busybox --force
# 再次在节点上查看,整个临时目录已经不存在了
root@worker32:~# ls /var/lib/kubelet/pods/8417656f-4b6c-4b2a-a2f3-f4a248c4c043/volumes/kubernetes.io~empty-dir/datavolume
ls: cannot access '/var/lib/kubelet/pods/8417656f-4b6c-4b2a-a2f3-f4a248c4c043/volumes/kubernetes.io~empty-dir/datavolume': No such file or directory
注意:容器删除后,节点上的临时卷数据可能需要等待一小段时间才会被清理回收,属于正常现象。
1.4 hostPath:挂载节点本地目录
hostPath 允许 Pod 将 Node 节点文件系统中的某个目录 挂载到 Pod 内部。与 emptyDir 最大的区别是:Pod 删除后,hostPath 卷里的数据仍然保留在节点上。
注意 :hostPath 的数据只存在于某一个固定节点上,Pod 一旦被重新调度到其他节点就看不到原数据了。因此官方建议 hostPath 仅用于单节点测试或访问节点系统文件(如日志、Docker Socket),生产环境多节点集群请使用
local卷或网络存储。
实验 :准备一个 Pod,使用 hostPath 把节点的 /busyboxdir 挂载到容器内 /data。
bash
root@master30:~# vim pod-with-hostPath.yaml
yaml
apiVersion: v1
kind: Pod
metadata:
name: busybox
labels:
app: busybox
spec:
volumes:
- name: datavolume
hostPath:
path: /busyboxdir # 节点上的真实目录,不存在时 kubelet 会自动创建(Directory 类型)
containers:
- name: busybox1
image: hub.laoma.cloud/library/busybox
imagePullPolicy: IfNotPresent
command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600']
volumeMounts:
- mountPath: /data
name: datavolume
- name: busybox2
image: hub.laoma.cloud/library/busybox
imagePullPolicy: IfNotPresent
command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600']
volumeMounts:
- mountPath: /data
name: datavolume
# 设置 readOnly 控制读写权限,默认值是 false(读写访问);设为 true 则该容器只能读不能写
readOnly: false
bash
# 创建 Pod
root@master30:~# kubectl apply -f pod-with-hostPath.yaml
pod/busybox created
# 获取容器 ID
root@master30:~# kubectl describe pod busybox |egrep -o 'Container ID.*//.{12}'
Container ID: containerd://318922d0c666
Container ID: containerd://0688146babe5
# 获取 Pod 所在节点
root@master30:~# kubectl describe pod busybox |grep Node:
Node: worker32.hyc.cloud/10.1.8.32
# 登录到 worker32 查看容器挂载情况,确认容器内 /data 对应节点的 /busyboxdir
root@worker32:~# crictl inspect 318922d0c666|grep busyboxdir
"hostPath": "/busyboxdir",
"host_path": "/busyboxdir"
"source": "/busyboxdir",
# busybox1 在 /data 中创建文件(读写挂载)
root@master30:~# kubectl exec busybox -c busybox1 -- touch /data/b1-f1
# busybox2 能看到该文件
root@master30:~# kubectl exec busybox -c busybox2 -- ls /data
b1-f1
# 如果把 busybox2 的 readOnly 设为 true,写入会被拒绝,报 Read-only file system
root@master30:~# kubectl exec busybox -c busybox2 -- touch /data/b2-f2
touch: /data/b2-f2: Read-only file system
说明 :上面的报错演示的是
readOnly: true时的效果;如果按当前 YAML 配置(readOnly: false),busybox2 应当可以正常写入。修改 YAML 后用kubectl apply重建 Pod 即可观察到两种挂载的差异。
bash
# 在节点上查看数据,容器写入的文件直接出现在节点 /busyboxdir 目录中
root@worker32:~# ls /busyboxdir/
b1-f1
# 删除 Pod,验证 hostPath 数据是否保留
root@master30:~# kubectl delete pod busybox --force
# Pod 删除后,hostPath 卷的数据仍然保留在节点上
root@worker32:~# ls /busyboxdir/
b1-f1
小结:emptyDir 是"Pod 在数据在,Pod 亡数据亡";hostPath 是"数据留在节点上,但绑定固定节点"。想要跨节点、跨 Pod 生命周期保留数据,需要网络存储,NFS 就是最简单的一种。
1.5 NFS 存储:网络文件共享
NFS 卷把数据存储在 NFS 共享目录中,多个节点上的 Pod 都可以挂载同一个 NFS 共享,Pod 删除后数据同样保留在 NFS 服务器上。
准备 NFS 共享
bash
# 在 master30 上安装 NFS 服务端
root@master30:~# apt install -y nfs-kernel-server
# 创建 NFS 共享目录,-m 777 直接放开权限,方便实验读写
root@master30:~# mkdir -m 777 /nfsshares
# 准备一个测试页面,后面用 nginx 访问验证
root@master30:~# echo hello hyc > /nfsshares/index.html
# 配置共享目录,* 表示允许所有客户端访问,rw 表示读写权限
root@master30:~# cat << EOF > /etc/exports
/nfsshares *(rw)
EOF
# 重启 NFS 服务使配置生效
root@master30:~# systemctl restart nfs-server.service
# 两台 worker 节点安装 NFS 客户端(kubelet 挂载 NFS 卷时依赖它)
root@worker31:~# apt install -y nfs-common
root@worker32:~# apt install -y nfs-common
准备 Pod
把 NFS 共享目录挂载到 nginx 容器的网站根目录 /usr/share/nginx/html:
bash
root@master30:~# vim pod-with-nfs.yaml
yaml
apiVersion: v1
kind: Pod
metadata:
labels:
run: nginx
name: nginx
spec:
volumes:
- name: nfs
nfs:
server: 10.1.8.30 # NFS 服务器地址,这里就是 master30
path: "/nfsshares" # NFS 服务器上的共享目录
containers:
- image: hub.laoma.cloud/library/nginx
name: nginx
volumeMounts:
- name: nfs
mountPath: "/usr/share/nginx/html" # 挂载到 nginx 默认网站根目录
bash
# 创建 Pod
root@master30:~# kubectl apply -f pod-with-nfs.yaml
pod/nginx created
# 获取容器 ID
root@master30:~# kubectl describe pod nginx |egrep -o 'Container ID.*//.{12}'
Container ID: containerd://c5e734dab9b5
# 获取 Pod 所在节点
root@master30:~# kubectl describe pod nginx |grep Node:
Node: worker32.hyc.cloud/10.1.8.32
# 登录 worker32,查看容器内挂载情况:NFS 卷同样由 kubelet 统一管理
root@worker32:~# crictl inspect c5e734dab9b5 | grep -e nfs -e html
"containerPath": "/usr/share/nginx/html",
"hostPath": "/var/lib/kubelet/pods/b5bd3f1a-3ef4-4327-95ff-61352268ee41/volumes/kubernetes.io~nfs/nfs",
"container_path": "/usr/share/nginx/html",
"host_path": "/var/lib/kubelet/pods/b5bd3f1a-3ef4-4327-95ff-61352268ee41/volumes/kubernetes.io~nfs/nfs"
"destination": "/usr/share/nginx/html",
"source": "/var/lib/kubelet/pods/b5bd3f1a-3ef4-4327-95ff-61352268ee41/volumes/kubernetes.io~nfs/nfs",
# 节点上可以看到 NFS 共享已被挂载(df 查看挂载点)
root@worker32:~# df | grep nfsshare
10.1.8.30:/nfsshares 101590016 6659072 89723904 7% /var/lib/kubelet/pods/b5bd3f1a-3ef4-4327-95ff-61352268ee41/volumes/kubernetes.io~nfs/nfs
# 查看 Pod IP,访问 nginx 验证
root@master30:~# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx 1/1 Running 0 5m19s 10.224.51.171 worker32.hyc.cloud <none> <none>
# curl 访问,返回的正是 NFS 共享目录里的 index.html 内容
root@master30:~# curl 10.224.51.171
hello hyc
# 在容器中写入文件,NFS 服务器上同步出现
root@master30:~# kubectl exec nginx -- ls /usr/share/nginx/html
index.html
root@master30:~# kubectl exec nginx -- touch /usr/share/nginx/html/test.html
root@master30:~# ls /nfsshares
index.html test.html
# 删除 Pod,验证 NFS 卷数据保留
root@master30:~# kubectl delete pod nginx --force
pod "nginx" deleted
root@master30:~# ls /nfsshares
index.html test.html
结论:Pod 删除后,NFS 卷的数据保留在 NFS 服务器上。之后无论新建的 Pod 被调度到哪个节点,只要重新挂载这个 NFS 共享,就能读到同样的数据。
1.6 持久化存储:PV 与 PVC
直接在 Pod YAML 里写 NFS 配置有个问题:业务开发人员需要知道后端存储的具体细节(NFS 地址、共享路径等) ,这显然不合理。Kubernetes 使用 PV(Persistent Volume,持久卷) 架构解决这个问题,把"存储的提供"和"存储的使用"分离开。
1.6.1 PV 和 PVC 架构
开发人员不需要了解任何云环境或存储后端的细节,只需要创建一个 PVC(PersistentVolumeClaim,持久卷申领) 去"申请"存储资源,Pod 挂载 PVC 即可实现持久化:
- Persistent Volume(PV) :由 PersistentVolume API 对象定义,代表集群中已经存在的一块实际存储。PV 的生命周期与使用它的 Pod 无关。PV 是集群级别资源,不属于任何命名空间,通常由集群管理员创建。
- Persistent Volume Claim(PVC) :由 PersistentVolumeClaim API 对象定义,代表用户对存储的一次"申领"(要多大、什么访问模式)。PVC 是 namespace 级别资源,由开发人员创建。
通俗理解:PV 就像停车场里的一个个车位(管理员提前划好),PVC 就是车主提交的停车申请("我要一个标准车位"),master 节点负责把合适的 PV 和 PVC 绑定(Bound),Pod 则通过 PVC 拿到车位。
1.6.2 创建 PV
集群管理员可以创建任意数量的 PV,数量取决于后端存储容量。为了方便演示,这里继续使用上面的 NFS 后端存储。
bash
root@master30:~# vim pv.yaml
yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: web
spec:
capacity:
storage: 5Gi # 存储容量
volumeMode: Filesystem # 卷模式:文件系统(默认)
accessModes:
- ReadWriteOnce # 访问模式:单节点读写,后面会详细讲
nfs: # 后端存储:NFS
path: /nfsshares
server: 10.1.8.30
bash
root@master30:~# kubectl apply -f pv.yaml
root@master30:~# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
web 5Gi RWO Recycle Available 3s
说明 :刚创建的 PV 状态为
Available(可用),表示还没有被任何 PVC 绑定。
1.6.3 创建 PVC
用户创建 PVC,声明自己需要的容量、访问模式和存储类别;master 中的 PV 控制器会持续监控 PVC,寻找一个匹配的 PV(或等待动态制备),然后把两者绑定。
bash
root@master30:~# vim pvc.yaml
yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: webclaim
spec:
accessModes:
- ReadWriteOnce # 申请的访问模式
resources:
requests:
storage: 5Gi # 申请 5Gi 容量
bash
root@master30:~# kubectl apply -f pvc.yaml
root@master30:~# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
webclaim Bound web 5Gi RWO 4s
说明 :PVC 状态变为
Bound、VOLUME 列显示web,说明它已经和上面创建的 PV 成功绑定。
1.6.4 创建 Pod 使用 PVC
Pod 不再直接写 NFS 信息,而是通过 persistentVolumeClaim.claimName 引用 PVC:
bash
root@master30:~# vim pod-with-pvc.yaml
yaml
apiVersion: v1
kind: Pod
metadata:
name: web
labels:
name: web
spec:
containers:
- image: hub.laoma.cloud/library/nginx
name: web
ports:
- containerPort: 80
name: web-port
volumeMounts:
- name: web-persistent-storage
mountPath: /usr/share/nginx/html
volumes:
- name: web-persistent-storage
persistentVolumeClaim:
claimName: webclaim # 引用刚才创建的 PVC
bash
root@master30:~# kubectl apply -f pod-with-pvc.yaml
pod/web created
root@master30:~# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
web 1/1 Running 0 35s 10.224.193.69 worker32.hyc.cloud <none> <none>
# 测试:访问 Pod 的 web 页面,内容来自 NFS 共享目录
root@master30:~# curl http://10.224.193.69
hello hyc
# 直接往 NFS 目录写一个 test.html,通过 Pod 同样可以访问,验证存储确实打通
root@master30:~# echo test > /nfsshares/test.html
root@master30:~# curl http://10.224.193.69/test.html
test
# 删除 Pod(PVC、PV 和 NFS 数据都不受影响)
root@master30:~# kubectl delete pod web --force
1.6.5 PV accessModes(访问模式)
PersistentVolume 支持以下四种访问模式:
- ReadWriteOnce(RWO) :卷可以被一个节点以读写方式挂载,同时允许该节点上的多个 Pod 读写访问卷。
- ReadOnlyMany(ROX) :卷可以被多个节点以只读方式挂载。
- ReadWriteMany(RWX) :卷可以被多个节点以读写方式挂载。
- ReadWriteOncePod(RWOP) :卷可以被单个 Pod 以读写方式挂载。如果想确保整个集群中只有一个 Pod 能读写该 PVC,请使用此模式。仅支持 CSI 卷,需要 Kubernetes 1.22 以上版本。
更详细的说明可参考官方博客文章 Introducing Single Pod Access Mode for PersistentVolumes。
CLI 中访问模式的缩写形式:
- RWO - ReadWriteOnce
- ROX - ReadOnlyMany
- RWX - ReadWriteMany
- RWOP - ReadWriteOncePod
不同存储后端支持的访问模式不同:
| 卷插件 | ReadWriteOnce | ReadOnlyMany | ReadWriteMany | ReadWriteOncePod |
|---|---|---|---|---|
| AzureFile | ✓ | ✓ | ✓ | - |
| CephFS | ✓ | ✓ | ✓ | - |
| CSI | 取决于驱动 | 取决于驱动 | 取决于驱动 | 取决于驱动 |
| FC | ✓ | ✓ | - | - |
| FlexVolume | ✓ | ✓ | 取决于驱动 | - |
| GCEPersistentDisk | ✓ | ✓ | - | - |
| Glusterfs | ✓ | ✓ | ✓ | - |
| HostPath | ✓ | - | - | - |
| iSCSI | ✓ | ✓ | - | - |
| NFS | ✓ | ✓ | ✓ | - |
| RBD | ✓ | ✓ | - | - |
| VsphereVolume | ✓ | - | -(Pod 运行于同一节点上时可行) | - |
| PortworxVolume | ✓ | - | ✓ | - |
重要 :每个卷在同一时刻只能以一种访问模式挂载,即使该卷能够同时支持多种模式。例如一个 GCEPersistentDisk 卷可以被某节点以 RWO 挂载,或被多个节点以 ROX 挂载,但不可以同时以两种模式挂载。
常见的 NAS 存储(如 NFS、CephFS)都支持三种模式:RWO、ROX、RWX。
1.6.6 PV volumeModes(卷模式)
特性状态: Kubernetes v1.18 [stable]
针对 PV,Kubernetes 支持两种卷模式(volumeModes):Filesystem(文件系统)和 Block(块)。volumeMode 是一个可选参数,省略时默认为 Filesystem。
- Filesystem 卷 :会被 Pod 挂载(Mount) 到某个目录。如果卷的存储来自某块空设备,Kubernetes 会在第一次挂载前在设备上创建文件系统。
- Block 卷 :作为原始块设备交给 Pod 使用,上面没有任何文件系统,Pod 与卷之间不存在文件系统层,访问速度最快,但应用必须知道如何处理原始块设备。详见 原始块卷支持。
1.6.7 PVC 与 PV 的匹配规则
PVC 能绑定到哪个 PV,由三条规则共同决定:
- PV 的模式必须满足 PVC 申请的最低要求 :模式优先级可简单理解为
ROX < RWO < RWX。例如用户请求 RWO,但当前只有支持 RWO+ROX+RWX 的 NFS PV,PVC 仍会匹配该 NFS PV。 - 容量满足最低要求:相同访问模式的卷会被分组,然后按容量从小到大分类匹配,避免"大材小用"。
- storageClassName(存储类) :用于对 PV 分类,PVC 可以通过
storageClassName申请特定类型的 PV。如果 PV 设置了 storageClassName,PVC 申请时也必须指定相同的 storageClassName 。例如 PV 指定为ns1-storage,ns1 中的 PVC 也必须指定ns1-storage。
1.6.8 PV 回收策略
当用户不再使用存储卷时,可以删除 PVC 对象释放资源。PV 的回收策略(Reclaim Policy) 决定卷被释放后,集群如何处理这块数据卷。支持三种:Retain(保留)、Recycle(回收)、Delete(删除)。
Retain(保留)
Retain 策略允许用户手动回收资源。PVC 被删除后,PV 仍然存在,数据卷状态变为 Released(已释放)。由于卷上还保留着上一次关联的 PVC 信息(claimRef),必须手动清理这些信息后,PV 才能重新分配给其他 PVC。Retain 是默认的回收策略。
bash
root@master30:~# vim pv-pvc-Retain.yaml
yaml
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: web
spec:
capacity:
storage: 5Gi
# persistentVolumeReclaimPolicy: Retain # 不写默认就是 Retain
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
nfs:
path: /nfsshares
server: 10.1.8.30
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: webclaim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
bash
root@master30:~# kubectl apply -f pv-pvc-Retain.yaml
root@master30:~# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
web 5Gi RWO Retain Bound default/webclaim 22s
root@master30:~# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
webclaim Bound web 5Gi RWO 23s
删除 PVC 验证:
bash
# 删除 PVC
root@master30:~# kubectl delete pvc webclaim
root@master30:~# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
web 5Gi RWO Retain Released default/webclaim 3m4s
# PV 没有被删除,状态变为 Released,CLAIM 列还记录着上一个 PVC
此时再创建一个新的 PVC,会发现无法绑定到这个 PV:
bash
root@master30:~# vim pvc-db.yaml
yaml
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: dbclaim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
bash
root@master30:~# kubectl apply -f pvc-db.yaml
root@master30:~# kubectl get pvc dbclaim
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
dbclaim Pending 11s
# 一直 Pending,因为 PV 还保留着上一次的 claimRef
手动编辑 PV,删除卷上的 claimRef 信息:
bash
root@master30:~# kubectl edit pv web
yaml
......
spec:
accessModes:
- ReadWriteOnce
capacity:
storage: 5Gi
# 删除 claimRef 这一整段
# claimRef:
# apiVersion: v1
# kind: PersistentVolumeClaim
# name: webclaim
# namespace: test
# resourceVersion: "112157"
# uid: 0839cdc1-81bb-11e9-9ca8-52540000fa0a
# 删除 claimRef 这一整段
nfs:
path: /web
server: 10.1.8.30
......
bash
# 保存退出后,dbclaim 立刻绑定成功
root@master30:~# kubectl get pvc dbclaim
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
dbclaim Bound web 5Gi RWO 3m49s
清理环境:
bash
root@master30:~# kubectl delete pvc dbclaim
root@master30:~# kubectl delete pv web
root@master30:~# ls /nfsshares/
index.html test.html
注意:删除 PV 并不会删除后端存储(NFS 目录)中的数据,Retain 策略下数据需要管理员手动清理。
Recycle(回收)
警告 :
Recycle回收策略已被废弃,建议改用动态制备(Dynamic Provisioning,通过 StorageClass 自动创建 PV)。
Recycle 策略的效果与 Retain 基本一致:PVC 删除后,集群会执行一次基本的数据卷擦除(rm -rf /volume/*),之后卷重新变为 Available 可被新 PVC 绑定。
Delete(删除)
对于支持 Delete 策略的卷插件(如 AWS EBS、GCE PD 等云盘),删除 PVC 时会同时把 PV 对象和后端基础设施中的存储资产(如云盘本身)一并删除。
bash
root@master30:~# vim pv-pvc-Delete.yaml
yaml
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: web
spec:
capacity:
storage: 5Gi
persistentVolumeReclaimPolicy: Delete # 回收策略设置为 Delete
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
nfs:
path: /nfsshares
server: 10.1.8.30
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: webclaim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
bash
root@master30:~# kubectl apply -f pv-pvc-Delete.yaml
root@master30:~# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
web 5Gi RWO Delete Bound default/webclaim 22s
root@master30:~# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
webclaim Bound web 5Gi RWO 23s
删除 PVC 验证:
bash
root@master30:~# kubectl delete pvc webclaim
# 查看 PV 状态,变为 Failed,回收失败
root@master30:~# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
web 5Gi RWO Delete Failed default/webclaim 33s
# describe 查看失败原因
root@master30:~# kubectl describe pv web
......
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning VolumeFailedDelete 50s persistentvolume-controller error getting deleter volume plugin for volume "web": no deletable volume plugin matched
说明 :Kubernetes 提示
no deletable volume plugin matched,即没有找到匹配的删除卷插件,所以无法自动删除。原因是 NFS 插件本身不支持 Delete 策略(Kubernetes 不知道如何删除 NFS 服务器上的目录)。此时可以手动删除 PV,且手动删除 PV 同样不会删除后端存储中的数据。
思考题:如何让 PV 只能绑定给特定的 PVC?
答案:在定义 PV 时指定 claimRef(申领引用),预先写死它要绑定的 PVC 名称和命名空间,其他 PVC 就无法再绑定该卷:
yaml
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: web
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
nfs:
path: /nfsshares
server: 10.1.8.30
claimRef:
name: webclaim # 只允许名为 webclaim 的 PVC 绑定
namespace: test # 且该 PVC 必须在 test 命名空间
1.6.9 PV 卷的类型
PV 持久卷以插件形式实现,Kubernetes 目前支持以下插件:
csi- 容器存储接口 (CSI),如今的主流方向fc- Fibre Channel (FC) 存储hostPath- HostPath 卷(仅供单节点测试,不适用于多节点集群;可尝试用local卷替代)iscsi- iSCSI (SCSI over IP) 存储local- 节点上挂载的本地存储设备nfs- 网络文件系统 (NFS) 存储
以下持久卷已被弃用(当前仍支持,但未来版本会移除):
azureFile- Azure File(于 v1.21 弃用)flexVolume- FlexVolume(于 v1.23 弃用)gcePersistentDisk- GCE Persistent Disk(于 v1.17 弃用)portworxVolume- Portworx 卷(于 v1.25 弃用)vsphereVolume- vSphere VMDK 卷(于 v1.19 弃用)cephfs- CephFS 卷(于 v1.28 弃用)rbd- Rados Block Device (RBD) 卷(于 v1.28 弃用)
旧版本 Kubernetes 支持的"树内(In-Tree)"持久卷类型:
awsElasticBlockStore- AWS Elastic Block Store (EBS)(v1.27 开始不可用)azureDisk- Azure Disk(v1.27 开始不可用)cinder- Cinder (OpenStack block storage)(v1.27 开始不可用)photonPersistentDisk- Photon 控制器持久盘(v1.15 开始不可用)scaleIO- ScaleIO 卷(v1.21 之后不可用)flocker- Flocker 存储(v1.25 之后不可用)quobyte- Quobyte 卷(v1.25 之后不可用)storageos- StorageOS 卷(v1.25 之后不可用)
1.6.10 原始块卷支持
特性状态: Kubernetes v1.18 [stable]
以下卷插件支持原始块卷(含动态制备,如果支持):
- CSI
- FC(光纤通道)
- GCEPersistentDisk(已弃用)
- iSCSI
- Local 卷
- OpenStack Cinder
- RBD(已弃用)
- RBD(Ceph 块设备,已弃用)
- VsphereVolume
PV 示例:
yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: block-pv
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
volumeMode: Block # 声明为原始块卷
persistentVolumeReclaimPolicy: Retain
fc:
targetWWNs: ["50060e801049cfd1"]
lun: 0
readOnly: false
PVC 示例:
yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: block-pvc
spec:
accessModes:
- ReadWriteOnce
volumeMode: Block # 同样声明为 Block
resources:
requests:
storage: 10Gi
Pod 示例:
yaml
apiVersion: v1
kind: Pod
metadata:
name: pod-with-block-volume
spec:
containers:
- name: fc-container
image: fedora:26
command: ["/bin/sh", "-c"]
args: [ "tail -f /dev/null" ]
volumeDevices: # 注意:块卷用 volumeDevices 而不是 volumeMounts
- name: data
devicePath: /dev/xvda # 在容器内呈现为一块裸设备
volumes:
- name: data
persistentVolumeClaim:
claimName: block-pvc
说明 :向 Pod 中添加原始块设备时,设置的是容器内的设备路径 (
volumeDevices.devicePath),而不是文件系统的挂载路径(volumeMounts.mountPath)。
1.7 环境清理
bash
# 删除整个 storage 命名空间,其中的 Pod、PVC 等资源会一并清除
root@master30:~# kubectl delete ns storage
二、Kubernetes Configure
存储解决了"数据放哪"的问题,配置管理解决的是"配置怎么给容器"的问题。Kubernetes 提供了两个核心对象:ConfigMap (明文配置)和 Secret(敏感配置)。
2.1 环境准备
bash
# 创建并切换到 configure 命名空间
root@master30:~# kubectl create ns configure
root@master30:~# kubectl config set-context --current --namespace configure
2.2 ConfigMap
学习参考:ConfigMap
2.2.1 ConfigMap 介绍
ConfigMap 是一个用来存储其他对象所需配置的 API 对象,使用 data 和 binaryData 字段保存键值对数据:
data字段:保存 UTF-8 字符串;binaryData字段:保存二进制数据(base64 编码的字串)。
ConfigMap 的名字必须是合法的 DNS 子域名;data 或 binaryData 下的每个键名必须由字母、数字或 -、_、. 组成,且 data 和 binaryData 下的键名不能重叠。
从 v1.19 开始,可以在 ConfigMap 定义中添加 immutable 字段,创建不可变更的 ConfigMap。
ConfigMap 使用建议:
- 使用 ConfigMap 存储配置数据,不要存数据/文件,单个 ConfigMap 上限为 1 MiB;
- ConfigMap 在设计上不是用来保存大量数据的,如果需要保存大量数据,请使用挂载存储卷、独立数据库或文件服务。
2.2.2 ConfigMap 创建
命令帮助:
bash
kubectl create configmap NAME [--from-file=[key=]source]
[--from-literal=key1=value1] [--dry-run=server|client|none] [options]
方式一:键值对类型(--from-literal)
bash
# 直接在命令行写键值对,适合少量配置项
root@master30:~# kubectl create configmap mysql --from-literal=password=redhat
# 查看 ConfigMap 内容
root@master30:~# kubectl get configmaps mysql -o yaml |grep ^data -A1
data:
password: redhat
方式二:文件类型(--from-file)
bash
# 准备一个文件,文件名会作为 key,文件内容作为 value
root@master30:~# echo Hello World > index.html
root@master30:~# kubectl create configmap web1 --from-file=./index.html
root@master30:~# kubectl get configmaps web1 -o yaml |grep ^data -A2
data:
index.html: |
Hello World
方式三:目录类型(--from-file 指向目录)
bash
# 准备目录,目录下的每个文件都会成为一个键值对
root@master30:~# echo error > error.html
root@master30:~# mkdir web2
root@master30:~# mv index.html error.html web2
root@master30:~# kubectl create configmap web2 --from-file=./web2
root@master30:~# kubectl get configmaps web2 -o yaml |grep ^data -A4
data:
error.html: |
error
index.html: |
Hello World
2.2.3 环境变量方式引用 ConfigMap
注意 :环境变量属于容器(container)级别 ,在
containers[].env中配置。
bash
root@master30:~# vim pod-with-env-from-configmap.yaml
yaml
---
apiVersion: v1
kind: Pod
metadata:
name: mysql
labels:
name: mysql
spec:
containers:
- image: hub.laoma.cloud/library/mysql:latest
imagePullPolicy: IfNotPresent
name: mysql
ports:
- containerPort: 3306
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
# 原先通过 value 直接写死环境变量,现在改为从 ConfigMap 中取值
valueFrom:
configMapKeyRef:
name: mysql # ConfigMap 的名称
key: password # ConfigMap 中对应的 key
验证:
bash
root@master30:~# kubectl apply -f pod-with-env-from-configmap.yaml
root@master30:~# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
mysql 1/1 Running 0 4m33s 10.224.193.70 worker32.hyc.cloud <none>
# 进入容器查看环境变量,值来自 ConfigMap
root@master30:~# kubectl exec -it mysql -- bash -c 'echo $MYSQL_ROOT_PASSWORD'
redhat
# 安装 mysql 客户端,使用该密码连接数据库验证
root@master30:~# apt install -y mysql-client
root@master30:~# mysql -u root -predhat -h 10.224.193.70
mysql: [Warning] Using a password on the command line interface can be insecure.
......
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> quit
Bye
# 删除 Pod
root@master30:~# kubectl delete pod mysql --force
2.2.4 以 Volume 方式引用 ConfigMap
注意 :volumes 属于 Pod 级别 ,通过
volumeMounts挂载到容器中。
引用整体(ConfigMap 中每个 key 挂载为一个文件)
bash
root@master30:~# vim pod-with-volume-from-configmap.yaml
yaml
---
apiVersion: v1
kind: Pod
metadata:
labels:
run: web
name: web
spec:
containers:
- image: hub.laoma.cloud/library/nginx:latest
imagePullPolicy: IfNotPresent
name: web
volumeMounts:
- name: webcontent
# mountPath 是一个挂载点(目录),ConfigMap 中所有键值对都会以文件形式挂载到该目录
mountPath: "/usr/share/nginx/html"
volumes:
- name: webcontent
configMap:
name: web2 # 引用名为 web2 的 ConfigMap
验证:
bash
root@master30:~# kubectl apply -f pod-with-volume-from-configmap.yaml
root@master30:~# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
web 1/1 Running 0 66s 10.224.193.69 worker32.hyc.cloud <none>
# 网站根目录下出现了 ConfigMap 中的两个 key(文件名)
root@master30:~# kubectl exec web -- ls /usr/share/nginx/html
error.html
index.html
# 访问验证,文件内容就是 ConfigMap 中对应 key 的 value
root@master30:~# curl http://10.224.193.69
Hello World
root@master30:~# curl http://10.224.193.69/error.html
error
只引用特定 key(items)
如果不想挂载整个 ConfigMap,只需要其中某个 key,可以用 items 字段指定:
bash
root@master30:~# vim pod-cm-volume-single.yaml
yaml
---
apiVersion: v1
kind: Pod
metadata:
labels:
run: web
name: web
spec:
containers:
- name: web
image: hub.laoma.cloud/library/nginx:latest
imagePullPolicy: IfNotPresent
volumeMounts:
- name: webcontent
mountPath: "/usr/share/nginx/html"
volumes:
- name: webcontent
configMap:
name: web2
items:
- key: index.html # 只引用 ConfigMap 中的 index.html
path: index.html # 挂载后的文件名
上面的写法等同于下面的 subPath 写法:
yaml
apiVersion: v1
kind: Pod
metadata:
labels:
run: web
name: web
spec:
containers:
- image: hub.laoma.cloud/library/nginx:latest
imagePullPolicy: IfNotPresent
name: web
volumeMounts:
- name: webcontent
mountPath: "/usr/share/nginx/html/index.html" # 直接指定到具体文件
subPath: index.html # subPath 表示 mountPath 是一个文件而非目录
volumes:
- name: webcontent
configMap:
name: web2
热更新说明 :以卷方式引用时,被挂载的 ConfigMap 内容会被自动更新。当 ConfigMap 更新时,所投射的键最终也会被更新,kubelet 会在每次周期性同步时检查挂载的 ConfigMap 是否为最新(默认有数十秒的同步延迟)。
注意 :以环境变量方式引用的 ConfigMap 不支持热更新,环境变量在容器启动时注入后就固定了,需要重启 Pod 才能生效;使用
subPath挂载的条目同样不会自动更新。
2.2.5 k8s 集群中自带的 ConfigMap
以 kube-proxy 为例,集群组件自身的配置也大量存放在 ConfigMap 中:
bash
# kube-system 命名空间下有大量系统组件的 ConfigMap
root@master30:~# kubectl get configmaps -n kube-system
NAME DATA AGE
calico-config 4 37h
coredns 1 37h
extension-apiserver-authentication 6 37h
kube-apiserver-legacy-service-account-token-tracking 1 37h
kube-proxy 2 37h
kube-root-ca.crt 1 37h
kubeadm-config 1 37h
kubelet-config 1 37h
# kubeadm-config 保存了集群初始化配置
root@master30:~# kubectl get cm -n kube-system kubeadm-config -o yaml
apiVersion: v1
data:
ClusterConfiguration: |
apiServer:
timeoutForControlPlane: 4m0s
apiVersion: kubeadm.k8s.io/v1beta3
certificatesDir: /etc/kubernetes/pki
clusterName: kubernetes
controllerManager: {}
dns: {}
etcd:
local:
dataDir: /var/lib/etcd
imageRepository: registry.k8s.io
kind: ClusterConfiguration
kubernetesVersion: v1.30.2
networking:
dnsDomain: cluster.local
podSubnet: 10.224.0.0/16
serviceSubnet: 10.96.0.0/12
scheduler: {}
kind: ConfigMap
metadata:
creationTimestamp: "2026-06-23T08:50:46Z"
name: kubeadm-config
namespace: kube-system
resourceVersion: "202"
uid: 7c5cc481-8de7-4751-9c62-48529e2f7baa
2.2.6 综合案例:HAProxy + 两个 Web 后端负载均衡
需求:
- 创建一个名为 haproxy 的 Pod,使用镜像 haproxy 。Pod 的配置文件保存在 ConfigMap 中,通过 volume 方式挂载到
/usr/local/etc/haproxy/haproxy.cfg,将流量转发到 Pod webapp-1 和 webapp-2。
HAProxy 配置文件模板如下:
ini
global
daemon
maxconn 256
defaults
mode http
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms
frontend http-in
bind *:8080
default_backend servers
backend servers
server app1 10.224.84.80:80 check
server app2 10.224.149.25:80 check
- 创建第一个 Pod,名称为 webapp-1 ,使用镜像 nginx 。为它创建一个名为 webapp-1 的 ConfigMap,保存两个值:
- index.html="hello webapp-1"
- error.html="sorry, error."
两个值通过 volume 方式挂载到 /usr/share/nginx/html 目录。
- 创建第二个 Pod,名称为 webapp-2 ,使用镜像 nginx 。为它创建一个名为 webapp-2 的 ConfigMap,保存两个值:
- index.html="hello webapp-2"
- error.html="sorry, error."
两个值通过 volume 方式挂载到 /usr/share/nginx/html 目录。
解答:
1)创建第一个 ConfigMap 和 Pod:
bash
root@master30:~# mkdir web && cd web
# 用 --from-literal 一次性创建两个键值对
root@master30:~/web# kubectl create cm webapp-1 --from-literal=index.html="hello webapp-1" --from-literal=error.html="sorry, error."
root@master30:~/web# vim pod-webapp-1.yaml
yaml
apiVersion: v1
kind: Pod
metadata:
labels:
run: webapp-1
name: webapp-1
spec:
containers:
- name: nginx
image: hub.laoma.cloud/library/nginx:latest
imagePullPolicy: IfNotPresent
volumeMounts:
- name: config
mountPath: "/usr/share/nginx/html"
readOnly: true # 配置文件只读挂载,防止容器内误改
volumes:
- name: config
configMap:
name: webapp-1
bash
root@master30:~/web# kubectl apply -f pod-webapp-1.yaml
2)创建第二个 ConfigMap 和 Pod:
bash
root@master30:~/web# kubectl create cm webapp-2 --from-literal=index.html="hello webapp-2" --from-literal=error.html="sorry, error."
root@master30:~/web# vim pod-webapp-2.yaml
yaml
apiVersion: v1
kind: Pod
metadata:
labels:
run: webapp-2
name: webapp-2
spec:
containers:
- name: nginx
image: hub.laoma.cloud/library/nginx:latest
imagePullPolicy: IfNotPresent
volumeMounts:
- name: config
mountPath: "/usr/share/nginx/html"
readOnly: true
volumes:
- name: config
configMap:
name: webapp-2
bash
root@master30:~/web# kubectl apply -f pod-webapp-2.yaml
3)创建 HAProxy 配置文件。
bash
# 获取两个 webapp 的 Pod IP,HAProxy 配置中要用
root@master30:~/web# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
webapp-1 1/1 Running 0 63m 10.224.84.80 worker1.hyc.fun <none> <none>
webapp-2 1/1 Running 0 63m 10.224.149.25 worker2.hyc.fun <none> <none>
# 编写 haproxy.cfg
root@master30:~/web# vim haproxy.cfg
ini
global
daemon
maxconn 256
defaults
mode http
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms
frontend http-in
bind *:8080
default_backend servers
backend servers
server app1 10.224.84.80:80 check
server app2 10.224.149.25:80 check
bash
# 从配置文件创建 ConfigMap(显式指定 key 名为 haproxy.cfg)
root@master30:~/web# kubectl create cm haproxy.cfg --from-file=haproxy.cfg=./haproxy.cfg
# 创建 haproxy Pod
root@master30:~/web# vim pod-haproxy.yaml
yaml
---
apiVersion: v1
kind: Pod
metadata:
name: haproxy
spec:
containers:
- name: haproxy
image: hub.laoma.cloud/library/haproxy
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: true
volumeMounts:
- name: config
mountPath: "/usr/local/etc/haproxy" # 挂载到 HAProxy 配置目录
readOnly: true
volumes:
- name: config
configMap:
name: haproxy.cfg
bash
root@master30:~/web# kubectl apply -f pod-haproxy.yaml
# 查看 haproxy Pod IP
root@master30:~/web# kubectl get pods haproxy -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
haproxy 1/1 Running 0 33s 10.224.149.35 worker32.hyc.cloud <none> <none>
# 连续访问 8080 端口,HAProxy 会把请求轮询到两个后端
root@master30:~# curl http://10.224.149.35:8080
hello webapp-1
root@master30:~# curl http://10.224.149.35:8080
hello webapp-2
小结:这个案例把 ConfigMap 的两种典型用法都用上了------两个 webapp 用 ConfigMap 挂载网页内容,HAProxy 用 ConfigMap 挂载配置文件。配置和镜像彻底解耦,改配置只需更新 ConfigMap,不用重新构建镜像。
2.3 Secret
学习参考: Secret
2.3.1 Secret 介绍
Secret 是一种包含少量敏感信息的对象,例如密码、令牌或密钥。这类信息原本可能被写在 Pod 规约或镜像中,使用 Secret 后,应用程序代码里就不再需要包含机密数据。
创建 Secret 可以独立于使用它们的 Pod,减少在创建、查看、编辑 Pod 的工作流程中暴露机密数据的风险。Kubernetes 和集群中的应用还可以对 Secret 采取额外保护,例如避免把敏感数据写入非易失性存储。
Secret 类似于 ConfigMap,但专门用于保存机密数据。二者的核心区别是:Secret 对数据做 base64 编码,ConfigMap 不编码(明文)。
注意:base64 只是编码,不是加密!任何能拿到 Secret 的人都可以轻松 base64 解码看到明文。生产环境中要配合 RBAC 权限控制、Secret 静态加密(EncryptionConfiguration)或外部方案(如 Vault、Sealed Secrets)使用。
2.3.2 Secret 类型
- generic:自定义键值对,类型为 Opaque,最常用;
- docker-registry :用于保存访问镜像仓库(registry)的凭据,类型为
kubernetes.io/dockerconfigjson; - tls :保存 TLS 公钥证书和私钥,类型为
kubernetes.io/tls。
2.3.3 创建 generic Secret
1)基于键值对(--from-literal)
一般用于传递变量值:
bash
root@master30:~# kubectl create secret generic mysecret1 --from-literal=user=tom --from-literal=password1=redhat --from-literal=password2=redhat
root@master30:~# kubectl get secrets
NAME TYPE DATA AGE
mysecret1 Opaque 3 21s
# 查看 YAML,可以看到 value 已经被 base64 编码
root@master30:~# kubectl get secrets mysecret1 -o yaml
apiVersion: v1
data:
password1: cmVkaGF0
password2: cmVkaGF0
user: dG9t
kind: Secret
metadata:
......
name: mysecret1
namespace: hyc
resourceVersion: "24180"
selfLink: /api/v1/namespaces/hyc/secrets/mysecret1
uid: d510d58f-8737-4d2f-bd00-dd9ce6b50a9b
type: Opaque
# 用 jsonpath 取出 data 字段,再用 json_reformat 美化输出(json_reformat 由 yajl-tools 软件包提供)
root@master30:~# kubectl get secrets mysecret1 -o jsonpath={.data} | json_reformat
{
"password1": "cmVkaGF0",
"password2": "cmVkaGF0",
"user": "dG9t"
}
# 只取某个 key
root@master30:~# kubectl get secrets mysecret1 -o jsonpath={.data.user}
dG9t
# describe 看不到明文值,只能看到每个值的字节数
root@master30:~# kubectl describe secrets mysecret1
Name: mysecret1
Namespace: hyc
Labels: <none>
Annotations: <none>
Type: Opaque
Data
====
password1: 6 bytes
password2: 6 bytes
user: 3 bytes
# Secret 通过 base64 编码,自己动手验证编码/解码
root@master30:~# echo -n tom | base64
dG9t
root@master30:~# echo -n redhat | base64
cmVkaGF0
root@master30:~# echo -n cmVkaGF0 | base64 -d
redhat
注意 :
echo一定要加-n,否则换行符也会被一起编码,导致值多出一个字节。
2)基于普通文件(--from-file)
一般用于传递配置文件。文件名用作 key,文件内容用作 value,也可以自定义 key 名:
bash
# 准备文件,-n 避免写入换行符
root@master30:~# echo -n tom > user
root@master30:~# echo -n redhat > password1
root@master30:~# echo -n redhat > password2
# 文件名直接作为 key
root@master30:~# kubectl create secret generic mysecret2 --from-file=./user --from-file=./password1 --from-file=./password2
# 自定义第一个 key 名为 username
root@master30:~# kubectl create secret generic mysecret3 --from-file=username=./user --from-file=./password1 --from-file=./password2
root@master30:~# kubectl get secrets mysecret2 -o yaml
apiVersion: v1
data:
password1: cmVkaGF0
password2: cmVkaGF0
user: dG9t
kind: Secret
metadata:
......
name: mysecret2
namespace: hyc
resourceVersion: "25329"
selfLink: /api/v1/namespaces/hyc/secrets/mysecret2
uid: 047366b6-e8d2-4cf0-b68c-d1f5f9999458
type: Opaque
root@master30:~# kubectl get secrets mysecret3 -o yaml
apiVersion: v1
data:
password1: cmVkaGF0
password2: cmVkaGF0
username: dG9t
kind: Secret
metadata:
......
name: mysecret3
namespace: hyc
resourceVersion: "25532"
selfLink: /api/v1/namespaces/hyc/secrets/mysecret3
uid: e19df9a7-7ce3-4539-b6f5-92f83417ac1f
type: Opaque
3)基于键值对内容的文件(--from-env-file)
文件内容写成 key=value 的形式,每行一对:
bash
root@master30:~# echo 'user=tom
password1=redhat
password2=redhat' > env.txt
root@master30:~# kubectl create secret generic mysecret4 --from-env-file=./env.txt
secret/mysecret4 created
root@master30:~# kubectl get secrets mysecret4 -o yaml
apiVersion: v1
data:
password1: cmVkaGF0
password2: cmVkaGF0
user: dG9t
kind: Secret
metadata:
......
name: mysecret4
namespace: hyc
resourceVersion: "27240"
selfLink: /api/v1/namespaces/hyc/secrets/mysecret4
uid: 935c0392-8e54-492d-b093-99021e0dc5bb
type: Opaque
4)基于目录(--from-file 指向目录)
目录下每个文件名作为 key,文件内容作为 value:
bash
root@master30:~# kubectl create secret generic mysecret5 --from-file=./config
secret/mysecret5 created
root@master30:~# kubectl get secrets mysecret5 -o yaml
apiVersion: v1
data:
password1: cmVkaGF0
password2: cmVkaGF0
user: dG9t
kind: Secret
metadata:
......
name: mysecret5
namespace: hyc
resourceVersion: "26687"
selfLink: /api/v1/namespaces/hyc/secrets/mysecret5
uid: 04eccd8b-8686-4856-82c3-0ac697d75746
type: Opaque
5)基于 YAML 文件
直接写 YAML 时,data 中的值必须是已经 base64 编码后的值:
yaml
apiVersion: v1
kind: Secret
metadata:
name: mysecret6
type: Opaque
data:
user: dG9t
password1: cmVkaGF0
password2: cmVkaGF0
2.3.4 docker-registry 类型
先回顾 docker login 命令:
bash
docker login DOCKER_REGISTRY_SERVER -u=DOCKER_USER -p=DOCKER_PASSWORD
Docker 登录信息默认保存在 ~/.dockercfg,后续 docker pull/push 时使用该凭据访问 registry。Kubernetes 中可以创建 docker-registry 类型的 Secret,让 kubelet 拉取私有镜像时自动带上凭据:
bash
Usage:
kubectl create secret docker-registry NAME --docker-username=user --docker-password=password --docker-email=email [--docker-server=string] [--from-literal=key1=value1] [--dry-run=server|client|none] [options]
示例:
bash
root@master30:~# kubectl create secret docker-registry registry --docker-username=hyc --docker-password=redhat --docker-email=admin@hyc.cloud --docker-server=registry.hyc.cloud
secret/registry created
root@master30:~# kubectl get secret registry -o yaml
apiVersion: v1
data:
.dockerconfigjson: eyJhdXRocyI6eyJyZWdpc3RyeS5yZWRoYXQuZnVuIjp7InVzZXJuYW1lIjoiaHljIiwicGFzc3dvcmQiOiJyZWRoYXQiLCJlbWFpbCI6Imh5Y0ByeWRoYXQuZnVuIiwiYXV0aCI6IjNOM2FHbDR4In19fQ==
kind: Secret
metadata:
......
name: registry
namespace: hyc
resourceVersion: "29629"
selfLink: /api/v1/namespaces/hyc/secrets/registry
uid: 017f736a-11f5-42d8-a9ed-47dad254f2c2
type: kubernetes.io/dockerconfigjson
说明 :
.dockerconfigjson中是整段 docker 登录配置的 base64 编码。Pod 使用时在spec.imagePullSecrets中引用该 Secret 即可拉取私有仓库镜像。
2.3.5 TLS 类型
创建 TLS Secret 保存公钥证书/私钥对,公钥证书必须是 .PEM 编码:
bash
Usage:
kubectl create secret tls NAME --cert=path/to/cert/file --key=path/to/key/file [--dry-run=server|client|none] [options]
先生成私钥和自签名证书(实验用,生产环境请使用正规 CA 签发的证书):
bash
#--1--生成私钥
root@master30:~# mkdir certs && cd certs
root@master30:~/certs# openssl genrsa -out www.key 2048
#--2--生成证书请求文件 csr,CN 的值必须是网站域名
root@master30:~/certs# openssl req -new -key www.key -out www.csr -subj "/C=CN/ST=JS/L=NJ/O=LM/OU=DEVOPS/CN=www.hyc.cloud/emailAddress=admin@hyc.cloud"
#--3--用自己的私钥对请求文件签名,生成有效期 3650 天的自签名证书
root@master30:~/certs# openssl x509 -req -days 3650 -in www.csr -signkey www.key -out www.crt
创建 tls 类型 Secret:
bash
root@master30:~/certs# kubectl create secret tls www-tls --cert=./www.crt --key=./www.key
2.3.6 以环境变量方式引用 Secret
与 ConfigMap 类似,Secret 也可以通过环境变量注入容器:
bash
root@master30:~# vim pod-with-env-from-secret.yaml
yaml
---
apiVersion: v1
kind: Pod
metadata:
name: mysql
labels:
name: mysql
spec:
containers:
- image: hub.laoma.cloud/library/mysql:latest
imagePullPolicy: IfNotPresent
name: mysql
ports:
- containerPort: 3306
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
# 原先通过 value 直接写死密码,现在改为从 Secret 取值(容器内看到的是解码后的明文)
valueFrom:
secretKeyRef:
name: mysecret1 # Secret 名称
key: password1 # Secret 中的 key
验证:
bash
root@master30:~# kubectl apply -f pod-with-env-from-secret.yaml
root@master30:~# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
mysql 1/1 Running 0 90s 10.224.51.131 worker31.hyc.cloud <none> <none>
# 容器内环境变量已经是解码后的明文
root@master30:~# kubectl exec -it mysql -- bash -c 'echo $MYSQL_ROOT_PASSWORD'
redhat
# 连接 MySQL 验证
root@master30:~# mysql -u root -predhat -h 10.224.51.131 --ssl-mode=DISABLED
# 清理 Pod
root@master30:~# kubectl delete pod mysql --force
2.3.7 以 Volume 方式引用 Secret
先准备 Secret:
bash
root@master30:~# echo Hello world > index.html
root@master30:~# echo error > error.html
root@master30:~# kubectl create secret generic web --from-file=./index.html --from-file=./error.html
引用整体
bash
root@master30:~# vim pod-with-volume-from-secret.yaml
yaml
apiVersion: v1
kind: Pod
metadata:
labels:
run: web
name: web
spec:
containers:
- image: hub.laoma.cloud/library/nginx:latest
imagePullPolicy: IfNotPresent
name: web
volumeMounts:
- name: webcontent
# mountPath 是挂载点,Secret 中所有键值对会解码后以文件形式挂载到该目录
mountPath: "/usr/share/nginx/html"
volumes:
- name: webcontent
secret:
secretName: web
验证:
bash
root@master30:~# kubectl apply -f pod-with-volume-from-secret.yaml
root@master30:~# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
web 1/1 Running 0 15s 10.224.51.136 worker31.hyc.cloud <none>
# 访问时 kubelet 已经自动完成 base64 解码,看到的是明文内容
root@master30:~# curl 10.224.51.136
Hello World
root@master30:~# curl 10.224.51.136/error.html
error
# 清理 Pod
root@master30:~# kubectl delete pod web --force
只引用特定 key
bash
root@master30:~# vim pod-secret-volume-single.yaml
yaml
apiVersion: v1
kind: Pod
metadata:
labels:
run: web
name: web
spec:
containers:
- image: hub.laoma.cloud/library/nginx:latest
imagePullPolicy: IfNotPresent
name: web
volumeMounts:
- name: webcontent
mountPath: "/usr/share/nginx/html"
volumes:
- name: webcontent
secret:
secretName: web
items:
- key: index.html
# 通过 path 设置挂载后的文件名,这里 path 与 key 保持一致
path: index.html
或者使用 subPath 直接指定文件:
yaml
apiVersion: v1
kind: Pod
metadata:
labels:
run: web
name: web
spec:
containers:
- image: hub.laoma.cloud/library/nginx:latest
imagePullPolicy: IfNotPresent
name: web
volumeMounts:
- name: webcontent
# 将 Secret 中的 index.html 挂载为 /usr/share/nginx/html/index.html
mountPath: "/usr/share/nginx/html/index.html"
# 使用 subPath 表示 mountPath 的值是一个文件
subPath: index.html
volumes:
- name: webcontent
secret:
secretName: web
验证:
bash
root@master30:~# kubectl create -f pod-secret-volume-single.yaml
root@master30:~# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
web 1/1 Running 0 21s 10.224.51.137 worker31.hyc.cloud <none>
root@master30:~# curl 10.224.51.137/index.html
Hello World
# 目录中只有被选中的 index.html,没有 error.html
root@master30:~# kubectl exec web -- ls /usr/share/nginx/html
index.html
以卷方式引用 Secret 时,同样支持动态更新(等待约 30 秒后生效):
bash
# 先把新内容做 base64 编码
root@master30:~# echo 'Hello Nginx' | base64
SGVsbG8gTmdpbng=
# 编辑 Secret,把 index.html 的值改成新的 base64 编码结果
root@master30:~# kubectl edit secrets web
......
apiVersion: v1
data:
index.html: SGVsbG8gTmdpbng=
......
# 等 30 秒左右再次访问,内容已经更新
root@master30:~# curl 10.224.51.137/index.html
Hello Nginx
2.3.8 挂载文件的权限
Secret 卷映射过去的文件默认权限是 0644(八进制),对应十进制 420。
通过 defaultMode 统一设置所有映射文件的权限:
yaml
volumes:
- name: foo
secret:
secretName: mysecret1
defaultMode: 256 # 十进制 256 = 八进制 0400(只读)
说明:因为 JSON/YAML 不支持八进制数字,所以这里使用十进制表示。
bash
root@master30:~# kubectl exec mypod1 -- ls -l /etc/foo/..data/user
-r-------- 1 root root 4 Aug 9 08:58 /etc/foo/..data/user
通过 items[].mode 给单独的文件设置权限:
yaml
volumes:
- name: foo
secret:
secretName: mysecret1
items:
- key: user
path: user
mode: 511 # 十进制 511 = 八进制 0777(全部权限)
bash
root@master30:~# kubectl exec mypod1 -- ls -l /etc/foo/..data/user
-rwxrwxrwx 1 root root 4 Aug 9 08:58 /etc/foo/..data/user
说明 :ConfigMap 卷同样支持
defaultMode和mode字段,用法完全一致。
2.4 综合实验:MySQL + WordPress 部署博客
使用 mysql 和 wordpress 镜像部署一个 blog 应用,要求如下:
-
部署数据库应用:MySQL 相关用户和密码使用 Secret 存储。
-
blog 站点:
- blog 站点证书使用 tls 类型 Secret 存储;
- 配置文件 vhost 使用 ConfigMap 提供;
- 应用数据使用 nfs 类型 PV 提供。
WordPress 安装包:
text
http://192.168.50.200/class/course-materials/softwares/stage01/wordpress-4.9.4-zh_CN.zip
练习提示:这道题把本文所有知识点串了起来------Secret 管密码和证书、ConfigMap 管 Nginx/Apache 的 vhost 配置、PV/PVC(NFS)管 MySQL 和 WordPress 的持久化数据。建议对照前面的实验独立完成一遍。
2.5 环境清理
bash
# 删除整个 configure 命名空间
root@master30:~# kubectl delete ns configure
三、总结
3.1 四种存储卷对比
| 对比项 | emptyDir | hostPath | NFS | PV/PVC |
|---|---|---|---|---|
| 数据位置 | 节点 kubelet 临时目录 | 节点指定目录 | NFS 服务器共享目录 | 各类后端存储(NFS/云盘/CSI 等) |
| Pod 删除后 | 数据立即删除 | 数据保留 | 数据保留 | 由回收策略决定 |
| 跨节点可用 | 否(随 Pod 绑定节点) | 否(数据在固定节点) | 是 | 取决于后端存储 |
| 生命周期 | 与 Pod 相同 | 与节点相同 | 与 NFS 服务器相同 | 独立于 Pod,由集群管理 |
| 典型场景 | 容器间临时共享、缓存 | 单节点测试、访问节点文件 | 多节点共享文件 | 生产环境持久化存储 |
3.2 PV/PVC 核心要点
- PV 是集群级资源,PVC 是命名空间级资源,Pod 通过 PVC 使用存储,不直接接触后端细节;
- 绑定时看三件事:访问模式(ROX < RWO < RWX)、容量、storageClassName;
- 三种回收策略:Retain (默认,保留数据,手动清理 claimRef 才能复用)、Recycle (已废弃)、Delete(连后端存储一起删,需插件支持,NFS 不支持);
volumeMode: Filesystem挂载为目录,volumeMode: Block呈现为裸设备(用volumeDevices)。
3.3 ConfigMap vs Secret
| 对比项 | ConfigMap | Secret |
|---|---|---|
| 数据形式 | 明文 | base64 编码(非加密) |
| 适用内容 | 配置文件、环境变量、网页文件 | 密码、令牌、镜像仓库凭据、TLS 证书 |
| 容量上限 | 1 MiB | 1 MiB |
| 环境变量引用 | valueFrom.configMapKeyRef |
valueFrom.secretKeyRef |
| 卷引用 | volumes[].configMap |
volumes[].secret |
| 卷挂载热更新 | 支持(env 与 subPath 除外) | 支持(env 与 subPath 除外) |
| 内置类型 | 无 | Opaque、dockerconfigjson、tls |
3.4 写在最后
Kubernetes 存储与配置管理可以浓缩成两条主线:
- 数据持久化主线:emptyDir(临时)→ hostPath(节点级)→ NFS(网络共享)→ PV/PVC(声明式、生产可用),存储的生命周期与 Pod 一步步解耦;
- 配置注入主线:ConfigMap 管明文配置、Secret 管敏感信息,二者都支持环境变量和卷两种引用方式,卷方式支持热更新。