007-Kubernetes 存储

  1. 一、存储分类
    1. 1. 存储各类特性
  2. 二、ConfigMap
    1. 1. 概述
    2. 2. ConfigMap创建方式
      1. 2.1 通过目录创建
        1. 2.1.1 文件及目录
        2. 2.1.2 文件内容
        3. 2.1.3 创建 ConfigMap
        4. 2.1.4 查看 ConfigMap 数据
      2. 2.2 通过文件创建
        1. 2.2.1 文件内容
        2. 2.2.2 创建 ConfigMap
        3. 2.2.3 查看 ConfigMap 数据
      3. 2.3 通过命令行创建
        1. 2.3.1 创建 ConfigMap
        2. 2.3.2 查看 ConfigMap
      4. 2.4 通过yaml文件创建
        1. 2.4.1 文件及目录
        2. 2.4.2 文件内容
        3. 2.4.3 创建 ConfigMap
        4. 2.4.4 查看 ConfigMap 数据
    3. 3. Pod中使用ConfigMap
      1. 3.1 使用ConfigMap来替代环境变量
        1. 3.1.1 定义资源清单
        2. 3.1.2 创建ConfigMap 、Pod
        3. 3.1.3 查看Pod日志
        4. 3.1.4 进入Pod容器内查看环境变量
      2. 3.2 使用 ConfigMap 设置命令行参数
        1. 3.2.1 定义资源清单
        2. 3.2.2 创建 ConfigMap 、Pod
        3. 3.2.3 查看Pod日志
      3. 3.3 通过数据卷插件使用ConfigMap【推荐】
        1. 3.3.1 定义资源清单
        2. 3.3.2 创建 ConfigMap、Pod
        3. 3.3.3 进入 Pod,查看挂载文件
      4. 3.4 ConfigMap 热更新
        1. 3.4.1 创建 ConfigMap
        2. 3.4.2 定义资源清单
        3. 3.4.3 创建容器
        4. 3.4.4 编辑修改 ConfigMap
        5. 3.4.5 手动触发热更新
        6. 3.4.6 热更新注意事项
      5. 3.5 configmap-不可改变
  3. 三、Secret
    1. 1. Secret - 定义
    2. 2. Secret - 特性
    3. 3. Secret - 类型
    4. 4. Secret-Opaque
      1. 4.1 Opaque - 概念
      2. 4.2 Opaque - 创建
        1. 4.2.1 手动加密,基于Base64加密
        2. 4.2.2 使用 yml 文件创建 Opaque
        3. 4.2.3 使用命令行创建 Opaque
      3. 4.3 查看 Opaque
      4. 4.4 Opaque 用于环境变量
      5. 4.4 Opaque 用于 Volume 卷
      6. 4.5 Opaque - Volume - 热更新
      7. 4.6 Opaque - Volume - 不可更改
    5. 5. Secret - docker-registry
      1. 5.1 搭建 Harbor 镜像仓库
      2. 5.2 配置 Docker
      3. 5.3 创建Harbor私有仓库
      4. 5.4 上传镜像到 Harbor 私服
      5. 5.5 Pod 直接拉取镜像
      6. 5.6 Pod 通过 Secret 拉取镜像
  4. 四、Downward API
    1. 1. 将 Pod 字段应用到环境变量中
      1. 1.1 资源清单
      2. 1.2 创建 Pod
      3. 1.3 查看 Pod 状态
      4. 1.4 查看 Pod 运行日志
      5. 1.5 进入Pod容器,查看环境变量
    2. 2. 将容器字段应用到环境变量中
      1. 2.1 资源清单
      2. 2.2 创建 Pod
      3. 2.3 查看 Pod 状态
      4. 2.4 查看 Pod 运行日志
      5. 2.5 进入 Pod容器,查看环境变量
    3. 3. 挂载 Pod 字段到 Volume
      1. 3.1 资源清单
      2. 3.2 创建 Pod
      3. 3.3 查看 Pod 状态
      4. 3.4 查看 Pod 挂载目录
    4. 4. 挂载容器字段到 Volume
      1. 4.1 资源清单
      2. 4.2 创建 Pod
      3. 4.3 查看 Pod 状态
      4. 4.4 查看 Pod 日志
      5. 4.5 进入 Pod 目录,查看挂载文件
    5. 5. Downward API 常用属性
    6. 6. volume 相较于 env 优势
  5. 五、Volume
    1. 1. Volume概述
    2. 2. Volume类型
    3. 3. EmptyDir 卷
      1. 3.1 emptyDir的一些用途
      2. 3.2 emptyDir 使用案例
        1. 3.2.1 资源清单
        2. 3.2.2 创建 Deployment
        3. 3.2.3 查看 Pod 详情
        4. 3.2.4 进入 nginx-container 容器查看日志
        5. 3.2.5 进入 busybox-container 容器查看日志
    4. 4. HostPath
      1. 4.1 hostPath 的一些用法
      2. 4.2 支持类型
      3. 4.3 注意事项
      4. 4.4 hostPath 使用案例
        1. 4.4.1 资源清单
        2. 4.4.2 创建 Deployment
        3. 4.4.3 进入 busybox-container 容器查看日志
        4. 4.4.4 查看宿主机挂载目录
    5. 5. NFS
      1. 5.1 安装 NFS 文件服务器
        1. 5.1.1 安装 NFS 服务
        2. 5.1.2 创建共享目录
        3. 5.1.3 编辑共享目录读写配置
        4. 5.1.4 启动NFS服务
        5. 5.1.5 测试NFS服务
        6. 5.1.6 客户端卸载 NFS 挂载目录
        7. 5.1.7 使用 NFS4 服务
      2. 5.2 使用 NFS 挂载 Pod 目录
        1. 5.2.1 资源清单
        2. 5.2.2 创建 Deployment
        3. 5.2.3 查看 NFS 挂载目录
  6. 六、PV/PVC
    1. 1. 概述
    2. 2. PV概述
    3. 3. PVC概述
    4. 4. PV 和 PVC 的生命周期
      1. 4.1 Persistent Volume (PV) 的生命周期
      2. 4.2 Persistent Volume Claim (PVC) 的生命周期
    5. 5. 持久化声明保护
    6. 6. 回收策略
      1. 6.1 Retain (保留)
      2. 6.2 Delete (删除)
      3. 6.3 Recycle (回收)
    7. 7. Persistent Volumes 类型
    8. 8. PV示例与参数说明
      1. 8.1 PV示例
      2. 8.2 PV卷状态
      3. 8.3 PV 类型与支持的访问模式
    9. 9. PV-PVC示例
      1. 9.1 主机信息
      2. 9.2 NFS服务部署
      3. 9.3 PV 部署
      4. 9.4 StatefulSet 创建并使用 PVC
        1. 9.4.1 资源清单
        2. 9.4.2 启动pod并查看状态
        3. 9.4.3 PV和PVC状态信息查看
        4. 9.4.4 curl访问验证
      5. 9.5 删除 StatefulSet 并回收 PV
        1. 9.5.1 删除 StatefulSet
        2. 9.5.2 查看PVC和PV,并删除PVC
        3. 9.5.3 回收 PV
    10. 10. StatefulSet网络标识与PVC
  7. 七、StorageClass
    1. 1. 理论
      1. 1.1 什么是 StorageClass
      2. 1.2 为什么需要 StorageClass
      3. 1.3 运行原理
    2. 2. StorageClass 资源
      1. 2.1 存储制备器
      2. 2.2 回收策略
      3. 2.3 允许卷扩展
      4. 2.4 挂载选项
      5. 2.5 卷绑定模式
      6. 2.6 允许的拓扑结构
    3. 3. 案例演示
      1. 3.1 部署流程
      2. 3.2 搭建 NFS 服务
      3. 3.3 创建命名空间
      4. 3.4 创建 Service Account
        1. 3.4.1 ServiceAccount
        2. 3.4.2 ClusterRole
        3. 3.4.3 ClusterRoleBinding
        4. 3.4.4 Role
        5. 3.4.5 RoleBinding
        6. 3.4.6 StorageClass
        7. 3.4.7 Deployment
        8. 3.4.8 PersistentVolumeClaim (PVC)
        9. 3.4.9 测试 Pod
        10. 3.4.10 测试 Service

在K8S中,容器的生命周期可能很短,会被频繁地创建和销毁。那么容器在销毁时,保存在容器中的数据也会被清除。这种结果对用户来说,在某些情况下是不乐意看到的。为了持久化保存容器的数据,kubernetes 引入了 Volume 的概念。

Volume 是 Pod 中能够被多个容器访问的共享目录,它被定义在Pod上,然后被一个 Pod 里的多个容器挂载到具体的文件目录下,kubernetes 通过 Volume 实现同一个 Pod 中不同容器之间的数据共享以及数据的持久化存储。Volume 的生命容器不与 Pod 中单个容器的生命周期相关,当容器终止或者重启时,Volume 中的数据也不会丢失。

一、存储分类

kubernetes 的 Volume 支持多种类型,比较常见的有下面几个:

  • 简单存储:EmptyDir、HostPath、NFS
  • 高级存储:PV、PVC
  • 配置存储:ConfigMap、Secret

1. 存储各类特性

  • 元数据
    • configMap:用于保存配置数据(明文)
    • Secret:用于保存敏感性数据(编码)
    • Downward API:容器在运行时从 Kubernetes API 服务器获取有关它们自身的信息
  • 真实数据
    • Volume:用于存储临时或者持久性数据
    • PersistentVolume:申请制的持久化存储

二、ConfigMap

1. 概述

ConfigMap 是一种 API 对象,用来将非机密性的数据保存到健值对中。使用时可以用作环境变量、命令行参数或者存储卷中的配置文件。

ConfigMap 将环境配置信息和容器镜像解耦,便于应用配置的修改。当你需要储存机密信息时可以使用 Secret 对象。

备注:ConfigMap 并不提供保密或者加密功能。如果你想存储的数据是机密的,请使用 Secret;或者使用其他第三方工具来保证数据的私密性,而不是用 ConfigMap。

2. ConfigMap创建方式

2.1 通过目录创建

2.1.1 文件及目录

# 当前目录位置
$ pwd
/opt/k8s/07/configMap/dir

# 目录下的文件
$ ll
total 8
-rw-r--r-- 1 root root 158 May 10 14:40 game.properties
-rw-r--r-- 1 root root  83 May 10 14:41 ui.properties

2.1.2 文件内容

game.properties

enemies=aliens
lives=3
enemies.cheat=true
enemies.cheat.level=noGoodRotten
secret.code.passphrase=UUDDLRLRBABAs
secret.code.allowed=true
secret.code.lives=30

ui.properties

color.good=purple
color.bad=yellow
allow.textmode=true
how.nice.to.look=fairlyNice

2.1.3 创建 ConfigMap

# 指定目录创建 ConfigMap
$ kubectl create configmap dir-config --from-file=/opt/k8s/07/configMap/dir/

# 查看 ConfigMap
$ kubectl get configmap
NAME               DATA   AGE
dir-config         2      24s

2.1.4 查看 ConfigMap 数据

方式一: 以yaml格式展示

$ kubectl get configmap dir-config -o yaml
# 内容如下:
apiVersion: v1
data:
  game.properties: |
    enemies=aliens
    lives=3
    enemies.cheat=true
    enemies.cheat.level=noGoodRotten
    secret.code.passphrase=UUDDLRLRBABAs
    secret.code.allowed=true
    secret.code.lives=30
  ui.properties: |
    color.good=purple
    color.bad=yellow
    allow.textmode=true
    how.nice.to.look=fairlyNice
kind: ConfigMap
metadata:
  creationTimestamp: "2025-05-10T06:46:58Z"
  name: dir-config
  namespace: default
  resourceVersion: "6014969"
  uid: b5283e4e-dfbd-466c-bd4f-b4cc6fe2ef2a

方式二

$ kubectl describe configmap dir-config
# 内容如下:
Name:         dir-config
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
game.properties:
----
enemies=aliens
lives=3
enemies.cheat=true
enemies.cheat.level=noGoodRotten
secret.code.passphrase=UUDDLRLRBABAs
secret.code.allowed=true
secret.code.lives=30

ui.properties:
----
color.good=purple
color.bad=yellow
allow.textmode=true
how.nice.to.look=fairlyNice


BinaryData
====

Events:  <none>

2.2 通过文件创建

2.2.1 文件内容

文件路径:/opt/k8s/07/configMap/dir/game.properties

enemies=aliens
lives=3
enemies.cheat=true
enemies.cheat.level=noGoodRotten
secret.code.passphrase=UUDDLRLRBABAs
secret.code.allowed=true
secret.code.lives=30

2.2.2 创建 ConfigMap

# 指定文件创建 ConfigMap
$ kubectl create configmap file-config --from-file=/opt/k8s/07/configMap/dir/game.properties

# 查看 ConfigMap
$ kubectl get configmap
NAME               DATA   AGE
dir-config         2      11m
file-config        1      13s

2.2.3 查看 ConfigMap 数据

方式一: 以yaml格式展示

$ kubectl get configmap file-config -o yaml

方式二

$ kubectl describe configmap file-config

2.3 通过命令行创建

2.3.1 创建 ConfigMap

# 通过命令行创建 ConfigMap
$ kubectl create configmap cmd-config --from-literal=special.how=very --from-literal="special.type=charm"

# 查看 ConfigMap
$ kubectl get configmap
NAME               DATA   AGE
cmd-config         2      107s
dir-config         2      21m
file-config        1      10m

--from-literal= 后面的参数 " " 可加可不加

2.3.2 查看 ConfigMap

方式一: 以yaml格式展示

$ kubectl get configmap cmd-config -o yaml
# 内容如下:
apiVersion: v1
data:
  special.how: very
  special.type: charm
kind: ConfigMap
metadata:
  creationTimestamp: "2025-05-10T07:07:01Z"
  name: cmd-config
  namespace: default
  resourceVersion: "6016743"
  uid: 9cd7d408-d7c1-47f8-99db-ecb4bb33f74f

方式二

$ kubectl describe configmap cmd-config
# 内容如下:
Name:         cmd-config
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
special.how:
----
very
special.type:
----
charm

BinaryData
====

Events:  <none>

2.4 通过yaml文件创建

2.4.1 文件及目录

$ pwd
/opt/k8s/07/configMap/yaml

$ ll
total 4
-rw-r--r-- 1 root root 487 May 10 15:58 configmap-demo.yaml

2.4.2 文件内容

configmap-demo.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: configmap-demo
  namespace: default
  labels:
    configmap: myconfig
data:
  # 类属性键,每个键都映射一个简单的值
  username: "george"
  user_file: 'user.properties'
  game_file: 'game.properties'
  # 类文件键,每个键都对应一个文件,| 下是文件的内容
  user.properties: |
    age=23
    address=中国
  game.propertie: |
    enemy.types=aliens,monsters
    player.maximum-lives=5

2.4.3 创建 ConfigMap

# 通过 ConfigMap yaml 资源清单创建
$ kubectl apply -f configmap-demo.yaml

# 查看 ConfigMap
$ kubectl get configmap
NAME               DATA   AGE
configmap-demo     5      20s

2.4.4 查看 ConfigMap 数据

方式一: 以yaml格式展示

$ kubectl get configmap configmap-demo -o yaml
# 内容如下
apiVersion: v1
data:
  game.propertie: |
    enemy.types=aliens,monsters
    player.maximum-lives=5
  game_file: game.properties
  user.properties: |
    age=23
    address=中国
  user_file: user.properties
  username: george
kind: ConfigMap
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"v1","data":{"game.propertie":"enemy.types=aliens,monsters\nplayer.maximum-lives=5\n","game_file":"game.properties","user.properties":"age=23\naddress=中国\n","user_file":"user.properties","username":"george"},"kind":"ConfigMap","metadata":{"annotations":{},"labels":{"configmap":"myconfig"},"name":"configmap-demo","namespace":"default"}}
  creationTimestamp: "2025-05-10T07:59:18Z"
  labels:
    configmap: myconfig
  name: configmap-demo
  namespace: default
  resourceVersion: "6021397"
  uid: f3538c6c-4bd6-41e9-9b48-2c040181e2dc

方式二

$ kubectl describe configmap configmap-demo
Name:         configmap-demo
Namespace:    default
Labels:       configmap=myconfig
Annotations:  <none>

Data
====
game_file:
----
game.properties
user.properties:
----
age=23
address=中国

user_file:
----
user.properties
username:
----
george
game.propertie:
----
enemy.types=aliens,monsters
player.maximum-lives=5


BinaryData
====

Events:  <none>

3. Pod中使用ConfigMap

3.1 使用ConfigMap来替代环境变量

3.1.1 定义资源清单

资源清单:pod_configmap_env.yaml, 定义了 2 个 configMap 和 1个 Pod

apiVersion: v1
kind: ConfigMap
metadata:
  name: configmap-1
  namespace: default
data:
  # 类属性键,每个键都映射一个简单的值
  username: "george"
  password: "123456"

---

apiVersion: v1
kind: ConfigMap
metadata:
  name: configmap-2
  namespace: default
data:
  # 类文件键,每个键都对应一个文件,| 下是文件的内容
  user.properties: |
    age=23
    address=中国

---

apiVersion: v1
kind: Pod
metadata:
  name: pod-configmap-env
  namespace: default
spec:
  restartPolicy: Never # Pod 重启策略
  containers:
    - name: myapp
      image: wangyanglinux/myapp:v1.0
      imagePullPolicy: IfNotPresent
      command: # 打印完环境变量后休眠3600秒
        - "/bin/sh"
        - "-c"
        - "env && sleep 3600"
      env:
        - name: USERNAME # 容器内环境变量的名称
          valueFrom:
            configMapKeyRef:
              name: configmap-1 # configMap名称
              key: username # configMap中定义的属性键
        - name: PASSWORD
          valueFrom:
            configMapKeyRef:
              name: configmap-1
              key: password
      envFrom:
        - configMapRef:
            name: configmap-2 # configMap名称

3.1.2 创建ConfigMap 、Pod

$ kubectl apply -f pod_configmap_env.yaml 
configmap/configmap-1 created
configmap/configmap-2 created
pod/pod-configmap-env created

查看Pod运行状态

$ kubectl get pods -o wide
NAME                READY   STATUS    RESTARTS   AGE   IP              NODE         NOMINATED NODE   READINESS GATES
pod-configmap-env   1/1     Running   0          5s    172.16.58.195   k8s-node02   <none>           <none>

3.1.3 查看Pod日志

$ kubectl logs pod-configmap-env
KUBERNETES_SERVICE_PORT=443
KUBERNETES_PORT=tcp://10.96.0.1:443
HOSTNAME=pod-configmap-env
SHLVL=1
HOME=/root
USERNAME=george
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_SERVICE_HOST=10.96.0.1
PWD=/
user.properties=age=23
address=中国

PASSWORD=123456

打印环境变量

3.1.4 进入Pod容器内查看环境变量

