拒绝配置写死镜像!K8s 存储卷与配置管理,PV/PVC、ConfigMap、Secret 实战避坑指南

📦 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

需求:

  1. 创建一个名称为 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 表示健康检查(后端挂了自动踢掉)。

  1. 创建第一个 pod,名称为webapp-1 ,使用镜像nginx 。为该 pod 创建一个 cm,名称为webapp-1,该 cm 保存两个值:
  • index.html="hello webapp-1"
  • error.html="sorry, error."

这个两个值通过 volume 方式挂载到**/usr/share/nginx/html**目录。

  1. 创建第二个 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 之间来回切换------这就是负载均衡!

解答:

  1. 创建第一个 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
  1. 创建第二个 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
  1. 创建 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 → 请求被轮询分发到两个后端 ✅

🎯 整套思路串起来:

  1. ConfigMap 里存 haproxy.cfg(配置文件)
  2. Pod 把 CM 挂到 haproxy 的配置目录
  3. haproxy 按配置把流量转发给 webapp-1 / webapp-2
  4. 两个 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 看日志 🎯。

相关推荐
服务器硬件项目经理1 小时前
《超节点修仙录》· 第一卷·炼气期【第04话】 超节点 vs 普通服务器——不就是多插了几张卡吗?
运维·服务器·人工智能·嵌入式硬件·深度学习·机器学习
杨云龙UP2 小时前
MySQL 主从复制管理:暂停、恢复、清理主从关系及命令执行位置
运维·数据库·mysql·主从配置·主从命令·reset replica·清除复制
艾伦_耶格宇2 小时前
【kubernetes - 1.28版本】-10 K8S命名空间
云原生·容器·kubernetes
Android系统攻城狮2 小时前
Linux FFmpeg深度解析之avcodec_parameters_from_context调用流程与实战(十六)
linux·运维·ffmpeg·音视频进阶·ffmpeg音视频·车载音视频进阶
隔窗听雨眠2 小时前
从存储引擎到执行计划:OceanBase高效运维的调优逻辑与工程实践
运维·oceanbase
运维行者_2 小时前
供应链安全成最大风险源,企业如何构建可控的运维体系?
运维·开发语言·网络·数据库·安全·云计算·流量监控
xiaoyunxuexibiji3 小时前
采购协同管理:从备货计划、进度确认到到货签收
大数据·运维·人工智能
画中有画3 小时前
在自动化脚本中调用其他脚本文件
运维·自动化
Android系统攻城狮3 小时前
Linux FFmpeg深度解析之avcodec_alloc_context3调用流程与实战(十四)
linux·运维·ffmpeg·车载音视频·音视频进阶·ffmpeg音视频进阶