K8s存储卷与配置管理实战

容器一删数据就没了?K8s 存储卷与配置管理实战:emptyDir/hostPath/NFS/PV/PVC + ConfigMap/Secret 一篇通关

导读 :容器里的数据默认写在容器的可写层,Pod 一旦被删除,数据就跟着"人间蒸发"。本文从最基础的 emptyDir 临时卷讲起,一路实操 hostPath、NFS、PV/PVC 持久化存储,再到配置管理双件套 ConfigMap 与 Secret,全部配有可直接复制的 YAML 和命令,跟着敲一遍就能彻底搞懂 K8s 的数据持久化与配置注入。

实验环境 :1 个 master 节点(master30,10.1.8.30)+ 2 个 worker 节点(worker31/worker32),容器运行时为 containerd,集群内部私有镜像仓库为 hub.laoma.cloud。

官方文档参考 :卷 Volumes、持久卷 PV、ConfigMap、Secret


回顾

昨天内容

  1. Namespace and Contexts(命名空间与上下文)
  2. Kubernetes Pod(Pod 基础管理)

练习

实验题 1:Namespace and Contexts 综合实验

  1. 使用 yaml 方式创建命名空间 dev-ns;
  2. 在 dev-ns 命名空间创建 nginx pod,镜像使用 nginx:alpine;
  3. 使用原生 kubectl config 命令,将当前上下文默认命名空间设置为 dev-ns,不使用 -n 参数查看该 pod;
  4. 安装 kubens 工具,使用 kubens 切换回 default 命名空间,查看 dev-ns 下 pod(必须带 -n 参数);
  5. 编写一个简易 shell 脚本实现命名空间切换功能(参考笔记自定义脚本);
  6. 删除 dev-ns 命名空间。

验证点

  1. kubectl get ns 可以看到 dev-ns;
  2. 不指定 -n,kubectl get pod 能看到 nginx pod;
  3. kubens 切换 default 后,直接 get pod 看不到 dev-ns 的 pod;
  4. 删除 ns 后,该 ns 下 pod 自动被清除。

提示:删除命名空间是"级联删除",命名空间下的所有资源(Pod、Service、ConfigMap 等)都会被一并清除,实验环境清理时非常好用。

实验题 2:Kubernetes Pod 基础管理

章节:Kubernetes Pod(pod 基本管理、多容器 pod)

  1. 创建命名空间 pod-lab,设置上下文默认 ns 为 pod-lab;

  2. 编写 yaml 创建多容器 Pod,pod 名称为 two-container-pod:

    • 容器 1:nginx,name:web,镜像 nginx:alpine;
    • 容器 2:busybox,name:client,镜像 busybox:1.36,command 执行 sleep 3600 保持运行;
  3. 使用 kubectl get pod -o wide 查看 pod 状态与 podIP;确认状态为 2/2 Running;

  4. 在 client 容器内部,使用 wget 127.0.0.1 访问同 pod 内 nginx 服务;

  5. 将 master 节点 /etc/hosts 文件复制到 pod 的 web 容器内 /tmp/hosts 路径;

  6. 查看 nginx 容器日志;进入 client 容器执行 hostname;

  7. 删除 pod,观察 terminating 状态。

提示 :同一个 Pod 内的多个容器共享网络命名空间(同一 IP、同一 localhost)和存储卷,所以 client 容器访问 127.0.0.1:80 就能访问到同 Pod 的 nginx,这也是多容器 Pod 的典型用法。

今天内容

  1. 静态 pod
  2. 编写 yaml 文件
  3. Kubernetes Volume(存储卷)
  4. Kubernetes Configure(配置管理)

一、Kubernetes Volume

学习参考:卷 Volumes

容器磁盘上的文件生命周期是短暂的,容器重启或 Pod 被删除时数据就会丢失;而且一个 Pod 中多个容器想要共享文件也不方便。Volume(卷) 就是为了解决这两个问题:它独立于容器的文件系统,被 Pod 挂载使用,可以为 Pod 提供临时共享或持久化的数据存储。

1.1 环境准备

先创建一个独立的命名空间 storage,并把当前上下文的默认命名空间切过去,后续所有命令都不用再手动加 -n storage:

bash 复制代码
# 创建命名空间 storage
root@master30:~# kubectl create ns storage

# 将当前上下文(context)的默认命名空间设置为 storage
root@master30:~# kubectl config set-context --current --namespace storage

说明 :kubectl config set-context --current --namespace <ns> 只改 kubeconfig 里的默认命名空间,不影响其他上下文,是实验前隔离资源的常用操作。

1.2 Volume 类型一览

Kubernetes 支持的 Volume 类型非常丰富,本文重点实操加粗的四种(emptyDir、hostPath、nfs、persistentVolumeClaim):

  • emptyDir:临时空目录,随 Pod 生灭
  • hostPath:挂载节点本地目录
  • gcePersistentDisk
  • awsElasticBlockStore
  • nfs:网络文件系统共享存储
  • iscsi
  • fc (fibre channel)
  • flocker
  • glusterfs
  • rbd
  • cephfs
  • gitRepo
  • secret
  • persistentVolumeClaim:声明式使用持久卷 PV
  • downwardAPI
  • projected
  • azureFileVolume
  • azureDisk
  • vsphereVolume
  • Quobyte
  • PortworxVolume
  • ScaleIO
  • StorageOS
  • local

1.3 emptyDir:Pod 的"临时草稿纸"

默认情况下,当 Pod 被分配到 Node 上时,就会创建一个 emptyDir(初始为空目录)。只要 Pod 在节点上一直运行,Volume 就一直存在;当 Pod 因为任何原因从节点上被删除时,emptyDir 也会被同时删除,里面的数据永久丢失。

注意:容器崩溃(Crash)并不会导致 emptyDir 数据丢失,因为容器崩溃后 Pod 还在、只是容器被 kubelet 重启;只有 Pod 对象本身被删除(手动删除、被控制器驱逐、节点故障等),emptyDir 才会被清空。

典型用途:同一 Pod 内多容器共享文件(如 sidecar 从主容器目录取日志)、临时缓存空间、排序大文件时的临时落盘。

实验:准备一个包含 2 个 busybox 容器的 Pod,两个容器共同挂载同一个 emptyDir 卷,验证数据共享。

bash 复制代码
root@master30:~# vim pod-with-emptyDir.yaml
yaml 复制代码
apiVersion: v1
kind: Pod
metadata:
  name: busybox
  labels:
    app: busybox
spec:
  # spec.volumes:在 Pod 级别定义卷,emptyDir: {} 表示使用默认配置的临时空目录
  volumes:
  - name: datavolume
    emptyDir: {}
  containers:
  - name: busybox1
    image: hub.laoma.cloud/library/busybox
    imagePullPolicy: IfNotPresent      # 本地有镜像就不拉取,实验环境常用
    command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600']
    # spec.containers.volumeMounts:容器引用上面定义的卷,挂载到容器内 /data 目录
    volumeMounts:
    - mountPath: /data
      name: datavolume
  - name: busybox2
    image: hub.laoma.cloud/library/busybox
    imagePullPolicy: IfNotPresent
    command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600']
    volumeMounts:
    - mountPath: /data
      name: datavolume

配置说明:

  • spec.volumes :定义卷,emptyDir 是默认的临时卷类型;
  • spec.containers.volumeMounts:在容器中引用已定义的卷,并指定挂载路径。
bash 复制代码
# 创建 Pod
root@master30:~# kubectl apply -f pod-with-emptyDir.yaml
pod/busybox created

# 获取两个容器的 ID(只取 // 后面前 12 位),后面 crictl 要用到
root@master30:~# kubectl describe pod busybox |egrep -o 'Container ID.*//.{12}'
Container ID:  containerd://07d189383321
Container ID:  containerd://bb02c680ca8a

# 获取 Pod 被调度到了哪个节点
root@master30:~# kubectl describe pod busybox |grep Node:
Node:             worker32.hyc.cloud/10.1.8.32

# 登录到 worker32 节点,用 crictl 查看容器的挂载情况
# 可以看到 emptyDir 实际落在节点的 /var/lib/kubelet/pods/<Pod-UID>/volumes/kubernetes.io~empty-dir/ 目录下
root@worker32:~# crictl inspect 07d189383321|grep datavolume
        "hostPath": "/var/lib/kubelet/pods/8417656f-4b6c-4b2a-a2f3-f4a248c4c043/volumes/kubernetes.io~empty-dir/datavolume",
          "host_path": "/var/lib/kubelet/pods/8417656f-4b6c-4b2a-a2f3-f4a248c4c043/volumes/kubernetes.io~empty-dir/datavolume"
          "source": "/var/lib/kubelet/pods/8417656f-4b6c-4b2a-a2f3-f4a248c4c043/volumes/kubernetes.io~empty-dir/datavolume",

# 验证共享:在 busybox1 容器的 /data 下创建文件
root@master30:~# kubectl exec busybox -c busybox1 -- touch /data/b1-f1

# 在 busybox2 容器中查看 /data,能看到 busybox1 创建的文件,说明两个容器共享同一个卷
root@master30:~# kubectl exec busybox -c busybox2 -- ls /data
b1-f1