# 进入 Pod 容器内部
$ kubectl exec -it pod-configmap-env /bin/bash

# 查看环境变量
pod-configmap-env:/# env
user.properties=age=23
address=中国

KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_SERVICE_PORT=443
CHARSET=UTF-8
HOSTNAME=pod-configmap-env
PWD=/
HOME=/root
USERNAME=george
LANG=C.UTF-8
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
PASSWORD=123456
TERM=xterm
SHLVL=1
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
KUBERNETES_SERVICE_HOST=10.96.0.1
KUBERNETES_PORT=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP_PORT=443
LC_COLLATE=C
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
_=/usr/bin/env

3.2 使用 ConfigMap 设置命令行参数

3.2.1 定义资源清单

pod_configmap_cmd.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: configmap-3
  namespace: default
data:
  # 类属性键,每个键都映射一个简单的值
  username: "george"
  password: "123456"

---

apiVersion: v1
kind: Pod
metadata:
  name: pod-configmap-cmd
  namespace: default
spec:
  restartPolicy: Never # Pod 重启策略
  containers:
    - name: myapp-cmd
      image: wangyanglinux/myapp:v1.0
      imagePullPolicy: IfNotPresent
      command: # 打印环境变量指定的键值
        - "/bin/sh"
        - "-c"
        - "echo $(USERNAME) $(PASSWORD)"
      env:
        - name: USERNAME # 容器内环境变量的名称
          valueFrom:
            configMapKeyRef:
              name: configmap-3 # configMap名称
              key: username # configMap中定义的属性键
        - name: PASSWORD
          valueFrom:
            configMapKeyRef:
              name: configmap-3
              key: password

3.2.2 创建 ConfigMap 、Pod

$ kubectl apply -f pod_configmap_cmd.yaml 
configmap/configmap-3 created
pod/pod-configmap-cmd created

查看Pod运行状态

$ kubectl get pod pod-configmap-cmd
NAME                READY   STATUS      RESTARTS   AGE
pod-configmap-cmd   0/1     Completed   0          59s

3.2.3 查看Pod日志

$ kubectl logs -f pod-configmap-cmd
george 123456

3.3 通过数据卷插件使用ConfigMap【推荐】

将 ConfigMap 以文件形式挂载到容器内, 在数据卷里面使用ConfigMap,最基本的就是将文件填入数据卷,在这个文件中,键就是文件名【第一层级的键】,键值就是文件内容。

3.3.1 定义资源清单

pod_configmap_volumn.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: configmap-4
  namespace: default
data:
  # 类文件键,每个键都对应一个文件,| 下是文件的内容
  user.properties: |
    age=23
    address=中国
  game.propertie: |
    enemy.types=aliens,monsters
    player.maximum-lives=5

---

apiVersion: v1
kind: ConfigMap
metadata:
  name: configmap-5
  namespace: default
data:
  # 类属性键,每个键都映射一个简单的值
  username: "george"
  password: "123456"

---

apiVersion: v1
kind: Pod
metadata:
  name: pod-configmap-volumn
  namespace: default
spec:
  restartPolicy: Never # Pod 重启策略
  containers:
    - name: myapp-volumn
      image: wangyanglinux/myapp:v1.0
      imagePullPolicy: IfNotPresent
      command: # 打印完环境变量后休眠3600秒
        - "/bin/sh"
        - "-c"
        - "env && sleep 3600"
      volumeMounts:
        - name: config-volumn # 挂载的volumn名称,必须在 volumes 有定义
          mountPath: /etc/config
        - name: volumn2
          mountPath: /etc/mydir # 挂载路径不能重复
  volumes:
    - name: config-volumn # volumn 名称
      configMap:
        name: configmap-4 # ConfigMap 名称
    - name: volumn2
      configMap:
        name: configmap-5

3.3.2 创建 ConfigMap、Pod

$ kubectl apply -f pod_configmap_volumn.yaml 
configmap/configmap-4 unchanged
configmap/configmap-5 unchanged
pod/pod-configmap-volumn created

3.3.3 进入 Pod,查看挂载文件

# 进入容器
$ kubectl exec -it pod-configmap-volumn /bin/bash

# 查看挂载目录 /etc/config
pod-configmap-volumn:/# ls -l /etc/config
total 0
lrwxrwxrwx    1 root     root            21 May 10 17:41 game.propertie -> ..data/game.propertie
lrwxrwxrwx    1 root     root            22 May 10 17:41 user.properties -> ..data/user.properties

# 查看挂载目录 /etc/mydir
pod-configmap-volumn:/# ls /etc/mydir
password  username

# 查看文件内容
pod-configmap-volumn:/etc/config# cat /etc/config/user.properties 
age=23
address=中国

# 查看文件内容
pod-configmap-volumn:/etc/config# cat /etc/mydir/username 
george

3.4 ConfigMap 热更新

3.4.1 创建 ConfigMap

# 创建配置文件
$ cat default.conf 
server {
    listen 80 default_server;
    server_name example.com www.example.com;
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }
}

# 将文件生成为 ConfigMap
$ kubectl create configmap default-nginx --from-file=default.conf

# 查看 configmap状态
$ kubectl get cm
NAME               DATA   AGE
default-nginx      1      25s

# 查看configmap 详情
$ kubectl describe cm default-nginx
Name:         default-nginx
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
default.conf:
----
server {\r
    listen 80 default_server;\r
    server_name example.com www.example.com;\r
    location / {\r
        root   /usr/share/nginx/html;\r
        index  index.html index.htm;\r
    }\r
}

BinaryData
====

Events:  <none>

3.4.2 定义资源清单

hotUpdate-nginx.yml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hotupdate-deployment
  namespace: default
spec:
  replicas: 3
  selector: # 标签选择器
    matchLabels:
      app: hotupdate-deploy
      version: v1
  template:
    metadata:
      labels:
        app: hotupdate-deploy
        version: v1
        env: test
    spec:
      containers:
        - name: nginx
          image: nginx:1.28.0
          imagePullPolicy: IfNotPresent
          volumeMounts:
            - name: config-volume # 挂载的volumn名称,必须 volumes 中定义存在
              mountPath: /etc/nginx/conf.d/
      volumes:
        - name: config-volume # 挂在卷名称
          configMap:
            name: default-nginx # 创建的configmap名称

3.4.3 创建容器

# 创建 Deployment
$ kubectl apply -f hotUpdate-nginx.yml 
deployment.apps/hotupdate-deployment created

# 查看 Deployment 状态
$ kubectl get deployment -o wide
NAME                   READY   UP-TO-DATE   AVAILABLE   AGE     CONTAINERS   IMAGES         SELECTOR
hotupdate-deployment   3/3     3            3           3m27s   nginx        nginx:1.28.0   app=hotupdate-deploy,version=v1

# 查看Pod
$ kubectl get pods -o wide
NAME                                   READY   STATUS    RESTARTS   AGE     IP               NODE         NOMINATED NODE   READINESS GATES
hotupdate-deployment-9ff5b5b8b-27frw   1/1     Running   0          3m46s   192.168.85.225   k8s-node01   <none>           <none>
hotupdate-deployment-9ff5b5b8b-c8k42   1/1     Running   0          3m46s   192.168.58.220   k8s-node02   <none>           <none>
hotupdate-deployment-9ff5b5b8b-cjwlh   1/1     Running   0          3m46s   192.168.58.221   k8s-node02   <none>           <none>

访问pod

$ curl http://192.168.85.225:80/index.html
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

Pod 可以正常访问

3.4.4 编辑修改 ConfigMap

$ kubectl edit configmap default-nginx

# 将 default-nginx 内容的端口,从 80 改为 90,然后保存

编辑修改 ConfigMap

进入 Pod 容器内,查看挂载配置文件是否修改

# 进入pod容器内容
$ kubectl exec -it hotupdate-deployment-9ff5b5b8b-27frw /bin/bash

# 查看配置文件(配置已成功修改)
hotupdate-deployment-747786dd9b-72wz8:/# cat /etc/nginx/conf.d/default.conf 
server {
    listen 90 default_server;
    server_name example.com www.example.com;
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }
}

使用 90 端口访问 Pod

$ curl http://192.168.85.225:90/index.html
curl: (7) Failed to connect to 192.168.85.225 port 90: Connection refused

可以看到,使用 90端口访问失败,Pod依然使用的是80端口,也就是 ConfigMap 没能触发容器的热更新。

这里需要手动触发热更新。

3.4.5 手动触发热更新

更新 ConfigMap 目前并不会触发相关 Pod 的滚动更新,可以通过修改 pod annotations 的方式强制触发滚动更新

# 手动更新 Deployment 的 annotations 版本号,触发Deployment 滚动升级
$ kubectl patch deployment hotupdate-deployment --patch '{"spec": {"template": {"metadata": {"annotations": {"version/config": "123" }}}}}'

访问Pod

# 查看 滚动更新后的 Pod
$ kubectl get pods -o wide
NAME                                    READY   STATUS    RESTARTS   AGE   IP               NODE         NOMINATED NODE   READINESS GATES
hotupdate-deployment-76d6c6b684-v5wcl   1/1     Running   0          14s   192.168.58.222   k8s-node02   <none>           <none>
hotupdate-deployment-76d6c6b684-v96hq   1/1     Running   0          16s   192.168.85.226   k8s-node01   <none>           <none>
hotupdate-deployment-76d6c6b684-xgbxb   1/1     Running   0          11s   192.168.58.223   k8s-node02   <none>           <none>


# 访问Pod
$ curl http://192.168.85.226:90/index.html
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

3.4.6 热更新注意事项

更新 ConfigMap 后:

  • 使用该 ConfigMap 挂载的 Env 不会同步更新
  • 使用该 ConfigMap 挂载的 Volume 中的数据需要一段时间(实测大概10秒)才能同步更新

3.5 configmap-不可改变

Kubernetes 给不可变的 ConfigMap 和 Secret 提供了一种可选配置, 可以设置各个 Secret 和 ConfigMap 为不可变的。 对于大量使用 configmap 的集群(至少有成千上万各不相同的 configmap 供 Pod 挂载), 禁止变更它们的数据有下列好处:

  • 防止意外(或非预期的)更新导致应用程序中断
  • 通过将 configmap 标记为不可变来关闭 kube-apiserver 对其的监视,从而显著降低 kube-apiserver 的负载,提升集群性能。
$ kubectl explain configmap.immutable
KIND:       ConfigMap
VERSION:    v1

FIELD: immutable <boolean>

DESCRIPTION:
    Immutable, if set to true, ensures that data stored in the ConfigMap cannot
    be updated (only object metadata can be modified). If not set to true, the
    field can be modified at any time. Defaulted to nil.

添加 immutable 参数

$ kubectl edit cm default-nginx -o yaml
apiVersion: v1
data:
  default.conf: "server {\r\n    listen 90 default_server;\r\n    server_name example.com
    www.example.com;\r\n    location / {\r\n        root   /usr/share/nginx/html;\r\n
    \       index  index.html index.htm;\r\n    }\r\n}"
immutable: true
kind: ConfigMap
metadata:
  creationTimestamp: "2025-05-11T13:09:40Z"
  name: default-nginx
  namespace: default
  resourceVersion: "161330"
  uid: a75df079-ef0d-499c-9769-13bbc76606ad

保存后,这个configmap将不能再被修改

三、Secret

1. Secret - 定义

Secret 对象类型用来保存敏感信息,例如密码、OAuth 令牌和 SSH 密钥。 将这些信息放在 secret 中比放在 Pod 的定义或者 容器镜像 中来说更加安全和灵活。

2. Secret - 特性

  • Kubernetes 通过仅仅将 Secret 分发到需要访问 Secret 的 Pod 所在的机器节点来保障其安全性

  • Secret 只会存储在节点的内存中,永不写入物理存储,这样从节点删除 secret 时就不需要擦除磁盘数据

  • 从 Kubernetes1.7 版本开始,etcd 会以加密形式存储 Secret,一定程度的保证了 Secret 安全性

3. Secret - 类型

内置类型 用法 使用示例
Opaque base64编码格式的Secret,用来存储密码、秘钥等。 kubectl create secret generic mysecret –from-literal=username=admin –from-literal=password=123456
kubernetes.io/service-account-token 服务账号令牌,用来访问Kubernetes API,由Kubernetes自动创建,并且会自动挂载到Pod的 /run/secrets/kubernetes.io/serviceaccount 目录中。
kubernetes.io/dockercfg ~/.dockercfg 文件序列化形式(老版本Docker),用来存储私有docker registry的认证信息。
kubernetes.io/dockerconfigjson ~/.docker/config.json 文件序列化形式(新版本Docker),用来存储私有docker registry的认证信息。 kubectl create secret docker-registry myregistry \ –docker-server=registry.example.com \ –docker-username=admin \ –docker-password=123456
kubernetes.io/basic-auth 用于基本身份认证凭据 kubectl create secret generic my-basic-auth –type=kubernetes.io/basic-auth –from-literal=username=admin –from-literal=password=123456
kubernetes.io/ssh-auth 用户SSH身份认证凭据 kubectl create secret generic my-ssh –type=kubernetes.io/ssh-auth –from-file=ssh-privatekey=~/.ssh/id_rsa
kubernetes.io/tls 用于TLS客户端或者服务端的数据 kubectl create secret tls my-tls –cert=./tls.crt –key=./tls.key
bootstrap.kubernetes.io/token 启动引导令牌数据

kubernetes.io/service-account-token 类型创建示例

apiVersion: v1
kind: Secret
metadata:
  name: example-service-account-token
  namespace: default
  annotations:
    kubernetes.io/service-account.name: example-service-account
    kubernetes.io/service-account.uid: "12345678-1234-1234-1234-1234567890ab"
type: kubernetes.io/service-account-token
data:
  token: eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5In0... # Base64 encoded JWT token
  ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t... # Base64 encoded CA certificate
  namespace: ZGVmYXVsdA== # Base64 encoded namespace

bootstrap.kubernetes.io/token 类型创建示例

apiVersion: v1
kind: Secret
metadata:
  name: bootstrap-token-abcdef
  namespace: kube-system
type: bootstrap.kubernetes.io/token
data:
  token-id: YWJjZGVm # Base64 encoded "abcdef"
  token-secret: MTIzNDU2Nzg5MGFiY2RlZg== # Base64 encoded "1234567890abcdef"
  usage-bootstrap-authentication: dHJ1ZQ== # Base64 encoded "true"
  usage-bootstrap-signing: dHJ1ZQ== # Base64 encoded "true"
  description: dG9rZW4gZm9yIG5vZGUgYm9vdHN0cmFwcGluZw== # Base64 encoded "token for node bootstrapping"
  expiration: MjAyNi0wNS0xM1QxMjowMDowMFo= # Base64 encoded "2026-05-13T12:00:00Z"

4. Secret-Opaque

4.1 Opaque - 概念

当 Secret 配置文件中未作显式设定时,默认的 Secret 类型是 Opaque。 当你使用 kubectl 来创建一个 Secret 时,你会使用 generic 子命令来标明要创建的是一个 Opaque 类型 Secret。

4.2 Opaque - 创建

4.2.1 手动加密,基于Base64加密

# 对用户名 admin 进行base64编码
$ echo -n 'admin' | base64
YWRtaW4=

# 对密码 abc123456 进行base64编码
$ echo -n 'abc123456' | base64
YWJjMTIzNDU2

# 对base64加密后的用户名进行解密
$ echo -n 'YWRtaW4=' | base64 -d
admin

# 对base64加密后的密码进行解密
$ echo -n 'YWJjMTIzNDU2' | base64 -d
abc123456

4.2.2 使用 yml 文件创建 Opaque

Opaque-Secret.yml

apiVersion: v1
kind: Secret # 资源类型 Secret
metadata:
  name: mysecret # 自定义 Secret 名称
type: Opaque # Secret类型指定为 Opaque
data:
  password: YWJjMTIzNDU2 # base64 编码后的密码
  username: YWRtaW4= # base64编码后的用户名

执行文件,创建 Opaque

$ kubectl get Secret mysecret -o yaml

4.2.3 使用命令行创建 Opaque

# generic的默认类型是 Opaque, 
# username=admin 是一个键值对,Secret 会自动进行 Base64 加密
$ kubectl create secret generic db-user-pass --from-literal=username=admin --from-literal=password=abc123456

4.3 查看 Opaque

# 查看方式一:
$ kubectl get Secret mysecret -o yaml
apiVersion: v1
data:
  password: YWJjMTIzNDU2 # 显示编码后的内容
  username: YWRtaW4=
kind: Secret
metadata:
  creationTimestamp: "2025-05-13T11:30:44Z"
  name: mysecret
  namespace: default
  resourceVersion: "6423295"
  uid: fbdb3db6-e47f-4eb2-863a-e6a8eb11c616
type: Opaque

# 查看方式二:
$ kubectl describe Secret mysecret
Name:         mysecret
Namespace:    default
Labels:       <none>
Annotations:  <none>

Type:  Opaque

Data
====
password:  9 bytes # 只显示编码后的字符串的长度
username:  5 bytes

4.4 Opaque 用于环境变量

资源清单:pod-opaque-env.yml

apiVersion: v1
kind: Secret # 资源类型 Secret
metadata:
  name: mysecret # 自定义 Secret 名称
type: Opaque # Secret类型指定为 Opaque
data:
  password: YWJjMTIzNDU2 # base64 编码后的密码
  username: YWRtaW4= # base64编码后的用户名

---

apiVersion: v1
kind: Pod
metadata:
  name: pod-opaque-env
spec:
  restartPolicy: Never
  containers:
    - name: myapp
      image: wangyanglinux/myapp:v1.0
      imagePullPolicy: IfNotPresent
      env:
        - name: SECRET_USERNAME # 环境变量Key
          valueFrom:
            secretKeyRef:
              key: username # Secret中定义的key
              name: mysecret # Secret名称
        - name: SECRET_PASSWORD
          valueFrom:
            secretKeyRef:
              key: password
              name: mysecret

创建Secret 和 Pod

$ kubectl apply -f pod-opaque-env.yml 
secret/mysecret created
pod/pod-opaque-env created

查看环境变量

# 查看Pod
$ kubectl get pods -o wide
NAME             READY   STATUS    RESTARTS   AGE    IP              NODE         NOMINATED NODE   READINESS GATES
pod-opaque-env   1/1     Running   0          2m1s   172.16.58.220   k8s-node02   <none>           <none>


# 进入Pod
$ kubectl exec -it pod-opaque-env -- /bin/bash


# 查看环境变量
pod-opaque-env:/# env
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_SERVICE_PORT=443
CHARSET=UTF-8
HOSTNAME=pod-opaque-env
PWD=/
HOME=/root
LANG=C.UTF-8
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
SECRET_USERNAME=admin # 用户名被自动base64解密
TERM=xterm
SHLVL=1
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
KUBERNETES_SERVICE_HOST=10.96.0.1
KUBERNETES_PORT=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP_PORT=443
LC_COLLATE=C
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
SECRET_PASSWORD=abc123456 # 密码被自动 base64 解密
_=/usr/bin/env

