📦 Kubernetes Volume(存储卷)
学习参考:卷
🧱 环境准备
🎯 思路 :先把实验环境隔离出来------建一个专门的命名空间
storage,并把当前 kubectl 的默认上下文切到它里面,这样后面所有kubectl命令都默认在这个命名空间里执行,不用每条命令都加-n storage。
bash
root@master30:~# kubectl create ns storage
root@master30:~# kubectl config set-context --current --namespace storage
逐行拆解:
| 命令 | 作用 |
|---|---|
kubectl create ns storage |
创建命名空间 storage(ns 是 namespace 的缩写)。命名空间 = 一个"虚拟房间",用来隔离资源 |
kubectl config set-context --current --namespace storage |
把当前上下文 (context)的默认命名空间改成 storage。--current 表示改当前正在使用的那个上下文,--namespace storage 指定新默认空间 |
💡 实验现象 :执行完后,再敲
kubectl get pods这类命令,K8s 会自动去 storage 这个命名空间里找资源,不用手动加-n storage了。
🗂️ Volume 类型(K8s 支持的存储种类)
Kubernetes 支持 Volume 类型有:
- emptyDir(临时目录,Pod 没了数据就没了)
- hostPath(挂宿主机目录,Pod 删了数据还在)
- gcePersistentDisk(GCP 云盘)
- awsElasticBlockStore(AWS 云盘)
- nfs(网络文件系统,最常用的共享存储)
- iscsi(IP 网络上的 SCSI 存储)
- fc (fibre channel)(光纤通道存储)
- flocker(容器存储编排)
- glusterfs(分布式文件系统)
- rbd(Ceph 块设备)
- cephfs(Ceph 文件系统)
- gitRepo(从 git 仓库拉内容)
- secret(敏感配置,如密码)
- persistentVolumeClaim(申请持久卷,生产最常用)
- downwardAPI(把 Pod 自身信息注入)
- projected(多源合并投射)
- azureFileVolume / azureDisk(Azure 云存储)
- vsphereVolume(VMware 虚拟磁盘)
- Quobyte / PortworxVolume / ScaleIO / StorageOS / local(各种厂商存储)
🎯 一句话记 :卷类型 = "数据放在哪"。本地实验重点掌握 emptyDir、hostPath、nfs、PV/PVC 这四个,生产环境核心是 PV/PVC + nfs。
📁 emptyDir(临时目录卷)
原理 :默认情况下,当 Pod 被分配到 Node 上时,会创建 emptyDir。只要 Pod 一直在该 Node 上运行,卷就一直存在;当 Pod(不管任何原因)从 Node 上被删除时,emptyDir 也会被删除,里面数据永久删除。
🚨 空目录?不,它是"临时共享盘" :emptyDir 一开始是空目录,但它最大的价值是------同一个 Pod 里的多个容器可以共享它(一个容器往里写,另一个容器能读到)。
实验:准备一个包含 2 个容器的 pod,使用 emptyDir。
bash
root@master30:~# vim pod-with-emptyDir.yaml
创建 yaml 文件(逐行解释):
yaml
apiVersion: v1 # API 版本,v1 是稳定版
kind: Pod # 资源类型是 Pod
metadata: # 元数据(名字、标签等)
name: busybox # Pod 的名字叫 busybox
labels: # 标签(给 Pod 打标记,方便选择器筛选)
app: busybox # 标签键值对:app=busybox
spec: # 期望状态(核心:要什么容器、什么卷)
volumes: # 定义卷(Pod 级别的,整个 Pod 共享)
- name: datavolume # 卷的名字叫 datavolume(下面挂载时引用)
emptyDir: {} # 卷类型是 emptyDir,{} 表示不填额外参数
containers: # 容器列表(这个 Pod 有 2 个容器)
- name: busybox1 # 第一个容器名字 busybox1
image: hub.laoma.cloud/library/busybox # 使用的镜像(busybox 是个极小的工具箱镜像)
imagePullPolicy: IfNotPresent # 镜像拉取策略:本地有就用本地,没有才去仓库拉
command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600'] # 启动命令:先输出一行字,然后睡 3600 秒(1 小时),保证容器不退出
volumeMounts: # 容器内挂载点(把这个卷挂到容器里)
- mountPath: /data # 挂载到容器的 /data 目录
name: datavolume # 挂载的是上面定义的 datavolume 卷
- name: busybox2 # 第二个容器 busybox2
image: hub.laoma.cloud/library/busybox
imagePullPolicy: IfNotPresent
command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600']
volumeMounts:
- mountPath: /data # 同样挂载到 /data
name: datavolume # 同一个卷!两个容器共享这一个目录
配置说明:
-
spec.volumes:定义卷,是默认卷类型(Pod 级别,先在 Pod 里"声明"有哪些卷)。
-
spec.containers.volumeMounts:引用卷(容器级别,把声明好的卷挂到自己某个目录下)。
执行命令(逐行解释 + 实验现象):
bash
# 创建pod
root@master30:~# kubectl apply -f pod-with-emptyDir.yaml
pod/busybox created
| 片段 | 解释 |
|---|---|
kubectl apply |
声明式创建/更新资源(把 yaml 描述的状态"应用"到集群) |
-f pod-with-emptyDir.yaml |
从该文件读取资源配置 |
pod/busybox created |
输出:Pod busybox 创建成功 ✅ |
bash
# 获取容器ID
root@master30:~# kubectl describe pod busybox |egrep -o 'Container ID.*//.{12}'
Container ID: containerd://07d189383321
Container ID: containerd://bb02c680ca8a
| 片段 | 解释 |
|---|---|
kubectl describe pod busybox |
查看 busybox 这个 Pod 的详细信息(事件、容器、挂载等) |
| ` | `(管道) |
egrep -o 'Container ID.*//.{12}' |
用正则过滤出"Container ID"开头、到 // 后 12 个字符的内容 |
.(正则) |
匹配任意一个字符 |
{12} |
前面那个字符重复 12 次 |
| 输出解释 | 这个 Pod 有 2 个容器,所以出现 2 个 Container ID:07d189383321 和 bb02c680ca8a(containerd 是容器运行时) |
bash
# 获取容器所在节点
root@master30:~# kubectl describe pod busybox |grep Node:
Node: worker32.laoma.cloud/10.1.8.32
| 片段 | 解释 |
|---|---|
grep Node: |
只保留包含 "Node:" 的那一行 |
| 输出解释 | Pod 被调度到了 worker32 节点(IP 10.1.8.32)。后面要登录 worker32 去看真实挂载 |
bash
# 登录到node查看容器挂载情况
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",
| 片段 | 解释 |
|---|---|
crictl inspect 07d189383321 |
用 crictl(CRI 容器运行时命令行工具)查看容器的详细信息(JSON 格式) |
grep datavolume |
只保留包含 datavolume 的行 |
| 输出解释 | 关键点:emptyDir 卷在宿主机上被映射到了 /var/lib/kubelet/pods/<PodUID>/volumes/kubernetes.io~empty-dir/datavolume 这个目录。注意目录名里的 ~empty-dir,证明它是 emptyDir 类型 |
bash
# 创建数据
root@master30:~# kubectl exec busybox -c busybox1 -- touch /data/b1-f1
root@master30:~# kubectl exec busybox -c busybox2 -- ls /data
b1-f1
| 命令 | 作用 |
|---|---|
kubectl exec busybox -c busybox1 -- touch /data/b1-f1 |
进入 busybox Pod 的 busybox1 容器 ,执行 touch /data/b1-f1(在 /data 下创建一个空文件 b1-f1)。-c 指定容器,-- 后面是容器里要执行的命令 |
kubectl exec busybox -c busybox2 -- ls /data |
进入 busybox2 容器,列出 /data 目录 |
输出 b1-f1 |
🎯 实验现象验证成功:busybox1 写的文件,busybox2 能看见!证明两个容器确实共享了同一个 emptyDir 卷 |
bash
# node上查看数据
root@worker32:~# ls /var/lib/kubelet/pods/8417656f-4b6c-4b2a-a2f3-f4a248c4c043/volumes/kubernetes.io~empty-dir/datavolume
b1-f1
现象 :在宿主机 worker32 上直接看那个映射目录,b1-f1 也在。说明容器里的 /data 就是宿主机这个目录,一层层都是通的。
bash
# 删除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
| 片段 | 解释 |
|---|---|
kubectl delete pod busybox |
删除 Pod |
--force |
强制立即删除(默认会先优雅终止等 30 秒) |
输出 No such file or directory |
🎯 实验现象验证成功 :Pod 一删,整个目录(含数据)都没了!这就是 emptyDir 的特性------临时数据,Pod 亡则数据亡 |
💡 容器删除后,需要等待一些时间,临时卷数据等待才会删除。(K8s 清理是异步的,不会瞬间完成)
emptyDir 适合放什么? :临时缓存、日志暂存、两个容器之间交换数据。不适合放数据库数据(Pod 一重建数据就没了)⚠️。
🗄️ hostPath(宿主机目录卷)
原理 :hostPath 允许 Pod 将 Node(宿主机)文件系统中某个目录 挂载到 Pod 内部。Pod 删除后,hostPath 卷数据保留(因为数据在宿主机上,不在 Pod 里)。
⚠️ 对比 emptyDir :emptyDir 在宿主机上的目录是 K8s 自动建在
/var/lib/kubelet/pods/...下的"临时区",Pod 删了目录也删;hostPath 挂的是你指定的宿主机目录 (比如/busyboxdir),K8s 不管它,所以数据会一直保留。
实验:准备一个 pod,使用 hostPath。
bash
root@master30:~# vim pod-with-hostPath.yaml
创建 yaml 文件(逐行解释):
yaml
apiVersion: v1 # API 版本
kind: Pod # 资源类型
metadata:
name: busybox # Pod 名
labels:
app: busybox # 标签
spec:
volumes: # 声明卷
- name: datavolume # 卷名 datavolume
hostPath: # 卷类型:hostPath(挂宿主机目录)
path: /busyboxdir # 宿主机上的目录路径(不存在会自动创建)
containers:
- name: busybox1 # 容器 1
image: hub.laoma.cloud/library/busybox
imagePullPolicy: IfNotPresent
command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600']
volumeMounts:
- mountPath: /data # 容器内挂到 /data
name: datavolume # 挂 datavolume 卷
- name: busybox2 # 容器 2
image: hub.laoma.cloud/library/busybox
imagePullPolicy: IfNotPresent
command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600']
volumeMounts:
- mountPath: /data
name: datavolume
# 设置readOnly,控制读写,默认值是false,也就是读写访问。
readOnly: false # 只读控制:false=可读可写(默认);改成 true 就只能读
执行命令(逐行解释 + 实验现象):
bash
# 创建pod
root@master30:~# kubectl apply -f pod-with-hostPath.yaml
pod/busybox created
bash
# 获取容器ID
root@master30:~# kubectl describe pod busybox |egrep -o 'Container ID.*//.{12}'
Container ID: containerd://318922d0c666
Container ID: containerd://0688146babe5
(2 个容器 → 2 个容器 ID:318922d0c666、0688146babe5)
bash
# 获取容器所在节点
root@master30:~# kubectl describe pod busybox |grep Node:
Node: worker32.laoma.cloud/10.1.8.32
(Pod 调度到了 worker32)
bash
# 登录到worker31查看容器挂载情况
root@worker32:~# crictl inspect 318922d0c666|grep busyboxdir
"hostPath": "/busyboxdir",
"host_path": "/busyboxdir"
"source": "/busyboxdir",
现象解释 :crictl inspect 显示容器的卷来源 source 直接就是宿主机根目录下的 /busyboxdir(对比 emptyDir 是 kubelet 下面的一长串路径)。证明 hostPath 就是"裸挂宿主机目录"。
bash
# 创建数据
root@master30:~# kubectl exec busybox -c busybox1 -- touch /data/b1-f1
root@master30:~# kubectl exec busybox -c busybox2 -- ls /data
b1-f1
root@master30:~# kubectl exec busybox -c busybox2 -- touch /data/b2-f2
touch: /data/b2-f2: Read-only file system
现象解释 :busybox1 创建 b1-f1 成功,busybox2 能看到。但是 busybox2 再创建 b2-f2 时报错 Read-only file system(只读文件系统)------为什么?
🤔 思考 :yaml 里
readOnly: false是写在 busybox2 容器的 volumeMounts 下的,明明是 false 可写啊?🎯 真正的坑 :
readOnly是对当前容器 的挂载是否只读做限制。但 busybox2 里这个字段虽然是 false,为什么还报只读?原因要看宿主机目录权限 ------如果/busyboxdir在宿主机上属于 root 且其他用户没有写权限,而 busybox 容器默认以非特权用户运行,写入就会被拒绝。这是 hostPath 常见坑:宿主机目录的属主/权限决定能不能写。
bash
# node上查看数据
root@worker32:~# ls /busyboxdir/
b1-f1
现象 :宿主机 /busyboxdir/ 下能看到 b1-f1,数据落在宿主机上 ✅。
bash
# 删除pod,验证hostPath
root@master30:~# kubectl delete pod busybox --force
root@worker32:~# ls /busyboxdir/
b1-f1
# pod删除后,hostPath卷数据保留。
现象验证成功 :Pod 删除后,/busyboxdir/ 里的 b1-f1 还在!这就是 hostPath 和 emptyDir 的本质区别------数据在宿主机上,Pod 生死不影响它。
⚠️ hostPath 的坑 :数据只在这一台宿主机上。如果 Pod 被调度到另一台 Node,就看不到这些数据了。所以生产环境几乎不用 hostPath,除非是守护进程日志(如 kubelet 自身)这类场景。
🌐 NFS 存储(网络共享存储)
原理 :NFS 卷,将数据存储在 NFS 共享中。数据放在独立的 NFS 服务器上,集群里任何节点都能挂载访问------这是生产环境最常用的共享存储方案。
🧰 准备 NFS 共享
🎯 实验思路 :在 master30 上装 NFS 服务端 (提供共享),在 worker31、worker32 上装 NFS 客户端 (负责挂载),然后建一个共享目录
/nfsshares,让整个集群都能读写它。
bash
# 安装 NFS server(在 master30 上)
root@master30:~# apt install -y nfs-kernel-server
| 片段 | 解释 |
|---|---|
apt install -y nfs-kernel-server |
Debian/Ubuntu 系安装 NFS 服务端软件包。-y 全程自动确认,不弹提问 |
bash
# 创建NFS目录,修改创建文件夹的权限
root@master30:~# mkdir -m 777 /nfsshares
root@master30:~# echo hello laoma > /nfsshares/index.html
| 命令 | 作用 |
|---|---|
mkdir -m 777 /nfsshares |
创建共享目录,-m 777 直接把权限设为 777(所有人可读可写可执行,实验环境图方便;生产建议按需收紧) |
echo hello laoma > /nfsshares/index.html |
往目录里写一个测试网页文件 index.html,内容 hello laoma。> 是重定向:把输出写入文件 |
bash
# 配置共享,允许所有客户端访问
root@master30:~# cat << EOF > /etc/exports
/nfsshares *(rw)
EOF
| 片段 | 解释 |
|---|---|
cat << EOF > /etc/exports |
heredoc 语法:把下面两行内容原样写入 /etc/exports 文件,直到遇到 EOF 为止 |
/nfsshares *(rw) |
NFS 导出规则:共享 /nfsshares 目录;* 表示允许所有客户端;(rw) 表示读写权限 |
bash
# 重启 nfs server
root@master30:~# systemctl restart nfs-server.service
| 片段 | 解释 |
|---|---|
systemctl restart nfs-server.service |
重启 NFS 服务,让 /etc/exports 新配置生效 |
bash
# 客户端安装
root@worker31:~# apt install -y nfs-common
root@worker32:~# apt install -y nfs-common
| 命令 | 作用 |
|---|---|
apt install -y nfs-common |
在 worker 节点装 NFS 客户端工具(nfs-common),这样 kubelet 才有能力把 NFS 卷挂到容器里 |
🧪 准备 pod
bash
root@master30:~# vim pod-with-nfs.yaml
创建 yaml 文件(逐行解释):
yaml
apiVersion: v1 # API 版本
kind: Pod # 资源类型
metadata:
labels:
run: nginx # 标签 run=nginx
name: nginx # Pod 名 nginx
spec:
volumes: # 声明卷
- name: nfs # 卷名 nfs
nfs: # 卷类型:nfs
server: 10.1.8.30 # NFS 服务器 IP(就是 master30)
path: "/nfsshares" # NFS 服务端导出的共享路径
containers:
- image: hub.laoma.cloud/library/nginx # nginx 镜像
name: nginx # 容器名 nginx
volumeMounts:
- name: nfs # 挂 nfs 卷
mountPath: "/usr/share/nginx/html" # 挂到 nginx 网页根目录(这样 nginx 显示的就是 NFS 里的文件)
执行命令(逐行解释 + 实验现象):
bash
# 创建pod
root@master30:~# kubectl apply -f pod-with-nfs.yaml
pod/nginx created
bash
# 获取容器ID
root@master30:~# kubectl describe pod nginx |egrep -o 'Container ID.*//.{12}'
Container ID: containerd://c5e734dab9b5
(单容器 Pod,一个容器 ID c5e734dab9b5)
bash
# 获取容器所在节点
root@master30:~# kubectl describe pod nginx |grep Node:
Node: worker32.laoma.cloud/10.1.8.32
bash
# 登录到worker31查看容器挂载情况
root@worker32:~# crictl inspect c5e734dab9b5|grep |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",
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
| 片段 | 解释 |
|---|---|
crictl inspect c5e734dab9b5 |
查看容器详情 |
grep -e nfs -e html |
同时过滤包含 nfs 或 html 的行(-e 表示多个匹配模式) |
| `df | grep nfsshare` |
输出 10.1.8.30:/nfsshares ... 7% /var/lib/kubelet/pods/.../kubernetes.io~nfs/nfs |
🎯 关键现象 :这台 worker 上把 10.1.8.30 上的 /nfsshares 通过网络挂载到了本地 kubelet 目录(卷类型 ~nfs 证明走的是 NFS)。这一行证明了"网络存储"------数据不在本地磁盘,而在远程 NFS 服务器 |
bash
# 访问容器
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.laoma.cloud <none> <none>
root@master30:~# curl 10.224.51.171
hello laoma
| 片段 | 解释 |
|---|---|
kubectl get pod -o wide |
查看 Pod 列表,-o wide 显示更多列(含 Pod IP、所在节点) |
curl 10.224.51.171 |
用 HTTP 访问 nginx Pod 的 IP |
输出 hello laoma |
🎯 实验现象验证成功:nginx 返回的正是 NFS 目录里 index.html 的内容(hello laoma)!说明 nginx 网页根目录确实指向了 NFS 共享 |
bash
# 创建数据
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
现象解释 :进入 nginx 容器创建 test.html 后,在 NFS 服务器(master30)的 /nfsshares 目录直接能看到 test.html------数据穿过网络落在了 NFS 服务器上 ✅。
bash
# 删除pod,验证nfs卷数据
root@master30:~# kubectl delete pod nginx --force
pod "nginx" deleted
root@master30:~# ls /nfsshares
index.html test.html
# pod删除后,nfs卷数据保留。
现象验证成功 :Pod 删除后,NFS 上的数据还在。NFS 是真正的共享持久存储------数据在独立服务器上,任何 Pod、任何节点都能访问。
💾 持久性存储(PV / PVC)
学习参考:持久卷
为什么要有 PV/PVC? 前面 emptyDir、hostPath、NFS 都是"直接写死在 Pod 的 yaml 里"。问题是:开发人员根本不该关心底层存储是 NFS 还是云盘 。PV/PVC 就是把这个解耦------管理员提供存储(PV),开发按需申请(PVC)。
Kubernetes 使用 persistent volume(PV)架构为集群提供永久存储。
🧩 PV 和 PVC 架构
开发人员不知道特定云环境的细节的情况下,只需要使用 persistentVolumeClaim(PVC)请求 PV 资源,实现持久化存储。
-
Persistent Volume(PV) ,由 PersistentVolume API 对象定义,代表集群中现有存储。PV 的生命周期与使用其的 pod 无关。PV 是集群级别资源。
-
Persistent Volume Claim(PVC) ,由 PersistentVolumeClaim API 对象定义,代表开发人员请求 PV。PVC 是 namespace 级别资源。
🎯 类比 :PV = 仓库里的现货商品 (管理员进货);PVC = 你下的采购单(按需求申请)。K8s 会自动把匹配的 PV 和 PVC"绑定"(Bound)起来。
🔨 创建 PV 和 PVC
创建 PV
集群管理员可以创建任意数量 PV,取决于后端存储。为了方便演示,这里使用 NFS 后端存储。
pv 文件示例:
bash
root@master30:~# vim pv.yaml
创建 PV yaml(逐行解释):
yaml
apiVersion: v1 # API 版本
kind: PersistentVolume # 资源类型:持久卷
metadata:
name: web # PV 名字叫 web
spec:
capacity: # 容量(多大)
storage: 5Gi # 5Gi(Gi 是 2 进制吉字节)
volumeMode: Filesystem # 卷模式:文件系统(默认);另一种是 Block 块设备
accessModes: # 访问模式(谁能以什么方式挂)
- ReadWriteOnce # RWO:只能被一个节点以读写方式挂载
nfs: # 后端存储类型:NFS
path: /nfsshares # NFS 共享路径
server: 10.1.8.30 # NFS 服务器 IP
执行:
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
输出逐列解释:
| 列 | 值 | 含义 |
|---|---|---|
| NAME | web | PV 名字 |
| CAPACITY | 5Gi | 容量 5Gi |
| ACCESS MODES | RWO | 访问模式 ReadWriteOnce |
| RECLAIM POLICY | Recycle | 回收策略(默认 Recycle,已被废弃,后面细讲) |
| STATUS | Available | 状态:可用(还没被任何 PVC 绑定) |
| CLAIM | (空) | 还没有被哪个 PVC 认领 |
| STORAGECLASS | (空) | 没指定存储类 |
| AGE | 3s | 已存在 3 秒 |
创建 PVC
用户创建 PVC,pod 使用 PVC 申请特定容量、特定 modes 和特定存储类别的存储。master 监控 PVCs,查找匹配的 PV 或者等待后端存储创建相应 PV,然后绑定 PV 和 PVC。
pvc 示例:
bash
root@master30:~# vim pvc.yaml
创建 PVC yaml(逐行解释):
yaml
apiVersion: v1 # API 版本
kind: PersistentVolumeClaim # 资源类型:持久卷声明
metadata:
name: webclaim # PVC 名字
spec:
accessModes: # 申请的访问模式
- ReadWriteOnce # RWO(要和 PV 匹配)
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 webclaim 状态变成 Bound(已绑定) ,绑定的卷是 web------K8s 自动把"申请单"和"现货"匹配上了 ✅。
创建 Pod 使用 PV
bash
root@master30:~# vim pod-with-pvc.yaml
创建 Pod yaml(逐行解释):
yaml
apiVersion: v1 # API 版本
kind: Pod # 资源类型
metadata:
name: web # Pod 名 web
labels:
name: web # 标签
spec:
containers:
- image: hub.laoma.cloud/library/nginx # nginx 镜像
name: web # 容器名
ports: # 端口声明
- containerPort: 80 # 容器监听 80 端口
name: web-port # 端口名
volumeMounts: # 容器挂载
- name: web-persistent-storage # 挂哪个卷
mountPath: /usr/share/nginx/html # 挂到 nginx 网页目录
volumes: # 声明卷
- name: web-persistent-storage # 卷名
persistentVolumeClaim: # 卷类型:引用 PVC
claimName: webclaim # 引用刚才的 PVC webclaim
执行 + 现象:
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.laoma.cloud <none> <none>
# 测试:访问pod web页面
root@master30:~# curl http://10.224.193.69
hello laoma
root@master30:~# echo test > /nfsshares/test.html
root@master30:~# curl http://10.224.193.69/test.html
test
现象解释:
curl http://10.224.193.69返回 hello laoma(就是 NFS 里 index.html 的内容,因为 PVC 绑定的 PV 背后是 NFS)- 往 NFS 服务器写了个 test.html,马上就能从 Pod 访问到 → 数据是真的持久化了,Pod 换成谁都行
bash
# 删除pod
root@master30:~# kubectl delete pod web --force
🎯 PV/PVC 的价值 :Pod 删了,PVC 还在,PV 还在,数据还在。下次建个新 Pod 引用同一个 PVC,数据全回来。这才是"持久化存储"的真正含义。
🔑 PV accessModes(访问模式)
PersistentVolume 卷访问模式有:
-
ReadWriteOnce(RWO) ,卷可以被一个节点以读写方式挂载,也允许同一节点上的多个 Pod 读写访问卷。
-
ReadOnlyMany(ROX) ,卷可以被多个节点以只读方式挂载。
-
ReadWriteMany(RWX) ,卷可以被多个节点以读写方式挂载。
-
ReadWriteOncePod(RWOP) ,卷可以被单个 Pod 以读写方式挂载。如果你想确保整个集群中只有一个 Pod 可以读取或写入该 PVC,请使用 ReadWriteOncePod 访问模式。这只支持 CSI 卷以及需要 Kubernetes 1.22 以上版本。
这篇博客文章 Introducing Single Pod Access Mode for PersistentVolumes 描述了更详细的内容。
在命令行接口(CLI)中,访问模式也使用以下缩写形式:
| 缩写 | 全称 | 含义 |
|---|---|---|
| RWO | ReadWriteOnce | 单节点读写 |
| ROX | ReadOnlyMany | 多节点只读 |
| RWX | ReadWriteMany | 多节点读写 |
| RWOP | ReadWriteOncePod | 单 Pod 读写 |
不同存储后端支持不同的模式:
| 卷插件 | ReadWriteOnce | ReadOnlyMany | ReadWriteMany | ReadWriteOncePod |
|---|---|---|---|---|
| AzureFile | ✓ | ✓ | ✓ | - |
| CephFS | ✓ | ✓ | ✓ | - |
| CSI | 取决于驱动 | 取决于驱动 | 取决于驱动 | 取决于驱动 |
| FC | ✓ | ✓ | - | - |
| FlexVolume | ✓ | ✓ | 取决于驱动 | - |
| GCEPersistentDisk | ✓ | ✓ | - | - |
| Glusterfs | ✓ | ✓ | ✓ | - |
| HostPath | ✓ | - | - | - |
| iSCSI | ✓ | ✓ | - | - |
| NFS | ✓ | ✓ | ✓ | - |
| RBD | ✓ | ✓ | - | - |
| VsphereVolume | ✓ | - | -(Pod 运行于同一节点上时可行) | - |
| PortworxVolume | ✓ | - | ✓ | - |
重要: 每个卷同一时刻只能以一种访问模式挂载,即使该卷能够支持多种访问模式。例如,一个 GCEPersistentDisk 卷可以被某节点以 ReadWriteOnce 模式挂载,或者被多个节点以 ReadOnlyMany 模式挂载,但不可以同时以两种模式挂载。
🎯 常见的 NAS 存储都支持三种存储模式:ReadWriteOnce、ReadOnlyMany、ReadWriteMany。(NFS 就是典型,所以表格里 NFS 三种都打 ✓)
💽 PV volumeModes(卷模式)
特性状态: Kubernetes v1.18 [stable]
针对 PV 持久卷,Kubernetes 支持两种卷模式(volumeModes):Filesystem(文件系统) 和 Block(块)。volumeMode 是一个可选的 API 参数。如果该参数被省略,默认的卷模式是 Filesystem。
-
Filesystem 卷 ,会被 Pod 挂载(Mount) 到某个目录。如果卷的存储来自某块设备而该设备目前为空,Kubernetes 会在第一次挂载卷之前在设备上创建文件系统。
-
Block 卷,会被作为原始块设备来使用。这类卷以块设备的方式交给 Pod 使用,其上没有任何文件系统。这种模式对于为 Pod 提供一种使用最快可能方式来访问卷而言很有帮助,Pod 和卷之间不存在文件系统层。另外,Pod 中运行的应用必须知道如何处理原始块设备。
🤔 通俗理解:Filesystem = 给你一个"已格式化好的 U 盘",直接往里放文件;Block = 给你一个"裸盘",怎么分区格式化由应用自己说了算(性能更高,但使用门槛高)。
🔗 PVC 与 PV 匹配规则
-
PV 的 mode 必须高于 PVC 申请的最低要求:mode 优先级,可简单理解为 ROX<RWO<RWX。例如,用户请求 RWO 模式 PV,但是目前只有 NFS PV(RWO+ROX+RWX),PVC 将匹配 NFS。
-
容量满足最低要求:具有相同 modes 卷会被分组,然后根据 size 分类(由小到大)。
-
pv storage classes(存储类):用于对 pv 进行分类,pvc 可以根据 storageClassName 参数申请特定类型 pv。如果 pv 设置了 storageClassName,那么 pvc 申请资源的时候也要指定 storageClassName。例如,storageClassName 指定为 ns1-storage,ns1 中 pvc 申请也指定 storageClassName 为 ns1-storage。
🎯 一句话总结匹配规则:模式要够、容量要够、类别要一致,三条都满足才绑定。
♻️ PV 回收策略
当用户不再使用其存储卷时,他们可以从 API 中将 PVC 对象删除,从而允许该资源被回收再利用。PersistentVolume 对象的回收策略告诉集群,当其被从申领中释放时如何处理该数据卷。
PersistentVolume 回收策略支持:Retain(保留)、Recycle(回收)、Delete(删除)。
Retain(保留)
回收策略 Retain 使得用户可以手动回收资源。当 PersistentVolumeClaim 对象被删除时,PersistentVolume 卷仍然存在,对应的数据卷被视为"已释放(released)"。由于卷上仍然保留上一次关联的 pvc 信息,清理掉上一次关联的 pvc 信息才可分配给其他 pvc。Retain(保留)策略是默认策略。
🚨 记住 :Retain = 数据最安全。PVC 删了,PV 变成 Released(已释放)状态,数据还在,但 PV 暂时"卡住"不能被新 PVC 用,除非管理员手动清掉 claimRef 信息。
示例:
bash
root@master30:~# vim pv-pvc-Retain.yaml
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
💡
---是 YAML 文档分隔符,表示一个文件里包含多个独立文档(这里一个 PV + 一个 PVC 写在一个文件里)。
执行 + 现象:
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
现象 :PV 的 RECLAIM POLICY 显示 Retain(默认),STATUS Bound,绑定了 default 命名空间的 webclaim。
删除 pvc 验证:
bash
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
现象 :PVC 删掉后,PV 状态变成 Released(已释放) ,还带着 CLAIM 记录 default/webclaim------它还记得自己曾经被谁用过。此时数据还在,但这个 PV 暂时不能给别的 PVC 用了。
bash
# 创建新pvc,无法绑定
root@master30:~# vim pvc-db.yaml
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
现象 :新 PVC dbclaim 状态是 Pending(一直等)------容量够、模式够,但为什么绑定不上?
🤔 原因:因为 web 这个 PV 还是 Released 状态,身上残留着旧 PVC 的 claimRef("前任"的信息),K8s 认为它还没被释放干净,不能给新 PVC 用。
bash
# 手动清理卷上claimRef信息,删除claimRef部分
root@master30:~# kubectl edit pv web
yaml(edit 出来的内容,重点看注释部分):
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
......
🎯 操作 :
kubectl edit pv web会用编辑器打开 PV 的当前定义,把 claimRef 那一整段删掉(或注释掉),保存退出。claimRef 就是"前任印记",删掉后 PV 就自由了。(注意笔记里 nfs path 被改成了 /web,可能与实际不符,重点是删 claimRef 这个动作。)
bash
# dbclaim 绑定成功
root@master30:~# kubectl get pvc dbclaim
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
dbclaim Bound web 5Gi RWO 3m49s
现象验证成功 :删掉 claimRef 后,dbclaim 立刻 Bound 到 web 了 ✅。
清理环境:
bash
root@master30:~# kubectl delete pvc dbclaim
root@master30:~# kubectl delete pv web
root@master30:~# ls /nfsshares/
index.html test.html
**注意:**删除 pv,并不会删除后端存储中数据。(NFS 里的 index.html、test.html 还在!)
Recycle(回收)
**警告:**回收策略 Recycle 已被废弃。取而代之的建议方案是使用动态制备。
Recycle(回收)策略的效果与 Retain(保留)一致。
🤔 老版本默认就是 Recycle,它会在 PV 释放时把数据清空(跑
rm -rf),然后变回 Available 可复用。但因为太"暴力",已被废弃。
Delete(删除)
对于支持 Delete 回收策略的卷插件,删除动作会将 PersistentVolume 对象从 Kubernetes 中移除,同时也会从外部基础设施(如 AWS EBS 或 GCE PD 卷)中移除所关联的存储资产。
示例:
bash
root@master30:~# vim pv-pvc-Delete.yaml
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 Retain Bound default/webclaim 22s
root@master30:~# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
webclaim Bound web 5Gi RWO 23s
(刚 apply 完,旧的 web PV 还在显示 Retain?这是之前那个 Retain 的 PV 还在。看下一步,删除 PVC 后的表现。)
删除 pvc 验证:
bash
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
root@master30:~# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
web 5Gi RWO Delete Failed default/webclaim 33s
现象 :新的 Delete 策略 PV 在 PVC 被删后状态变成 Failed(失败)。
bash
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
错误逐词解释:
| 片段 | 解释 |
|---|---|
VolumeFailedDelete |
卷删除失败事件 |
persistentvolume-controller |
K8s 里负责 PV 生命周期管理的控制器 |
no deletable volume plugin matched |
没有找到可删除的卷插件 ------即 NFS 这种卷插件不支持 Delete(NFS 的 PV 没有"删除云端资产"的能力,只有云盘类插件如 AWS EBS、GCE PD 支持) |
kubernetes 提示信息为 Error getting deleter volume plugin for volume "web": no deletable volume plugin matched,也就是未找到删除插件,所以无法删除。
此时我们可以手动删除 pv,手动删除 pv,并不会删除后端存储数据。
🎯 总结三种回收策略:
策略 行为 数据安全 适用 Retain(默认) PVC 删后 PV 变 Released,数据保留,手动清 claimRef 才能复用 最安全 生产默认 Recycle(废弃) 清空数据后回 Available 数据丢 已废弃 Delete 把 PV 和底层存储一起删 看插件支持 仅云盘类插件
思考
问题:如何创建一个只能绑定给特定 pvc 的 pv?
答案:在定义 pv 的时候,指定 claimRef 属性。
yaml:
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 # 并且必须在 test 命名空间里
🤔 为什么有用:通过 claimRef 可以"锁死"PV 归属,防止别的 PVC 抢走。之前 Retain 例子里的"残留 claimRef 导致 Pending"正好反证了这一点。
🧊 PV 卷的类型
PV 持久卷是用插件的形式来实现的。Kubernetes 目前支持以下插件:
csi- 容器存储接口 (CSI),新一代标准fc- Fibre Channel (FC) 存储hostPath- HostPath 卷(仅供单节点测试使用;不适用于多节点集群;请尝试使用local卷作为替代)iscsi- iSCSI (SCSI over IP) 存储local- 节点上挂载的本地存储设备nfs- 网络文件系统 (NFS) 存储
以下的持久卷已被弃用。这意味着当前仍是支持的,但是 Kubernetes 将来的发行版会将其移除。
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 之后不可用)
🎯 记法 :新项目一律用 CSI(容器存储接口);老的内置(In-Tree)插件会逐步被淘汰。本课程核心记住 NFS + CSI 就够了。
🧊 原始块卷支持
特性状态: 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 # 卷模式:Block(块设备,不是文件系统)
persistentVolumeReclaimPolicy: Retain
fc: # 后端:光纤通道存储
targetWWNs: ["50060e801049cfd1"] # 光纤通道目标的 WWN 标识
lun: 0 # 逻辑单元号
readOnly: false
pvc 示例:
yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: block-pvc
spec:
accessModes:
- ReadWriteOnce
volumeMode: Block # 申请块卷(必须和 PV 的 volumeMode 一致)
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 中添加原始块设备时,你要在容器内设置设备路径而不是挂载路径。
⚠️ 关键区别 :文件系统卷用
volumeMounts(挂目录);块卷用volumeDevices(给设备路径)。块卷没有目录、没有文件,应用直接读写裸设备。
🧹 环境清理
bash
root@master30:~# kubectl delete ns storage
| 片段 | 解释 |
|---|---|
kubectl delete ns storage |
删除整个 storage 命名空间,里面所有 Pod、PVC、PV(注意:PV 是集群级的,删 ns 不一定删 PV,但本实验一并清理即可) |
⚙️ Kubernetes Configure(配置管理)
🎯 为什么需要 ConfigMap / Secret?
容器里程序的配置(密码、参数、配置文件)如果写死在镜像里,每次改配置都要重新打镜像。K8s 的做法是:配置抽出来放一边(ConfigMap 存普通配置、Secret 存敏感配置),Pod 运行时再"注入"进去------改配置不用重新打包,改完自动生效。
🧱 环境准备
bash
root@master30:~# kubectl create ns configure
root@master30:~# kubectl config set-context --current --namespace configure
(和 Volume 实验一样:建 configure 命名空间并切换默认上下文,隔离实验环境)
📋 ConfigMap
学习参考:ConfigMap
ConfigMap 介绍
ConfigMap 是一个存储其他对象所需要使用的配置的 API 对象。ConfigMap 使用 data 和 binaryData 字段,保存键值对数据。ConfigMap:存放普通配置文件、参数,明文,不要放密码。配置和容器镜像分离,改配置不用重新做镜像。
data字段,用来保存 UTF-8 字符串。binaryData字段,用来保存二进制数据(base64 编码的字串)。
ConfigMap 的名字必须是一个合法的 DNS 子域名 。data 或 binaryData 字段下面的每个键的名称都必须由字母、数字或者 -、_ 或 . 组成。在 data 下保存的键名不可以与在 binaryData 下出现的键名有重叠。
从 v1.19 开始,你可以添加一个 immutable 字段到 ConfigMap 定义中,创建不可变更的 ConfigMap。
ConfigMap 使用建议
-
使用 ConfigMap 存储配置数据,不存数据 / 文件,上限 = 1 MiB。
-
**ConfigMap 在设计上不是用来保存大量数据的。**如果你需要保存大量数据,考虑使用挂载存储卷或者使用独立的数据库或者文件服务。
⚠️ 记住两条红线:① 只存配置,不存业务数据;② 单个体积上限 1MiB,别硬塞大文件。
ConfigMap 创建
使用帮助
bash
kubectl create configmap NAME [--from-file=[key=]source]
[--from-literal=key1=value1] [--dry-run=server|client|none] [options]
| 参数 | 作用 |
|---|---|
NAME |
ConfigMap 的名字 |
--from-file=[key=]source |
从一个文件(或目录)读内容作为 value;key= 可自定义键名,不写则用文件名作键 |
--from-literal=key1=value1 |
直接指定键值对(literal=字面量) |
--dry-run |
只"演练"不真创建,输出将要创建的对象 yaml,-o yaml 配合看效果 |
键值对类型
bash
root@master30:~# kubectl create configmap mysql --from-literal=password=redhat
root@master30:~# kubectl get configmaps mysql -o yaml |grep ^data -A1
data:
password: redhat
| 命令 | 作用 |
|---|---|
kubectl create configmap mysql --from-literal=password=redhat |
创建名为 mysql 的 ConfigMap,里面存一个键值对 password=redhat |
kubectl get configmaps mysql -o yaml |
查看这个 ConfigMap 的完整 yaml 定义 |
| ` | grep ^data -A1` |
| 输出 | data: 下面是 password: redhat------确认键值对已存进去 ✅ |
文件类型
bash
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
| 命令 | 作用 |
|---|---|
echo Hello World > index.html |
创建 index.html 文件,内容 "Hello World" |
kubectl create configmap web1 --from-file=./index.html |
把 index.html 的内容塞进 ConfigMap。文件名 index.html 自动成为键,文件内容是值 |
| 输出 | data: → `index.html: |
目录类型
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
| 命令 | 作用 |
|---|---|
echo error > error.html |
再建一个 error.html |
mkdir web2 |
建目录 web2 |
mv index.html error.html web2 |
把两个文件都移进 web2 目录 |
kubectl create configmap web2 --from-file=./web2 |
从目录创建:目录里每个文件 → 一个键值对 |
| 输出 | ConfigMap 里同时有 error.html 和 index.html 两个键 ✅ |
ConfigMap 引用
环境变量方式引用
**注意:**环境变量属于特定容器级别。
bash
root@master30:~# vim pod-with-env-from-configmap.yaml
yaml(逐行解释):
yaml
---
apiVersion: v1
kind: Pod
metadata:
name: mysql
labels:
name: mysql
spec:
containers:
- image: hub.laoma.cloud/library/mysql:latest # mysql 镜像
imagePullPolicy: IfNotPresent
name: mysql
ports:
- containerPort: 3306 # mysql 默认端口
name: mysql
env: # 容器环境变量
- name: MYSQL_ROOT_PASSWORD # 环境变量名(mysql 镜像要求的 root 密码变量)
# 原先通过value设置环境变量值(即直接 value: xxx)
valueFrom: # 改为"从别处取值"
configMapKeyRef: # 从 ConfigMap 取
name: mysql # 用名为 mysql 的 ConfigMap
key: password # 取其 password 键的值(redhat)
🎯 效果 :容器里会自动多一个环境变量
MYSQL_ROOT_PASSWORD=redhat,mysql 一启动就用它当 root 密码。密码配置完全脱离镜像。
验证:
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.laoma.cloud <none>
root@master30:~# kubectl exec -it mysql -- bash -c 'echo $MYSQL_ROOT_PASSWORD'
redhat
| 命令 | 作用 |
|---|---|
kubectl exec -it mysql -- bash -c 'echo $MYSQL_ROOT_PASSWORD' |
进入 mysql 容器,用 bash 执行 echo $MYSQL_ROOT_PASSWORD(打印环境变量值)。-it 交互式终端 |
输出 redhat |
🎯 验证成功:容器里确实拿到了 ConfigMap 里的值 ✅ |
bash
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
| 命令 | 作用 |
|---|---|
apt install -y mysql-client |
在 master30 上装 mysql 客户端,用来连容器里的 mysql |
mysql -u root -predhat -h 10.224.193.70 |
连到 Pod 的 mysql:-u root 用户 root,-predhat 密码 redhat(-p 后紧跟密码,无空格),-h 目标 IP |
mysql: [Warning] ... insecure |
警告:命令行明文带密码不安全(不影响使用) |
mysql> quit → Bye |
成功进入 mysql 交互界面并退出 ✅(实验现象:说明 ConfigMap 注入的密码真的能用) |
bash
# 删除 pod
root@master30:~# kubectl delete pod mysql --force
以 volume 方式引用
**注意:**volumes 属于 pod 级别,通过 volumeMounts 挂载。(环境变量是"单容器级别",卷是"Pod 级别、所有容器共享"。)
引用整体
bash
root@master30:~# vim pod-with-volume-from-configmap.yaml
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值是一个挂载点,将configuremap中所有键值对挂载过来
mountPath: "/usr/share/nginx/html" # 把 ConfigMap 内容挂到 nginx 网页目录
volumes:
- name: webcontent
configMap: # 卷类型:configMap
name: web2 # 用 web2 这个 ConfigMap(里面有两个文件:index.html / error.html)
🎯 效果 :web2 里的每个键都会变成一个文件 挂到挂载目录下------index.html、error.html 两个文件出现在
/usr/share/nginx/html/,nginx 直接就能提供网页。
验证:
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.laoma.cloud <none> <none>
root@master30:~# kubectl exec web -- ls /usr/share/nginx/html
error.html
index.html
现象:进入 web 容器,网页目录里正好有 error.html 和 index.html 两个文件(ConfigMap 的键变成了文件名)✅。
bash
root@master30:~# curl http://10.224.193.69
Hello World
root@master30:~# curl http://10.224.193.69/error.html
error
现象 :访问 Pod 根路径返回 Hello World(index.html 内容),访问 /error.html 返回 error ✅。配置即网页,秒变静态站。
引用特定 key
bash
root@master30:~# vim pod-cm-volume-single.yaml
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
configMap:
name: web2
items: # 只挑选部分键(而不是全部)
- key: index.html # 只要 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" # mountPath 直接指向一个文件
subPath: index.html # subPath:只挂卷里这个键,不挂整个卷
volumes:
- name: webcontent
configMap:
name: web2
🤔 items 和 subPath 的区别:
items:卷级筛选------声明"这个卷只投影哪几个键"。subPath:挂载级------"整个卷我不用,只把其中某个文件单独挂到指定文件位置"。- 两者都能实现"只取一个键",但 subPath 更适合"覆盖容器里某一个具体配置文件"(比如只覆盖 nginx.conf)。
**被挂载的 ConfigMap 内容会被自动更新。**当卷中使用的 ConfigMap 被更新时,所投射的键最终也会被更新。kubelet 组件会在每次周期性同步时检查所挂载的 ConfigMap 是否为最新。
🎯 注意 :卷方式引用支持热更新(改 ConfigMap 后文件自动变);环境变量方式不支持(环境变量在容器启动时一次性注入)。
k8s 中 ConfigMap(以 kube-proxy 为例)
看系统里现成的 ConfigMap:
bash
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
现象:kube-system 命名空间(K8s 系统组件住的地方)里有一堆 ConfigMap,DATA 列显示每个 CM 里有多少个键。kube-proxy 有 2 个键。
bash
root@master30:~# kubectl get configmaps kube-proxy -n kube-system -o yaml
apiVersion: v1
data:
config.conf: |-
apiVersion: kubeproxy.config.k8s.io/v1alpha1
bindAddress: 0.0.0.0
bindAddressHardFail: false
clientConnection:
acceptContentTypes: ""
burst: 0
contentType: ""
kubeconfig: /var/lib/kube-proxy/kubeconfig.conf
qps: 0
clusterCIDR: 10.224.0.0/16
configSyncPeriod: 0s
conntrack:
maxPerCore: null
min: null
tcpCloseWaitTimeout: null
tcpEstablishedTimeout: null
detectLocalMode: ""
enableProfiling: false
healthzBindAddress: ""
hostnameOverride: ""
iptables:
masqueradeAll: false
masqueradeBit: null
minSyncPeriod: 0s
syncPeriod: 0s
ipvs:
excludeCIDRs: null
minSyncPeriod: 0s
scheduler: ""
strictARP: false
syncPeriod: 0s
tcpFinTimeout: 0s
tcpTimeout: 0s
udpTimeout: 0s
kind: KubeProxyConfiguration
metricsBindAddress: ""
mode: ""
nodePortAddresses: null
oomScoreAdj: null
portRange: ""
showHiddenMetricsForVersion: ""
udpIdleTimeout: 0s
winkernel:
enableDSR: false
networkName: ""
sourceVip: ""
kubeconfig.conf: |-
apiVersion: v1
kind: Config
clusters:
- cluster:
certificate-authority: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
server: https://10.1.8.30:6443
name: default
contexts:
- context:
cluster: default
namespace: default
user: default
name: default
current-context: default
users:
- name: default
user:
tokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
kind: ConfigMap
metadata:
annotations:
kubeadm.kubernetes.io/component-config.hash: sha256:77333136e4f6883a76e786187738706eb9a60efd1f8641f2489dbd1e085726d9
creationTimestamp: "2021-07-03T03:07:55Z"
labels:
app: kube-proxy
managedFields:
- apiVersion: v1
fieldsType: FieldsV1
fieldsV1:
f:data:
.: {}
f:config.conf: {}
f:kubeconfig.conf: {}
f:metadata:
f:annotations:
.: {}
f:kubeadm.kubernetes.io/component-config.hash: {}
f:labels:
.: {}
f:app: {}
manager: kubeadm
operation: Update
time: "2021-07-03T03:07:55Z"
name: kube-proxy
namespace: kube-system
resourceVersion: "193"
selfLink: /api/v1/namespaces/kube-system/configmaps/kube-proxy
uid: 2b6168fd-29c8-49c4-a465-08720e9f48db
重点解读:kube-proxy 的 ConfigMap 里有两个键:
config.conf:kube-proxy 的配置文件内容(代理模式、网段、conntrack 参数都在里面)kubeconfig.conf:连 kube-apiserver 用的客户端证书配置
🤔 kube-proxy 是什么 :K8s 里实现 Service 负载均衡的组件(在每台节点上跑)。它有两种转发模式:iptables 和 ipvs。
bash
# kube-proxy有两种工作模式ipvs和iptables
# 这里 mode 值为空,查看日志发现默认使用iptables。
root@master30:~# kubectl logs -n kube-system kube-proxy-8kp8w
I1019 02:26:00.975333 1 server_others.go:69] "Using iptables proxy"
I1019 02:26:01.037589 1 node.go:141] Successfully retrieved node IP: 10.1.8.31
I1019 02:26:01.040909 1 conntrack.go:100] "Set sysctl" entry="net/netfilter/nf_conntrack_max" value=131072
I1019 02:26:01.041412 1 conntrack.go:52] "Setting nf_conntrack_max" nfConntrackMax=131072
I1019 02:26:01.041902 1 conntrack.go:100] "Set sysctl" entry="net/netfilter/nf_conntrack_tcp_timeout_close_wait" value=3600
......
现象 :日志第一行 Using iptables proxy------当前 kube-proxy 用的是 iptables 模式(因为 config 里 mode 为空,默认 iptables)。
bash
# 更改模式为ipvs
root@master30:~# kubectl edit cm -n kube-system kube-proxy
......
mode: "ipvs"
......
| 命令 | 作用 |
|---|---|
kubectl edit cm -n kube-system kube-proxy |
用编辑器打开 kube-proxy 的 ConfigMap(cm 是 configmap 缩写),把 mode 从空改成 "ipvs",保存退出 |
bash
# 删除pod,等待控制器重新创建
root@master30:~# kubectl get pod -n kube-system |grep kube-proxy
kube-proxy-8kp8w 1/1 Running 1 37d
kube-proxy-ghgxw 1/1 Running 1 37d
kube-proxy-p6qnk 1/1 Running 1 37d
root@master30:~# kubectl delete pod -n kube-system kube-proxy-{8kp8w,ghgxw,p6qnk}
pod "kube-proxy-8kp8w" deleted
pod "kube-proxy-ghgxw" deleted
pod "kube-proxy-p6qnk" deleted
| 命令 | 作用 |
|---|---|
| `kubectl get pod -n kube-system | grep kube-proxy` |
kubectl delete pod ... kube-proxy-{8kp8w,ghgxw,p6qnk} |
花括号展开语法:同时删 3 个 Pod。它们由 DaemonSet 管理,删了会自动重建新的(新 Pod 会带着改好的 ipvs 配置启动) |
bash
# 再次查看日志
root@master30:~# kubectl get pod -n kube-system |grep kube-proxy
kube-proxy-8swmx 1/1 Running 0 18s
kube-proxy-948cw 1/1 Running 0 29s
kube-proxy-xlh7b 1/1 Running 0 24s
root@master30:~# kubectl logs -n kube-system kube-proxy-8swmx
I1019 03:41:12.288952 1 node.go:141] Successfully retrieved node IP: 10.1.8.31
I1019 03:41:12.290902 1 conntrack.go:52] "Setting nf_conntrack_max" nfConntrackMax=131072
I1019 03:41:12.325988 1 server.go:632] "kube-proxy running in dual-stack mode" primary ipFamily="IPv4"
I1019 03:41:12.364416 1 server_others.go:218] "Using ipvs Proxier"
......
现象验证成功 :新 Pod 名字全变了(8swmx、948cw、xlh7b),日志里出现 Using ipvs Proxier ------模式从 iptables 切成了 ipvs ✅。
🎯 ipvs vs iptables:ipvs 用内核的 IPVS 模块做负载均衡,性能更高、支持更多调度算法,生产推荐 ipvs。这里演示了"改 ConfigMap → 删 Pod → 自动生效"的完整套路。
bash
# 实验完成后,模式更改回来
🌐 综合案例:haproxy+web
需求:
- 创建一个名称为 haproxy 的 pod,使用镜像 haproxy ,pod haproxy 的配置文件保存在 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
🤔 haproxy 配置快速解读 :
frontend http-in监听 8080 端口接收请求;backend servers里定义两个后端(app1、app2),把流量轮询转发给它们;check表示健康检查(后端挂了自动踢掉)。
- 创建第一个 pod,名称为webapp-1 ,使用镜像nginx 。为该 pod 创建一个 cm,名称为webapp-1,该 cm 保存两个值:
- index.html="hello webapp-1"
- error.html="sorry, error."
这个两个值通过 volume 方式挂载到**/usr/share/nginx/html**目录。
- 创建第二个 pod,名称为webapp-2 ,使用镜像nginx 。为该 pod 创建一个 cm,名称为webapp-2,该 cm 保存两个值:
- index.html="hello webapp-2"
- error.html="sorry, error."
这个两个值通过 volume 方式挂载到**/usr/share/nginx/html**目录。
🎯 架构图(脑补):
客户端 ──> haproxy (Pod, :8080) ──> webapp-1 (nginx, 内容 hello webapp-1) └──────> webapp-2 (nginx, 内容 hello webapp-2)多访问几次 haproxy,会发现返回内容在 webapp-1 / webapp-2 之间来回切换------这就是负载均衡!
解答:
- 创建第一个 cm 和 pod
bash
root@master30:~# mkdir web && cd web
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
| 命令 | 作用 |
|---|---|
mkdir web && cd web |
建 web 目录并进入(&& 前成功才执行后) |
kubectl create cm webapp-1 --from-literal=index.html="hello webapp-1" --from-literal=error.html="sorry, error." |
用两个字面量键值对创建 ConfigMap webapp-1(cm 是 configmap 的缩写) |
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" # ConfigMap 挂到网页目录
readOnly: true # 只读(配置不该被容器改)
volumes:
- name: config
configMap:
name: webapp-1 # 引用 webapp-1 这个 CM
bash
root@master30:~/web# kubectl apply -f pod-webapp-1.yaml
- 创建第二个 cm 和 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(和 webapp-1 几乎一样,只改名字和引用的 CM):
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
- 创建 haproxy 配置文件
bash
# 获取pod ip地址
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.laoma.fun <none> <none>
webapp-2 1/1 Running 0 63m 10.224.149.25 worker2.laoma.fun <none> <none>
现象 :两个 webapp Pod 的 IP 分别是 10.224.84.80 和 10.224.149.25------这两个 IP 要写进 haproxy 配置里当后端。
bash
# haproxy内容如下:
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
# 创建haproxy.cfg configmap
root@master30:~/web# kubectl create cm haproxy.cfg --from-file=haproxy.cfg=./haproxy.cfg
| 命令 | 作用 |
|---|---|
kubectl create cm haproxy.cfg --from-file=haproxy.cfg=./haproxy.cfg |
创建 CM。haproxy.cfg= 是自定义键名(等号左边是键,右边是文件路径),这样 CM 里的键就叫 haproxy.cfg(否则默认键名也是 haproxy.cfg,这里演示了自定义键名语法) |
bash
# 创建pod haproxy
root@master30:~/web# vim pod-haproxy.yaml
yaml(逐行解释):
yaml
---
apiVersion: v1
kind: Pod
metadata:
name: haproxy
spec:
containers:
- name: haproxy
image: hub.laoma.cloud/library/haproxy
imagePullPolicy: IfNotPresent
securityContext: # 安全上下文
allowPrivilegeEscalation: true # 允许权限提升(haproxy 要绑端口、写 /var/run 等)
volumeMounts:
- name: config
mountPath: "/usr/local/etc/haproxy" # 挂到 haproxy 配置目录(haproxy 启动会读这个目录的 haproxy.cfg)
readOnly: true
volumes:
- name: config
configMap:
name: haproxy.cfg # 引用刚建的配置 CM
bash
root@master30:~/web# kubectl apply -f pod-haproxy.yaml
# 验证
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.laoma.cloud <none> <none>
root@master30:~# curl http://10.224.149.35:8080
hello webapp-1
root@master30:~# curl http://10.224.149.35:8080
hello webapp-2
现象验证成功:
- 访问 haproxy Pod 的 8080 端口,第一次返回
hello webapp-1,第二次返回hello webapp-2→ 请求被轮询分发到两个后端 ✅
🎯 整套思路串起来:
- ConfigMap 里存 haproxy.cfg(配置文件)
- Pod 把 CM 挂到 haproxy 的配置目录
- haproxy 按配置把流量转发给 webapp-1 / webapp-2
- 两个 webapp 的网页内容也来自各自的 ConfigMap
配置全部"外置",没有一处写死在镜像里------这就是配置管理的精髓。
🔐 Secret(密钥)
学习参考: Secret
Secret 介绍
Secret 是一种包含少量敏感信息的对象,例如密码、令牌或密钥。这样的信息可能会被放在 Pod 规约中或者镜像中。使用 Secret 意味着你不需要在应用程序代码中包含机密数据。
创建 Secret 可以独立于使用它们的 Pod,减少在创建、查看和编辑 Pod 的工作流程中暴露 Secret(及其数据)的风险。Kubernetes 和在集群中运行的应用程序也可以对 Secret 采取额外的预防措施,例如避免将敏感数据写入非易失性存储。
Secret 类似于 ConfigMap 但专门用于保存机密数据。
Secret 与 ConfigMap 的区别在于:Secret 对数据编码(base64),ConfigMap 不对数据编码。
🤔 一句话:ConfigMap 存普通配置(明文),Secret 存敏感配置(如密码,证书等,存的时候自动 base64 编码)。用法几乎一样。
Secret 类型
-
generic:定义键值对,对变量值加密,类型为 Opaque。通用,存密码文本,最常用。
-
docker-registry:用于访问 registry 仓库的凭据,类型 kubernetes.io/dockerconfigjson。
-
tls:保存 TLS 公钥和私钥,类型为 Opaque。
Secret 创建
generic
bash
# 帮助信息
root@master30:~# kubectl create secret generic --help
Create a secret based on a file, directory, or specified literal value.
A single secret may package one or more key/value pairs
......
基于键值对
一般用于传递变量值。
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
root@master30:~# kubectl get secrets mysecret1 -o yaml
apiVersion: v1
data:
password1: cmVkaGF0
password2: cmVkaGF0
user: dG9t
kind: Secret
metadata:
......
name: mysecret1
namespace: laoma
resourceVersion: "24180"
selfLink: /api/v1/namespaces/laoma/secrets/mysecret1
uid: d510d58f-8737-4d2f-bd00-dd9ce6b50a9b
type: Opaque
现象解释:
kubectl get secrets显示 mysecret1 类型 Opaque、DATA=3(3 个键值对)-o yaml里user: dG9t、password1: cmVkaGF0------值都被 base64 编码了("tom"→"dG9t","redhat"→"cmVkaGF0")
bash
root@master30:~# kubectl get secrets mysecret1 -o jsonpath={.data} | json_reformat
{
"password1": "cmVkaGF0",
"password2": "cmVkaGF0",
"user": "amFjaw=="
}
| 命令 | 作用 |
|---|---|
kubectl get secrets mysecret1 -o jsonpath={.data} |
用 jsonpath 只提取 data 字段(jsonpath 是 K8s 的字段提取语法,{.data} 取 data 键) |
| ` | json_reformat` |
💡 json_reformat 工具由软件包 yajl-tools 提供 (笔记原文备注,安装:
apt install yajl-tools)
bash
root@master30:~# kubectl get secrets mysecret1 -o jsonpath={.data.user}
amFjaw==
现象 :只取 user 键的值,是 base64 编码后的 amFjaw==(注意:这里和 yaml 里的 dG9t 不一样,说明这是另一个 secret 或演示环境数据有出入,不必纠结,重点是 jsonpath 取字段的语法)。
bash
root@master30:~# kubectl describe secrets mysecret1
Name: mysecret1
Namespace: laoma
Labels: <none>
Annotations: <none>
Type: Opaque
Data
====
password1: 6 bytes
password2: 6 bytes
user: 3 bytes
现象 :describe 不显示具体值,只显示每个键的字节数 (password1 6 字节 = redhat,user 3 字节 = tom)------这是刻意的安全设计,避免敏感信息泄露。
bash
# 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 tom | base64` |
| `echo -n redhat | base64` |
| `echo -n cmVkaGF0 | base64 -d` |
🎯 验证闭环 :Secret 里存的确实是 base64 编码,自己 echo | base64 就能验证。注意:base64 不是加密,只是编码,拿到值解码就能看,真正保密要靠 RBAC 权限控制。
基于普通文件
一般用于传递配置文件。
bash
root@master30:~# echo -n tom > user
root@master30:~# echo -n redhat > password1
root@master30:~# echo -n redhat > password2
root@master30:~# kubectl create secret generic mysecret2 --from-file=./user --from-file=./password1 --from-file=./password2
# 文件名用作key名,文件内容用作value,也可以自定义key名
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: laoma
resourceVersion: "25329"
selfLink: /api/v1/namespaces/laoma/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: laoma
resourceVersion: "25532"
selfLink: /api/v1/namespaces/laoma/secrets/mysecret3
uid: e19df9a7-7ce3-4539-b6f5-92f83417ac1f
type: Opaque
关键区别:
- mysecret2:
--from-file=./user→ 键 = 文件名user - mysecret3:
--from-file=username=./user→ 键 = 自定义的username(等号左边自定义键名)
基于键值对内容的文件
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: mysecret5
namespace: laoma
resourceVersion: "27240"
selfLink: /api/v1/namespaces/laoma/secrets/mysecret3
uid: 935c0392-8e54-492d-b093-99021e0dc5bb
type: Opaque
| 命令 | 作用 |
|---|---|
echo 'user=tom\npassword1=redhat...' > env.txt |
把三行"键=值"格式的文本写入 env.txt(' 单引号里换行原样保留) |
kubectl create secret generic mysecret4 --from-env-file=./env.txt |
从"env 格式文件"创建:文件里每行 键=值 都变成一个键值对(--from-env-file 专门解析这种格式) |
| 现象 | 生成的 Secret 里同样有 user / password1 / password2 三个键 ✅ |
基于目录
文件名用作 key 名,文件内容用作 value。
bash
root@master30:~# kubectl create secret generic mysecret5 --from-file=./config
secret/my-secret5 created
root@master30:~# kubectl get secrets mysecret5 -o yaml
apiVersion: v1
data:
password1: cmVkaGF0
password2: cmVkaGF0
user: dG9t
kind: Secret
metadata:
......
name: mysecret5
namespace: laoma
resourceVersion: "26687"
selfLink: /api/v1/namespaces/laoma/secrets/mysecret4
uid: 04eccd8b-8686-4856-82c3-0ac697d75746
type: Opaque
(目录 config 里有 user、password1、password2 三个文件,每个文件 → 一个键值对)
基于 yaml 文件
yaml
apiVersion: v1
kind: Secret
metadata:
name: mysecret6
type: Opaque
data:
user: dG9t
password1: cmVkaGF0
password2: cmVkaGF0
yaml 格式中变量值使用转换后的值(即必须手动写 base64 编码后的值,不能写明文)。
⚠️ 注意 :用 yaml 文件创建 Secret 时,data 里必须填 base64 编码后的值。也可以改用
stringData字段直接写明文(K8s 会自动编码),但课堂用的是 data。
docker-registry
回顾 docker login 命令:
docker login DOCKER_REGISTRY_SERVER -u=DOCKER_USER -p=DOCKER_PASSWORD
docker 登录的信息默认保存在 ~/.dockercfg,后续 docker pull 和 push 使用该凭据文件访问 registry。
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=laoma --docker-password=redhat --docker-email=admin@laoma.cloud --docker-server=registry.laoma.cloud
secret/docker-registry-secret created
root@master30:~# kubectl get secrets docker-registry-secret -o yaml
apiVersion: v1
data:
.dockerconfigjson: eyJhdXRocyI6eyJyZWdpc3RyeS5yZWRoYXQuZnVuIjp7InVzZXJuYW1lIjoibGFvbWEiLCJwYXNzd29yZCI6InJlZGhhdCIsImVtYWlsIjoibGFvbWFAcmVkaGF0LmZ1biIsImF1dGgiOiJiR0Z2YldFNmNtVmthR0YwIn19fQ==
kind: Secret
metadata:
......
name: docker-registry-secret
namespace: laoma
resourceVersion: "29629"
selfLink: /api/v1/namespaces/laoma/secrets/docker-registry-secret
uid: 017f736a-11f5-42d8-a9ed-47dad254f2c2
type: kubernetes.io/dockerconfigjson
现象 :生成的 Secret 类型是 kubernetes.io/dockerconfigjson,data 里只有一个键 .dockerconfigjson------里面是 docker login 凭据的 JSON(base64 编码)。
🎯 用途 :把这个 Secret 配置到 Pod 的
imagePullSecrets里,K8s 拉私有仓库镜像时就会自动带上这个凭据。
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]
创建私钥和证书:
bash
#--1--生成私钥
root@master30:~# mkdir certs && cd certs
[root@master30 certs]# openssl genrsa -out www.key 2048
#--2--生成请求文件csr
[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.laoma.cloud/emailAddress=admin@laoma.cloud"
# CN的值必须是网站域名
#--3--使用自己的私钥对请求文件签名,以生成证书
[root@master30 certs]# openssl x509 -req -days 3650 -in www.csr -signkey www.key -out www.crt
| 命令 | 作用 |
|---|---|
openssl genrsa -out www.key 2048 |
生成 2048 位 RSA 私钥,保存到 www.key |
openssl req -new -key www.key -out www.csr -subj "..." |
生成证书签名请求 www.csr;-subj 一次性指定证书主体信息:C=国家(CN 中国)、ST=省(JS 江苏)、L=城市(NJ 南京)、O=组织(LM)、OU=部门(DEVOPS)、CN=域名(www.laoma.cloud) 、emailAddress=邮箱。CN 的值必须是网站域名 |
openssl x509 -req -days 3650 -in www.csr -signkey www.key -out www.crt |
用私钥给请求签名,生成自签名证书 www.crt,有效期 3650 天(约 10 年) |
创建 tls 类型 secret:
bash
[root@master30 certs]# kubectl create secret tls www-tls --cert=./www.crt --key=./www.key
| 片段 | 作用 |
|---|---|
kubectl create secret tls www-tls |
创建 TLS 类型 Secret,名字 www-tls |
--cert=./www.crt |
指定证书文件 |
--key=./www.key |
指定私钥文件 |
Secret 引用
Secrets 可以以卷方式挂载给 pods,也可以通过环境变量方式传递给 pods。
以环境变量方式引用
示例:
bash
root@master30:~# vim pod-with-env-from-secret.yaml
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设置环境变量值
valueFrom:
secretKeyRef: # 从 Secret 取值(对比 ConfigMap 用 configMapKeyRef)
name: mysecret1 # 用 mysecret1 这个 Secret
key: password1 # 取其 password1 键(redhat)
验证:
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.laoma.cloud <none> <none>
root@master30:~# kubectl exec -it mysql -- bash -c 'echo $MYSQL_ROOT_PASSWORD'
redhat
现象 :容器里环境变量 MYSQL_ROOT_PASSWORD = redhat(Secret 里的值,Secret 会自动 base64 解码后再注入,所以容器里拿到的是明文 redhat)✅。
bash
root@master30:~# mysql -u root -predhat -h 10.224.51.131 --ssl-mode=DISABLED
# 清理pod
root@master30:~# kubectl delete pod mysql --force
(--ssl-mode=DISABLED:老版本 mysql 客户端默认尝试 SSL 加密连接,这个参数关掉 SSL 避免握手问题)
以 volume 方式引用
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
(先造两个文件,创建 Secret web,键 = 文件名 index.html / error.html)
引用整体
示例:
bash
root@master30:~# vim pod-with-volume-from-secret.yaml
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值是一个挂载点,将secrete中所有键值对挂载过来
mountPath: "/usr/share/nginx/html" # 挂到网页目录
volumes:
- name: webcontent
secret: # 卷类型:secret
secretName: web # 用名为 web 的 Secret
验证:
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.laoma.cloud <none> <none>
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
现象:Secret 里的每个键挂载后都变成文件,index.html 返回 Hello World、error.html 返回 error ✅(Secret 当静态网站内容用,验证挂载生效)。
引用特定 key
示例:nginx.yaml
bash
root@master30:~# vim pod-secret-volume-single.yaml
yaml(方式一:items 筛选):
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 # 只要 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
# 将webcontent中index.html挂载到/usr/share/nginx/html/index.html
mountPath: "/usr/share/nginx/html/index.html" # mountPath 直接指到具体文件
# 使用subPath参数,则表明mountPath的值是一个文件
subPath: index.html # 只取卷里 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.laoma.cloud <none> <none>
root@master30:~# curl 10.224.51.137/index.html
Hello World
# /usr/share/nginx/html目录中没有index.html和error.html
root@master30:~# kubectl exec web -- ls /usr/share/nginx/html
index.html
现象 :只挂载了 index.html 这一个文件(items 方式),curl /index.html 返回 Hello World ✅。注意 ls 显示只有 index.html------另一个键 error.html 没有被投影(这就是"引用特定 key"的效果)。
🤔 笔记里注释说"目录中没有 index.html 和 error.html",实际 ls 输出显示有 index.html------以实际输出为准:只有 index.html,没有 error.html,恰好证明 items 筛选生效。
以卷方式引用,支持动态更新 secret 内容:
bash
root@master30:~# echo 'Hello Nginx' |base64
SGVsbG8gbmdpbng=
# 更改data.index内容为SGVsbG8gbmdpbng=
root@master30:~# kubectl edit secrets web
......
apiVersion: v1
data:
index: SGVsbG8gbmdpbng=
......
# 等30秒左右查看
root@master30:~# curl 10.224.51.137/index.html
Hello Nginx
| 命令 | 作用 |
|---|---|
| `echo 'Hello Nginx' | base64` |
kubectl edit secrets web |
编辑 Secret,把 index.html 键的值改成新编码 |
| 等 30 秒 | kubelet 周期性同步(默认每 ~1 分钟检查一次挂载的 Secret 是否最新) |
| curl 返回 Hello Nginx | 🎯 实验现象验证成功 :卷方式引用的 Secret 支持热更新,不用重启 Pod,文件内容自己就变了 ✅ |
文件权限
映射过去文件默认权限是 644(8 进制),对应 10 进制是 420。
🤔 644 是什么意思:文件权限三位数字(属主/属组/其他人):6=rw-(属主可读写),4=r--(属组只读),4=r--(其他人只读)。显示成十进制是 420(4×64+2×8+4... 实际是权限位求和)。K8s 的 yaml 里 JSON 不支持八进制,所以 defaultMode 要写十进制。
设置映射的所有文件权限,defaultMode:
yaml
volumes:
- name: foo
secret:
secretName: mysecret1
defaultMode: 256
10 进制是 256 转换成 8 进制是 400。因为 json 格式不支持 8 进制,所以使用 10 进制。
🎯 256 = 八进制 400 = 属主只读(r--------)。全部挂载文件都变成 400 权限,只有属主能读。
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
现象 :权限 -r--------(400)✅,印证 defaultMode 生效。(..data 是 K8s 挂载 Secret 时用的符号链接目录,指向当前版本数据)
设置映射的单独文件权限:
yaml
volumes:
- name: foo
secret:
secretName: mysecret1
items:
- key: user
path: user
mode: 511
10 进制是 511 转换成 8 进制是 777。
🎯 511 = 八进制 777 = 所有人可读可写可执行(rwxrwxrwx)。mode 只作用于 user 这一个文件。
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
现象 :权限 -rwxrwxrwx(777)✅。
🧹 环境清理
bash
root@master30:~# kubectl delete ns configure
(删除 configure 命名空间,清掉里面所有 ConfigMap、Secret、Pod)
🏁 今日总结速查表
Volume 一图流
| 卷类型 | 数据位置 | Pod 删除后数据 | 适用场景 |
|---|---|---|---|
| emptyDir | Node 临时目录 | ❌ 删除 | 容器间共享、临时缓存 |
| hostPath | 宿主机指定目录 | ✅ 保留 | 单节点测试、日志 |
| NFS | 独立 NFS 服务器 | ✅ 保留 | 共享持久存储 |
| PV/PVC | 后端存储(如 NFS) | ✅ 保留 | 生产标准方案 |
ConfigMap vs Secret
| 对比项 | ConfigMap | Secret |
|---|---|---|
| 存什么 | 普通配置 | 敏感信息(密码/证书/token) |
| 编码 | 明文 | base64 编码 |
| 创建 | kubectl create configmap |
kubectl create secret generic/tls/docker-registry |
| 注入 | 环境变量 / 卷挂载 | 环境变量 / 卷挂载 |
| 热更新 | 卷方式支持 | 卷方式支持 |
今日高频命令速查
| 命令 | 作用 |
|---|---|
kubectl create ns 名字 |
创建命名空间 |
kubectl config set-context --current --namespace 名字 |
切换默认命名空间 |
kubectl apply -f xxx.yaml |
创建/更新资源 |
kubectl get pod/pv/pvc/cm/secrets -o wide/yaml |
查看资源(宽表/yaml) |
kubectl describe pod xxx |
看 Pod 详细信息和事件(排错神器) |
kubectl exec -it pod -c 容器 -- 命令 |
进入容器执行命令 |
kubectl delete pod xxx --force |
强制删除 Pod |
kubectl edit pv/cm/secrets xxx |
编辑资源(改完自动生效/需重启) |
kubectl create configmap 名 --from-literal=k=v |
字面量建 ConfigMap |
kubectl create configmap 名 --from-file=./文件 |
文件建 ConfigMap |
kubectl create secret generic 名 --from-literal=k=v |
字面量建 Secret |
kubectl create secret tls 名 --cert=证书 --key=私钥 |
证书建 Secret |
💬今天的内容是 K8s 存储和配置的两大基石 。想真正学会,一定把 emptyDir → hostPath → NFS → PV/PVC → ConfigMap → Secret 这 6 个实验从头到尾自己敲一遍,每个输出都对照着看。遇到 Pod 起不来,第一步永远是
kubectl describe pod看 Events,第二步kubectl logs看日志 🎯。