# 在节点上直接查看,容器里写入的文件真实落盘在 kubelet 管理的临时目录中
root@worker32:~# ls /var/lib/kubelet/pods/8417656f-4b6c-4b2a-a2f3-f4a248c4c043/volumes/kubernetes.io~empty-dir/datavolume
b1-f1

# 删除 Pod,验证 emptyDir 的生命周期
root@master30:~# kubectl delete pod busybox --force

# 再次在节点上查看,整个临时目录已经不存在了
root@worker32:~# ls /var/lib/kubelet/pods/8417656f-4b6c-4b2a-a2f3-f4a248c4c043/volumes/kubernetes.io~empty-dir/datavolume
ls: cannot access '/var/lib/kubelet/pods/8417656f-4b6c-4b2a-a2f3-f4a248c4c043/volumes/kubernetes.io~empty-dir/datavolume': No such file or directory

注意:容器删除后,节点上的临时卷数据可能需要等待一小段时间才会被清理回收,属于正常现象。

1.4 hostPath:挂载节点本地目录

hostPath 允许 Pod 将 Node 节点文件系统中的某个目录 挂载到 Pod 内部。与 emptyDir 最大的区别是:Pod 删除后,hostPath 卷里的数据仍然保留在节点上。

注意 :hostPath 的数据只存在于某一个固定节点上,Pod 一旦被重新调度到其他节点就看不到原数据了。因此官方建议 hostPath 仅用于单节点测试或访问节点系统文件(如日志、Docker Socket),生产环境多节点集群请使用 local 卷或网络存储。

实验 :准备一个 Pod,使用 hostPath 把节点的 /busyboxdir 挂载到容器内 /data。

bash 复制代码
root@master30:~# vim pod-with-hostPath.yaml
yaml 复制代码
apiVersion: v1
kind: Pod
metadata:
  name: busybox
  labels:
    app: busybox
spec:
  volumes:
  - name: datavolume
    hostPath:
      path: /busyboxdir          # 节点上的真实目录,不存在时 kubelet 会自动创建(Directory 类型)
  containers:
  - name: busybox1
    image: hub.laoma.cloud/library/busybox
    imagePullPolicy: IfNotPresent
    command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600']
    volumeMounts:
    - mountPath: /data
      name: datavolume
  - name: busybox2
    image: hub.laoma.cloud/library/busybox
    imagePullPolicy: IfNotPresent
    command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600']
    volumeMounts:
    - mountPath: /data
      name: datavolume
      # 设置 readOnly 控制读写权限,默认值是 false(读写访问);设为 true 则该容器只能读不能写
      readOnly: false
bash 复制代码
# 创建 Pod
root@master30:~# kubectl apply -f pod-with-hostPath.yaml
pod/busybox created

# 获取容器 ID
root@master30:~# kubectl describe pod busybox |egrep -o 'Container ID.*//.{12}'
Container ID:  containerd://318922d0c666
Container ID:  containerd://0688146babe5

# 获取 Pod 所在节点
root@master30:~# kubectl describe pod busybox |grep Node:
Node:             worker32.hyc.cloud/10.1.8.32

# 登录到 worker32 查看容器挂载情况,确认容器内 /data 对应节点的 /busyboxdir
root@worker32:~# crictl inspect 318922d0c666|grep busyboxdir
        "hostPath": "/busyboxdir",
          "host_path": "/busyboxdir"
          "source": "/busyboxdir",

# busybox1 在 /data 中创建文件(读写挂载)
root@master30:~# kubectl exec busybox -c busybox1 -- touch /data/b1-f1

# busybox2 能看到该文件
root@master30:~# kubectl exec busybox -c busybox2 -- ls /data
b1-f1

# 如果把 busybox2 的 readOnly 设为 true,写入会被拒绝,报 Read-only file system
root@master30:~# kubectl exec busybox -c busybox2 -- touch /data/b2-f2
touch: /data/b2-f2: Read-only file system

说明 :上面的报错演示的是 readOnly: true 时的效果;如果按当前 YAML 配置(readOnly: false),busybox2 应当可以正常写入。修改 YAML 后用 kubectl apply 重建 Pod 即可观察到两种挂载的差异。

bash 复制代码
# 在节点上查看数据,容器写入的文件直接出现在节点 /busyboxdir 目录中
root@worker32:~# ls /busyboxdir/
b1-f1

# 删除 Pod,验证 hostPath 数据是否保留
root@master30:~# kubectl delete pod busybox --force

# Pod 删除后,hostPath 卷的数据仍然保留在节点上
root@worker32:~# ls /busyboxdir/
b1-f1

小结:emptyDir 是"Pod 在数据在,Pod 亡数据亡";hostPath 是"数据留在节点上,但绑定固定节点"。想要跨节点、跨 Pod 生命周期保留数据,需要网络存储,NFS 就是最简单的一种。

1.5 NFS 存储:网络文件共享

NFS 卷把数据存储在 NFS 共享目录中,多个节点上的 Pod 都可以挂载同一个 NFS 共享,Pod 删除后数据同样保留在 NFS 服务器上。

准备 NFS 共享

bash 复制代码
# 在 master30 上安装 NFS 服务端
root@master30:~# apt install -y nfs-kernel-server

# 创建 NFS 共享目录,-m 777 直接放开权限,方便实验读写
root@master30:~# mkdir -m 777 /nfsshares

# 准备一个测试页面,后面用 nginx 访问验证
root@master30:~# echo hello hyc > /nfsshares/index.html

# 配置共享目录,* 表示允许所有客户端访问,rw 表示读写权限
root@master30:~# cat << EOF > /etc/exports
/nfsshares *(rw)
EOF

# 重启 NFS 服务使配置生效
root@master30:~# systemctl restart nfs-server.service

# 两台 worker 节点安装 NFS 客户端(kubelet 挂载 NFS 卷时依赖它)
root@worker31:~# apt install -y nfs-common
root@worker32:~# apt install -y nfs-common

准备 Pod

把 NFS 共享目录挂载到 nginx 容器的网站根目录 /usr/share/nginx/html:

bash 复制代码
root@master30:~# vim pod-with-nfs.yaml
yaml 复制代码
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: nginx
  name: nginx
spec:
  volumes:
  - name: nfs
    nfs:
      server: 10.1.8.30          # NFS 服务器地址,这里就是 master30
      path: "/nfsshares"        # NFS 服务器上的共享目录
  containers:
  - image: hub.laoma.cloud/library/nginx
    name: nginx
    volumeMounts:
    - name: nfs
      mountPath: "/usr/share/nginx/html"   # 挂载到 nginx 默认网站根目录
bash 复制代码
# 创建 Pod
root@master30:~# kubectl apply -f pod-with-nfs.yaml
pod/nginx created

# 获取容器 ID
root@master30:~# kubectl describe pod nginx |egrep -o 'Container ID.*//.{12}'
Container ID:   containerd://c5e734dab9b5

# 获取 Pod 所在节点
root@master30:~# kubectl describe pod nginx |grep Node:
Node:             worker32.hyc.cloud/10.1.8.32

# 登录 worker32,查看容器内挂载情况:NFS 卷同样由 kubelet 统一管理
root@worker32:~# crictl inspect c5e734dab9b5 | grep -e nfs -e html
        "containerPath": "/usr/share/nginx/html",
        "hostPath": "/var/lib/kubelet/pods/b5bd3f1a-3ef4-4327-95ff-61352268ee41/volumes/kubernetes.io~nfs/nfs",
          "container_path": "/usr/share/nginx/html",
          "host_path": "/var/lib/kubelet/pods/b5bd3f1a-3ef4-4327-95ff-61352268ee41/volumes/kubernetes.io~nfs/nfs"
          "destination": "/usr/share/nginx/html",
          "source": "/var/lib/kubelet/pods/b5bd3f1a-3ef4-4327-95ff-61352268ee41/volumes/kubernetes.io~nfs/nfs",

# 节点上可以看到 NFS 共享已被挂载(df 查看挂载点)
root@worker32:~# df | grep nfsshare
10.1.8.30:/nfsshares              101590016 6659072  89723904   7% /var/lib/kubelet/pods/b5bd3f1a-3ef4-4327-95ff-61352268ee41/volumes/kubernetes.io~nfs/nfs

# 查看 Pod IP,访问 nginx 验证
root@master30:~# kubectl get pod -o wide
NAME    READY   STATUS    RESTARTS   AGE     IP              NODE               NOMINATED NODE   READINESS GATES
nginx   1/1     Running   0          5m19s   10.224.51.171   worker32.hyc.cloud   <none>           <none>

# curl 访问,返回的正是 NFS 共享目录里的 index.html 内容
root@master30:~# curl 10.224.51.171
hello hyc

# 在容器中写入文件,NFS 服务器上同步出现
root@master30:~# kubectl exec nginx -- ls /usr/share/nginx/html
index.html
root@master30:~# kubectl exec nginx -- touch /usr/share/nginx/html/test.html
root@master30:~# ls /nfsshares
index.html  test.html