由上可见,在pod中的secret信息实际已经被解密。

4.4 Opaque 用于 Volume 卷

pod-opaque-volume.yml

apiVersion: v1
kind: Secret # 资源类型 Secret
metadata:
  name: mysecret # 自定义 Secret 名称
type: Opaque # Secret类型指定为 Opaque
data:
  password: YWJjMTIzNDU2 # base64 编码后的密码
  username: YWRtaW4= # base64编码后的用户名

---

apiVersion: v1
kind: Pod
metadata:
  name: pod-opaque-env
spec:
  restartPolicy: Never
  containers:
    - name: myapp
      image: wangyanglinux/myapp:v1.0
      imagePullPolicy: IfNotPresent
      volumeMounts:
        - mountPath: /etc/secret # 挂载到容器内的目录
          name: secret-volume # 挂载的 volume 名称
          readOnly: true # 设置容器 /etc/secret 只读
  volumes:
    - name: secret-volume # 生成的挂在卷名称
      secret: # 挂载 Secret
        secretName: mysecret # Secret名称

创建Secret 和 Pod

$ kubectl apply -f pod-opaque-volume.yml 
secret/mysecret created
pod/pod-opaque-env created

查看Pod挂载的目录

# 查看 Pod
$ kubectl get pods
NAME             READY   STATUS    RESTARTS   AGE
pod-opaque-env   1/1     Running   0          85s

# 进入 Pod 容器
$ kubectl exec -it pod-opaque-env -- /bin/bash

# 查看挂载目录
# ls -l /etc/secret
total 0
lrwxrwxrwx    1 root     root            15 May 13 20:07 password -> ..data/password
lrwxrwxrwx    1 root     root            15 May 13 20:07 username -> ..data/username

# 查看username
# cat /etc/secret/username 
admin

# 查看password
# cat /etc/secret/password
abc123456

Pod 内 Secret 自动使用 Base64 解密。

Volume 特殊挂载方式

# 方式一:设置文件权限,
volumes:
- name: volumes12
  secret:
    secretName: mysecret
    defaultMode: 256 # 256的8进制是 400, 表示挂载的文件权限为 400
    
# 方式二:只挂载 Secret 的部分字段,比如下面,只用了 Secret 的 username 字段
volumes:
- name: volumes12
  secret:
    secretName: mysecret
    items:
    - key: username
      path: my-group/my-username

4.5 Opaque - Volume - 热更新

当已经存储于卷中被使用的 Secret 被更新时,被映射的键也将终将被更新。 组件 kubelet 在周期性同步时检查被挂载的 Secret 是不是最新的。 但是,它会使用其本地缓存的数值作为 Secret 的当前值。使用 Secret 作为子路径卷挂载的容器不会收到 Secret 更新。

  • 当Opaque 挂载为 Volume 时支持热更新(编辑Secret 保存后,需要过大约1分钟),pod内挂载的目录内容会更新,无需手动重启Pod

作为子路径挂载容器不支持热更新,示例如下:

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  namespace: default
spec:
  containers:
  - name: my-app
    image: nginx
    volumeMounts:
    - name: secret-volume
      mountPath: /etc/secret/username
      subPath: username
      readOnly: true
  volumes:
  - name: secret-volume
    secret:
      secretName: my-secret

subPath: username,使用 subPath 只挂载 username,将不会触发热更新。

4.6 Opaque - Volume - 不可更改

Kubernetes 给不可变的 Secret 和 ConfigMap 提供了一种可选配置, 可以设置各个 Secret 和 ConfigMap 为不可变的。 对于大量使用 Secret 的集群(至少有成千上万各不相同的 Secret 供 Pod 挂载), 禁止变更它们的数据有下列好处:

  • 防止意外(或非预期的)更新导致应用程序中断
  • 通过将 Secret 标记为不可变来关闭 kube-apiserver 对其的监视,从而显著降低 kube-apiserver 的负载,提升集群性能
apiVersion: v1
kind: Secret # 资源类型 Secret
metadata:
  name: mysecret # 自定义 Secret 名称
type: Opaque # Secret类型指定为 Opaque
data:
  password: YWJjMTIzNDU2 # base64 编码后的密码
  username: YWRtaW4= # base64编码后的用户名
immutable: true

5. Secret - docker-registry

存储私有docker registry的认证信息

5.1 搭建 Harbor 镜像仓库

搭建过程参考:Harbor企业级部署

Harbor 部分配置文件信息

$ vim harbor.yml

hostname: 192.168.6.201
http:
  # port for http, default is 80. If https enabled, this port will redirect to https port
  port: 5000

# https related config
https:
  # https port for harbor, default is 443
  #port: 443
  # The path of cert and key files for nginx
  #certificate: /your/certificate/path
  #private_key: /your/private/key/path
harbor_admin_password: Harbor12345

# Harbor DB configuration
database:
  password: root123
  max_idle_conns: 100
  max_open_conns: 900
  conn_max_lifetime: 5m
  conn_max_idle_time: 0

# The default data volume
data_volume: /data/harbor
trivy:
  # ignoreUnfixed The flag to display only fixed vulnerabilities
  ignore_unfixed: false
  skip_update: false
  offline_scan: false
  #
  # Comma-separated list of what security issues to detect. Possible values are `vuln`, `config` and `secret`. Defaults to `vuln`.
  security_check: vuln
  insecure: false
jobservice:
  # Maximum number of job workers in job service
  max_job_workers: 10

notification:
  # Maximum retry count for webhook job
  webhook_job_max_retry: 10

chart:
  # Change the value of absolute_url to enabled can enable absolute url in chart
  absolute_url: disabled

# Log configurations
log:
  # options are debug, info, warning, error, fatal
  level: info
  # configs for logs in local storage
  local:
    rotate_count: 50
    # are all valid.
    rotate_size: 200M
    # The directory on your host that store log
    location: /var/log/harbor

#This attribute is for migrator to detect the version of the .cfg file, DO NOT MODIFY!
_version: 2.7.0

# uaa:
#   ca_file: /path/to/ca
proxy:
  http_proxy:
  https_proxy:
  no_proxy:
  components:
    - core
    - jobservice
    - trivy

# enable purge _upload directories
upload_purging:
  enabled: true
  # remove files in _upload directories which exist for a period of time, default is one week.
  age: 168h
  # the interval of the purge operations
  interval: 24h
  dryrun: false
cache:
  # not enabled by default
  enabled: false
  # keep cache for one day by default
  expire_hours: 24

5.2 配置 Docker

集群内所有服务器都需要配置

$ vim /etc/docker/daemon.json
{
  "log-driver": "json-file",
  "log-opts": {
        "max-size": "100m",
        "max-file": "10"
  },
  "data-root":"/data/docker",
  "exec-opts": ["native.cgroupdriver=systemd"],
  "registry-mirrors": [
   "https://kfp63jaj.mirror.aliyuncs.com",
    "https://hub-mirror.c.163.com",
    "https://mirror.baidubce.com"
  ],
  "insecure-registries": ["192.168.6.201:5000"]
}

192.168.6.201:5000 是harbor服务内网IP和端口

5.3 创建Harbor私有仓库

创建Harbor私有仓库

重启集群内的Docker服务

集群内所有机器,在配置完:/etc/docker/daemon.json 都需要重启Docker服务才能生效

$ systemctl restart docker

5.4 上传镜像到 Harbor 私服

# 拉取镜像(需要科学上网,参考:https://georgechan95.github.io/blog/b01d5c62.html)
$ docker pull wangyanglinux/myapp:v1.0

# 给镜像打标签
$ docker tag wangyanglinux/myapp:v1.0 192.168.6.201:5000/k8s-secret/myapp:v1

# 登录Harbor
$ docker login 192.168.6.201:5000 -u admin -p Harbor12345

# 将镜像推送到Harbor中
$ docker push 192.168.6.201:5000/k8s-secret/myapp:v1

上传镜像到 Harbor 私服

退出Harbor登录

这里退出登录,目的是为了演示使用Secret完成Harbor登录

# 退出登录 Harbor
$ docker logout 192.168.6.201:5000
Removing login credentials for 192.168.6.201:5000

# 从Harbor中拉取镜像权限异常
$ docker pull 192.168.6.201:5000/k8s-secret/myapp:v1

5.5 Pod 直接拉取镜像

资源清单:pod-secret-registry.yaml

apiVersion: v1
kind: Pod
metadata:
  name: pod-secret-registry
spec:
  containers:
    - name: myapp
      image: 192.168.6.201:5000/k8s-secret/myapp:v1

启动Pod并查看状态

# 启动Pod
$ kubectl apply -f pod-secret-registry.yaml 
pod/pod-secret-registry created

# 查看Pod状态
$ kubectl get pods
NAME                  READY   STATUS             RESTARTS   AGE
pod-secret-registry   0/1     ImagePullBackOff   0          16s

# 查看Pod详情
$ kubectl describe pod pod-secret-registry
Name:             pod-secret-registry
Events:
  Type     Reason     Age                 From               Message
  ----     ------     ----                ----               -------
  Normal   Scheduled  2m5s                default-scheduler  Successfully assigned default/pod-secret-registry to k8s-node02
  Normal   Pulling    43s (x4 over 2m4s)  kubelet            Pulling image "192.168.6.201:5000/k8s-secret/myapp:v1"
  Warning  Failed     43s (x4 over 2m4s)  kubelet            Failed to pull image "192.168.6.201:5000/k8s-secret/myapp:v1": Error response from daemon: unauthorized: unauthorized to access repository: k8s-secret/myapp, action: pull: unauthorized to access repository: k8s-secret/myapp, action: pull
  Warning  Failed     43s (x4 over 2m4s)  kubelet            Error: ErrImagePull
  Warning  Failed     29s (x6 over 2m4s)  kubelet            Error: ImagePullBackOff
  Normal   BackOff    14s (x7 over 2m4s)  kubelet            Back-off pulling image "192.168.6.201:5000/k8s-secret/myapp:v1"

镜像拉取失败,未登录Harbor私服,没有拉取权限

5.6 Pod 通过 Secret 拉取镜像

资源清单:pod-secret-registry.yaml

apiVersion: v1
kind: Secret
metadata:
  name: harbor-secret
  namespace: default
type: kubernetes.io/dockerconfigjson
stringData: # stringData 会自动对内容进行base64编码,并存储在data 字段中
  .dockerconfigjson: |
    {
      "auths": {
        "192.168.6.201:5000": {
            "username": "admin",
            "password": "Harbor12345"
        }
      }
    }

---

apiVersion: v1
kind: Pod
metadata:
  name: pod-secret-registry
spec:
  containers:
    - name: myapp
      image: 192.168.6.201:5000/k8s-secret/myapp:v1
  imagePullSecrets: # 镜像拉取密钥
    - name: harbor-secret # Secret 名称

补充:手动创建 kubernetes.io/dockerconfigjson 类型的 Secret 方式

kubectl create secret docker-registry harbor-secret --docker-server='192.168.6.201:5000' --docker-username='admin' --docker-password='Harbor12345'

启动 Pod

$ kubectl apply -f pod-secret-registry.yaml 
secret/harbor-secret created
pod/pod-secret-registry created

查看Secret

# 查看方式1
$ kubectl describe secret harbor-secret
Name:         harbor-secret
Namespace:    default
Labels:       <none>
Annotations:  <none>

Type:  kubernetes.io/dockerconfigjson

Data
====
.dockerconfigjson:  118 bytes


# 查看方式2
$ kubectl get secret harbor-secret -o yaml
apiVersion: v1
data:
  .dockerconfigjson: ewogICJhdXRocyI6IHsKICAgICIxOTIuMTY4LjYuMjAxOjUwMDAiOiB7CiAgICAgICAgInVzZXJuYW1lIjogImFkbWluIiwKICAgICAgICAicGFzc3dvcmQiOiAiSGFyYm9yMTIzNDUiCiAgICB9CiAgfQp9Cg==
kind: Secret
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"v1","kind":"Secret","metadata":{"annotations":{},"name":"harbor-secret","namespace":"default"},"stringData":{".dockerconfigjson":"{\n  \"auths\": {\n    \"192.168.6.201:5000\": {\n        \"username\": \"admin\",\n        \"password\": \"Harbor12345\"\n    }\n  }\n}\n"},"type":"kubernetes.io/dockerconfigjson"}
  creationTimestamp: "2025-05-14T01:41:01Z"
  name: harbor-secret
  namespace: default
  resourceVersion: "6500261"
  uid: 8d70288d-5a40-413a-94dc-4dcc6e5c405e
type: kubernetes.io/dockerconfigjson

查看Pod状态

# 查看Pod状态
$ kubectl get pods -o wide
NAME                  READY   STATUS    RESTARTS   AGE   IP              NODE         NOMINATED NODE   READINESS GATES
pod-secret-registry   1/1     Running   0          20s   172.16.58.244   k8s-node02   <none>           <none>


# 查看Pod详情
$ kubectl describe pod pod-secret-registry
Name:             pod-secret-registry
Namespace:        default
......
Events:
  Type    Reason     Age    From               Message
  ----    ------     ----   ----               -------
  Normal  Scheduled  8m26s  default-scheduler  Successfully assigned default/pod-secret-registry to k8s-node02
  Normal  Pulling    8m26s  kubelet            Pulling image "192.168.6.201:5000/k8s-secret/myapp:v1"
  Normal  Pulled     8m25s  kubelet            Successfully pulled image "192.168.6.201:5000/k8s-secret/myapp:v1" in 94ms (94ms including waiting)
  Normal  Created    8m25s  kubelet            Created container: myapp
  Normal  Started    8m25s  kubelet            Started container myapp

从 Harbor 私服中成功拉取镜像并运行了容器。

四、Downward API

Downward API 是 Kubernetes 中的一个功能,它允许容器在运行时从 Kubernetes API 服务器获取有关它们自身的信息。这些信息可以作为容器内部的环境变量或文件注入到容器中,以便容器可以获取有关其运行环境的各种信息,如 Pod 名称、命名空间、标签等。

对于容器来说,有时候拥有自己的信息是很有用的,可避免与Kubernetes过度耦合。Downward API使得容器使用自己或者集群的信息,而不必通过Kubernetes客户端或API服务器。

举个例子:有一个现有的应用假定要用一个非常熟悉的环境变量来保存一个唯一标识。一种可能是给应用增加处理层,但这样是冗余和易出错的,而且它违反了低耦合的目标。更好的选择是使用Pod名称作为标识,把Pod名称注入这个环境变量中。

主要作用

  • 提供容器元数据
  • 动态配置
  • 与 Kubernetes 环境集成

1. 将 Pod 字段应用到环境变量中

1.1 资源清单

资源清单:dapi-pod-env.yaml

apiVersion: v1
kind: Pod
metadata:
  name: dapi-pod-env
  namespace: default
spec:
  containers:
    - name: busybox-container
      image: busybox:latest
      imagePullPolicy: IfNotPresent
      command: ['sh', '-c'] # 容器启动后,每 10秒打印一次环境变量
      args:
        - while true; do
            echo -en '\n';
            printenv MY_NODE_NAME MY_POD_NAMESPACE MY_POD_NAME;
            printenv MY_POD_IP MY_POD_SERVICE_ACCOUNT;
            sleep 10;
          done;
      env:
        - name: MY_NODE_NAME # 环境变量,获取服务器节点名称
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName # Pod 所在服务器节点的名称,例如:k8s-node01、k8s-node02
        - name: MY_POD_NAME # 环境变量,获取 Pod 名称
          valueFrom:
            fieldRef:
              fieldPath: metadata.name # 读取 Pod 名称属性
        - name: MY_POD_NAMESPACE # 环境变量,获取 POD 所在的名称空间
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace # 读取名称空间属性
        - name: MY_POD_IP # 读取Pod Ip
          valueFrom:
            fieldRef:
              fieldPath: status.podIP # 读取当前 Pod ip,由于IP是Pod运行后才生成的,属于状态信息,从 status 字段读取
        - name: MY_POD_SERVICE_ACCOUNT
          valueFrom:
            fieldRef:
              fieldPath: spec.serviceAccountName # Pod 服务账户
  restartPolicy: Never

这个配置文件中,你可以看到五个环境变量。env字段是一个EnvVars类型的数组。 数组中第一个元素指定MY_NODE_NAME这个环境变量从Pod的spec.nodeName字段获取变量值。同样,其它环境变量也是从Pod的字段获取它们的变量值。

1.2 创建 Pod

$ kubectl apply -f dapi-pod-env.yaml

1.3 查看 Pod 状态

# 查看 Pod 状态
$ kubectl get pods -o wide 
NAME           READY   STATUS    RESTARTS   AGE   IP              NODE         NOMINATED NODE   READINESS GATES
dapi-pod-env   1/1     Running   0          7s    172.16.58.205   k8s-node02   <none>           <none>

# 查看 Pod 详情
$ kubectl get pod dapi-pod-env -o yaml
apiVersion: v1
kind: Pod
metadata:
  ......
  name: dapi-pod-env
  namespace: default
  resourceVersion: "6534780"
  uid: e8856c8b-340f-4080-823c-89c0353e05e6
spec:
  containers:
  - args:
    - while true; do echo -en '\n'; printenv MY_NODE_NAME MY_POD_NAMESPACE MY_POD_NAME;
      printenv MY_POD_IP MY_POD_SERVICE_ACCOUNT; sleep 10; done;
    command:
    - sh
    - -c
    env:
    - name: MY_NODE_NAME
      valueFrom:
        fieldRef:
          apiVersion: v1
          fieldPath: spec.nodeName
    - name: MY_POD_NAME
      valueFrom:
        fieldRef:
          apiVersion: v1
          fieldPath: metadata.name
    - name: MY_POD_NAMESPACE
      valueFrom:
        fieldRef:
          apiVersion: v1
          fieldPath: metadata.namespace
    - name: MY_POD_IP
      valueFrom:
        fieldRef:
          apiVersion: v1
          fieldPath: status.podIP
    - name: MY_POD_SERVICE_ACCOUNT
      valueFrom:
        fieldRef:
          apiVersion: v1
          fieldPath: spec.serviceAccountName
    image: busybox:latest
    imagePullPolicy: IfNotPresent
    name: busybox-container
    ......
  dnsPolicy: ClusterFirst
  enableServiceLinks: true
  nodeName: k8s-node02
  ......
  serviceAccount: default
  serviceAccountName: default
  ......
  volumes:
  - name: kube-api-access-wknrw
    ......
status:
  conditions:
  - lastProbeTime: null
    ......
  containerStatuses:
  - containerID: docker://d8efb6784802d645e67bfaa2fc3708a42b06a15b2356c346786876ed9119538f
    ......
  hostIP: 192.168.6.141
  hostIPs:
  - ip: 192.168.6.141
  phase: Running
  podIP: 172.16.58.205
  podIPs:
  - ip: 172.16.58.205
  qosClass: BestEffort
  startTime: "2025-05-14T08:03:58Z"