# 删除 Pod,验证 NFS 卷数据保留
root@master30:~# kubectl delete pod nginx --force
pod "nginx" deleted
root@master30:~# ls /nfsshares
index.html  test.html

结论:Pod 删除后,NFS 卷的数据保留在 NFS 服务器上。之后无论新建的 Pod 被调度到哪个节点,只要重新挂载这个 NFS 共享,就能读到同样的数据。

1.6 持久化存储:PV 与 PVC

学习参考:持久卷 Persistent Volumes

直接在 Pod YAML 里写 NFS 配置有个问题:业务开发人员需要知道后端存储的具体细节(NFS 地址、共享路径等) ,这显然不合理。Kubernetes 使用 PV(Persistent Volume,持久卷) 架构解决这个问题,把"存储的提供"和"存储的使用"分离开。

1.6.1 PV 和 PVC 架构

开发人员不需要了解任何云环境或存储后端的细节,只需要创建一个 PVC(PersistentVolumeClaim,持久卷申领) 去"申请"存储资源,Pod 挂载 PVC 即可实现持久化:

  • Persistent Volume(PV) :由 PersistentVolume API 对象定义,代表集群中已经存在的一块实际存储。PV 的生命周期与使用它的 Pod 无关。PV 是集群级别资源,不属于任何命名空间,通常由集群管理员创建。
  • Persistent Volume Claim(PVC) :由 PersistentVolumeClaim API 对象定义,代表用户对存储的一次"申领"(要多大、什么访问模式)。PVC 是 namespace 级别资源,由开发人员创建。

通俗理解:PV 就像停车场里的一个个车位(管理员提前划好),PVC 就是车主提交的停车申请("我要一个标准车位"),master 节点负责把合适的 PV 和 PVC 绑定(Bound),Pod 则通过 PVC 拿到车位。

1.6.2 创建 PV

集群管理员可以创建任意数量的 PV,数量取决于后端存储容量。为了方便演示,这里继续使用上面的 NFS 后端存储。

bash 复制代码
root@master30:~# vim pv.yaml
yaml 复制代码
apiVersion: v1
kind: PersistentVolume
metadata:
  name: web
spec:
  capacity:
    storage: 5Gi                 # 存储容量
  volumeMode: Filesystem         # 卷模式:文件系统(默认)
  accessModes:
    - ReadWriteOnce              # 访问模式:单节点读写,后面会详细讲
  nfs:                           # 后端存储:NFS
    path: /nfsshares
    server: 10.1.8.30
bash 复制代码
root@master30:~# kubectl apply -f pv.yaml
root@master30:~# kubectl get pv
NAME CAPACITY  ACCESS MODES   RECLAIM POLICY   STATUS     CLAIM   STORAGECLASS   REASON   AGE
web  5Gi       RWO            Recycle          Available                                  3s

说明 :刚创建的 PV 状态为 Available(可用),表示还没有被任何 PVC 绑定。

1.6.3 创建 PVC

用户创建 PVC,声明自己需要的容量、访问模式和存储类别;master 中的 PV 控制器会持续监控 PVC,寻找一个匹配的 PV(或等待动态制备),然后把两者绑定。

bash 复制代码
root@master30:~# vim pvc.yaml
yaml 复制代码
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: webclaim
spec:
  accessModes:
    - ReadWriteOnce              # 申请的访问模式
  resources:
    requests:
      storage: 5Gi               # 申请 5Gi 容量
bash 复制代码
root@master30:~# kubectl apply -f pvc.yaml
root@master30:~# kubectl get pvc
NAME       STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
webclaim   Bound    web      5Gi        RWO                           4s

说明 :PVC 状态变为 Bound、VOLUME 列显示 web,说明它已经和上面创建的 PV 成功绑定。

1.6.4 创建 Pod 使用 PVC

Pod 不再直接写 NFS 信息,而是通过 persistentVolumeClaim.claimName 引用 PVC:

bash 复制代码
root@master30:~# vim pod-with-pvc.yaml
yaml 复制代码
apiVersion: v1
kind: Pod
metadata:
  name: web
  labels:
    name: web
spec:
  containers:
    - image: hub.laoma.cloud/library/nginx
      name: web
      ports:
        - containerPort: 80
          name: web-port
      volumeMounts:
        - name: web-persistent-storage
          mountPath: /usr/share/nginx/html
  volumes:
    - name: web-persistent-storage
      persistentVolumeClaim:
        claimName: webclaim        # 引用刚才创建的 PVC
bash 复制代码
root@master30:~# kubectl apply -f pod-with-pvc.yaml
pod/web created
root@master30:~# kubectl get pod -o wide
NAME   READY   STATUS    RESTARTS   AGE   IP              NODE                  NOMINATED NODE   READINESS GATES
web    1/1     Running   0          35s   10.224.193.69   worker32.hyc.cloud   <none>           <none>

# 测试:访问 Pod 的 web 页面,内容来自 NFS 共享目录
root@master30:~# curl http://10.224.193.69
hello hyc

# 直接往 NFS 目录写一个 test.html,通过 Pod 同样可以访问,验证存储确实打通
root@master30:~# echo test > /nfsshares/test.html
root@master30:~# curl http://10.224.193.69/test.html
test

# 删除 Pod(PVC、PV 和 NFS 数据都不受影响)
root@master30:~# kubectl delete pod web --force

1.6.5 PV accessModes(访问模式)

PersistentVolume 支持以下四种访问模式:

  • ReadWriteOnce(RWO) :卷可以被一个节点以读写方式挂载,同时允许该节点上的多个 Pod 读写访问卷。
  • ReadOnlyMany(ROX) :卷可以被多个节点以只读方式挂载。
  • ReadWriteMany(RWX) :卷可以被多个节点以读写方式挂载。
  • ReadWriteOncePod(RWOP) :卷可以被单个 Pod 以读写方式挂载。如果想确保整个集群中只有一个 Pod 能读写该 PVC,请使用此模式。仅支持 CSI 卷,需要 Kubernetes 1.22 以上版本。

更详细的说明可参考官方博客文章 Introducing Single Pod Access Mode for PersistentVolumes。

CLI 中访问模式的缩写形式:

  • RWO - ReadWriteOnce
  • ROX - ReadOnlyMany
  • RWX - ReadWriteMany
  • RWOP - ReadWriteOncePod

不同存储后端支持的访问模式不同:

卷插件 ReadWriteOnce ReadOnlyMany ReadWriteMany ReadWriteOncePod
AzureFile ✓ ✓ ✓ -
CephFS ✓ ✓ ✓ -
CSI 取决于驱动 取决于驱动 取决于驱动 取决于驱动
FC ✓ ✓ - -
FlexVolume ✓ ✓ 取决于驱动 -
GCEPersistentDisk ✓ ✓ - -
Glusterfs ✓ ✓ ✓ -
HostPath ✓ - - -
iSCSI ✓ ✓ - -
NFS ✓ ✓ ✓ -
RBD ✓ ✓ - -
VsphereVolume ✓ - -(Pod 运行于同一节点上时可行) -
PortworxVolume ✓ - ✓ -

重要 :每个卷在同一时刻只能以一种访问模式挂载,即使该卷能够同时支持多种模式。例如一个 GCEPersistentDisk 卷可以被某节点以 RWO 挂载,或被多个节点以 ROX 挂载,但不可以同时以两种模式挂载。

常见的 NAS 存储(如 NFS、CephFS)都支持三种模式:RWO、ROX、RWX。

1.6.6 PV volumeModes(卷模式)

特性状态: Kubernetes v1.18 [stable]

针对 PV,Kubernetes 支持两种卷模式(volumeModes):Filesystem(文件系统)和 Block(块)。volumeMode 是一个可选参数,省略时默认为 Filesystem。

  • Filesystem 卷 :会被 Pod 挂载(Mount) 到某个目录。如果卷的存储来自某块空设备,Kubernetes 会在第一次挂载前在设备上创建文件系统。
  • Block 卷 :作为原始块设备交给 Pod 使用,上面没有任何文件系统,Pod 与卷之间不存在文件系统层,访问速度最快,但应用必须知道如何处理原始块设备。详见 原始块卷支持。

1.6.7 PVC 与 PV 的匹配规则

PVC 能绑定到哪个 PV,由三条规则共同决定:

  1. PV 的模式必须满足 PVC 申请的最低要求 :模式优先级可简单理解为 ROX < RWO < RWX。例如用户请求 RWO,但当前只有支持 RWO+ROX+RWX 的 NFS PV,PVC 仍会匹配该 NFS PV。
  2. 容量满足最低要求:相同访问模式的卷会被分组,然后按容量从小到大分类匹配,避免"大材小用"。
  3. storageClassName(存储类) :用于对 PV 分类,PVC 可以通过 storageClassName 申请特定类型的 PV。如果 PV 设置了 storageClassName,PVC 申请时也必须指定相同的 storageClassName 。例如 PV 指定为 ns1-storage,ns1 中的 PVC 也必须指定 ns1-storage。

1.6.8 PV 回收策略

当用户不再使用存储卷时,可以删除 PVC 对象释放资源。PV 的回收策略(Reclaim Policy) 决定卷被释放后,集群如何处理这块数据卷。支持三种:Retain(保留)、Recycle(回收)、Delete(删除)。

Retain(保留)

Retain 策略允许用户手动回收资源。PVC 被删除后,PV 仍然存在,数据卷状态变为 Released(已释放)。由于卷上还保留着上一次关联的 PVC 信息(claimRef),必须手动清理这些信息后,PV 才能重新分配给其他 PVC。Retain 是默认的回收策略。

bash 复制代码
root@master30:~# vim pv-pvc-Retain.yaml
yaml 复制代码
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: web
spec:
  capacity:
    storage: 5Gi
  # persistentVolumeReclaimPolicy: Retain   # 不写默认就是 Retain
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  nfs:
    path: /nfsshares
    server: 10.1.8.30
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: webclaim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
bash 复制代码
root@master30:~# kubectl apply -f pv-pvc-Retain.yaml
root@master30:~# kubectl get pv
NAME   CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM              STORAGECLASS   REASON   AGE
web    5Gi        RWO            Retain           Bound    default/webclaim                           22s
root@master30:~# kubectl get pvc
NAME       STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
webclaim   Bound    web      5Gi        RWO                           23s

删除 PVC 验证:

bash 复制代码
# 删除 PVC
root@master30:~# kubectl delete pvc webclaim
root@master30:~# kubectl get pv
NAME   CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS     CLAIM              STORAGECLASS   REASON   AGE
web    5Gi        RWO            Retain           Released   default/webclaim                           3m4s
# PV 没有被删除,状态变为 Released,CLAIM 列还记录着上一个 PVC

此时再创建一个新的 PVC,会发现无法绑定到这个 PV:

bash 复制代码
root@master30:~# vim pvc-db.yaml
yaml 复制代码
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: dbclaim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
bash 复制代码
root@master30:~# kubectl apply -f pvc-db.yaml
root@master30:~# kubectl get pvc dbclaim
NAME      STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
dbclaim   Pending                                                     11s
# 一直 Pending,因为 PV 还保留着上一次的 claimRef

手动编辑 PV,删除卷上的 claimRef 信息:

bash 复制代码
root@master30:~# kubectl edit pv web
yaml 复制代码
......
spec:
  accessModes:
  - ReadWriteOnce
  capacity:
    storage: 5Gi
  # 删除 claimRef 这一整段
  # claimRef:
  #   apiVersion: v1
  #   kind: PersistentVolumeClaim
  #   name: webclaim
  #   namespace: test
  #   resourceVersion: "112157"
  #   uid: 0839cdc1-81bb-11e9-9ca8-52540000fa0a
  # 删除 claimRef 这一整段
  nfs:
    path: /web
    server: 10.1.8.30
......
bash 复制代码
# 保存退出后,dbclaim 立刻绑定成功
root@master30:~# kubectl get pvc dbclaim
NAME      STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
dbclaim   Bound    web      5Gi        RWO                           3m49s

清理环境:

bash 复制代码
root@master30:~# kubectl delete pvc dbclaim
root@master30:~# kubectl delete pv web
root@master30:~# ls /nfsshares/
index.html  test.html

注意:删除 PV 并不会删除后端存储(NFS 目录)中的数据,Retain 策略下数据需要管理员手动清理。

Recycle(回收)

警告 :Recycle 回收策略已被废弃,建议改用动态制备(Dynamic Provisioning,通过 StorageClass 自动创建 PV)。