1.4 查看 Pod 运行日志

$ kubectl logs -f --tail=100 dapi-pod-env busybox-container
# 打印结果:
k8s-node02 # Pod 所在服务器节点名称
default # Pod 所在名称空间
dapi-pod-env # Pod名称
172.16.58.205 # Pod启动后分配的IP
default # Pod 的服务账户

k8s-node02
default
dapi-pod-env
172.16.58.205
default

......

1.5 进入Pod容器,查看环境变量

# 进入 Pod 容器
$ kubectl exec -it dapi-pod-env -c busybox-container -- sh

# 打印环境变量
/ # env
MY_POD_SERVICE_ACCOUNT=default
KUBERNETES_SERVICE_PORT=443
KUBERNETES_PORT=tcp://10.96.0.1:443
HOSTNAME=dapi-pod-env
SHLVL=1
HOME=/root
MY_POD_NAMESPACE=default
TERM=xterm
MY_POD_IP=172.16.58.205
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_PROTO=tcp
MY_NODE_NAME=k8s-node02
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
KUBERNETES_SERVICE_HOST=10.96.0.1
PWD=/
MY_POD_NAME=dapi-pod-env

结论:通过 Downward API 可以将Pod的属性、状态信息设置为容器的环境变量

2. 将容器字段应用到环境变量中

2.1 资源清单

dapi-container-env.yaml

apiVersion: v1
kind: Pod
metadata:
  name: dapi-container-env
  namespace: default
spec:
  containers:
    - name: busybox-container
      image: busybox:latest
      imagePullPolicy: IfNotPresent
      command: ['sh', '-c'] # 容器启动后,每 10秒打印一次环境变量
      args:
        - while true; do
            echo -en '\n';
            printenv MY_CPU_REQUEST MY_CPU_LIMIT;
            printenv MY_MEMORY_REQUEST MY_MEMORY_LIMIT;
            sleep 10;
          done;
      resources: # 定义容器使用的资源
        requests:
          memory: "32Mi" # 请求 32 MiB 内存,保证 Pod 调度时节点有足够内存。
          cpu: "125m" # 请求 125 毫核(0.125 CPU 核心),保证 CPU 资源
        limits:
          memory: "64Mi" # 限制内存使用不超过 64 MiB,防止过度消耗
          cpu: "250m" # 限制 CPU 使用不超过 250 毫核(0.25 核心)
      env:
        - name: MY_CPU_REQUEST # 容器请求的CPU资源大小
          valueFrom:
            resourceFieldRef:
              containerName: busybox-container # 指定容器
              resource: requests.cpu
              divisor: 1m # 指定资源值的单位转换因子,用于将 Kubernetes 内部的资源值(如 CPU 或内存)转换为用户期望的单位,并在注入到文件或环境变量时以该单位表示。支持如 1、1m(毫)、1Mi(Mebibyte)、1k(千)等单位
        - name: MY_CPU_LIMIT # 容器请求的CPU资源上限
          valueFrom:
            resourceFieldRef:
              containerName: busybox-container
              resource: limits.cpu
              divisor: 1m
        - name: MY_MEMORY_REQUEST # 容器请求的内存资源大小
          valueFrom:
            resourceFieldRef:
              containerName: busybox-container
              resource: requests.memory
              divisor: 1Mi
        - name: MY_MEMORY_LIMIT # 容器请求的内存资源上限
          valueFrom:
            resourceFieldRef:
              containerName: busybox-container
              resource: limits.memory
              divisor: 1Mi
  restartPolicy: Never

这个配置文件中,你可以看到四个环境变量。env字段是一个 EnvVars 类型的数组。数组中第一个元素指定MY_CPU_REQUEST这个环境变量从容器的requests.cpu字段获取变量值。同样,其它环境变量也是从容器的字段获取它们的变量值。

2.2 创建 Pod

$ kubectl apply -f dapi-container-env.yaml

2.3 查看 Pod 状态

$ kubectl get pods -o wide
NAME                 READY   STATUS    RESTARTS   AGE   IP              NODE         NOMINATED NODE   READINESS GATES
dapi-container-env   1/1     Running   0          11s   172.16.58.241   k8s-node02   <none>           <none>

2.4 查看 Pod 运行日志

$ kubectl logs -f --tail=100 dapi-container-env 

125
250
32
64

125
250
32
64

注意,divisor 显式指定 CPU 和内存的单位,如果不添加此参数,日志显示会有异常。

2.5 进入 Pod容器,查看环境变量

$ kubectl exec -it dapi-container-env -c busybox-container -- env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=dapi-container-env
TERM=xterm
MY_CPU_REQUEST=125
MY_CPU_LIMIT=250
MY_MEMORY_REQUEST=32
MY_MEMORY_LIMIT=64
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
KUBERNETES_SERVICE_HOST=10.96.0.1
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP_PROTO=tcp
HOME=/root

3. 挂载 Pod 字段到 Volume

3.1 资源清单

dapi-pod-volume.yaml

apiVersion: v1
kind: Pod
metadata:
  name: dapi-pod-volume
  namespace: default
  labels:
    zone: us-est-coast
    cluster: test-cluster1
    env: test
  annotations:
    build: two
    builder: George
spec:
  containers:
    - name: busybox-container
      image: busybox:latest
      imagePullPolicy: IfNotPresent
      command: [ 'sh', '-c' ] # 容器启动后,每 10秒打印一次环境变量
      args:
        - while true; do
            if [[ -e /etc/podinfo/labels ]]; then
              echo -en '\n\n'; cat /etc/podinfo/labels; fi;
            if [[ -e /etc/podinfo/annotations ]]; then
              echo -en '\n\n'; cat /etc/podinfo/annotations; fi;
            sleep 5;
          done;
      volumeMounts:
        - name: podinfo
          mountPath: /etc/podinfo
  volumes:
    - name: podinfo
      downwardAPI:
        items:
          - path: "labels"
            fieldRef:
              fieldPath: metadata.labels
          - path: "annotations"
            fieldRef:
              fieldPath: metadata.annotations

Pod有一个downwardAPI类型的Volume,并且挂载到容器中的/etc/podinfo 目录下

查看downwardAPI下面的items数组。每个数组元素都是一个 DownwardAPIVolumeFile。 第一个元素指示Pod的metadata.labels字段的值保存在名为labels的文件中。 第二个元素指示Pod的annotations字段的值保存在名为annotations的文件中。

3.2 创建 Pod

$ kubectl apply -f dapi-pod-volume.yaml

3.3 查看 Pod 状态

$ kubectl get pods
NAME              READY   STATUS    RESTARTS   AGE
dapi-pod-volume   1/1     Running   0          59s

3.4 查看 Pod 挂载目录

# 进入Pod容器
$ kubectl exec -it dapi-pod-volume -- sh

# 进入挂载目录
/ # cd /etc/podinfo/

# 挂载目录下有两个文件,正是 volume 挂载的路径
/etc/podinfo # ls
annotations  labels

# 查看 labes 文件
/etc/podinfo # cat labels 
cluster="test-cluster1"
env="test"

# 查看 annotations 文件
/etc/podinfo # cat annotations
build="two"
builder="George"
cni.projectcalico.org/containerID="31eef5f93dca9ee1b124d4974e8522c31d291fabf43178009b9c4ccb8fb2ebd6"
cni.projectcalico.org/podIP="172.16.58.255/32"
cni.projectcalico.org/podIPs="172.16.58.255/32"
kubectl.kubernetes.io/last-applied-configuration="{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{\"build\":\"two\",\"builder\":\"George\"},\"labels\":{\"cluster\":\"test-cluster1\",\"env\":\"test\",\"zone\":\"us-est-coast\"},\"name\":\"dapi-pod-volume\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"args\":[\"while true; do if [[ -e /etc/podinfo/labels ]]; then echo -en '\\\\n\\\\n'; cat /etc/podinfo/labels; fi; if [[ -e /etc/podinfo/annotations ]]; then echo -en '\\\\n\\\\n'; cat /etc/podinfo/annotations; fi; sleep 5; done;\"],\"command\":[\"sh\",\"-c\"],\"image\":\"busybox:latest\",\"imagePullPolicy\":\"IfNotPresent\",\"name\":\"busybox-container\",\"volumeMounts\":[{\"mountPath\":\"/etc/podinfo\",\"name\":\"podinfo\"}]}],\"volumes\":[{\"downwardAPI\":{\"items\":[{\"fieldRef\":{\"fieldPath\":\"metadata.labels\"},\"path\":\"labels\"},{\"fieldRef\":{\"fieldPath\":\"metadata.annotations\"},\"path\":\"annotations\"}]},\"name\":\"podinfo\"}]}}\n"
kubernetes.io/config.seen="2025-05-15T09:15:54.979291949+08:00"
kubernetes.io/config.source="api"/etc/podinfo #

查看/etc目录下的文件:

ls -laR /etc
/etc:
total 32
drwxrwxrwt    3 root     root           120 May 15 01:15 podinfo
-rw-r--r--    1 root     root           103 May 15 01:15 resolv.conf
-rw-------    1 root     root           136 Sep 26  2024 shadow

/etc/podinfo:
drwxr-xr-x    2 root     root            80 May 15 01:15 ..2025_05_15_01_15_56.854842629
lrwxrwxrwx    1 root     root            31 May 15 01:15 ..data -> ..2025_05_15_01_15_56.854842629
lrwxrwxrwx    1 root     root            18 May 15 01:15 annotations -> ..data/annotations
lrwxrwxrwx    1 root     root            13 May 15 01:15 labels -> ..data/labels

/etc/podinfo/..2025_05_15_01_15_56.854842629:
-rw-r--r--    1 root     root          1313 May 15 01:15 annotations
-rw-r--r--    1 root     root            54 May 15 01:15 labels

在输出中可以看到,labelsannotations文件都在一个临时子目录中:这个例子,..2025_05_15_01_15_56.854842629。在/etc目录中,..data是一个指向临时子目录 的符号链接。/etc目录中,labelsannotations也是符号链接。

用符号链接可实现元数据的动态原子刷新;更新将写入一个新的临时目录,然后..data符号链接完成原子更新,通过使用rename(2)

查看临时子目录

cat /etc/podinfo/..2025_05_15_01_15_56.854842629/labels 
# 内容如下:
cluster="test-cluster1"
env="test"

4. 挂载容器字段到 Volume

4.1 资源清单

dapi-container-volume.yaml

apiVersion: v1
kind: Pod
metadata:
  name: dapi-container-volume
  namespace: default
spec:
  containers:
    - name: busybox-container
      image: busybox:latest
      imagePullPolicy: IfNotPresent
      command: ['sh', '-c'] # 容器启动后,每 10秒打印一次环境变量
      args:
        - while true; do
            echo -en '\n';
            if [[ -e /etc/container/cpu_limit ]]; then
              echo -en '\n'; cat /etc/container/cpu_limit; fi;
            if [[ -e /etc/container/cpu_request ]]; then
              echo -en '\n'; cat /etc/container/cpu_request; fi;
            if [[ -e /etc/container/memory_limit ]]; then
              echo -en '\n'; cat /etc/container/memory_limit; fi;
            if [[ -e /etc/container/memory_request ]]; then
              echo -en '\n'; cat /etc/container/memory_request; fi;
            sleep 5;
          done;
      resources: # 定义容器使用的资源
        requests:
          memory: "32Mi" # 请求 32 MiB 内存,保证 Pod 调度时节点有足够内存。
          cpu: "125m" # 请求 125 毫核(0.125 CPU 核心),保证 CPU 资源
        limits:
          memory: "64Mi" # 限制内存使用不超过 64 MiB,防止过度消耗
          cpu: "250m" # 限制 CPU 使用不超过 250 毫核(0.25 核心)
      volumeMounts:
        - name: container-volume
          mountPath: /etc/container
  volumes:
    - name: container-volume
      downwardAPI:
        items:
          - path: "cpu_request"
            resourceFieldRef:
              containerName: busybox-container
              resource: requests.cpu
              divisor: 1m # 指定资源值的单位转换因子,用于将 Kubernetes 内部的资源值(如 CPU 或内存)转换为用户期望的单位,并在注入到文件或环境变量时以该单位表示。支持如 1、1m(毫)、1Mi(Mebibyte)、1k(千)等单位
          - path: "cpu_limit"
            resourceFieldRef:
              containerName: busybox-container
              resource: limits.cpu
              divisor: 1m
          - path: "memory_request"
            resourceFieldRef:
              containerName: busybox-container
              resource: requests.memory
              divisor: 1Mi
          - path: "memory_limit"
            resourceFieldRef:
              containerName: busybox-container
              resource: limits.memory
              divisor: 1Mi
  restartPolicy: Never

4.2 创建 Pod

$ kubectl apply -f dapi-container-volume.yaml

4.3 查看 Pod 状态

$ kubectl get pods
NAME                    READY   STATUS    RESTARTS   AGE
dapi-container-volume   1/1     Running   0          4s

4.4 查看 Pod 日志

$ kubectl logs -f --tail=100 dapi-container-volume

250 # limits.cpu
125 # requests.cpu
64  # limits.memory
32  # requests.memory

250
125
64
32

4.5 进入 Pod 目录,查看挂载文件

# 进入 Pod 容器
$ kubectl exec -it dapi-container-volume -- sh
/ 

# 进入就挂载目录
# cd /etc/container/

# 查看挂载文件
/etc/container # ls
cpu_limit       cpu_request     memory_limit    memory_request

# 查看 cpu_limit 
/etc/container # cat cpu_limit 
250

5. Downward API 常用属性

下面这些信息可以通过环境变量和DownwardAPIVolumeFiles提供给容器:

能通过fieldRef获得的:

  • metadata.name - Pod名称
  • metadata.namespace - Pod名字空间
  • metadata.uid - Pod的UID, 版本要求 v1.8.0-alpha.2
  • metadata.labels['<KEY>'] - 单个 pod 标签值 <KEY> (例如, metadata.labels['mylabel']); 版本要求 Kubernetes 1.9+
  • metadata.annotations['<KEY>'] - 单个 pod 的标注值 <KEY> (例如, metadata.annotations['myannotation']); 版本要求 Kubernetes 1.9+
  • status.podIP - 节点IP
  • spec.serviceAccountName - Pod服务帐号名称, 版本要求 v1.4.0-alpha.3
  • spec.nodeName - 节点名称, 版本要求 v1.4.0-alpha.3
  • status.hostIP - 节点IP, 版本要求 v1.7.0-alpha.1

说明: 如果容器未指定CPU和memory limits,则Downward API默认为节点可分配值。

能通过resourceFieldRef获得的:

  • 容器的CPU约束值
  • 容器的CPU请求值
  • 容器的内存约束值
  • 容器的内存请求值
  • 容器的临时存储约束值, 版本要求 v1.8.0-beta.0
  • 容器的临时存储请求值, 版本要求 v1.8.0-beta.0

6. volume 相较于 env 优势

  • 会保持热更新的特性
  • 传递一个容器的资源字段到另一个容器中

五、Volume

1. Volume概述

在容器中的文件在磁盘上是临时存放的,当容器关闭时这些临时文件也会被一并清除。这给容器中运行的特殊应用程序带来一些问题。首先,当容器崩溃时,kubelet 将重新启动容器,容器中的文件将会丢失——因为容器会以干净的状态重建。其次,当在一个 Pod 中同时运行多个容器时,常常需要在这些容器之间共享文件。

Kubernetes 抽象出 Volume 对象来解决这两个问题。

Kubernetes Volume卷具有明确的生命周期——与包裹它的 Pod 相同。 因此,Volume比 Pod 中运行的任何容器的存活期都长,在容器重新启动时数据也会得到保留。 当然,当一个 Pod 不再存在时,Volume也将不再存在。更重要的是,Kubernetes 可以支持许多类型的Volume卷,Pod 也能同时使用任意数量的Volume卷。

使用卷时,Pod 声明中需要提供卷的类型 (.spec.volumes 字段)和卷挂载的位置 (.spec.containers.volumeMounts 字段).

2. Volume类型

Kubernetes 支持下列类型的卷:

  • awsElasticBlockStore
  • azureDisk
  • azureFile
  • cephfs
  • cinder
  • configMap
  • csi
  • downwardAPI
  • emptyDir
  • fc (fibre channel)
  • flexVolume
  • flocker
  • gcePersistentDisk
  • gitRepo (deprecated)
  • glusterfs
  • hostPath
  • iscsi
  • local
  • nfs
  • persistentVolumeClaim
  • projected
  • portworxVolume
  • quobyte
  • rbd
  • scaleIO
  • secret
  • storageos
  • vsphereVolume

其中 Secret、ConfigMap 前面已经介绍过了,这里再介绍三种,分别是:emptyDir、hostPath、nfs

3. EmptyDir 卷

当 Pod 指定到某个节点上时,首先创建的是一个 emptyDir 卷,并且只要 Pod 在该节点上运行,卷就一直存在。就像它的名称表示的那样,卷最初是空的。

尽管 Pod 中每个容器挂载 emptyDir 卷的路径可能相同也可能不同,但是这些容器都可以读写 emptyDir 卷中相同的文件

如果Pod中有多个容器,其中某个容器重启,不会影响emptyDir 卷中的数据。当 Pod 因为某些原因被删除时,emptyDir 卷中的数据也会永久删除。

注意:容器崩溃并不会导致 Pod 被从节点上移除,因此容器崩溃时 emptyDir 卷中的数据是安全的。

3.1 emptyDir的一些用途

  • 缓存空间,例如基于磁盘的归并排序
  • 为耗时较长的计算任务提供检查点,以便任务能方便地从崩溃前状态恢复执行
  • 在 Web 服务器容器服务数据时,保存内容管理器容器获取的文件

3.2 emptyDir 使用案例

3.2.1 资源清单

001-volume-emptydir-01.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: default
  name: emptydir-deployment
  labels:
    name: volumn-dep
spec:
  replicas: 1
  selector:
    matchLabels: # Deployment控制器管理具有如下标签的Pod
      type: emptydir
  template:
    metadata:
      labels: # Pod模板设定生成的Pod有这些标签项
        app: myapp
        type: emptydir
    spec:
      containers:
        - name: nginx-container # Pod1,nginx
          image: wangyanglinux/myapp:v1.0
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 80
          volumeMounts: # 设置 pod 挂载的容器卷
            - name: log-volume
              mountPath: /usr/local/nginx/logs # 镜像日志输出目录,必须存在
        - name: busybox-container # Pod2,busybox
          image: busybox:latest
          imagePullPolicy: IfNotPresent
          volumeMounts: # 设置 pod 挂载的容器卷
            - name: log-volume
              mountPath: /logs
          command:
            - '/bin/sh'
            - '-c'
            - 'tail -f /logs/access.log'
      volumes:
        - name: log-volume # 设置自定义容器卷的名称
          emptyDir: {} # 容器卷类型:emptyDir