Recycle 策略的效果与 Retain 基本一致:PVC 删除后,集群会执行一次基本的数据卷擦除(rm -rf /volume/*),之后卷重新变为 Available 可被新 PVC 绑定。

Delete(删除)

对于支持 Delete 策略的卷插件(如 AWS EBS、GCE PD 等云盘),删除 PVC 时会同时把 PV 对象和后端基础设施中的存储资产(如云盘本身)一并删除。

bash 复制代码
root@master30:~# vim pv-pvc-Delete.yaml
yaml 复制代码
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: web
spec:
  capacity:
    storage: 5Gi
  persistentVolumeReclaimPolicy: Delete    # 回收策略设置为 Delete
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  nfs:
    path: /nfsshares
    server: 10.1.8.30
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: webclaim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
bash 复制代码
root@master30:~# kubectl apply -f pv-pvc-Delete.yaml
root@master30:~# kubectl get pv
NAME   CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM              STORAGECLASS   REASON   AGE
web    5Gi        RWO            Delete           Bound    default/webclaim                           22s
root@master30:~# kubectl get pvc
NAME       STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
webclaim   Bound    web      5Gi        RWO                           23s

删除 PVC 验证:

bash 复制代码
root@master30:~# kubectl delete pvc webclaim

# 查看 PV 状态,变为 Failed,回收失败
root@master30:~# kubectl get pv
NAME   CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM              STORAGECLASS   REASON   AGE
web    5Gi        RWO            Delete           Failed   default/webclaim                           33s

# describe 查看失败原因
root@master30:~# kubectl describe pv web
......
Events:
  Type     Reason              Age   From                         Message
  ----     ------              ----  ----                         -------
  Warning  VolumeFailedDelete  50s   persistentvolume-controller  error getting deleter volume plugin for volume "web": no deletable volume plugin matched

说明 :Kubernetes 提示 no deletable volume plugin matched,即没有找到匹配的删除卷插件,所以无法自动删除。原因是 NFS 插件本身不支持 Delete 策略(Kubernetes 不知道如何删除 NFS 服务器上的目录)。

此时可以手动删除 PV,且手动删除 PV 同样不会删除后端存储中的数据。

思考题:如何让 PV 只能绑定给特定的 PVC?

答案:在定义 PV 时指定 claimRef(申领引用),预先写死它要绑定的 PVC 名称和命名空间,其他 PVC 就无法再绑定该卷:

yaml 复制代码
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: web
spec:
  capacity:
    storage: 5Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  nfs:
    path: /nfsshares
    server: 10.1.8.30
  claimRef:
    name: webclaim        # 只允许名为 webclaim 的 PVC 绑定
    namespace: test       # 且该 PVC 必须在 test 命名空间

1.6.9 PV 卷的类型

PV 持久卷以插件形式实现,Kubernetes 目前支持以下插件:

  • csi - 容器存储接口 (CSI),如今的主流方向
  • fc - Fibre Channel (FC) 存储
  • hostPath - HostPath 卷(仅供单节点测试,不适用于多节点集群;可尝试用 local 卷替代)
  • iscsi - iSCSI (SCSI over IP) 存储
  • local - 节点上挂载的本地存储设备
  • nfs - 网络文件系统 (NFS) 存储

以下持久卷已被弃用(当前仍支持,但未来版本会移除):

  • azureFile - Azure File(于 v1.21 弃用)
  • flexVolume - FlexVolume(于 v1.23 弃用)
  • gcePersistentDisk - GCE Persistent Disk(于 v1.17 弃用)
  • portworxVolume - Portworx 卷(于 v1.25 弃用)
  • vsphereVolume - vSphere VMDK 卷(于 v1.19 弃用)
  • cephfs - CephFS 卷(于 v1.28 弃用)
  • rbd - Rados Block Device (RBD) 卷(于 v1.28 弃用)

旧版本 Kubernetes 支持的"树内(In-Tree)"持久卷类型:

  • awsElasticBlockStore - AWS Elastic Block Store (EBS)(v1.27 开始不可用)
  • azureDisk - Azure Disk(v1.27 开始不可用)
  • cinder - Cinder (OpenStack block storage)(v1.27 开始不可用)
  • photonPersistentDisk - Photon 控制器持久盘(v1.15 开始不可用)
  • scaleIO - ScaleIO 卷(v1.21 之后不可用)
  • flocker - Flocker 存储(v1.25 之后不可用)
  • quobyte - Quobyte 卷(v1.25 之后不可用)
  • storageos - StorageOS 卷(v1.25 之后不可用)

1.6.10 原始块卷支持

特性状态: Kubernetes v1.18 [stable]

以下卷插件支持原始块卷(含动态制备,如果支持):

  • CSI
  • FC(光纤通道)
  • GCEPersistentDisk(已弃用)
  • iSCSI
  • Local 卷
  • OpenStack Cinder
  • RBD(已弃用)
  • RBD(Ceph 块设备,已弃用)
  • VsphereVolume

PV 示例:

yaml 复制代码
apiVersion: v1
kind: PersistentVolume
metadata:
  name: block-pv
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  volumeMode: Block                       # 声明为原始块卷
  persistentVolumeReclaimPolicy: Retain
  fc:
    targetWWNs: ["50060e801049cfd1"]
    lun: 0
    readOnly: false

PVC 示例:

yaml 复制代码
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: block-pvc
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Block                       # 同样声明为 Block
  resources:
    requests:
      storage: 10Gi

Pod 示例:

yaml 复制代码
apiVersion: v1
kind: Pod
metadata:
  name: pod-with-block-volume
spec:
  containers:
    - name: fc-container
      image: fedora:26
      command: ["/bin/sh", "-c"]
      args: [ "tail -f /dev/null" ]
      volumeDevices:                      # 注意:块卷用 volumeDevices 而不是 volumeMounts
        - name: data
          devicePath: /dev/xvda           # 在容器内呈现为一块裸设备
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: block-pvc

说明 :向 Pod 中添加原始块设备时,设置的是容器内的设备路径 (volumeDevices.devicePath),而不是文件系统的挂载路径(volumeMounts.mountPath)。

1.7 环境清理

bash 复制代码
# 删除整个 storage 命名空间,其中的 Pod、PVC 等资源会一并清除
root@master30:~# kubectl delete ns storage

二、Kubernetes Configure

存储解决了"数据放哪"的问题,配置管理解决的是"配置怎么给容器"的问题。Kubernetes 提供了两个核心对象:ConfigMap (明文配置)和 Secret(敏感配置)。

2.1 环境准备

bash 复制代码
# 创建并切换到 configure 命名空间
root@master30:~# kubectl create ns configure
root@master30:~# kubectl config set-context --current --namespace configure

2.2 ConfigMap

学习参考:ConfigMap

2.2.1 ConfigMap 介绍

ConfigMap 是一个用来存储其他对象所需配置的 API 对象,使用 data 和 binaryData 字段保存键值对数据:

  • data 字段:保存 UTF-8 字符串;
  • binaryData 字段:保存二进制数据(base64 编码的字串)。

ConfigMap 的名字必须是合法的 DNS 子域名;data 或 binaryData 下的每个键名必须由字母、数字或 -、_、. 组成,且 data 和 binaryData 下的键名不能重叠。

从 v1.19 开始,可以在 ConfigMap 定义中添加 immutable 字段,创建不可变更的 ConfigMap。

ConfigMap 使用建议:

  • 使用 ConfigMap 存储配置数据,不要存数据/文件,单个 ConfigMap 上限为 1 MiB;
  • ConfigMap 在设计上不是用来保存大量数据的,如果需要保存大量数据,请使用挂载存储卷、独立数据库或文件服务。

2.2.2 ConfigMap 创建

命令帮助:

bash 复制代码
kubectl create configmap NAME [--from-file=[key=]source]
[--from-literal=key1=value1] [--dry-run=server|client|none] [options]
方式一:键值对类型(--from-literal)
bash 复制代码
# 直接在命令行写键值对,适合少量配置项
root@master30:~# kubectl create configmap mysql --from-literal=password=redhat

# 查看 ConfigMap 内容
root@master30:~# kubectl get configmaps mysql -o yaml |grep ^data -A1
data:
  password: redhat
方式二:文件类型(--from-file)
bash 复制代码
# 准备一个文件,文件名会作为 key,文件内容作为 value
root@master30:~# echo Hello World > index.html
root@master30:~# kubectl create configmap web1 --from-file=./index.html
root@master30:~# kubectl get configmaps web1 -o yaml |grep ^data -A2
data:
  index.html: |
    Hello World
方式三:目录类型(--from-file 指向目录)
bash 复制代码
# 准备目录,目录下的每个文件都会成为一个键值对
root@master30:~# echo error > error.html
root@master30:~# mkdir web2
root@master30:~# mv index.html error.html web2
root@master30:~# kubectl create configmap web2 --from-file=./web2
root@master30:~# kubectl get configmaps web2 -o yaml |grep ^data -A4
data:
  error.html: |
    error
  index.html: |
    Hello World

2.2.3 环境变量方式引用 ConfigMap

注意 :环境变量属于容器(container)级别 ,在 containers[].env 中配置。

bash 复制代码
root@master30:~# vim pod-with-env-from-configmap.yaml
yaml 复制代码
---
apiVersion: v1
kind: Pod
metadata:
  name: mysql
  labels:
    name: mysql
spec:
  containers:
  - image: hub.laoma.cloud/library/mysql:latest
    imagePullPolicy: IfNotPresent
    name: mysql
    ports:
    - containerPort: 3306
      name: mysql
    env:
    - name: MYSQL_ROOT_PASSWORD
      # 原先通过 value 直接写死环境变量,现在改为从 ConfigMap 中取值
      valueFrom:
        configMapKeyRef:
          name: mysql           # ConfigMap 的名称
          key: password         # ConfigMap 中对应的 key

验证:

bash 复制代码
root@master30:~# kubectl apply -f pod-with-env-from-configmap.yaml
root@master30:~# kubectl get pods -o wide
NAME    READY   STATUS    RESTARTS   AGE     IP              NODE                  NOMINATED NODE   READINESS GATES
mysql   1/1     Running   0          4m33s   10.224.193.70   worker32.hyc.cloud   <none>

# 进入容器查看环境变量,值来自 ConfigMap
root@master30:~# kubectl exec -it mysql -- bash -c 'echo $MYSQL_ROOT_PASSWORD'
redhat

# 安装 mysql 客户端,使用该密码连接数据库验证
root@master30:~# apt install -y mysql-client
root@master30:~# mysql -u root -predhat -h 10.224.193.70
mysql: [Warning] Using a password on the command line interface can be insecure.
......

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> quit
Bye

# 删除 Pod
root@master30:~# kubectl delete pod mysql --force

2.2.4 以 Volume 方式引用 ConfigMap

注意 :volumes 属于 Pod 级别 ,通过 volumeMounts 挂载到容器中。

引用整体(ConfigMap 中每个 key 挂载为一个文件)
bash 复制代码
root@master30:~# vim pod-with-volume-from-configmap.yaml
yaml 复制代码
---
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: web
  name: web
spec:
  containers:
  - image: hub.laoma.cloud/library/nginx:latest
    imagePullPolicy: IfNotPresent
    name: web
    volumeMounts:
    - name: webcontent
      # mountPath 是一个挂载点(目录),ConfigMap 中所有键值对都会以文件形式挂载到该目录
      mountPath: "/usr/share/nginx/html"
  volumes:
  - name: webcontent
    configMap:
      name: web2              # 引用名为 web2 的 ConfigMap

验证:

bash 复制代码
root@master30:~# kubectl apply -f pod-with-volume-from-configmap.yaml
root@master30:~# kubectl get pods -o wide
NAME   READY   STATUS    RESTARTS   AGE   IP              NODE                  NOMINATED NODE   READINESS GATES
web    1/1     Running   0          66s   10.224.193.69   worker32.hyc.cloud   <none>

# 网站根目录下出现了 ConfigMap 中的两个 key(文件名)
root@master30:~# kubectl exec web -- ls /usr/share/nginx/html
error.html
index.html

# 访问验证,文件内容就是 ConfigMap 中对应 key 的 value
root@master30:~# curl http://10.224.193.69
Hello World
root@master30:~# curl http://10.224.193.69/error.html
error
只引用特定 key(items)

如果不想挂载整个 ConfigMap,只需要其中某个 key,可以用 items 字段指定:

bash 复制代码
root@master30:~# vim pod-cm-volume-single.yaml
yaml 复制代码
---
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: web
  name: web
spec:
  containers:
  - name: web
    image: hub.laoma.cloud/library/nginx:latest
    imagePullPolicy: IfNotPresent
    volumeMounts:
    - name: webcontent
      mountPath: "/usr/share/nginx/html"
  volumes:
  - name: webcontent
    configMap:
      name: web2
      items:
        - key: index.html        # 只引用 ConfigMap 中的 index.html
          path: index.html       # 挂载后的文件名

上面的写法等同于下面的 subPath 写法:

yaml 复制代码
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: web
  name: web
spec:
  containers:
  - image: hub.laoma.cloud/library/nginx:latest
    imagePullPolicy: IfNotPresent
    name: web
    volumeMounts:
    - name: webcontent
      mountPath: "/usr/share/nginx/html/index.html"   # 直接指定到具体文件
      subPath: index.html                             # subPath 表示 mountPath 是一个文件而非目录
  volumes:
  - name: webcontent
    configMap:
      name: web2

热更新说明 :以卷方式引用时,被挂载的 ConfigMap 内容会被自动更新。当 ConfigMap 更新时,所投射的键最终也会被更新,kubelet 会在每次周期性同步时检查挂载的 ConfigMap 是否为最新(默认有数十秒的同步延迟)。

注意 :以环境变量方式引用的 ConfigMap 不支持热更新,环境变量在容器启动时注入后就固定了,需要重启 Pod 才能生效;使用 subPath 挂载的条目同样不会自动更新。

2.2.5 k8s 集群中自带的 ConfigMap

以 kube-proxy 为例,集群组件自身的配置也大量存放在 ConfigMap 中:

bash 复制代码
# kube-system 命名空间下有大量系统组件的 ConfigMap
root@master30:~# kubectl get configmaps -n kube-system
NAME                                                   DATA   AGE
calico-config                                          4      37h
coredns                                                1      37h
extension-apiserver-authentication                     6      37h
kube-apiserver-legacy-service-account-token-tracking   1      37h
kube-proxy                                             2      37h
kube-root-ca.crt                                       1      37h
kubeadm-config                                         1      37h
kubelet-config                                         1      37h

# kubeadm-config 保存了集群初始化配置
root@master30:~# kubectl get cm -n kube-system kubeadm-config -o yaml
apiVersion: v1
data:
  ClusterConfiguration: |
    apiServer:
      timeoutForControlPlane: 4m0s
    apiVersion: kubeadm.k8s.io/v1beta3
    certificatesDir: /etc/kubernetes/pki
    clusterName: kubernetes
    controllerManager: {}
    dns: {}
    etcd:
      local:
        dataDir: /var/lib/etcd
    imageRepository: registry.k8s.io
    kind: ClusterConfiguration
    kubernetesVersion: v1.30.2
    networking:
      dnsDomain: cluster.local
      podSubnet: 10.224.0.0/16
      serviceSubnet: 10.96.0.0/12
    scheduler: {}
kind: ConfigMap
metadata:
  creationTimestamp: "2026-06-23T08:50:46Z"
  name: kubeadm-config
  namespace: kube-system
  resourceVersion: "202"
  uid: 7c5cc481-8de7-4751-9c62-48529e2f7baa

2.2.6 综合案例:HAProxy + 两个 Web 后端负载均衡

需求:

  1. 创建一个名为 haproxy 的 Pod,使用镜像 haproxy 。Pod 的配置文件保存在 ConfigMap 中,通过 volume 方式挂载到 /usr/local/etc/haproxy/haproxy.cfg,将流量转发到 Pod webapp-1 和 webapp-2。

HAProxy 配置文件模板如下:

ini 复制代码
global
    daemon
    maxconn 256

defaults
    mode http
    timeout connect 5000ms
    timeout client 50000ms
    timeout server 50000ms

frontend http-in
    bind *:8080
    default_backend servers

backend servers
    server  app1 10.224.84.80:80 check
    server  app2 10.224.149.25:80 check
  1. 创建第一个 Pod,名称为 webapp-1 ,使用镜像 nginx 。为它创建一个名为 webapp-1 的 ConfigMap,保存两个值:
  • index.html="hello webapp-1"
  • error.html="sorry, error."

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

  1. 创建第二个 Pod,名称为 webapp-2 ,使用镜像 nginx 。为它创建一个名为 webapp-2 的 ConfigMap,保存两个值:
  • index.html="hello webapp-2"
  • error.html="sorry, error."

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

解答:

1)创建第一个 ConfigMap 和 Pod:

bash 复制代码
root@master30:~# mkdir web && cd web
# 用 --from-literal 一次性创建两个键值对
root@master30:~/web# kubectl create cm webapp-1 --from-literal=index.html="hello webapp-1" --from-literal=error.html="sorry, error."
root@master30:~/web# vim pod-webapp-1.yaml
yaml 复制代码
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: webapp-1
  name: webapp-1
spec:
  containers:
  - name: nginx
    image: hub.laoma.cloud/library/nginx:latest
    imagePullPolicy: IfNotPresent
    volumeMounts:
    - name: config
      mountPath: "/usr/share/nginx/html"
      readOnly: true               # 配置文件只读挂载,防止容器内误改
  volumes:
  - name: config
    configMap:
      name: webapp-1
bash 复制代码
root@master30:~/web# kubectl apply -f pod-webapp-1.yaml

2)创建第二个 ConfigMap 和 Pod:

bash 复制代码
root@master30:~/web# kubectl create cm webapp-2 --from-literal=index.html="hello webapp-2" --from-literal=error.html="sorry, error."
root@master30:~/web# vim pod-webapp-2.yaml
yaml 复制代码
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: webapp-2
  name: webapp-2
spec:
  containers:
  - name: nginx
    image: hub.laoma.cloud/library/nginx:latest
    imagePullPolicy: IfNotPresent
    volumeMounts:
    - name: config
      mountPath: "/usr/share/nginx/html"
      readOnly: true
  volumes:
  - name: config
    configMap:
      name: webapp-2
bash 复制代码
root@master30:~/web# kubectl apply -f pod-webapp-2.yaml

3)创建 HAProxy 配置文件。

bash 复制代码
# 获取两个 webapp 的 Pod IP,HAProxy 配置中要用
root@master30:~/web# kubectl get pods -o wide
NAME       READY   STATUS    RESTARTS   AGE   IP              NODE                NOMINATED NODE   READINESS GATES
webapp-1   1/1     Running   0          63m   10.224.84.80    worker1.hyc.fun   <none>           <none>
webapp-2   1/1     Running   0          63m   10.224.149.25   worker2.hyc.fun   <none>           <none>

# 编写 haproxy.cfg
root@master30:~/web# vim haproxy.cfg
ini 复制代码
global
    daemon
    maxconn 256

defaults
    mode http
    timeout connect 5000ms
    timeout client 50000ms
    timeout server 50000ms

frontend http-in
    bind *:8080
    default_backend servers

backend servers
    server  app1 10.224.84.80:80 check
    server  app2 10.224.149.25:80 check
bash 复制代码
# 从配置文件创建 ConfigMap(显式指定 key 名为 haproxy.cfg)
root@master30:~/web# kubectl create cm haproxy.cfg --from-file=haproxy.cfg=./haproxy.cfg

# 创建 haproxy Pod
root@master30:~/web# vim pod-haproxy.yaml
yaml 复制代码
---
apiVersion: v1
kind: Pod
metadata:
  name: haproxy
spec:
  containers:
  - name: haproxy
    image: hub.laoma.cloud/library/haproxy
    imagePullPolicy: IfNotPresent
    securityContext:
      allowPrivilegeEscalation: true
    volumeMounts:
    - name: config
      mountPath: "/usr/local/etc/haproxy"   # 挂载到 HAProxy 配置目录
      readOnly: true
  volumes:
  - name: config
    configMap:
      name: haproxy.cfg
bash 复制代码
root@master30:~/web# kubectl apply -f pod-haproxy.yaml

# 查看 haproxy Pod IP
root@master30:~/web# kubectl get pods haproxy -o wide
NAME      READY   STATUS    RESTARTS   AGE   IP              NODE                  NOMINATED NODE   READINESS GATES
haproxy   1/1     Running   0          33s   10.224.149.35   worker32.hyc.cloud   <none>           <none>

# 连续访问 8080 端口,HAProxy 会把请求轮询到两个后端
root@master30:~# curl http://10.224.149.35:8080
hello webapp-1
root@master30:~# curl http://10.224.149.35:8080
hello webapp-2

小结:这个案例把 ConfigMap 的两种典型用法都用上了------两个 webapp 用 ConfigMap 挂载网页内容,HAProxy 用 ConfigMap 挂载配置文件。配置和镜像彻底解耦,改配置只需更新 ConfigMap,不用重新构建镜像。

2.3 Secret

学习参考: Secret

2.3.1 Secret 介绍

Secret 是一种包含少量敏感信息的对象,例如密码、令牌或密钥。这类信息原本可能被写在 Pod 规约或镜像中,使用 Secret 后,应用程序代码里就不再需要包含机密数据。

创建 Secret 可以独立于使用它们的 Pod,减少在创建、查看、编辑 Pod 的工作流程中暴露机密数据的风险。Kubernetes 和集群中的应用还可以对 Secret 采取额外保护,例如避免把敏感数据写入非易失性存储。

Secret 类似于 ConfigMap,但专门用于保存机密数据。二者的核心区别是:Secret 对数据做 base64 编码,ConfigMap 不编码(明文)。

注意:base64 只是编码,不是加密!任何能拿到 Secret 的人都可以轻松 base64 解码看到明文。生产环境中要配合 RBAC 权限控制、Secret 静态加密(EncryptionConfiguration)或外部方案(如 Vault、Sealed Secrets)使用。

2.3.2 Secret 类型

  • generic:自定义键值对,类型为 Opaque,最常用;
  • docker-registry :用于保存访问镜像仓库(registry)的凭据,类型为 kubernetes.io/dockerconfigjson;
  • tls :保存 TLS 公钥证书和私钥,类型为 kubernetes.io/tls。

2.3.3 创建 generic Secret

1)基于键值对(--from-literal)

一般用于传递变量值:

bash 复制代码
root@master30:~# kubectl create secret generic mysecret1 --from-literal=user=tom --from-literal=password1=redhat --from-literal=password2=redhat

root@master30:~# kubectl get secrets
NAME                  TYPE                                DATA   AGE
mysecret1             Opaque                              3      21s

# 查看 YAML,可以看到 value 已经被 base64 编码
root@master30:~# kubectl get secrets mysecret1 -o yaml
apiVersion: v1
data:
  password1: cmVkaGF0
  password2: cmVkaGF0
  user: dG9t
kind: Secret
metadata:
......
  name: mysecret1
  namespace: hyc
  resourceVersion: "24180"
  selfLink: /api/v1/namespaces/hyc/secrets/mysecret1
  uid: d510d58f-8737-4d2f-bd00-dd9ce6b50a9b
type: Opaque

# 用 jsonpath 取出 data 字段,再用 json_reformat 美化输出(json_reformat 由 yajl-tools 软件包提供)
root@master30:~# kubectl get secrets mysecret1 -o jsonpath={.data} | json_reformat
{
    "password1": "cmVkaGF0",
    "password2": "cmVkaGF0",
    "user": "dG9t"
}

# 只取某个 key
root@master30:~# kubectl get secrets mysecret1 -o jsonpath={.data.user}
dG9t

# describe 看不到明文值,只能看到每个值的字节数
root@master30:~# kubectl describe secrets mysecret1
Name:         mysecret1
Namespace:    hyc
Labels:       <none>
Annotations:  <none>

Type:  Opaque

Data
====
password1:  6 bytes
password2:  6 bytes
user:       3 bytes

# Secret 通过 base64 编码,自己动手验证编码/解码
root@master30:~# echo -n tom | base64
dG9t
root@master30:~# echo -n redhat | base64
cmVkaGF0
root@master30:~# echo -n cmVkaGF0 | base64 -d
redhat

注意 :echo 一定要加 -n,否则换行符也会被一起编码,导致值多出一个字节。

2)基于普通文件(--from-file)

一般用于传递配置文件。文件名用作 key,文件内容用作 value,也可以自定义 key 名:

bash 复制代码
# 准备文件,-n 避免写入换行符
root@master30:~# echo -n tom > user
root@master30:~# echo -n redhat > password1
root@master30:~# echo -n redhat > password2

# 文件名直接作为 key
root@master30:~# kubectl create secret generic mysecret2 --from-file=./user --from-file=./password1 --from-file=./password2

# 自定义第一个 key 名为 username
root@master30:~# kubectl create secret generic mysecret3 --from-file=username=./user --from-file=./password1 --from-file=./password2

root@master30:~# kubectl get secrets mysecret2 -o yaml
apiVersion: v1
data:
  password1: cmVkaGF0
  password2: cmVkaGF0
  user: dG9t
kind: Secret
metadata:
......
  name: mysecret2
  namespace: hyc
  resourceVersion: "25329"
  selfLink: /api/v1/namespaces/hyc/secrets/mysecret2
  uid: 047366b6-e8d2-4cf0-b68c-d1f5f9999458
type: Opaque

root@master30:~# kubectl get secrets mysecret3 -o yaml
apiVersion: v1
data:
  password1: cmVkaGF0
  password2: cmVkaGF0
  username: dG9t
kind: Secret
metadata:
......
  name: mysecret3
  namespace: hyc
  resourceVersion: "25532"
  selfLink: /api/v1/namespaces/hyc/secrets/mysecret3
  uid: e19df9a7-7ce3-4539-b6f5-92f83417ac1f
type: Opaque
3)基于键值对内容的文件(--from-env-file)

文件内容写成 key=value 的形式,每行一对:

bash 复制代码
root@master30:~# echo 'user=tom
password1=redhat
password2=redhat' > env.txt

root@master30:~# kubectl create secret generic mysecret4 --from-env-file=./env.txt
secret/mysecret4 created

root@master30:~# kubectl get secrets mysecret4 -o yaml
apiVersion: v1
data:
  password1: cmVkaGF0
  password2: cmVkaGF0
  user: dG9t
kind: Secret
metadata:
......
  name: mysecret4
  namespace: hyc
  resourceVersion: "27240"
  selfLink: /api/v1/namespaces/hyc/secrets/mysecret4
  uid: 935c0392-8e54-492d-b093-99021e0dc5bb
type: Opaque
4)基于目录(--from-file 指向目录)

目录下每个文件名作为 key,文件内容作为 value:

bash 复制代码
root@master30:~# kubectl create secret generic mysecret5 --from-file=./config
secret/mysecret5 created

root@master30:~# kubectl get secrets mysecret5 -o yaml
apiVersion: v1
data:
  password1: cmVkaGF0
  password2: cmVkaGF0
  user: dG9t
kind: Secret
metadata:
......
  name: mysecret5
  namespace: hyc
  resourceVersion: "26687"
  selfLink: /api/v1/namespaces/hyc/secrets/mysecret5
  uid: 04eccd8b-8686-4856-82c3-0ac697d75746
type: Opaque
5)基于 YAML 文件

直接写 YAML 时,data 中的值必须是已经 base64 编码后的值:

yaml 复制代码
apiVersion: v1
kind: Secret
metadata:
  name: mysecret6
type: Opaque
data:
  user: dG9t
  password1: cmVkaGF0
  password2: cmVkaGF0

2.3.4 docker-registry 类型

先回顾 docker login 命令:

bash 复制代码
docker login DOCKER_REGISTRY_SERVER -u=DOCKER_USER -p=DOCKER_PASSWORD

Docker 登录信息默认保存在 ~/.dockercfg,后续 docker pull/push 时使用该凭据访问 registry。Kubernetes 中可以创建 docker-registry 类型的 Secret,让 kubelet 拉取私有镜像时自动带上凭据:

bash 复制代码
Usage:
  kubectl create secret docker-registry NAME --docker-username=user --docker-password=password --docker-email=email [--docker-server=string] [--from-literal=key1=value1] [--dry-run=server|client|none] [options]

示例:

bash 复制代码
root@master30:~# kubectl create secret docker-registry registry --docker-username=hyc --docker-password=redhat --docker-email=admin@hyc.cloud --docker-server=registry.hyc.cloud
secret/registry created

root@master30:~# kubectl get secret registry -o yaml
apiVersion: v1
data:
  .dockerconfigjson: eyJhdXRocyI6eyJyZWdpc3RyeS5yZWRoYXQuZnVuIjp7InVzZXJuYW1lIjoiaHljIiwicGFzc3dvcmQiOiJyZWRoYXQiLCJlbWFpbCI6Imh5Y0ByeWRoYXQuZnVuIiwiYXV0aCI6IjNOM2FHbDR4In19fQ==
kind: Secret
metadata:
......
  name: registry
  namespace: hyc
  resourceVersion: "29629"
  selfLink: /api/v1/namespaces/hyc/secrets/registry
  uid: 017f736a-11f5-42d8-a9ed-47dad254f2c2
type: kubernetes.io/dockerconfigjson

说明 :.dockerconfigjson 中是整段 docker 登录配置的 base64 编码。Pod 使用时在 spec.imagePullSecrets 中引用该 Secret 即可拉取私有仓库镜像。

2.3.5 TLS 类型

创建 TLS Secret 保存公钥证书/私钥对,公钥证书必须是 .PEM 编码:

bash 复制代码
Usage:
  kubectl create secret tls NAME --cert=path/to/cert/file --key=path/to/key/file [--dry-run=server|client|none] [options]

先生成私钥和自签名证书(实验用,生产环境请使用正规 CA 签发的证书):

bash 复制代码
#--1--生成私钥
root@master30:~# mkdir certs && cd certs
root@master30:~/certs# openssl genrsa -out www.key 2048

#--2--生成证书请求文件 csr,CN 的值必须是网站域名
root@master30:~/certs# openssl req -new -key www.key -out www.csr -subj "/C=CN/ST=JS/L=NJ/O=LM/OU=DEVOPS/CN=www.hyc.cloud/emailAddress=admin@hyc.cloud"

#--3--用自己的私钥对请求文件签名,生成有效期 3650 天的自签名证书
root@master30:~/certs# openssl x509 -req -days 3650 -in www.csr -signkey www.key -out www.crt

创建 tls 类型 Secret:

bash 复制代码
root@master30:~/certs# kubectl create secret tls www-tls --cert=./www.crt --key=./www.key

2.3.6 以环境变量方式引用 Secret

与 ConfigMap 类似,Secret 也可以通过环境变量注入容器:

bash 复制代码
root@master30:~# vim pod-with-env-from-secret.yaml
yaml 复制代码
---
apiVersion: v1
kind: Pod
metadata:
  name: mysql
  labels:
    name: mysql
spec:
  containers:
  - image: hub.laoma.cloud/library/mysql:latest
    imagePullPolicy: IfNotPresent
    name: mysql
    ports:
    - containerPort: 3306
      name: mysql
    env:
    - name: MYSQL_ROOT_PASSWORD
      # 原先通过 value 直接写死密码,现在改为从 Secret 取值(容器内看到的是解码后的明文)
      valueFrom:
        secretKeyRef:
          name: mysecret1         # Secret 名称
          key: password1          # Secret 中的 key

验证:

bash 复制代码
root@master30:~# kubectl apply -f pod-with-env-from-secret.yaml
root@master30:~# kubectl get pod -o wide
NAME    READY   STATUS    RESTARTS   AGE   IP              NODE               NOMINATED NODE   READINESS GATES
mysql   1/1     Running   0          90s   10.224.51.131   worker31.hyc.cloud   <none>           <none>

# 容器内环境变量已经是解码后的明文
root@master30:~# kubectl exec -it mysql -- bash -c 'echo $MYSQL_ROOT_PASSWORD'
redhat

# 连接 MySQL 验证
root@master30:~# mysql -u root -predhat -h 10.224.51.131 --ssl-mode=DISABLED

# 清理 Pod
root@master30:~# kubectl delete pod mysql --force

2.3.7 以 Volume 方式引用 Secret

先准备 Secret:

bash 复制代码
root@master30:~# echo Hello world > index.html
root@master30:~# echo error > error.html
root@master30:~# kubectl create secret generic web --from-file=./index.html --from-file=./error.html
引用整体
bash 复制代码
root@master30:~# vim pod-with-volume-from-secret.yaml
yaml 复制代码
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: web
  name: web
spec:
  containers:
  - image: hub.laoma.cloud/library/nginx:latest
    imagePullPolicy: IfNotPresent
    name: web
    volumeMounts:
    - name: webcontent
      # mountPath 是挂载点,Secret 中所有键值对会解码后以文件形式挂载到该目录
      mountPath: "/usr/share/nginx/html"
  volumes:
  - name: webcontent
    secret:
      secretName: web

验证:

bash 复制代码
root@master30:~# kubectl apply -f pod-with-volume-from-secret.yaml
root@master30:~# kubectl get pod -o wide
NAME   READY   STATUS    RESTARTS   AGE   IP              NODE               NOMINATED NODE   READINESS GATES
web    1/1     Running   0          15s   10.224.51.136   worker31.hyc.cloud   <none>

# 访问时 kubelet 已经自动完成 base64 解码,看到的是明文内容
root@master30:~# curl 10.224.51.136
Hello World
root@master30:~# curl 10.224.51.136/error.html
error

# 清理 Pod
root@master30:~# kubectl delete pod web --force
只引用特定 key
bash 复制代码
root@master30:~# vim pod-secret-volume-single.yaml
yaml 复制代码
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: web
  name: web
spec:
  containers:
  - image: hub.laoma.cloud/library/nginx:latest
    imagePullPolicy: IfNotPresent
    name: web
    volumeMounts:
    - name: webcontent
      mountPath: "/usr/share/nginx/html"
  volumes:
  - name: webcontent
    secret:
      secretName: web
      items:
      - key: index.html
        # 通过 path 设置挂载后的文件名,这里 path 与 key 保持一致
        path: index.html

或者使用 subPath 直接指定文件:

yaml 复制代码
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: web
  name: web
spec:
  containers:
  - image: hub.laoma.cloud/library/nginx:latest
    imagePullPolicy: IfNotPresent
    name: web
    volumeMounts:
    - name: webcontent
      # 将 Secret 中的 index.html 挂载为 /usr/share/nginx/html/index.html
      mountPath: "/usr/share/nginx/html/index.html"
      # 使用 subPath 表示 mountPath 的值是一个文件
      subPath: index.html
  volumes:
  - name: webcontent
    secret:
      secretName: web

验证:

bash 复制代码
root@master30:~# kubectl create -f pod-secret-volume-single.yaml

root@master30:~# kubectl get pod -o wide
NAME   READY   STATUS    RESTARTS   AGE   IP              NODE               NOMINATED NODE   READINESS GATES
web    1/1     Running   0          21s   10.224.51.137   worker31.hyc.cloud   <none>
root@master30:~# curl 10.224.51.137/index.html
Hello World

# 目录中只有被选中的 index.html,没有 error.html
root@master30:~# kubectl exec web -- ls /usr/share/nginx/html
index.html

以卷方式引用 Secret 时,同样支持动态更新(等待约 30 秒后生效):

bash 复制代码
# 先把新内容做 base64 编码
root@master30:~# echo 'Hello Nginx' | base64
SGVsbG8gTmdpbng=

# 编辑 Secret,把 index.html 的值改成新的 base64 编码结果
root@master30:~# kubectl edit secrets web
......
apiVersion: v1
data:
  index.html: SGVsbG8gTmdpbng=
......

# 等 30 秒左右再次访问,内容已经更新
root@master30:~# curl 10.224.51.137/index.html
Hello Nginx

2.3.8 挂载文件的权限

Secret 卷映射过去的文件默认权限是 0644(八进制),对应十进制 420。

通过 defaultMode 统一设置所有映射文件的权限:

yaml 复制代码
  volumes:
  - name: foo
    secret:
      secretName: mysecret1
      defaultMode: 256        # 十进制 256 = 八进制 0400(只读)

说明:因为 JSON/YAML 不支持八进制数字,所以这里使用十进制表示。

bash 复制代码
root@master30:~# kubectl exec mypod1 -- ls -l /etc/foo/..data/user
-r--------     1 root     root             4 Aug  9 08:58 /etc/foo/..data/user

通过 items[].mode 给单独的文件设置权限:

yaml 复制代码
  volumes:
  - name: foo
    secret:
      secretName: mysecret1
      items:
      - key: user
        path: user
        mode: 511             # 十进制 511 = 八进制 0777(全部权限)
bash 复制代码
root@master30:~# kubectl exec mypod1 -- ls -l /etc/foo/..data/user
-rwxrwxrwx     1 root     root             4 Aug  9 08:58 /etc/foo/..data/user

说明 :ConfigMap 卷同样支持 defaultMode 和 mode 字段,用法完全一致。

2.4 综合实验:MySQL + WordPress 部署博客

使用 mysql 和 wordpress 镜像部署一个 blog 应用,要求如下:

  1. 部署数据库应用:MySQL 相关用户和密码使用 Secret 存储。

  2. blog 站点:

    • blog 站点证书使用 tls 类型 Secret 存储;
    • 配置文件 vhost 使用 ConfigMap 提供;
    • 应用数据使用 nfs 类型 PV 提供。

WordPress 安装包:

text 复制代码
http://192.168.50.200/class/course-materials/softwares/stage01/wordpress-4.9.4-zh_CN.zip

练习提示:这道题把本文所有知识点串了起来------Secret 管密码和证书、ConfigMap 管 Nginx/Apache 的 vhost 配置、PV/PVC(NFS)管 MySQL 和 WordPress 的持久化数据。建议对照前面的实验独立完成一遍。

2.5 环境清理

bash 复制代码
# 删除整个 configure 命名空间
root@master30:~# kubectl delete ns configure

三、总结

3.1 四种存储卷对比

对比项 emptyDir hostPath NFS PV/PVC
数据位置 节点 kubelet 临时目录 节点指定目录 NFS 服务器共享目录 各类后端存储(NFS/云盘/CSI 等)
Pod 删除后 数据立即删除 数据保留 数据保留 由回收策略决定
跨节点可用 否(随 Pod 绑定节点) 否(数据在固定节点) 是 取决于后端存储
生命周期 与 Pod 相同 与节点相同 与 NFS 服务器相同 独立于 Pod,由集群管理
典型场景 容器间临时共享、缓存 单节点测试、访问节点文件 多节点共享文件 生产环境持久化存储

3.2 PV/PVC 核心要点

  • PV 是集群级资源,PVC 是命名空间级资源,Pod 通过 PVC 使用存储,不直接接触后端细节;
  • 绑定时看三件事:访问模式(ROX < RWO < RWX)、容量、storageClassName;
  • 三种回收策略:Retain (默认,保留数据,手动清理 claimRef 才能复用)、Recycle (已废弃)、Delete(连后端存储一起删,需插件支持,NFS 不支持);
  • volumeMode: Filesystem 挂载为目录,volumeMode: Block 呈现为裸设备(用 volumeDevices)。

3.3 ConfigMap vs Secret

对比项 ConfigMap Secret
数据形式 明文 base64 编码(非加密)
适用内容 配置文件、环境变量、网页文件 密码、令牌、镜像仓库凭据、TLS 证书
容量上限 1 MiB 1 MiB
环境变量引用 valueFrom.configMapKeyRef valueFrom.secretKeyRef
卷引用 volumes[].configMap volumes[].secret
卷挂载热更新 支持(env 与 subPath 除外) 支持(env 与 subPath 除外)
内置类型 无 Opaque、dockerconfigjson、tls

3.4 写在最后

Kubernetes 存储与配置管理可以浓缩成两条主线:

  1. 数据持久化主线:emptyDir(临时)→ hostPath(节点级)→ NFS(网络共享)→ PV/PVC(声明式、生产可用),存储的生命周期与 Pod 一步步解耦;
  2. 配置注入主线:ConfigMap 管明文配置、Secret 管敏感信息,二者都支持环境变量和卷两种引用方式,卷方式支持热更新。
相关推荐
暖核1 小时前
K8s 存储与配置:Volume PV‑PVC ConfigMap Secret 实战解析
云原生·容器·kubernetes
每天一道题1 小时前
从单体到微服务:Service、Instance 与服务拆分到底是什么关系?
后端·微服务·云原生·架构
木雷坞2 小时前
Docker Compose端口映射排查:用port和inspect定位网页访问失败
运维·docker·容器
jonyleek15 小时前
JVS数字底座私有化部署实践:Spring Cloud + K8S 实现文档平台开箱即用
elasticsearch·kubernetes·私有化部署·springcloud·minio·jvs企业文档·文档治理
阿里云云原生16 小时前
欢迎报名丨Alibaba Cloud AgentCore 八城巡回沙龙上海首站开启
云原生
阿里云云原生17 小时前
AgentTeams Dashboard:统一管理团队、项目与资产
云原生·agent
千桐科技17 小时前
qModel算法模型平台功能解析:从源码到 Docker 镜像,部署能自动完成哪些工作?
运维·docker·容器·qmodel
阿里云云原生18 小时前
HiRoute:Jev 模型智能路由实践
云原生
阿里云云原生19 小时前
云栖实录丨面向 AgenticOps 的开源建设:采集标准化、统一语义建模与能力评测
云原生·开源