3.2.2 创建 Deployment

# 使用资源清单创建 Deployment
$ kubectl apply -f 001-volume-emptydir-01.yaml

# 查看 Pod
$ kubectl get pods -o wide 
NAME                                   READY   STATUS    RESTARTS   AGE   IP              NODE         NOMINATED NODE   READINESS GATES
emptydir-deployment-6cf749f46f-fb8ww   2/2     Running   0          6s    172.16.58.197   k8s-node02   <none>           <none>

成功创建Pod,两个容器已就绪。

**访问 nginx-container **

$ curl 172.16.58.197
www.xinxianghf.com | hello MyAPP | version v1.0

3.2.3 查看 Pod 详情

$ kubectl describe pod emptydir-deployment-6cf749f46f-fb8ww
Name:             emptydir-deployment-6cf749f46f-fb8ww
Namespace:        default
Containers:
  nginx-container:
    Container ID:   docker://56c2112d34006320b474374d8e385c61b9d4a0402d1d5ad89d83d83d5a256ba0
	......
    Mounts:
      /usr/local/nginx/logs from log-volume (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-lp4hs (ro)
  busybox-container:
    Container ID:  docker://7f0a784a4e5cd47cc1efe0bc16749e29b38bfababa33f6be3cbda676d38f53c8
    Command:
      /bin/sh
      -c
      tail -f /logs/access.log
    ......
    Mounts:
      /logs from log-volume (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-lp4hs (ro)

两个容器内不同的目录挂载了同一个 Volume (log-volume)

3.2.4 进入 nginx-container 容器查看日志

$ kubectl exec -it emptydir-deployment-6cf749f46f-fb8ww -c nginx-container -- tail -f /usr/local/nginx/logs/access.log
172.16.167.128 - - [15/May/2025:15:00:13 +0800] "GET / HTTP/1.1" 200 48 "-" "curl/7.76.1"
172.16.167.128 - - [15/May/2025:15:01:50 +0800] "GET / HTTP/1.1" 200 48 "-" "curl/7.76.1"

3.2.5 进入 busybox-container 容器查看日志

$ kubectl exec -it emptydir-deployment-6cf749f46f-fb8ww -c busybox-container -- tail -f /logs/access.log
172.16.167.128 - - [15/May/2025:15:00:13 +0800] "GET / HTTP/1.1" 200 48 "-" "curl/7.76.1"
172.16.167.128 - - [15/May/2025:15:01:50 +0800] "GET / HTTP/1.1" 200 48 "-" "curl/7.76.1"

总结:通过 emptyDir 可以实现在一个 Pod 内,多个容器挂载到同一个 Volume,共享挂载文件。且容器可以将 Volume 挂载到不同的路径下。

4. HostPath

EmptyDir 中数据不会被持久化,它会随着 Pod 的结束而销毁,如果想简单的将数据持久化到主机中,可以选择 HostPath。

HostPath 就是将 Node 主机中一个实际目录挂在到 Pod 中,以供容器使用,这样的设计就可以保证 Pod 销毁了,但是数据依据可以存在于 Node 主机上。

4.1 hostPath 的一些用法

  • 运行一个需要访问 Docker 引擎内部机制的容器;请使用 hostPath 挂载 /var/lib/docker 路径。
  • 在容器中运行 cAdvisor 时,以 hostPath 方式挂载 /sys。
  • 允许 Pod 指定给定的 hostPath 在运行 Pod 之前是否应该存在,是否应该创建以及应该以什么方式存在。

4.2 支持类型

除了必需的 path 属性之外,用户可以选择性地为 hostPath 卷指定 type。支持的 type 值如下:

取值 行为
空字符串(默认)用于向后兼容,这意味着在安装 hostPath 卷之前不会执行任何检查
DirectoryOrCreate 如果指定的路径不存在,那么将根据需要创建空目录,权限设置为 0755,具有与 Kubelet 相同的组和所有权
Directory 给定的路径必须存在
FileOrCreate 如果给定路径的文件不存在,那么将在那里根据需要创建空文件,权限设置为 0644,具有与 Kubelet 相同的组和所有权【前提:文件所在目录必须存在;目录不存在则不能创建文件】
File 给定路径上的文件必须存在
Socket 在给定路径上必须存在的 UNIX 套接字
CharDevice 在给定路径上必须存在的字符设备
BlockDevice 在给定路径上必须存在的块设备

4.3 注意事项

当使用这种类型的卷时要小心,因为:

  • 具有相同配置(例如从 podTemplate 创建)的多个 Pod 会由于节点上文件的不同而在不同节点上有不同的行为。
  • 当 Kubernetes 按照计划添加资源感知的调度时,这类调度机制将无法考虑由 hostPath 卷使用的资源。
  • 基础主机上创建的文件或目录只能由 root 用户写入。需要在 特权容器 中以 root 身份运行进程,或者修改主机上的文件权限以便容器能够写入 hostPath 卷。

4.4 hostPath 使用案例

4.4.1 资源清单

002-volume-hostpath-01.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: default
  name: hostpath-deployment
  labels:
    name: volumn-dep
spec:
  replicas: 1
  selector:
    matchLabels: # Deployment控制器管理具有如下标签的Pod
      type: hostpath
  template:
    metadata:
      labels: # Pod模板设定生成的Pod有这些标签项
        app: myapp
        type: hostpath
    spec:
      containers:
        - name: nginx-container # Pod1,nginx
          image: wangyanglinux/myapp:v1.0
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 80
          volumeMounts: # 设置 pod 挂载的容器卷
            - name: log-volume
              mountPath: /usr/local/nginx/logs # 镜像日志输出目录,必须存在
        - name: busybox-container # Pod2,busybox
          image: busybox:latest
          imagePullPolicy: IfNotPresent
          volumeMounts: # 设置 pod 挂载的容器卷
            - name: log-volume
              mountPath: /logs
          command:
            - '/bin/sh'
            - '-c'
            - 'tail -f /logs/access.log'
      volumes:
        - name: log-volume # 设置自定义容器卷的名称
          hostPath: # 容器卷类型:hostPath
            path: /root/logs
            type: DirectoryOrCreate # 目录存在就使用,不存在就先创建后使用

4.4.2 创建 Deployment

# 加载资源清单,创建 Deployment
$ kubectl apply -f 002-volume-hostpath-01.yaml --record

# 查看 Pod 状态
$ kubectl get pods -o wide
NAME                                   READY   STATUS    RESTARTS   AGE   IP              NODE         NOMINATED NODE   READINESS GATES
hostpath-deployment-774b569d9f-ntpqp   2/2     Running   0          41s   172.16.58.247   k8s-node02   <none>           <none>

# 访问 nginx 容器
$ curl 172.16.58.247
www.xinxianghf.com | hello MyAPP | version v1.0

4.4.3 进入 busybox-container 容器查看日志

$ kubectl exec -it hostpath-deployment-774b569d9f-ntpqp -c busybox-container -- tail -f /logs/access.log
172.16.167.128 - - [30/Apr/2025:14:17:15 +0800] "GET / HTTP/1.1" 200 48 "-" "curl/7.76.1"
172.16.167.128 - - [30/Apr/2025:14:17:20 +0800] "GET / HTTP/1.1" 200 48 "-" "curl/7.76.1"

4.4.4 查看宿主机挂载目录

当前 Pod 运行在 k8s-node02 节点上,挂载目录为 /root/logs (资源清单配置挂载目录)

[root@k8s-node02 /]$ tail -f /root/logs/access.log 
172.16.167.128 - - [30/Apr/2025:14:17:15 +0800] "GET / HTTP/1.1" 200 48 "-" "curl/7.76.1"
172.16.167.128 - - [30/Apr/2025:14:17:20 +0800] "GET / HTTP/1.1" 200 48 "-" "curl/7.76.1"

5. NFS

HostPath可以解决数据持久化的问题,但是一旦Node节点故障了,Pod如果转移到了别的节点,又会出现问题了,此时需要准备单独的网络存储系统,比较常用的用NFS、CIFS。

NFS是一个网络文件存储系统,可以搭建一台NFS服务器,然后将Pod中的存储直接连接到NFS系统上,这样的话,无论Pod在节点上怎么转移,只要Node跟NFS的对接没问题,数据就可以成功访问。

NFS文件存储

5.1 安装 NFS 文件服务器

NFS 文件服务器与 FTP 类似,都有服务端和客户端。这里为了简单,以 Master 节点作为 NFS 服务端,其它 Node 节点作为 NFS 客户端。

5.1.1 安装 NFS 服务

在所有节点都要执行

$ yum install -y nfs-utils rpcbind

5.1.2 创建共享目录

仅在 Master 节点执行

# 创建 共享目录
$ mkdir -p /root/data/nfs/

# 目录提权
chmod 777 /root/data/nfs/

# 变更用户组
chown nobody /root/data/nfs/

5.1.3 编辑共享目录读写配置

仅在 NFS 服务端节点操作

$ vim /etc/exports

# 内容如下:
/root/data/nfs     192.168.6.0/24(rw,no_root_squash,no_all_squash,sync)

表示 192.168.6. 网段的ip 都可以与 nfs 主服务器共享 /root/data/nfs 目录内容

参数说明:

参数 说明
ro 只读访问
rw 读写访问
sync 所有数据在请求时写入共享目录
async nfs 在写入大数据时可以回应请求
secure nfs 通过 1024 以下的安全 TCP/IP 端口发送
insecure nfs 通过 1024 以上的端口发送
wdelay 如果多个用户要写入 nfs 目录,则归组写入(默认)
no_wdelay 如果多个用户要写入 nfs 目录,则立即写入,当使用 async 时,无需此设置
hide 在 nfs 共享目录中不共享其子目录
no_hide 共享 nfs 目录的子目录
subtree_check 如果共享子目录如 /usr/bin 之类子目录,则强制检查子目录的权限(默认)
no_subtree_check 不检查子目录权限
all_squash 共享文件的 UID 和 GID 映射匿名用户 anonymous,通常会用匿名用户
no_all_squash 保留共享文件的 UID 和 GID(默认)
root_squash root 用户的所有请求映射成 anonymous 用户一样的权限(默认)
no_root_squash root 用户具有根目录的完全管理访问权限
anonuid=xxx 指定 nfs 服务器 /etc/passwd 文件中匿名用户的 UID
anongid=xxx 指定 nfs 服务器 /etc/passwd 文件中匿名用户的 GID

5.1.4 启动NFS服务

在集群内所有节点操作

 # 启动服务
$ systemctl start rpcbind
$ systemctl restart nfs-server.service

# 设置开机自启
$ systemctl enable rpcbind
$ systemctl enable nfs-server.service

5.1.5 测试NFS服务

# 在Node01节点测试一下,是否能够正确挂载:
root@k8s-node01 ~]$ showmount -e 192.168.6.139
Export list for 192.168.6.139:
/root/data/nfs 192.168.6.0/24

# 在客户端创建挂在目录
[root@k8s-node01 ~]$ mkdir /data/testnfs

# 挂载远端目录到本地 /data/testnfs 目录
[root@k8s-node01 ~]# mount 192.168.6.139:/root/data/nfs /data/testnfs

# NFS服务端写入
[root@k8s-master01 ~]# echo "This is NFS server." > /root/data/nfs/nfs.txt

# 客户端读取
[root@k8s-node01 ~]# cat /data/testnfs/nfs.txt
This is NFS server.

# 客户端写入
[root@k8s-node01 ~]# echo "This is NFS client." >> /data/testnfs/nfs.txt

# 服务端读取
[root@k8s-master01 ~]# cat /root/data/nfs/nfs.txt
This is NFS server.
This is NFS client.

都是没问题的,这是因为上边设置了 NFS 远端目录权限为 rw 拥有读写权限,如果设置为 ro,那么
客户端只能读取,不能写入,根据实际应用场景合理配置,这里就不在演示了。这里提一下,NFS 默认
使用 UDP 协议来进行挂载,为了提高 NFS 的稳定性,可以使用 TCP 协议挂载,那么客户端挂载命
令可使用如下命令
mount 192.168.6.139:/root/data/nfs /data/testnfs -o proto=tcp -o nolock

5.1.6 客户端卸载 NFS 挂载目录

umount /data/testnfs

# 强制卸载
umount -l /data/testnfs

5.1.7 使用 NFS4 服务

相较于 NFS v2 v3 版本更新安全和高效

  • 修改 NFS 服务端配置

    $ vim /etc/exports
    /root/data/nfs     192.168.6.0/24(rw,fsid=0,no_root_squash)

    配置 /etc/exports 使用 NFSv4 伪文件系统(fsid=0)

  • 客户端使用 NFS4 挂载

    # 在客户端创建挂在目录
    [root@k8s-node01 ~]$ mkdir /data/testnfs
    
    # 挂载远端目录到本地 /data/testnfs 目录
    [root@k8s-node01 ~]# mount -t nfs4 192.168.6.139:/root/data/nfs /data/testnfs

5.2 使用 NFS 挂载 Pod 目录

5.2.1 资源清单

003-volume-nfs-01.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: default
  name: nfs-deployment
  labels:
    name: volumn-dep
spec:
  replicas: 1
  selector:
    matchLabels: # Deployment控制器管理具有如下标签的Pod
      type: nfs
  template:
    metadata:
      labels: # Pod模板设定生成的Pod有这些标签项
        app: myapp
        type: nfs
    spec:
      containers:
        - name: nginx-container # Pod1,nginx
          image: wangyanglinux/myapp:v1.0
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 80
          volumeMounts: # 设置 pod 挂载的容器卷
            - name: log-volume
              mountPath: /usr/local/nginx/logs # 镜像日志输出目录,必须存在
        - name: busybox-container # Pod2,busybox
          image: busybox:latest
          imagePullPolicy: IfNotPresent
          volumeMounts: # 设置 pod 挂载的容器卷
            - name: log-volume
              mountPath: /logs
          command:
            - '/bin/sh'
            - '-c'
            - 'tail -f /logs/access.log'
      volumes:
        - name: log-volume # 设置自定义容器卷的名称
          nfs: # 容器卷类型:hostPath
            server: 192.168.6.139 # NFS 服务端
            path: /root/data/nfs

5.2.2 创建 Deployment

# 加载资源清单,创建 Deployment
$ kubectl apply -f 003-volume-nfs-01.yaml

# 查看 Pod 状态
$ kubectl get pods -o wide
NAME                                   READY   STATUS    RESTARTS   AGE    IP              NODE         NOMINATED NODE   READINESS GATES
nfs-deployment-5c6b9ccfd-c2szn         2/2     Running   0          2m6s   172.16.85.201   k8s-node01   <none>           <none>

# 访问 nginx 容器
$ curl 172.16.85.201
www.xinxianghf.com | hello MyAPP | version v1.0

5.2.3 查看 NFS 挂载目录

[root@k8s-master01 ~]$ cat /root/data/nfs/access.log 
172.16.167.128 - - [15/May/2025:17:01:59 +0800] "GET / HTTP/1.1" 200 48 "-" "curl/7.76.1"
172.16.167.128 - - [15/May/2025:17:02:01 +0800] "GET / HTTP/1.1" 200 48 "-" "curl/7.76.1"
172.16.167.128 - - [15/May/2025:17:02:01 +0800] "GET / HTTP/1.1" 200 48 "-" "curl/7.76.1"

pod 容器日志,通过 NFS 挂载到了 宿主机 Master 节点上,即便Pod删除,数据也不会丢失。

六、PV/PVC

1. 概述

与管理计算实例相比,管理存储是一个明显的问题。PersistentVolume 子系统为用户和管理员提供了一个API,该API从如何使用存储中,抽象出如何提供存储的详细信息。为此,我们引入了两个新的API资源:PersistentVolume 和 PersistentVolumeClaim。

2. PV概述

PersistentVolume (PV) 是集群中由管理员提供或使用存储类动态提供的一块存储。它是集群中的资源,就像节点是集群资源一样。

PV 是与 Volumes 类似的卷插件,但其生命周期与使用PV的任何单个Pod无关。此API对象捕获存储实现的详细信息,包括NFS,iSCSI或特定于云提供程序的存储系统。

3. PVC概述

PersistentVolumeClaim (PVC) 是用户对存储的请求。它类似于Pod;Pods消耗节点资源,而PVC消耗PV资源。Pods可以请求特定级别的资源(CPU和内存)。Claim可以请求特定的存储大小和访问模式(例如,它们可以挂载一次读写或多次只读)。

但是通过 PVC 请求到一定的存储空间也很有可能不足以满足应用对于存储设备的各种需求,而且不同的应用程序对于存储性能的要求可能也不尽相同,比如读写速度、并发性能等,为了解决这一问题,Kubernetes 又为我们引入了一个新的资源对象:StorageClass,通过 StorageClass 的定义,管理员可以将存储资源定义为某种类型的资源,比如快速存储、慢速存储等,用户根据 StorageClass 的描述就可以非常直观的知道各种存储资源的具体特性了,这样就可以根据应用的特性去申请合适的存储资源了。

4. PV 和 PVC 的生命周期

在 Kubernetes 中,Persistent Volume (PV) 和 Persistent Volume Claim (PVC) 是管理持久存储的核心组件。它们的生命周期与存储的分配、使用和释放密切相关。

4.1 Persistent Volume (PV) 的生命周期

PV 是一个集群级别的存储资源,表示一块具体的存储(如 NFS、云存储卷等)。其生命周期包括以下阶段:

1. 创建(Provisioning)

  • 静态分配:管理员手动创建 PV,定义容量、访问模式(如 ReadWriteOnce、ReadOnlyMany)、存储类型等。

    apiVersion: v1
    kind: PersistentVolume
    metadata:
      name: my-pv
    spec:
      capacity:
        storage: 1Gi
      accessModes:
        - ReadWriteOnce
      persistentVolumeReclaimPolicy: Retain
      storageClassName: standard
      hostPath:
        path: /mnt/data
  • 动态分配:通过 PVC 和 StorageClass 自动创建 PV,无需管理员手动干预。

2. 绑定(Binding)

  • PV 与符合其条件的 PVC 绑定(匹配容量、访问模式、StorageClass 等)。
  • 绑定后,PV 状态变为 Bound,并记录绑定的 PVC 信息。
  • 如果没有合适的 PVC,PV 保持 Available 状态。

3. 使用(Usage)

  • 绑定的 PV 被 Pod 通过 PVC 引用,挂载到容器中的指定路径。
  • Pod 运行期间,容器可以读写 PV 提供的存储。
  • PV 的底层存储(如云卷、NFS)决定数据的持久性和性能。

4. 释放(Releasing)

  • 当绑定的 PVC 被删除时,PV 进入释放阶段。

  • PV 的回收策略(persistentVolumeReclaimPolicy)决定后续行为:

    • Retain:PV 保留,数据和元数据不删除,需手动清理 。
    • Delete:PV 和底层存储资源自动删除(常见于云存储)。
    • Recycle(已弃用):数据被擦除,PV 可被复用。(只有NFS、HostPath支持回收)

5. 删除(Deletion)

  • 如果回收策略为 Delete,PV 和底层存储被销毁。
  • 如果为 Retain,管理员需手动删除 PV 或重新绑定到新 PVC。

4.2 Persistent Volume Claim (PVC) 的生命周期

PVC 是命名空间级别的存储请求,充当 Pod 和 PV 之间的抽象层。其生命周期包括以下阶段:

1. 创建(Request)

  • 用户创建 PVC,指定存储需求(如容量、访问模式、StorageClass)

    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: my-pvc
      namespace: default
    spec:
      accessModes:
        - ReadWriteOnce
      resources:
        requests:
          storage: 1Gi
      storageClassName: standard
  • PVC 进入 Pending 状态,等待绑定。

2. 绑定(Binding)

  • Kubernetes 控制器寻找匹配的 PV(容量、访问模式等)。
  • 静态分配:绑定到现有的 PV。
  • 动态分配:通过 StorageClass 自动创建 PV 并绑定。
  • 绑定成功后,PVC 状态变为 Bound,记录绑定的 PV 名称。

3. 使用(Usage)

  • Pod 通过 PVC 引用存储,配置在 spec.volumes 和 spec.containers.volumeMounts 中。

    apiVersion: v1
    kind: Pod
    metadata:
      name: my-pod
    spec:
      containers:
      - name: my-container
        image: nginx
        volumeMounts:
        - mountPath: "/data"
          name: my-volume
      volumes:
      - name: my-volume
        persistentVolumeClaim:
          claimName: my-pvc
  • PVC 提供与底层 PV 无关的接口,Pod 直接访问存储。

4. 释放(Releasing)

  • 当用户删除 PVC 时,Kubernetes 解除 PVC 与 PV 的绑定。
  • PV 根据回收策略(Retain、Delete)进入相应处理流程。

5. 删除(Deletion)

  • PVC 删除后,其资源被销毁。
  • 如果 PV 的回收策略为 Delete,底层存储可能被销毁;如果为 Retain,PV 可重新绑定到新 PVC。

5. 持久化声明保护

“使用中的存储对象保护” :该功能的目的是确保在Pod活动时使用的PersistentVolumeClaims (PVC)和绑定到PVC的PersistentVolume (PV)不会从系统中删除,因为这可能会导致数据丢失。

如果用户删除了Pod正在使用的PVC,则不会立即删除该PVC;PVC的清除被推迟,直到任何Pod不再主动使用PVC。另外,如果管理员删除绑定到PVC的PV,则不会立即删除该PV;PV的去除被推迟,直到PV不再与PVC结合。

6. 回收策略

当用户处理完他们的卷时,他们可以从允许回收资源的API中删除PVC对象。PersistentVolume 的回收策略告诉集群在释放卷的声明后该如何处理它。目前,卷可以被保留、回收或删除。

6.1 Retain (保留)

保留回收策略允许手动回收资源。当 PersistentVolumeClaim 被删除时,PersistentVolume 仍然存在,并且该卷被认为是“释放”的。但是,由于之前声明的数据仍然存在,因此另一个声明尚无法得到。管理员可以手动回收卷。

6.2 Delete (删除)

对于支持 Delete 回收策略的卷插件,删除操作会同时从 Kubernetes 中删除 PersistentVolume 对象以及外部基础架构中的关联存储资产,例如 AWS EBS,GCE PD,Azure Disk 或 Cinder 卷。动态配置的卷将继承其 StorageClass 的回收策略,默认为 Delete。管理员应根据用户的期望配置 StorageClass。

6.3 Recycle (回收)

如果基础卷插件支持,Recycle 回收策略将 rm -rf /thevolume/* 对该卷执行基本的擦除并使其可用于新的声明。

7. Persistent Volumes 类型

PersistentVolume类型作为插件实现。Kubernetes当前支持以下插件:

  • GCEPersistentDisk
  • AWSElasticBlockStore
  • AzureFile
  • AzureDisk
  • CSI
  • FC (Fibre Channel)
  • FlexVolume
  • Flocker
  • NFS
  • iSCSI
  • RBD (Ceph Block Device)
  • CephFS
  • Cinder (OpenStack block storage)
  • Glusterfs
  • VsphereVolume
  • Quobyte Volumes
  • HostPath (仅用于单节点测试——本地存储不受任何方式的支持,也不能在多节点集群中工作)
  • Portworx Volumes
  • ScaleIO Volumes
  • StorageOS

8. PV示例与参数说明

8.1 PV示例

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-demo1
spec:
  capacity:
    storage: 5Gi # PV的容量大小 5G
  volumeMode: Filesystem
  accessModes: # PV 访问模式
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete # 回收策略
  storageClassName: slow
  mountOptions:
    - hard # 指定挂载选项为 hard,表示 NFS 挂载是硬挂载,客户端会在连接中断时不断重试。
    - nfsvers=4.1 # 指定使用 NFS 协议版本 4.1。
  nfs:
    path: /root/data/nfs/
    server: 192.168.142.199

Capacity:通常,PV将具有特定的存储容量设置。当前,存储大小是可以设置或请求的唯一资源。将来的属性可能包括IOPS,吞吐量等。

volumeMode:可选参数,为Filesystem或Block。Filesystem是volumeMode省略参数时使用的默认模式。

accessModes:PersistentVolume可以通过资源提供者支持的任何方式安装在主机上。如下文表中所示,提供商将具有不同的功能,并且每个PV的访问模式都将设置为该特定卷支持的特定模式。例如,NFS可以支持多个读/写客户端,但是特定的NFS PV可能以只读方式在服务器上导出。每个PV都有自己的一组访问模式,用于描述该特定PV的功能。

访问方式为:

  • ReadWriteOnce-该卷可以被单个节点以读写方式挂载
  • ReadOnlyMany-该卷可以被许多节点以只读方式挂载
  • ReadWriteMany-该卷可以被多个节点以读写方式挂载

在CLI命令行中,访问模式缩写为:

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

说明:一个卷一次只能使用一种访问模式挂载,即使它支持多种访问模式。

storageClassName:PV可以有一个类,通过将 storageClassName 属性设置为一个 StorageClass 的名称来指定这个类。特定类的 PV只能绑定到请求该类的 PVC。没有 storageClassName 的 PV 没有类,只能绑定到不请求特定类的PVC。

persistentVolumeReclaimPolicy:当前的回收政策是:Retain (保留)-手动回收、Recycle (回收)-基本擦除(rm -rf /thevolume/*)、Delete (删除)-删除相关的存储资产 (例如 AWS EBS,GCE PD,Azure Disk 或 OpenStack Cinder 卷)。

备注:当前,仅NFS和HostPath支持回收。AWS EBS,GCE PD,Azure Disk和Cinder卷支持删除。

8.2 PV卷状态

卷将处于以下某种状态:

  • Available:尚未绑定到声明(claim)的空闲资源
  • Bound:卷已被声明绑定
  • Released:声明已被删除,但群集尚未回收该资源
  • Failed:该卷自动回收失败

CLI 将显示绑定到 PV 的 PVC 的名称。

8.3 PV 类型与支持的访问模式

Volume Plugin ReadWriteOnce ReadOnlyMany ReadWriteMany
AWSElasticBlockStore - -
AzureFile
AzureDisk - -
CephFS
Cinder - -
CSI depends on the driver depends on the driver depends on the driver
FC -
FlexVolume depends on the driver
Flocker - -
GCEPersistentDisk -
Glusterfs
HostPath - -
iSCSI -
Quobyte
NFS
RBD -
VsphereVolume - - (works when Pods are collocated)
PortworxVolume -
ScaleIO -
StorageOS - -

9. PV-PVC示例

9.1 主机信息

服务器名称(hostname) 系统版本 配置 内网IP 部署模块
k8s-master Rocky 9.3 2C/4G/100G 192.168.204.199 k8s-master,NFS
k8s-node01 Rocky 9.3 2C/4G/100G 192.168.204.201 k8s-node
k8s-node02 Rocky 9.3 2C/4G/100G 192.168.204.202 k8s-node

9.2 NFS服务部署

部署及测试过程,参考上一段。

创建共享目录

# 创建共享目录
[root@k8s-master01 ~]$ mkdir -p /root/data/nfs{1..6}
# 给目录授权
[root@k8s-master01 ~]$ chown -R nobody:nobody /root/data/
# 查看目录权限
[root@k8s-master01 ~]$ ll /root/data/
total 0
drwxr-xr-x 2 nobody nobody 6 May 20 22:48 nfs1
drwxr-xr-x 2 nobody nobody 6 May 20 22:48 nfs2
drwxr-xr-x 2 nobody nobody 6 May 20 22:48 nfs3
drwxr-xr-x 2 nobody nobody 6 May 20 22:48 nfs4
drwxr-xr-x 2 nobody nobody 6 May 20 22:48 nfs5
drwxr-xr-x 2 nobody nobody 6 May 20 22:48 nfs6


# 编辑 NFS 配置文件
[root@k8s-master01 ~]$ vim /etc/exports
# 添加如下内容
/root/data     192.168.142.0/24(rw,fsid=0,no_root_squash)
/root/data/nfs1     192.168.142.0/24(rw,no_root_squash,no_all_squash,sync)
/root/data/nfs2     192.168.142.0/24(rw,no_root_squash,no_all_squash,sync)
/root/data/nfs3     192.168.142.0/24(rw,no_root_squash,no_all_squash,sync)
/root/data/nfs4     192.168.142.0/24(rw,no_root_squash,no_all_squash,sync)
/root/data/nfs5     192.168.142.0/24(rw,no_root_squash,no_all_squash,sync)
/root/data/nfs6     192.168.142.0/24(rw,no_root_squash,no_all_squash,sync)

# 重启 NFS 服务
[root@k8s-master01 ~]$ systemctl restart rpcbind.service
[root@k8s-master01 ~]$ systemctl restart nfs-server

# 检查NFS服务 , 其中 192.168.142.199 为服务端IP
[root@k8s-master01 ~]$ showmount -e 192.168.142.199
Export list for 192.168.142.199:
/root/data/nfs6 192.168.142.0/24
/root/data/nfs5 192.168.142.0/24
/root/data/nfs4 192.168.142.0/24
/root/data/nfs3 192.168.142.0/24
/root/data/nfs2 192.168.142.0/24
/root/data/nfs1 192.168.142.0/24
/root/data      192.168.142.0/24

NFS客户端验证

在k8s-node02机器验证

# 查看rpcbind服务,默认是启动的,如果没有启动则启动并加入开机自启动
 [root@k8s-node02 ~]$ systemctl status rpcbind.service
 
 # 查看NFS服务信息
 [root@k8s-node02 ~]$ showmount -e 192.168.142.199
Export list for 192.168.142.199:
/root/data/nfs6 192.168.142.0/24
/root/data/nfs5 192.168.142.0/24
/root/data/nfs4 192.168.142.0/24
/root/data/nfs3 192.168.142.0/24
/root/data/nfs2 192.168.142.0/24
/root/data/nfs1 192.168.142.0/24
/root/data      192.168.142.0/24

# 挂载,并进行读写验证
[root@k8s-node02 ~]$ mount -t nfs 192.168.142.199:/root/data/nfs1 /mnt/

# 验证完毕,去掉NFS挂载
[root@k8s-node02 ~]$ umount -lf 192.168.142.199:/root/data/nfs1

9.3 PV 部署

资源清单:pv.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-nfs1
spec:
  capacity:
    storage: 1Gi # 1G磁盘容量
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Recycle # 自动回收的策略
  storageClassName: nfs # 使用NFS
  nfs:
    path: /root/data/nfs1
    server: 192.168.142.199

---

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-nfs2
spec:
  capacity:
    storage: 3Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Recycle # 自动回收的策略
  storageClassName: nfs # 使用NFS
  nfs:
    path: /root/data/nfs2
    server: 192.168.142.199

---

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-nfs3
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Recycle # 自动回收的策略
  storageClassName: nfs # 使用NFS
  nfs:
    path: /root/data/nfs3
    server: 192.168.142.199

---

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-nfs4
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Recycle # 自动回收的策略
  storageClassName: nfs # 使用NFS
  nfs:
    path: /root/data/nfs4
    server: 192.168.142.199

---

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-nfs5
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Recycle # 自动回收的策略
  storageClassName: nfs # 使用NFS
  nfs:
    path: /root/data/nfs5
    server: 192.168.142.199

---

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-nfs6
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Recycle # 自动回收的策略
  storageClassName: nfs # 使用NFS
  nfs:
    path: /root/data/nfs6
    server: 192.168.142.199

启动PV,并查看状态

# 创建PV
$ kubectl create -f pv.yaml

# 查看PV详情
$ kubectl get pv -o wide
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE     VOLUMEMODE
pv-nfs1   1Gi        RWO            Recycle          Available           nfs            <unset>                          3m14s   Filesystem
pv-nfs2   3Gi        RWO            Recycle          Available           nfs            <unset>                          3m14s   Filesystem
pv-nfs3   5Gi        RWO            Recycle          Available           nfs            <unset>                          3m14s   Filesystem
pv-nfs4   10Gi       RWO            Recycle          Available           nfs            <unset>                          3m14s   Filesystem
pv-nfs5   5Gi        RWX            Recycle          Available           nfs            <unset>                          3m13s   Filesystem
pv-nfs6   5Gi        RWO            Recycle          Available           nfs            <unset>                          3m13s   Filesystem

9.4 StatefulSet 创建并使用 PVC

9.4.1 资源清单

sts-pod-pvc.yaml

apiVersion: v1
kind: Service
metadata:
  name: nginx # 服务的名称为 nginx
  labels:
    app: nginx # 给服务打一个标签
spec:
  ports: # 指定服务暴露的端口
    - port: 80 # 服务监听在 80 端口,外部请求通过该端口访问
  clusterIP: None # 设置为 None,表示这是一个 Headless Service。Headless Service 不会分配集群 IP,而是直接通过 DNS 解析到后端的 Pod。
  selector:
    app: nginx # 服务通过标签选择器 app: nginx 找到匹配的 Pod,将流量路由到这些 Pod。

---

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web # StatefulSet 名称为 web,用于管理一组 Pod。
spec:
  selector:
    matchLabels: # 通过标签 app: nginx 选择要管理的 Pod,确保 StatefulSet 只控制带有该标签的 Pod。
      app: nginx
  serviceName: nginx # 指定关联的 Service 的名称为 nginx, StatefulSet 使用此 Service 来为每个 Pod 提供稳定的 DNS 名称(例如 web-0.nginx.default.svc.cluster.local.)
  replicas: 3 # 创建 3 个 Pod 副本,分别命名为 web-0、web-1 和 web-2
  template:
    metadata:
      labels: # 为 Pod 添加标签 app: nginx,与 Service 的选择器匹配。
        app: nginx
    spec:
      containers:
        - name: nginx
          image: wangyanglinux/myapp:v1.0
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 80
              name: web
          volumeMounts:
            - name: www # 挂载的卷名称为 www
              mountPath: /usr/local/nginx/html
  volumeClaimTemplates:
    - metadata:
        name: www # PVC 名称为 www,与 volumeMounts 中的名称对应
      spec:
        accessModes:
          - ReadWriteOnce
        storageClassName: nfs
        resources:
          requests:
            storage: 3Gi

9.4.2 启动pod并查看状态

# 执行资源清单,创建Service 和 StatufulSet 和 Pod
$ kubectl apply -f sts-pod-pvc.yaml

# 查看 Service 详情
$ kubectl get svc -o wide
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE   SELECTOR
kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   66d   <none>
nginx        ClusterIP   None         <none>        80/TCP    6s    app=nginx

# 查看 StatefulSet 详情
$ kubectl get statefulSet -o wide
NAME   READY   AGE   CONTAINERS   IMAGES
web    3/3     52s   nginx        wangyanglinux/myapp:v1.0

# 查看 Pod详情
$ kubectl get pods -o wide
NAME    READY   STATUS    RESTARTS   AGE   IP               NODE         NOMINATED NODE   READINESS GATES
web-0   1/1     Running   0          61s   192.168.85.228   k8s-node01   <none>           <none>
web-1   1/1     Running   0          56s   192.168.58.226   k8s-node02   <none>           <none>
web-2   1/1     Running   0          50s   192.168.58.227   k8s-node02   <none>           <none>

9.4.3 PV和PVC状态信息查看

# PV 明细
$ kubectl get pods -o wide
NAME    READY   STATUS    RESTARTS   AGE   IP               NODE         NOMINATED NODE   READINESS GATES
web-0   1/1     Running   0          30m   192.168.58.233   k8s-node02   <none>           <none>
web-1   1/1     Running   0          30m   192.168.85.237   k8s-node01   <none>           <none>
web-2   1/1     Running   0          30m   192.168.58.234   k8s-node02   <none>           <none>

# PVC 明细
$ kubectl get pvc -o wide
NAME        STATUS   VOLUME    CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE   VOLUMEMODE
www-web-0   Bound    pv-nfs2   3Gi        RWO            nfs            <unset>                 31m   Filesystem
www-web-1   Bound    pv-nfs3   5Gi        RWO            nfs            <unset>                 31m   Filesystem
www-web-2   Bound    pv-nfs6   5Gi        RWO            nfs            <unset>                 31m   Filesystem

PVC与PV绑定时会根据storageClassName(存储类名称)和accessModes(访问模式)判断哪些PV符合绑定需求。然后再根据存储量大小判断,首先存PV储量必须大于或等于PVC声明量;其次就是PV存储量越接近PVC声明量,那么优先级就越高(PV量越小优先级越高)。

从上面的信息可以看到 web-0 对应的集群内的IP是 192.168.58.233, 运行在 k8s-node02 节点上,对应的 PVC 是 www-web-0, 绑定的PV 是 pv-nfs2

9.4.4 curl访问验证

在NFS服务端对应NFS共享目录创建文件

pv-nfs2、pv-nfs3、pv-nfs6 被绑定,只需要在这三个pv对应的目录写入文件即可

$ echo "hello nfs2" >> /root/data/nfs2/index.html
$ echo "hello nfs3" >> /root/data/nfs3/index.html
$ echo "hello nfs6" >> /root/data/nfs6/index.html

curl 访问 pod

NAME    READY   STATUS    RESTARTS   AGE   IP               NODE         NOMINATED NODE   READINESS GATES
web-0   1/1     Running   0          40m   192.168.58.233   k8s-node02   <none>           <none>
web-1   1/1     Running   0          40m   192.168.85.237   k8s-node01   <none>           <none>
web-2   1/1     Running   0          39m   192.168.58.234   k8s-node02   <none>           <none>
[root@k8s-master01 /opt/k8s/07/pv-pvc]$ curl 192.168.58.233
hello nfs2
[root@k8s-master01 /opt/k8s/07/pv-pvc]$ curl 192.168.58.234
hello nfs6
[root@k8s-master01 /opt/k8s/07/pv-pvc]$ curl 192.168.85.237
hello nfs3

即使删除其中一个pod,pod被拉起来后也能正常访问。

9.5 删除 StatefulSet 并回收 PV

9.5.1 删除 StatefulSet

$ kubectl delete -f sts-pod-pvc.yaml 
service "nginx" deleted
statefulset.apps "web" deleted

$ kubectl get svc 
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   70d

$ kubectl get pod 
No resources found in default namespace.

9.5.2 查看PVC和PV,并删除PVC

# 查看PVC
$ kubectl get pvc -o wide
NAME        STATUS   VOLUME    CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE   VOLUMEMODE
www-web-0   Bound    pv-nfs2   3Gi        RWO            nfs            <unset>                 61m   Filesystem
www-web-1   Bound    pv-nfs3   5Gi        RWO            nfs            <unset>                 61m   Filesystem
www-web-2   Bound    pv-nfs6   5Gi        RWO            nfs            <unset>                 61m   Filesystem

# 查看PV
$ kubectl get pv -o wide
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM               STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE   VOLUMEMODE
pv-nfs1   1Gi        RWO            Recycle          Available                       nfs            <unset>                          62m   Filesystem
pv-nfs2   3Gi        RWO            Recycle          Bound       default/www-web-0   nfs            <unset>                          62m   Filesystem
pv-nfs3   5Gi        RWO            Recycle          Bound       default/www-web-1   nfs            <unset>                          62m   Filesystem
pv-nfs4   10Gi       RWO            Recycle          Available                       nfs            <unset>                          62m   Filesystem
pv-nfs5   5Gi        RWX            Recycle          Available                       nfs            <unset>                          62m   Filesystem
pv-nfs6   5Gi        RWO            Recycle          Bound       default/www-web-2   nfs            <unset>                          62m   Filesystem

9.5.3 回收 PV

删除PVC后再次查看PV,发现PV从 Bound 状态变成了 Released 状态,没有真正释放掉(变回Available状态),这时有两种办法,一是等待,让PV自动释放掉,第二种方式是手动释放PV

# 再次查看PV
$ kubectl get pv -o wide
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM               STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE   VOLUMEMODE
pv-nfs1   1Gi        RWO            Recycle          Available                       nfs            <unset>                          67m   Filesystem
pv-nfs2   3Gi        RWO            Recycle          Released    default/www-web-0   nfs            <unset>                          67m   Filesystem
pv-nfs3   5Gi        RWO            Recycle          Released    default/www-web-1   nfs            <unset>                          67m   Filesystem
pv-nfs4   10Gi       RWO            Recycle          Available                       nfs            <unset>                          67m   Filesystem
pv-nfs5   5Gi        RWX            Recycle          Available                       nfs            <unset>                          67m   Filesystem
pv-nfs6   5Gi        RWO            Recycle          Released    default/www-web-2   nfs            <unset>                          67m   Filesystem

# 编辑,手动释放PV, 将 spec.claimRef 部分删除
$ kubectl edit pv pv-nfs2
$ kubectl edit pv pv-nfs3
$ kubectl edit pv pv-nfs6

手动释放PV

手动修改完成后,再次查看PV状态

$ kubectl get pv -o wide
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE   VOLUMEMODE
pv-nfs1   1Gi        RWO            Recycle          Available           nfs            <unset>                          72m   Filesystem
pv-nfs2   3Gi        RWO            Recycle          Available           nfs            <unset>                          72m   Filesystem
pv-nfs3   5Gi        RWO            Recycle          Available           nfs            <unset>                          72m   Filesystem
pv-nfs4   10Gi       RWO            Recycle          Available           nfs            <unset>                          72m   Filesystem
pv-nfs5   5Gi        RWX            Recycle          Available           nfs            <unset>                          72m   Filesystem
pv-nfs6   5Gi        RWO            Recycle          Available           nfs            <unset>                          72m   Filesystem

结论: PV 已成功释放。

10. StatefulSet网络标识与PVC

  • 匹配 StatefulSet 的 Pod name (网络标识)的模式为:$(statefulset名称)-$(序号),比如 StatefulSet 名称为web,副本数为3。则为:web-0、web-1、web-2

  • StatefulSet 为每个Pod副本创建了一个DNS域名,这个域名的格式为:$(podname).(headless service name),也就意味着服务之间是通过Pod域名来通信而非Pod IP。当Pod所在Node发生故障时,Pod会被漂移到其他Node上,Pod IP会发生改变,但Pod域名不会变化

  • StatefulSet 使用 Headless 服务来控制 Pod 的域名,这个 Headless 服务域名的为:$(service name).$(namespace).svc.cluster.local,其中 cluster.local 指定的集群的域名

  • 根据 volumeClaimTemplates,为每个 Pod 创建一个 PVC,PVC的命令规则为:$(volumeClaimTemplates name)-$(pod name),比如 volumeClaimTemplates 为 www,pod name 为 web-0、web-1、web-2;那么创建出来的PVC为:www-web-0、www-web-1、www-web-2

  • 删除 Pod 不会删除对应的 PVC,手动删除 PVC 将自动释放 PV。

七、StorageClass

1. 理论

在动态资源供应模式下,通过StorageClass和PVC完成资源动态绑定(系统自动生成PV),并供Pod使用的存储管理机制。

volumeClaimTemplates 实现了pvc 的自动化,StorageClass 实现了 pv 的自动化

存储

1.1 什么是 StorageClass

Kubernetes 提供了一套可以自动创建PV的机制,即:Dynamic Provisioning。而这个机制的核心在于StorageClass这个API对象。

StorageClass 对象会定义下面两部分内容:

  • PV 的属性。比如,存储类型,Volume 的大小等。
  • 创建这种 PV 需要用到的存储插件,即存储制备器。

有了这两个信息之后,Kubernetes 就能够根据用户提交的 PVC,找到一个对应的 StorageClass,之后 Kubernetes 就会调用该 StorageClass 声明的存储插件,进而创建出需要的PV。

但是其实使用起来是一件很简单的事情,你只需要根据自己的需求,编写YAML文件即可,然后使用 kubectl create 命令执行即可。

StorageClass 为管理员提供了描述存储 “类” 的方法。不同的类型可能会映射到不同的服务质量等级或备份策略,或是由集群管理员制定的任意策略。 Kubernetes 本身并不清楚各种类代表的什么。这个类的概念在其他存储系统中有时被称为 “配置文件”。

1.2 为什么需要 StorageClass

在一个大规模的 Kubernetes 集群里,可能有成千上万个 PVC,这就意味着运维人员必须实现创建出这个多个 PV,此外,随着项目的需要,会有新的 PVC 不断被提交,那么运维人员就需要不断的添加新的,满足要求的 PV,否则新的Pod就会因为 PVC 绑定不到PV而导致创建失败。而且通过 PVC 请求到一定的存储空间也很有可能不足以满足应用对于存储设备的各种需求。

而且不同的应用程序对于存储性能的要求可能也不尽相同,比如读写速度、并发性能等,为了解决这一问题,Kubernetes 又为我们引入了一个新的资源对象:StorageClass,通过 StorageClass 的定义,管理员可以将存储资源定义为某种类型的资源,比如快速存储、慢速存储等,用户根据 StorageClass 的描述就可以非常直观的知道各种存储资源的具体特性了,这样就可以根据应用的特性去申请合适的存储资源了。

1.3 运行原理

要使用 StorageClass,我们就得安装对应的自动配置程序,比如我们这里存储后端使用的是 nfs,那么我们就需要使用到一个 nfs-client 的自动配置程序,我们也叫它 Provisioner(制备器),这个程序使用我们已经配置好的 nfs 服务器,来自动创建持久卷,也就是自动帮我们创建 PV。

  • 自动创建的 PV 以 ${namespace}-${pvcName}-${pvName} 这样的命名格式创建在 NFS 服务器上的共享数据目录中
  • 而当这个 PV 被回收后会以 archieved-${namespace}-${pvcName}-${pvName} 这样的命名格式存在 NFS 服务器上。

2. StorageClass 资源

StorageClass 资源清单示例:

apiVersion: storage.k8s.io/v1
kind: StorageClass # 资源的类型为 StorageClass
metadata:
  name: standard # 存储类的名称
provisioner: kubernetes.io/aws-ebs # 指定了存储的提供者(Provisioner),使用 AWS EBS(Elastic Block Store)作为存储后端
parameters:
  type: gp2 # 指定了 AWS EBS 卷的类型为 gp2。gp2 是 AWS EBS 的通用 SSD(General Purpose SSD)卷类型
reclaimPolicy: Retain # 定义了存储卷的回收策略为 Retain。当 PVC 被删除时,与之关联的持久卷(PV)不会被自动删除,而是保留下来,需要手动清理。这可以防止意外删除重要数据。
allowVolumeExpansion: true # 允许对使用此 StorageClass 创建的卷进行扩展, 如果 PVC 请求更大的容量,Kubernetes 允许动态扩展卷(前提是底层存储支持,例如 AWS EBS 的 gp2 支持扩展)。
mountOptions:
  - debug # 指定了挂载选项为 debug, 在挂载存储卷时启用调试模式,可能用于记录详细的挂载日志,便于排查问题。注意:生产环境中通常不建议使用 debug,因为它可能会增加日志输出
volumeBindingMode: Immediate # 指定了卷绑定模式为 Immediate, 表示一旦 PVC 创建,Kubernetes 将立即尝试为它绑定一个持久卷(PV),而不等待 Pod 调度。这适用于大多数场景,但可能导致存储资源分配不灵活(相比 WaitForFirstConsumer 模式)。

每个 StorageClass 都包含 provisionerparametersreclaimPolicy 字段, 这些字段会在 StorageClass 需要动态分配 PersistentVolume 时会使用到。

StorageClass 对象的命名很重要,用户使用这个命名来请求生成一个特定的类。 当创建 StorageClass 对象时,管理员设置 StorageClass 对象的命名和其他参数,一旦创建了对象就不能再对其更新。

管理员可以为没有申请绑定到特定 StorageClass 的 PVC 指定一个默认的存储类。

2.1 存储制备器

每个 StorageClass 都有一个制备器(Provisioner),用来决定使用哪个卷插件制备 PV。 该字段必须指定。

卷插件 内置制备器 配置例子
AWSElasticBlockStore AWS EBS
AzureFile Azure File
AzureDisk Azure Disk
CephFS - -
Cinder OpenStack Cinder
FC - -
FlexVolume - -
Flocker -
GCEPersistentDisk GCE PD
Glusterfs Glusterfs
iSCSI - -
Quobyte Quobyte
NFS - -
RBD Ceph RBD
VsphereVolume vSphere
PortworxVolume Portworx Volume
ScaleIO ScaleIO
StorageOS StorageOS
Local - Local

除了列出的 “内置” 制备器(其名称前缀为 “kubernetes.io” 并打包在 Kubernetes 中)。 还可以运行和指定外部制备器,这些独立的程序遵循由 Kubernetes 定义的 规范。 外部供应商的作者完全可以自由决定他们的代码保存于何处、打包方式、运行方式、使用的插件(包括 Flex)等。例如,NFS 没有内部制备器,但可以使用外部制备器。 也有第三方存储供应商提供自己的外部制备器。

2.2 回收策略

由 StorageClass 动态创建的 PersistentVolume 会在类的 reclaimPolicy 字段中指定回收策略,可以是 Delete 或者 Retain。如果 StorageClass 对象被创建时没有指定 reclaimPolicy,它将默认为 Delete

通过 StorageClass 手动创建并管理的 PersistentVolume 会使用它们被创建时指定的回收政策。

2.3 允许卷扩展

PersistentVolume 可以配置为可扩展。将此功能设置为 true 时,允许用户通过编辑相应的 PVC 对象来调整卷大小。

当下层 StorageClass 的 allowVolumeExpansion 字段设置为 true 时,以下类型的卷支持卷扩展。

卷类型 Kubernetes 版本要求
gcePersistentDisk 1.11
awsElasticBlockStore 1.11
Cinder 1.11
glusterfs 1.11
rbd 1.11
Azure File 1.11
Azure Disk 1.11
Portworx 1.11
FlexVolume 1.13
CSI 1.14 (alpha), 1.16 (beta)

说明: 此功能仅可用于扩容卷,不能用于缩小卷。

2.4 挂载选项

由 StorageClass 动态创建的 PersistentVolume 将使用类中 mountOptions 字段指定的挂载选项。

如果卷插件不支持挂载选项,却指定了该选项,则制备操作会失败。 挂载选项在 StorageClass 和 PV 上都不会做验证,所以如果挂载选项无效,那么这个 PV 就会失败。

2.5 卷绑定模式

volumeBindingMode 字段控制了卷绑定和动态制备 应该发生在什么时候。

默认情况下,Immediate 模式表示一旦创建了 PersistentVolumeClaim 也就完成了卷绑定和动态制备。 对于由于拓扑限制而非集群所有节点可达的存储后端,PersistentVolume 会在不知道 Pod 调度要求的情况下绑定或者制备。

集群管理员可以通过指定 WaitForFirstConsumer 模式来解决此问题。 该模式将延迟 PersistentVolume 的绑定和制备,直到使用该 PersistentVolumeClaim 的 Pod 被创建。 PersistentVolume 会根据 Pod 调度约束指定的拓扑来选择或制备。这些包括但不限于 资源需求、 节点筛选器、 pod 亲和性和互斥性、 以及污点和容忍度。

以下插件支持动态供应的 WaitForFirstConsumer 模式:

以下插件支持预创建绑定 PersistentVolume 的 WaitForFirstConsumer 模式:

2.6 允许的拓扑结构

当集群操作人员使用了 WaitForFirstConsumer 的卷绑定模式, 在大部分情况下就没有必要将制备限制为特定的拓扑结构。 然而,如果还有需要的话,可以使用 allowedTopologies

这个例子描述了如何将供应卷的拓扑限制在特定的区域,在使用时应该根据插件 支持情况替换 zonezones 参数。

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: standard
provisioner: kubernetes.io/gce-pd
parameters:
  type: pd-standard
volumeBindingMode: WaitForFirstConsumer # 指定了卷绑定模式为 WaitForFirstConsumer。
allowedTopologies: # 定义了存储卷可以被分配的拓扑范围。
  - matchLabelExpressions: # 使用标签选择器来限制存储卷的拓扑。
      - key: failure-domain.beta.kubernetes.io/zone # Kubernetes 内置标签,用于表示节点的可用区(Zone)
        values: # 限制存储卷只能在 Google Cloud 的 us-central1-a 和 us-central1-b 两个可用区中创建。
          - us-central1-a
          - us-central1-b

这个 StorageClass 配置定义了一个名为 standard 的存储类,适用于 Google Cloud Platform 的 Persistent Disk(GCE PD)存储,磁盘类型为 pd-standard(标准硬盘)。它使用 WaitForFirstConsumer 绑定模式,确保存储卷的分配与 Pod 的调度位置一致,并且限制存储卷只能在 us-central1-a 和 us-central1-b 两个可用区中创建。

3. 案例演示

3.1 部署流程

搭建StorageClass + NFS,大致有以下几个步骤:

  1. 创建一个可用的 NFS Server
  2. 创建 Service Account,这是用来管控 NFS Provisioner 在 k8s 集群中运行的权限
  3. 创建 StorageClass,负责建立PVC并调用 NFS provisioner 进行预定的工作,并让 PV 与 PVC 建立关联
  4. 创建 NFS provisioner,有两个功能,一个是在NFS共享目录下创建挂载点( volume ),另一个则是建了 PV 并将 PV 与 NFS 的挂载点建立关联
  5. 创建 Pod 测试

3.2 搭建 NFS 服务

见 5.5 章节

3.3 创建命名空间

创建新的命名空间,用于测试

$ kubectl create ns nfs-storageclass

3.4 创建 Service Account

3.4.1 ServiceAccount

ServiceAccount-01.yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  name: nfs-client-provisioner
  namespace: nfs-storageclass

解读

  • 类型:ServiceAccount 是 Kubernetes 中的一种资源,用于为 Pod 或其他资源提供身份认证。
  • 用途:这里创建了一个名为 nfs-client-provisioner 的 ServiceAccount,运行在 nfs-storageclass 命名空间中。这个 ServiceAccount 将被后续的 Deployment 使用,允许 NFS 客户端动态配置器(Provisioner)以指定的身份与 Kubernetes API 交互。
  • 命名空间:nfs-storageclass,表示该 ServiceAccount 仅在该命名空间内有效。
# 执行资源清单
$ kubectl apply -f ServiceAccount-01.yaml

# 查看 SA
$ kubectl get sa -n nfs-storageclass
NAME                     SECRETS   AGE
default                  0         3m36s
nfs-client-provisioner   0         20s

3.4.2 ClusterRole

ClusterRole-02.yaml

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: nfs-client-provisioner-runner
rules:
  - apiGroups: [""]
    resources: ["nodes"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["persistentvolumes"]
    verbs: ["get", "list", "watch", "create", "delete"]
  - apiGroups: [""]
    resources: ["persistentvolumeclaims"]
    verbs: ["get", "list", "watch", "update"]
  - apiGroups: ["storage.k8s.io"]
    resources: ["storageclasses"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["events"]
    verbs: ["create", "update", "patch"]

解读

  • 类型:ClusterRole 是 Kubernetes 的 RBAC(基于角色的访问控制)资源,定义了集群级别的权限规则
  • apiGroups: [“”]
    • 空字符串表示核心 API 组(core API group),包含 Kubernetes 的基本资源(如 nodes、pods 等)。
  • 用途:为 nfs-client-provisioner 定义了操作权限,允许其对以下资源执行指定操作:
    • Nodes:获取、列出和监听节点信息(get, list, watch)。
    • **PersistentVolumes (PV)**:获取、列出、监听、创建和删除持久卷(get, list, watch, create, delete)。
    • **PersistentVolumeClaims (PVC)**:获取、列出、监听和更新持久卷声明(get, list, watch, update)。
    • StorageClasses:获取、列出和监听存储类(get, list, watch)。
    • Events:创建、更新和修补事件(create, update, patch),用于记录动态配置过程中的事件。
  • 集群级别:ClusterRole 的权限是集群范围的,适用于所有命名空间。
# 执行资源清单
$ kubectl apply -f ClusterRole-02.yaml

# 查看 集群角色
$ kubectl get ClusterRole -n nfs-storageclass
NAME                                                                   CREATED AT
nfs-client-provisioner-runner                                          2025-05-30T02:12:39Z

3.4.3 ClusterRoleBinding

ClusterRoleBinding-03.yaml

kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: run-nfs-client-provisioner
subjects:
  - kind: ServiceAccount # 绑定类型 ServiceAccount
    name: nfs-client-provisioner # ServiceAccount 的名称
    namespace: nfs-storageclass
roleRef:
  kind: ClusterRole # 绑定的角色类型
  name: nfs-client-provisioner-runner # 集群角色名称 nfs-client-provisioner-runner
  apiGroup: rbac.authorization.k8s.io

解读

  • 类型:ClusterRoleBinding 将 ClusterRole 绑定到特定的主体(Subject)。
  • 用途:将前面定义的 nfs-client-provisioner-runner ClusterRole 绑定到 nfs-client-provisioner ServiceAccount 上,授予其相应的权限。
  • 主体:nfs-client-provisioner ServiceAccount,位于 nfs-storageclass 命名空间。
  • 作用:确保 nfs-client-provisioner ServiceAccount 能够以 ClusterRole 定义的权限操作 Kubernetes 资源。
# 执行资源清单
$ kubectl apply -f ClusterRoleBinding-03.yaml

# 查看 集群角色绑定
$ kubectl get ClusterRoleBinding -n nfs-storageclass
NAME                         ROLE                                                              AGE

run-nfs-client-provisioner   ClusterRole/nfs-client-provisioner-runner                         22s

3.4.4 Role

Role-04.yaml

kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: leader-locking-nfs-client-provisioner # 角色的名称,表明它与 NFS 客户端存储提供者的领导者选举(leader election)机制相关。
  namespace: nfs-storageclass
rules:
  - apiGroups: [""] # 空字符串表示核心 API 组(core API group),包含 Kubernetes 的基本资源,如 endpoints。
    resources: ["endpoints"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]

解读

  • 类型:Role 是命名空间级别的 RBAC 资源,定义了特定命名空间内的权限规则。
  • 用途:为 nfs-client-provisioner 定义了在 nfs-storageclass 命名空间内操作 endpoints 资源的权限,包括获取、列出、监听、创建、更新和修补(get, list, watch, create, update, patch)。
  • 背景:endpoints 资源通常用于实现领导者选举(Leader Election),确保只有一个 Provisioner 实例在动态配置存储时处于活动状态。
# 创建角色
$ kubectl apply -f Role-04.yaml

# 在指定名称空间内查看角色
$ kubectl get Role -n nfs-storageclass
NAME                                    CREATED AT
leader-locking-nfs-client-provisioner   2025-05-30T02:39:47Z

3.4.5 RoleBinding

RoleBinding-05.yaml

kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: run-leader-locking-nfs-client-provisioner
  namespace: nfs-storageclass
subjects:
  - kind: ServiceAccount # 绑定资源类型为 ServiceAccount
    name: nfs-client-provisioner # 绑定的ServiceAccount 名称
    namespace: nfs-storageclass
roleRef:
  kind: Role # 绑定角色(nfs-storageclass名称空间的角色)
  apiGroup: rbac.authorization.k8s.io
  name: leader-locking-nfs-client-provisioner # 角色的名称

解读

  • 类型:RoleBinding 将 Role 绑定到特定主体。
  • 用途:将 leader-locking-nfs-client-provisioner Role 绑定到 nfs-client-provisioner ServiceAccount,授予其在 nfs-storageclass 命名空间内操作 endpoints 的权限。
  • 作用:支持领导者选举机制,确保 NFS 客户端 Provisioner 的高可用性和一致性。
# 执行角色绑定
$ kubectl apply -f RoleBinding-05.yaml

# 查看角色绑定详情
$ kubectl get RoleBinding -n nfs-storageclass -o wide
NAME                                        ROLE                                         AGE     USERS   GROUPS   SERVICEACCOUNTS
run-leader-locking-nfs-client-provisioner   Role/leader-locking-nfs-client-provisioner   2m45s                    nfs-storageclass/nfs-client-provisioner

3.4.6 StorageClass

StorageClass-06.yaml

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: nfs-client # StorageClass 的名称
  namespace: nfs-storageclass
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner
parameters:
  pathPattern: ${.PVC.namespace}/${.PVC.name}
  onDelete: delete

解读

  • 类型:StorageClass 定义了动态存储配置的模板。
  • 用途:定义了一个名为 nfs-client 的存储类,位于 nfs-storageclass 命名空间,用于动态创建 NFS 持久卷。
  • 配置:
    • provisioner:指定了动态配置器为 k8s-sigs.io/nfs-subdir-external-provisioner,即 NFS 子目录外部配置器。
    • parameters
      • pathPattern:动态生成的 NFS 路径,格式为 <PVC 命名空间>/<PVC 名称>,例如 nfs-storageclass/test-claim。
      • onDelete: delete:当 PVC 被删除时,关联的 NFS 存储路径也会被删除。
  • 作用:当用户创建使用 nfs-client 存储类的 PVC 时,Provisioner 会自动在 NFS 服务器上创建对应的子目录。
# 创建 StorageClass
$ kubectl apply -f StorageClass-06.yaml

# 查看 StorageClass
$ kubectl get StorageClass -n nfs-storageclass
NAME         PROVISIONER                                   RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
nfs-client   k8s-sigs.io/nfs-subdir-external-provisioner   Delete          Immediate           false                  12s

3.4.7 Deployment

Deployment-07.yaml

kind: Deployment
apiVersion: apps/v1
metadata:
  name: nfs-client-provisioner
  namespace: nfs-storageclass
spec:
  replicas: 3 # 3个nfs客户端副本
  selector:
    matchLabels:
      app: nfs-client-provisioner
  strategy:
    type: Recreate # 更新策略为重新创建,即先删除旧 Pod 再创建新 Pod,适合单一实例场景。
  template:
    metadata:
      labels:
        app: nfs-client-provisioner
    spec:
      serviceAccountName: nfs-client-provisioner # 使用 nfs-client-provisioner ServiceAccount,赋予其 RBAC 权限。
      containers:
        - name: nfs-client-provisioner
          image: k8s.dockerproxy.com/sig-storage/nfs-subdir-external-provisioner:v4.0.2
          imagePullPolicy: IfNotPresent
          volumeMounts:
            - name: nfs-client-root # 挂载的卷名称,与 volumes 部分定义的卷对应
              mountPath: /persistentvolumes # 将 NFS 卷挂载到容器内的 /persistentvolumes 路径,供容器读写 NFS 共享数据。
          env:
            - name: PROVISIONER_NAME
              value: k8s-sigs.io/nfs-subdir-external-provisioner # 指定配置器名称,与 StorageClass 保持一致
            - name: NFS_SERVER
              value: 192.168.6.139 # NFS 服务器地址
            - name: NFS_PATH
              value: /root/data # NFS 共享路径
      volumes:
        - name: nfs-client-root # 挂载卷的名称
          nfs:
            server: 192.168.6.139
            path: /root/data

解读

  • 类型:Deployment 用于管理 Pod 的部署和运行。
  • 用途:部署 NFS 客户端 Provisioner,确保动态存储配置服务运行。
  • 配置
    • replicas: 3:运行3个 Pod 副本。
    • strategy: Recreate:更新策略为重新创建,即先删除旧 Pod 再创建新 Pod,适合单一实例场景。
    • serviceAccountName:使用 nfs-client-provisioner ServiceAccount,赋予其 RBAC 权限。
    • 容器
      • 镜像:k8s.dockerproxy.com/sig-storage/nfs-subdir-external-provisioner:v4.0.2,NFS 子目录外部配置器的镜像。
      • volumeMounts:将 NFS 卷挂载到容器内路径 /persistentvolumes。
      • 环境变量
        • PROVISIONER_NAME:指定配置器名称,与 StorageClass 保持一致。
        • NFS_SERVER:NFS 服务器地址(192.168.66.11)。
        • NFS_PATH:NFS 共享路径(/root/data)。
    • volumes:定义了一个 NFS 卷,连接到 NFS 服务器 192.168.6.139 的 /root/data 路径。
  • 作用:运行 NFS Provisioner,监听 PVC 请求并动态创建 NFS 子目录。
# 执行资源清单,部署 NFS 客户端 Provisioner
$ kubectl apply -f Deployment-07.yaml

# 查看 Deployment 控制器
$ kubectl get deployment -n nfs-storageclass
NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
nfs-client-provisioner   3/3     3            3           5s

# 查看Pod
$ kubectl get pods -o wide -n nfs-storageclass
NAME                                      READY   STATUS    RESTARTS   AGE   IP              NODE         NOMINATED NODE   READINESS GATES
nfs-client-provisioner-5896c4d9d4-8cqwm   1/1     Running   0          18s   172.16.58.193   k8s-node02   <none>           <none>
nfs-client-provisioner-5896c4d9d4-8wnnm   1/1     Running   0          18s   172.16.85.205   k8s-node01   <none>           <none>
nfs-client-provisioner-5896c4d9d4-nds6r   1/1     Running   0          18s   172.16.58.204   k8s-node02   <none>           <none>

3.4.8 PersistentVolumeClaim (PVC)

PersistentVolumeClaim-08.yaml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-claim
  annotations: # 注解部分为空(annotations: ),通常用于存储非关键的元数据信息(如描述、工具标记等)
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Mi # 请求 1MiB 的存储空间(实际存储大小由 NFS 服务器控制)
  storageClassName: nfs-client # StorageClass(存储类) 的名称

解读

  • 类型:PersistentVolumeClaim 是用户请求存储资源的声明。
  • 用途:定义一个名为 test-claim 的 PVC,请求使用 nfs-client 存储类动态分配存储。
  • 配置
    • accessModes: ReadWriteMany:支持多个节点读写访问(适合 NFS 的共享存储特性)。
    • resources.requests.storage: 1Mi:请求 1MiB 的存储空间(实际存储大小由 NFS 服务器控制)。
    • storageClassName: nfs-client:指定使用 nfs-client 存储类。
  • 作用:触发 NFS Provisioner 创建一个对应的 PV,并分配 NFS 服务器上的子目录(路径为 nfs-storageclass/test-claim)。

注意:PVC的资源清单没有指定命名空间,当执行资源清单时会在 default 命名空间下创建一个目录,后续会更新 Pod 所在命名空间,自动创建对应的目录。

# 创建PVC
$ kubectl apply -f PersistentVolumeClaim-08.yaml

# 指定 命名空间查看 PVC
$ kubectl get pvc -o wide -n nfs-storageclass
NAME         STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE   VOLUMEMODE
test-claim   Bound    pvc-3b195b57-522a-49c5-badc-32e07fd37f88   1Mi        RWX            nfs-client     <unset>                 15s   Filesystem

# 查看 PVC 自动创建的目录, 按照存储类定义的路径格式:${.PVC.namespace}/${.PVC.name}
$ ll /root/data/nfs-storageclass/
total 0
drwxrwxrwx 2 root root 6 May 30 17:12 test-claim

3.4.9 测试 Pod

test-pod-09.yaml

kind: Pod
apiVersion: v1
metadata:
  name: test-pod
  namespace: nfs-storageclass
spec:
  containers:
    - name: test-pod
      image: wangyanglinux/myapp:v1.0
      imagePullPolicy: IfNotPresent
      volumeMounts:
        - name: nfs-pvc # 使用的挂在卷名
          mountPath: /usr/local/nginx/html # 挂载到容器内的路径
  volumes:
    - name: nfs-pvc # 挂在卷名
      persistentVolumeClaim:
        claimName: test-claim # 使用的PVC名称

创建Pod,测试存储类

# 执行资源清单,创建Pod
$ kubectl apply -f test-pod-09.yaml

# 查看Pod列表
$ kubectl get pods -o wide -n nfs-storageclass
NAME                                      READY   STATUS    RESTARTS   AGE   IP              NODE         NOMINATED NODE   READINESS GATES
nfs-client-provisioner-5896c4d9d4-8cqwm   1/1     Running   0          37m   172.16.58.193   k8s-node02   <none>           <none>
nfs-client-provisioner-5896c4d9d4-8wnnm   1/1     Running   0          37m   172.16.85.205   k8s-node01   <none>           <none>
nfs-client-provisioner-5896c4d9d4-nds6r   1/1     Running   0          37m   172.16.58.204   k8s-node02   <none>           <none>
test-pod                                  1/1     Running   0          36s   172.16.58.249   k8s-node02   <none>           <none>

# 测试文件到PVC挂载目录
$ echo "hello test pod" >> /root/data/nfs-storageclass/test-claim/index.html

# 访问Pod
$ curl 172.16.58.249
hello test pod

3.4.10 测试 Service

test-sts-pod-10.yaml

apiVersion: v1
kind: Service
metadata:
  name: nginx # 服务的名称为 nginx
  namespace: nfs-storageclass
  labels:
    app: nginx # 给服务打一个标签
spec:
  ports: # 指定服务暴露的端口
    - port: 80 # 服务监听在 80 端口,外部请求通过该端口访问
  clusterIP: None # 设置为 None,表示这是一个 Headless Service。Headless Service 不会分配集群 IP,而是直接通过 DNS 解析到后端的 Pod。
  selector:
    app: nginx # 服务通过标签选择器 app: nginx 找到匹配的 Pod,将流量路由到这些 Pod。

---

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web # StatefulSet 名称为 web,用于管理一组 Pod。
  namespace: nfs-storageclass
spec:
  selector:
    matchLabels: # 通过标签 app: nginx 选择要管理的 Pod,确保 StatefulSet 只控制带有该标签的 Pod。
      app: nginx
  serviceName: nginx # 指定关联的 Service 的名称为 nginx, StatefulSet 使用此 Service 来为每个 Pod 提供稳定的 DNS 名称(例如 web-0.nginx.default.svc.cluster.local)
  replicas: 3 # 创建 3 个 Pod 副本,分别命名为 web-0、web-1 和 web-2
  template:
    metadata:
      labels: # 为 Pod 添加标签 app: nginx,与 Service 的选择器匹配。
        app: nginx
    spec:
      containers:
        - name: nginx
          image: wangyanglinux/myapp:v1.0
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 80
              name: web
          volumeMounts:
            - name: www # 挂载的卷名称为 www
              mountPath: /usr/local/nginx/html
  volumeClaimTemplates:
    - metadata:
        name: www # PVC 名称为 www,与 volumeMounts 中的名称对应
      spec:
        accessModes:
          - ReadWriteOnce
        storageClassName: nfs-client # StorageClass(存储类) 的名称
        resources:
          requests:
            storage: 1Gi

执行资源清单,测试Service

# 执行资源清单
$ kubectl apply -f test-sts-pod-10.yaml

# 查看 Service
$ kubectl get svc -o wide -n nfs-storageclass
NAME    TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE   SELECTOR
nginx   ClusterIP   None         <none>        80/TCP    30s   app=nginx

# 查看 StatefulSet 控制器
$ kubectl get sts -o wide -n nfs-storageclass
NAME   READY   AGE   CONTAINERS   IMAGES
web    3/3     40s   nginx        wangyanglinux/myapp:v1.0

# 查看 Pod
$ kubectl get pods -o wide -n nfs-storageclass
NAME                                      READY   STATUS    RESTARTS   AGE     IP              NODE         NOMINATED NODE   READINESS GATES
nfs-client-provisioner-5896c4d9d4-8cqwm   1/1     Running   0          43m     172.16.58.193   k8s-node02   <none>           <none>
nfs-client-provisioner-5896c4d9d4-8wnnm   1/1     Running   0          43m     172.16.85.205   k8s-node01   <none>           <none>
nfs-client-provisioner-5896c4d9d4-nds6r   1/1     Running   0          43m     172.16.58.204   k8s-node02   <none>           <none>
test-pod                                  1/1     Running   0          6m54s   172.16.58.249   k8s-node02   <none>           <none>
web-0                                     1/1     Running   0          58s     172.16.85.204   k8s-node01   <none>           <none>
web-1                                     1/1     Running   0          55s     172.16.58.245   k8s-node02   <none>           <none>
web-2                                     1/1     Running   0          52s     172.16.85.209   k8s-node01   <none>           <none>

# 查看 PV
$ kubectl get pv -o wide -n nfs-storageclass
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                         STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE     VOLUMEMODE
pvc-3b195b57-522a-49c5-badc-32e07fd37f88   1Mi        RWX            Delete           Bound    nfs-storageclass/test-claim   nfs-client     <unset>                          17m     Filesystem
pvc-42a8b9df-f53d-4cd4-92e4-47c46ea07e17   1Gi        RWO            Delete           Bound    nfs-storageclass/www-web-2    nfs-client     <unset>                          69s     Filesystem
pvc-8a8cefe1-00f1-4639-a1d4-d72e616b084b   1Gi        RWO            Delete           Bound    nfs-storageclass/www-web-1    nfs-client     <unset>                          72s     Filesystem
pvc-be68e955-0d6c-4f4b-8422-2158a89a833b   1Gi        RWO            Delete           Bound    nfs-storageclass/www-web-0    nfs-client     <unset>                          75s     Filesystem

# 查看PVC
$ kubectl get pvc -o wide -n nfs-storageclass
NAME         STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE   VOLUMEMODE
test-claim   Bound    pvc-3b195b57-522a-49c5-badc-32e07fd37f88   1Mi        RWX            nfs-client     <unset>                 18m   Filesystem
www-web-0    Bound    pvc-be68e955-0d6c-4f4b-8422-2158a89a833b   1Gi        RWO            nfs-client     <unset>                 92s   Filesystem
www-web-1    Bound    pvc-8a8cefe1-00f1-4639-a1d4-d72e616b084b   1Gi        RWO            nfs-client     <unset>                 89s   Filesystem
www-web-2    Bound    pvc-42a8b9df-f53d-4cd4-92e4-47c46ea07e17   1Gi        RWO            nfs-client     <unset>                 86s   Filesystem

# 查看存储类
$ kubectl get storageclass -n nfs-storageclass
NAME         PROVISIONER                                   RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
nfs-client   k8s-sigs.io/nfs-subdir-external-provisioner   Delete          Immediate           false                  6h18m

# 查看PVC自动创建的目录
$ ll /root/data/nfs-storageclass/
total 0
drwxrwxrwx 2 root root 45 May 30 17:23 test-claim
drwxrwxrwx 2 root root 27 May 30 17:28 www-web-0
drwxrwxrwx 2 root root 27 May 30 17:28 www-web-1
drwxrwxrwx 2 root root 27 May 30 17:28 www-web-2

# 测试文件到PVC挂载目录
$ echo "hello test svc pod" >> /root/data/nfs-storageclass/www-web-0/index.html

# 访问Pod
$ curl 172.16.85.204
hello test svc pod

# 进入Pod测试
$ kubectl exec -it web-0 -n nfs-storageclass -- /bin/bash
web-0:/# curl web-0.nginx.nfs-storageclass.svc.cluster.local.
hello test svc pod

参考链接

https://www.cnblogs.com/timelesszhuang/p/k8s.html

https://www.cnblogs.com/zhanglianghhh/p/13818190.html

https://www.cnblogs.com/zhanglianghhh/p/13743024.html

https://www.cnblogs.com/fengjian2016/p/13337686.html

https://www.cnblogs.com/zhanglianghhh/p/13844062.html

https://www.cnblogs.com/zhanglianghhh/p/13861817.html

https://blog.csdn.net/weixin_41947378/article/details/111509849


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 george_95@126.com