image-20251029174410947

一 configmap

用于保存非敏感配置数据(键值对形式),实现配置与镜像解耦,提升镜像可移植性和复用性,适用于向 Pod 注入配置。

1.1 configmap的功能

  • configMap用于保存配置数据,以键值对形式存储。
  • configMap 资源提供了向 Pod 注入配置数据的方法。
  • 镜像和配置文件解耦,以便实现镜像的可移植性和可复用性。
  • etcd限制了文件大小不能超过1M

1.2 configmap的使用场景

  • 填充环境变量的值
  • 设置容器内的命令行参数
  • 填充卷的配置文件

1.3 configmap创建方式

通过不同方式创建 configmap,适应不同的配置来源(零散键值、单个文件、多个文件、yaml 定义)。

1.3.1 字面值创建

通过不同方式创建 configmap,适应不同的配置来源(零散键值、单个文件、多个文件、yaml 定义)。

# 创建目录用于存放相关文件(非必须,仅为了管理方便)
[root@master ~]# mkdir configmap
[root@master ~]# cd configmap/
# 创建configmap,名称为rch-cm,通过--from-literal指定键值对(fname=rch和user=test)
[root@master configmap]# kubectl create cm rch-cm --from-literal fname=rch --from-literal user=test 
configmap/rch-cm created
# 查看创建的configmap详情,验证键值对是否正确
[root@master configmap]# kubectl describe cm rch-cm 
Name:         rch-cm
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
fname:  # 键fname
----
rch     # 值rch
user:   # 键user
----
test    # 值test

BinaryData
====

Events:  <none>

结果:--from-file指定文件后,configmap 会自动以文件名作为键,文件内容作为值,适合单个配置文件(如 nginx.conf)的场景。

思路:直接用现有文件创建,无需手动写键值对,适合配置已存在于文件中的情况。

1.3.2 通过文件创建

将目录下所有文件作为键值对(文件名→键,内容→值),适合多个配置文件的场景。

# 复制系统中的resolv.conf文件到当前目录(作为示例配置文件)
[root@master configmap]# cp /etc/resolv.conf .
# 创建configmap,名称为rch-cm,通过--from-file指定文件resolv.conf(文件名作为键,内容作为值)
[root@master configmap]# kubectl create cm rch-cm --from-file resolv.conf 
configmap/rch-cm created
# 查看详情,验证文件内容是否被正确存储
[root@master configmap]# kubectl describe cm rch-cm 
Name:         rch-cm
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
resolv.conf:  # 键为文件名resolv.conf
----
# Generated by NetworkManager
nameserver 114.114.114.114  # 文件内容作为值


BinaryData
====

Events:  <none>

1.3.3 通过目录创建

将目录下所有文件作为键值对(文件名→键,内容→值),适合多个配置文件的场景。

# 创建目录rchconfig用于存放多个配置文件
[root@master ~]# mkdir rchconfig
# 复制两个系统文件到rchconfig目录(作为示例多配置文件)
[root@master ~]# cp /etc/fstab /etc/rc.d/rc.local  rchconfig/
# 创建configmap,名称为rch-config,通过--from-file指定目录rchconfig/(目录下所有文件都会被作为键值对)
[root@master ~]# kubectl create cm rch-config --from-file rchconfig/
configmap/rch-config created
# 查看详情,验证目录下的文件是否都被正确存储
[root@master ~]# kubectl describe cm rch-config
Name:         rch-config
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
fstab:  # 键为文件名fstab
----
# 以下为fstab文件的内容(作为值)
# /etc/fstab
# Created by anaconda on Fri Jul 26 13:04:22 2024
...(省略部分内容)...

rc.local:  # 键为文件名rc.local
----
#!/bin/bash
# THIS FILE IS ADDED FOR COMPATIBILITY PURPOSES
...(省略部分内容)...

BinaryData
====

Events:  <none>

1.3.4 通过yaml文件创建

通过 yaml 文件定义 configmap,适合需要版本控制、重复使用或复杂配置的场景。

# 生成yaml模板:创建configmap(rch-cm),指定键值对(db_host=192.168.2.60,db_port=3306),--dry-run=client表示仅生成文件不实际创建,-o yaml输出为yaml格式,重定向到rch-cm.yml
[root@master configmap]# kubectl create cm rch-cm --from-literal db_host=192.168.2.60 --from-literal db_port=3306 --dry-run=client -o yaml > rch-cm.yml
# 编辑yaml文件(可手动修改键值对或添加其他配置)
[root@master configmap]# vim rch-cm.yml 
apiVersion: v1  # api版本
data:  # 配置数据(键值对)
  db_host: 192.168.2.60  # 键db_host,值192.168.2.60
  db_port: "3306"  # 键db_port,值3306(注意数字需用引号包裹为字符串)
kind: ConfigMap  # 资源类型为ConfigMap
metadata:  # 元数据
  creationTimestamp: null  # 创建时间(自动生成,此处为模板值)
  name: rch-cm  # configmap名称

# 通过yaml文件创建configmap
[root@master configmap]# kubectl apply -f rch-cm.yml
# 查看详情,验证配置是否正确
[root@master configmap]# kubectl describe cm rch-cm 
Name:         rch-cm
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
db_host:
----
192.168.2.60
db_port:
----
3306

BinaryData
====

Events:  <none>

**结果:**通过 yaml 文件定义后,可通过kubectl apply创建,方便纳入版本管理(如 git),且可重复使用。适合正式环境中需要规范管理的配置。

思路:先生成模板,手动编辑完善,再通过 yaml 创建,适合需要持久化定义、重复部署的场景。

小结:四种创建方式的区别

创建方式适用场景优点缺点
字面值创建少量简单键值对(临时测试)命令简单,一步创建不适合多配置,无法持久化定义
文件创建单个配置文件直接复用现有文件,无需手动输入内容仅支持单个文件,多文件需多次执行
目录创建多个配置文件(批量导入)一次性导入目录下所有文件无法筛选文件,目录下所有文件都会被导入
yaml 文件创建版本控制、重复使用、复杂配置可持久化定义,支持版本管理需手动编写 / 修改 yaml,步骤稍多

1.3.5 configmap的使用方式

  • 通过环境变量的方式直接传递给pod
  • 通过pod的 命令行运行方式
  • 作为volume的方式挂载到pod内
1.3.5.1 使用configmap填充环境变量
# 生成Pod的yaml模板:名称testpod,镜像busyboxplus,--dry-run=client仅生成文件,重定向到testpod.yml
[root@master configmap]# kubectl run testpod --image busyboxplus --dry-run=client -o yaml > testpod.yml
# 编辑yaml文件,添加环境变量配置
[root@master configmap]# vim testpod.yml 
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: testpod
  name: testpod
spec:
  containers:
  - image: busyboxplus  # 容器镜像
    name: testpod
    command:  # 容器启动命令:输出环境变量并休眠(方便查看)
    - /bin/sh
    - -c
    - "env;sleep 10000"
    env:  # 定义环境变量
    - name: key1  # 环境变量名key1
      valueFrom:  # 从configmap获取值
        configMapKeyRef:
          name: rch-cm  # 关联的configmap名称
          key: db_host  # 从configmap中取键db_host的值
    - name: key2  # 环境变量名key2
      valueFrom:
        configMapKeyRef:
          name: rch-cm
          key: db_port  # 从configmap中取键db_port的值
# 创建Pod
[root@master configmap]# kubectl apply -f testpod.yml 
pod/testpod created
# 查看Pod日志,验证环境变量是否生效
[root@master configmap]# kubectl logsspods/testpod 
...(省略其他环境变量)...
key1=192.168.2.60  # 环境变量key1的值(来自db_host)
key2=3306          # 环境变量key2的值(来自db_port)
...

结果:通过env字段关联 configmap 的指定键,容器启动后可通过env命令看到环境变量key1和key2,值与 configmap 一致。适合需要通过环境变量获取配置的场景(如应用程序读取环境变量初始化)。

映射所有键值对为环境变量

[root@master configmap]# kubectl delete -f testpod.yml --force 
[root@master configmap]# vim testpod.yml 
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: testpod
  name: testpod
spec:
  containers:
  - image: busyboxplus
    name: testpod
    command:
    - /bin/sh
    - -c
    - "env;sleep 10000"
    envFrom:  # 从configmap映射所有键值对为环境变量(键名即环境变量名)
    - configMapRef:
        name: rch-cm  # 关联的configmap名称
  restartPolicy: Never  # 容器退出后不重启
  
[root@master configmap]# kubectl apply -f testpod.yml 
pod/testpod created
[root@master configmap]# kubectl logs pods/testpod 
MYAPPV1_PORT_80_TCP_ADDR=10.102.136.32
KUBERNETES_PORT=tcp://10.96.0.1:443
KUBERNETES_SERVICE_PORT=443
MYAPPV2_PORT_80_TCP_ADDR=10.109.106.183
MYAPPV1_PORT_80_TCP_PORT=80
MYAPPV2_PORT_80_TCP_PORT=80
MYAPPV1_PORT_80_TCP_PROTO=tcp
HOSTNAME=testpod
SHLVL=1
MYAPPV2_PORT_80_TCP_PROTO=tcp
HOME=/
db_port=3306
MYAPPV1_PORT_80_TCP=tcp://10.102.136.32:80
MYAPPV2_PORT_80_TCP=tcp://10.109.106.183:80
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
MYAPPV1_SERVICE_HOST=10.102.136.32
MYAPPV2_SERVICE_HOST=10.109.106.183
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
PWD=/
KUBERNETES_SERVICE_HOST=10.96.0.1
MYAPPV1_PORT=tcp://10.102.136.32:80
MYAPPV1_SERVICE_PORT=80
MYAPPV2_SERVICE_PORT=80
MYAPPV2_PORT=tcp://10.109.106.183:80
db_host=192.168.2.60

结果:envFrom会将 configmap 的所有键值对映射为环境变量(键名即环境变量名),无需逐个指定,适合需要导入所有配置的场景。

在命令行中使用环境变量

[root@master configmap]# vim testpod.yml 
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: testpod
  name: testpod
spec:
  containers:
  - image: busyboxplus
    name: testpod
    command:	# 启动命令:输出db_host和db_port环境变量的值
    - /bin/sh
    - -c
    - echo ${db_host} ${db_port}
    envFrom:
    - configMapRef:
        name: rch-cm
  restartPolicy: Never
# 创建Pod并查看日志
[root@master configmap]# kubectl apply -f testpod.yml 
pod/testpod created
[root@master configmap]# kubectl logs pods/testpod 
192.168.2.60 3306	# 环境变量的值被正确输出

结果:容器启动命令中通过${变量名}引用环境变量,适合需要用配置参数初始化命令的场景(如启动命令需要指定端口、地址)。

1.3.5.2 通过数据卷使用configmap

将 ConfigMap 中的键值对以文件形式挂载到容器内(键为文件名,值为文件内容),适合需要配置文件的场景。

# 查看现有 ConfigMap
[root@master configmap]# kubectl get cm
NAME               DATA   AGE
kube-root-ca.crt   1      12d
rch-cm             2      25m

# 删除旧 Pod
[root@master configmap]# kubectl delete pods testpod 
pod "testpod" deleted
# 编辑 YAML,配置卷挂载
[root@master configmap]# vim testpod.yml 
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: testpod
  name: testpod
spec:
  containers:
  - image: busyboxplus
    name: testpod
    command:  # 容器启动命令:列出 /test 目录文件,并查看 db_host 文件内容
    - /bin/sh
    - -c
    - "ls /test;cat /test/db_host"
    volumeMounts:  # 挂载卷到容器内的 /test 目录
    - name: config-volume  # 卷名称(需与 volumes 中一致)
      mountPath: /test
  volumes:  # 定义卷
  - name: config-volume
    configMap: 
      name: rch-cm  # 关联的 ConfigMap 名称
  restartPolicy: Never
  
# 查看日志,验证挂载效果
[root@master configmap]# kubectl logs testpod
db_host  # /test 目录下有 db_host 文件(键名)
db_port  # /test 目录下有 db_port 文件(键名)
192.168.2.60  # db_host 文件内容为对应值

结果:ConfigMap 中的 db_host 和 db_port 被映射为 /test 目录下的两个文件,文件内容为对应的值。

思路:如果程序需要读取配置文件(比如 nginx.conf),把 ConfigMap 挂载成卷,键作为文件名,值作为文件内容,程序直接读文件就行。

1.3.5.3 利用configMap填充pod的配置文件

将完整的配置文件(如 nginx.conf)存入 ConfigMap,再挂载到容器的配置目录,替换默认配置。

# 创建 nginx 配置文件(自定义端口 8000)
[root@master configmap]# vim nginx.conf
server {
        listen 8000;
        server_name _;
        root /usr/share/nginx/html;
        index index.html;
}
# 基于 nginx.conf 创建 ConfigMap
[root@master configmap]# kubectl create cm nginx-conf --from-file nginx.conf
configmap/nginx-conf created
[root@master configmap]# kubectl describe cm nginx-conf
Name:         nginx-conf
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
nginx.conf:		# 键为 nginx.conf,值为配置文件内容
----
server {
  listen 8000;
  server_name _;
  root /usr/share/nginx/html;
  index index.html;
}


BinaryData
====

Events:  <none>

# 生成 nginx Pod 的 YAML 模板
[root@master service]# kubectl run testpod --image nginx:1.23 --dry-run=client -o yaml > nginx.yml
# 编辑 YAML,将 ConfigMap 挂载到 nginx 的配置目录
[root@master service]# vim nginx.yml
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: testpod
  name: testpod
spec:
  containers:
  - image: nginx:1.23
    name: testpod
    volumeMounts:  # 挂载卷到 nginx 的配置目录 /etc/nginx/conf.d
    - name: config-volume
      mountPath: /etc/nginx/conf.d
  volumes:  # 定义卷,关联 ConfigMap nginx-conf
    - name: config-volume
      configMap:
        name: nginx-conf

# 创建 Pod 并验证
[root@master configmap]# kubectl apply -f nginx.yml 
pod/testpod created
[root@master configmap]# kubectl get pods -o wide  # 查看 Pod IP
NAME      READY   STATUS    RESTARTS   AGE   IP            NODE    NOMINATED NODE   READINESS GATES
testpod   1/1     Running   0          7s    10.244.2.13   node2   <none>           <none>
# 访问 Pod 的 8000 端口,验证配置生效(nginx 已按自定义配置监听 8000 端口)
[root@master configmap]# curl 10.244.2.13:8000
<!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>

结果:ConfigMap 中的 nginx.conf 被挂载到 nginx 容器的配置目录,替换了默认配置,使 nginx 监听 8000 端口。

思路:程序(如 nginx)通常有固定的配置文件路径,把自定义配置存到 ConfigMap,再挂载到对应路径,就能覆盖默认配置,不用改镜像本身。

1.3.5.4 通过热更新cm修改配置

修改 ConfigMap 后,让容器内的配置文件同步更新(部分场景需重启 Pod)。

# 编辑 ConfigMap,将 nginx 监听端口改为 8080#修改端口为8080
[root@master configmap]# kubectl edit cm nginx-conf 
apiVersion: v1
data:
  nginx.conf: "server {\n\tlisten 8080;\n\tserver_name _;\n\troot /usr/share/nginx/html;\n\tindex
    index.html;\n}\n"
# 查看 ConfigMap 详情,确认修改生效
[root@master configmap]# kubectl describe cm nginx-conf 
Name:         nginx-conf
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
nginx.conf:
----
server {
  listen 8080;
  server_name _;
  root /usr/share/nginx/html;
  index index.html;
}


BinaryData
====

Events:  <none>
# 查看容器内的配置文件,确认热更新生效(卷挂载的文件会自动同步)
[root@master configmap]# kubectl exec pods/testpod cat /etc/nginx/conf.d/nginx.conf
server {
	listen 8080;
	server_name _;
	root /usr/share/nginx/html;
	index index.html;
}

**注意:**配置文件更新后,若程序(如 nginx)不会自动加载新配置,需删除 Pod 重建(控制器会自动重建),新配置才会生效。

结果:修改 ConfigMap 后,通过卷挂载的文件会自动同步更新,但需程序支持热加载或重启 Pod 才能使新配置生效。

思路:如果配置需要频繁修改,改 ConfigMap 比改镜像方便,改完后卷挂载的文件会自动变,但有些程序得重启才认新配置,这时候删了 Pod 让它重建就行。

小结:ConfigMap 各类使用方式的区别

类型适用场景特点
环境变量使用程序依赖环境变量获取配置可单个或批量映射,直接作为变量使用
卷挂载使用程序依赖配置文件键为文件名,值为文件内容,适合文件读取
热更新配置配置需动态调整卷挂载文件自动同步,部分场景需重启 Pod

二 secrets配置管理

Secrets 用于存储敏感信息(如密码、令牌、SSH 密钥),比直接写在 Pod 定义或镜像中更安全(但 Opaque 类型仅 base64 编码,并非加密,需配合 RBAC 控制访问)。

2.1 secrets的功能介绍

  • Secret 对象类型用来保存敏感信息,例如密码、OAuth 令牌和 ssh key。

  • 敏感信息放在 secret 中比放在 Pod 的定义或者容器镜像中来说更加安全和灵活

  • Pod 可以用两种方式使用 secret:

    • 作为 volume 中的文件被挂载到 pod 中的一个或者多个容器里。

    • 当 kubelet 为 pod 拉取镜像时使用。

  • Secret的类型:

    • Service Account:Kubernetes 自动创建包含访问 API 凭据的 secret,并自动修改 pod 以使用此类型的 secret。

    • Opaque:使用base64编码存储信息,可以通过base64 --decode解码获得原始数据,因此安全性弱。

    • kubernetes.io/dockerconfigjson:用于存储docker registry的认证信息

2.2 secrets的创建

在创建secrets时我们可以用命令的方法或者yaml文件的方法

2.2.1从文件创建

将敏感信息(如用户名、密码)存入文件,再通过文件创建 Secret,适合简单敏感数据。

# 创建包含敏感信息的文件(-n 避免添加换行符)
[root@master configmap]# echo -n rch > username.txt  # 内容为用户名 rch
[root@master configmap]# echo -n 123 > password.txt  # 内容为密码 123

# 查看 Secret 创建命令帮助(generic 用于从文件/字面值创建)
[root@master configmap]# kubectl create secret 
docker-registry  (创建 Docker 仓库认证的 Secret)
generic          (从文件、目录或字面值创建 Secret)
tls              (创建 TLS 证书的 Secret)

# 通过文件创建名为 usermsg 的 Secret(Opaque 类型)
[root@master configmap]# kubectl create secret generic usermsg --from-file username.txt --from-file password.txt 
secret/usermsg created

# 查看 Secret 详情(敏感数据默认不显示明文)
[root@master configmap]# kubectl describe secrets usermsg 
Name:         usermsg
Namespace:    default
Labels:       <none>
Annotations:  <none>

Type:  Opaque  # 类型为 Opaque(base64 编码)

Data
====
password.txt:  3 bytes  # 密码文件大小
username.txt:  3 bytes  # 用户名文件大小

# 查看 Secret 的 YAML 格式(数据以 base64 编码存储)
[root@master configmap]# kubectl get secrets usermsg -o yaml
apiVersion: v1
data:
  password.txt: MTIz  # 密码 123 的 base64 编码
  username.txt: cmNo  # 用户名 rch 的 base64 编码
kind: Secret
metadata:
  ...
type: Opaque

# 解码验证(base64 -d 解码)
[root@master configmap]# echo -n MTIz | base64 -d
123  # 解码后为密码
[root@master configmap]# echo -n cmNo | base64 -d
rch  # 解码后为用户名

通过 YAML 文件创建

# 删除旧 Secret
[root@master configmap]# kubectl delete secrets usermsg 

# 生成 YAML 模板(--dry-run 不实际创建)
[root@master configmap]# kubectl create secret generic usermsg --from-file username.txt --from-file password.txt --dry-run=client -o yaml > usermsg.yml
# 编辑 YAML,自定义键名(将 username.txt 改为 username,password.txt 改为 password)
[root@master configmap]# vim usermsg.yml
apiVersion: v1
data:
  password: MTIz  # 密码 123 的编码
  username: cmNo  # 用户名 rch 的编码
kind: Secret
metadata:
  creationTimestamp: null
  name: usermsg

# 也可手动计算 base64 编码(如密码 rch123)
[root@master configmap]# echo -n rch123 | base64
cmNoMTIz  # 编码结果

# 编辑 YAML 更新密码
[root@master configmap]# vim usermsg.yml 
apiVersion: v1
data:
  password: cmNoMTIz  # 密码 rch123 的编码
  username: cmNo
kind: Secret
metadata:
  creationTimestamp: null
  name: usermsg

# 通过 YAML 创建 Secret
[root@master configmap]# kubectl apply -f usermsg.yml 
secret/usermsg created

# 查看详情
[root@master configmap]# kubectl describe secrets usermsg 
Name:         usermsg
...
Data
====
password:  6 bytes  # 密码长度变为 6(rch123)
username:  3 bytes

结果:Secret 中的数据以 base64 编码存储(非加密),从文件创建时文件名默认作为键,也可通过 YAML 自定义键名。

思路:敏感信息不能明文存,用 Secret 存 base64 编码的内容,虽然能解码,但比直接写在配置里安全,至少不会一眼看到明文。

2.3 Secret的使用方法

2.3.1 将Secret挂载到Volume中

将 Secret 中的敏感数据以文件形式挂载到容器,供程序读取(如配置文件中的密码)。

[root@master configmap]# vim nginx.yml 
[root@master configmap]# cat nginx.yml 
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: testpod
  name: testpod
spec:
  containers:
  - image: nginx:1.23
    name: testpod
    volumeMounts:	# 挂载卷到 /secrets 目录(只读)
    - name: config-volume
      mountPath: /secrets
      readOnly: true
  volumes:		# 定义卷,关联 Secret usermsg
  - name: config-volume
    secret:
      secretName: usermsg
# 创建 Pod 并验证
[root@master configmap]# kubectl apply -f nginx.yml     
# 进入容器,查看挂载的文件
[root@master configmap]# kubectl exec -it pods/testpod /bin/bash
root@testpod:/# ls /secrets/  # Secret 中的键(username、password)作为文件名
password  username
root@testpod:/# cat /secrets/password 
rch123  # 文件内容为解码后的密码
root@testpod:/# cat /secrets/username 
rch     # 文件内容为解码后的用户名

结果:Secret 中的 username 和 password 被映射为 /secrets 目录下的文件,内容为解码后的敏感信息(只读权限更安全)。

思路:如果程序需要读文件获取密码(比如数据库配置文件),把 Secret 挂载成卷,程序读文件就行,避免明文写在代码里。

2.3.2 向指定路径映射 secret 密钥

自定义 Secret 键的挂载路径(而非直接挂载到目录),适合需要特定文件路径的场景。

[root@master configmap]# vim nginx.yml 
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: testpod
  name: testpod
spec:
  containers:
  - image: nginx:1.23
    name: testpod
    volumeMounts:
    - name: config-volume
      mountPath: /secrets	# 挂载根目录
      readOnly: true
  volumes:
  - name: config-volume
    secret:
      secretName: usermsg  # 关联的 Secret
      items:  # 自定义键的挂载路径
      - key: username  # Secret 中的键 username
        path: my-users/username  # 挂载到 /secrets/my-users/username

# 创建 Pod 并验证
[root@master configmap]# kubectl apply -f nginx.yml 
pod/testpod created
# 进入容器查看
[root@master configmap]# kubectl exec pods/testpod -it -- /bin/bash
root@testpod:/# cd /secrets/
root@testpod:/secrets# ls
my-users  # 自定义目录
root@testpod:/secrets# cd my-users
root@testpod:/secrets/my-users# ls
username  # 文件名
root@testpod:/secrets/my-users# cat username 
rch  # 内容正确

结果:通过 items 配置,Secret 的 username 键被挂载到 /secrets/my-users/username 路径,而非直接放在 /secrets 目录下。

思路:如果程序要求密码文件必须放在特定路径(比如 /config/users/pass),用这种方式自定义挂载路径,更灵活。

2.3.3 将Secret设置为环境变量

将 Secret 中的敏感数据作为环境变量注入容器,供程序通过环境变量获取(如脚本中的密码)。

[root@master configmap]# vim testpod.yml
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: testpod
  name: testpod
spec:
  containers:
  - image: busyboxplus
    name: testpod
    command:
    - /bin/sh
    - -c
    - env
    env:  # 定义环境变量
    - name: USERNAME  # 环境变量名 USERNAME
      valueFrom:
        secretKeyRef:  # 从 Secret 取值
          name: usermsg  # 关联的 Secret 名称
          key: username  # 取 username 键的值
    - name: PASSWORD  # 环境变量名 PASSWORD
      valueFrom:
        secretKeyRef:
          name: usermsg
          key: password  # 取 password 键的值
  restartPolicy: Never
  
[root@master configmap]# kubectl apply -f testpod.yml 
pod/testpod created
[root@master configmap]# kubectl logs testpod 
KUBERNETES_PORT=tcp://10.96.0.1:443
KUBERNETES_SERVICE_PORT=443
HOSTNAME=testpod
SHLVL=1
HOME=/
USERNAME=rch
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
PWD=/
KUBERNETES_SERVICE_HOST=10.96.0.1
PASSWORD=rch123

结果:Secret 中的 username 和 password 被映射为容器的环境变量,程序可直接通过变量名获取。

思路:如果脚本或程序通过环境变量读取敏感信息(比如 $DB_PASSWORD),用这种方式最直接,和 ConfigMap 的环境变量用法类似,但针对敏感数据。

2.3.4 存储docker registry的认证信息

存储私有 Docker 仓库的认证信息,让 K8s 能拉取私有镜像(否则会因未认证而拉取失败)。

建立私有仓库并上传镜像

image-20251029205330965

# 登录私有仓库(示例仓库 rch.hjn.com)
[root@master configmap]# docker login rch.hjn.com
Username: admin
Password: 

WARNING! Your credentials are stored unencrypted in '/root/.docker/config.json'.
Configure a credential helper to remove this warning. See
https://docs.docker.com/go/credential-store/

Login Succeeded

# 标记本地镜像并推送到私有仓库
[root@master configmap]# docker tag busybox:latest rch.hjn.com/rch/busybox:latest
[root@master configmap]# docker push rch.hjn.com/rch/busybox:latest 
# 删除本地镜像,测试拉取(已登录状态可拉取)
[root@master configmap]# docker rmi rch.hjn.com/rch/busybox:latest
[root@master configmap]# docker pull rch.hjn.com/rch/busybox:latest
latest: Pulling from rch/busybox
Digest: sha256:28e01ab32c9dbcbaae96cf0d5b472f22e231d9e603811857b295e61197e40a9b
Status: Downloaded newer image for rch.hjn.com/rch/busybox:latest
rch.hjn.com/rch/busybox:latest
[root@master configmap]# docker rmi rch.hjn.com/rch/busybox:latest
# 再次删除本地镜像,尝试用 Pod 拉取(未配置认证,拉取失败)
[root@master configmap]# vim busybox.yml 
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: testpod
  name: busybox
spec:
  containers:
  - image: rch.hjn.com/rch/busybox:latest
    name: busybox

[root@master configmap]# kubectl get pods
NAME      READY   STATUS         RESTARTS   AGE
busybox   0/1     ErrImagePull   0          33s
testpod   0/1     Completed      0          8m3s
#私有的仓库未认证拉不下来镜像
[root@master configmap]# kubectl describe pod busybox 

"rch.hjn.com/rch/busybox:latest": Error response from daemon: unauthorized: unauthorized to access repository: rch/busybox, action: pull: unauthorized to access 

建立用于docker认证的secret

# 创建用于 Docker 认证的 Secret(类型为 kubernetes.io/dockerconfigjson)
[root@master configmap]# kubectl create secret docker-registry docker-auth   \
--docker-server=rch.hjn.com   \  # 私有仓库地址
--docker-username admin   \     # 用户名
--docker-password 123   \       # 密码
--docker-email=rch@rch.com      # 邮箱(可选)

# 查看 Secret 详情(存储认证信息)
[root@master configmap]# kubectl get secrets docker-auth -o yaml
apiVersion: v1
data:
  .dockerconfigjson: eyJhdXRocyI6eyJyY2guaGpuLmNvbSI6eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiIxMjMiLCJlbWFpbCI6InJjaEByY2guY29tIiwiYXV0aCI6IllXUnRhVzQ2TVRJeiJ9fX0=
kind: Secret
metadata:
  creationTimestamp: "2025-10-29T13:29:04Z"
  name: docker-auth
  namespace: default
  resourceVersion: "60602"
  uid: ac634aa7-c566-410b-a003-c0123288a618
type: kubernetes.io/dockerconfigjson	# 类型正确



# .dockerconfigjson方式
[root@master configmap]# cd 
[root@master ~]# cd .docker/
[root@master .docker]# ls
config.json
[root@master .docker]# cat config.json 
{
	"auths": {
		"rch.hjn.com": {
			"auth": "YWRtaW46MTIz"
		}
	}
}
# 编辑 Pod 的 YAML,添加 imagePullSecrets 引用认证 Secret
[root@master configmap]# vim busybox.yml
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: testpod
  name: busybox
spec:
  containers:
  - image: rch.hjn.com/rch/busybox:latest
    name: busybox
    command:
    - /bin/sh
    - -c
    - sleep 10000
  imagePullSecrets:  # 引用 Docker 认证 Secret
  - name: docker-auth

# 重新创建 Pod,验证拉取成功
[root@master configmap]# kubectl apply -f busybox.yml 
pod/busybox created
[root@master configmap]# kubectl get pods
NAME      READY   STATUS    RESTARTS   AGE
busybox   1/1     Running   0          3s  # 拉取成功

思路:通过 docker-registry 类型的 Secret 存储私有仓库认证信息,Pod 配置 imagePullSecrets 后可成功拉取私有镜像。

思路:私有镜像仓库需要账号密码才能拉取,把认证信息存在 Secret 里,Pod 引用这个 Secret 就能自动认证,不用在 Pod 里写明文密码。

Secrets 各类使用方式的区别

类型适用场景特点
卷挂载(默认路径)程序通过文件读取敏感信息键为文件名,值为文件内容,支持只读权限
卷挂载(指定路径)程序需要特定路径的敏感文件可自定义文件路径,更灵活
环境变量程序通过环境变量获取敏感信息直接作为变量使用,适合脚本或简单程序
Docker 仓库认证拉取私有镜像时的身份验证专用类型(dockerconfigjson),自动用于镜像拉取

三 volumes配置管理

解决容器文件临时存储的问题(容器崩溃后文件丢失),支持 Pod 内多容器共享文件,或跨 Pod / 节点共享数据,生命周期与 Pod 一致(部分类型如 NFS 可持久化)。

  • 容器中文件在磁盘上是临时存放的,这给容器中运行的特殊应用程序带来一些问题

  • 当容器崩溃时,kubelet将重新启动容器,容器中的文件将会丢失,因为容器会以干净的状态重建。

  • 当在一个 Pod 中同时运行多个容器时,常常需要在这些容器之间共享文件。

  • Kubernetes 卷具有明确的生命周期与使用它的 Pod 相同

  • 卷比 Pod 中运行的任何容器的存活期都长,在容器重新启动时数据也会得到保留

  • 当一个 Pod 不再存在时,卷也将不再存在。

  • Kubernetes 可以支持许多类型的卷,Pod 也能同时使用任意数量的卷。

  • 卷不能挂载到其他卷,也不能与其他卷有硬链接。 Pod 中的每个容器必须独立地指定每个卷的挂载位置。

3.1 kubernets支持的卷的类型

官网:https://kubernetes.io/zh/docs/concepts/storage/volumes/

k8s支持的卷的类型如下:

  • 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

3.2 emptyDir卷

**功能:**在 Pod 启动时创建的临时目录,供 Pod 内所有容器共享,Pod 被删除时数据丢失,适合临时缓存或多容器共享数据。

emptyDir 的使用场景:

  • 缓存空间,例如基于磁盘的归并排序。

  • 耗时较长的计算任务提供检查点,以便任务能方便地从崩溃前状态恢复执行。

  • 在 Web 服务器容器服务数据时,保存内容管理器容器获取的文件。

# 创建目录存放 YAML 文件
[root@master ~]# mkdir volumes
[root@master ~]# cd volumes/
# 生成 Pod 模板
[root@master volumes]# kubectl run testpod --image busyboxplus --dry-run=client -o yaml > empty.yml
# 编辑 YAML,配置 emptyDir 卷
[root@master volumes]# vim empty.yml
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: testpod
  name: testpod
spec:
  containers:
  - image: busyboxplus
    name: busyboxplus
    command:  # 休眠,保持容器运行
    - /bin/sh
    - -c
    - sleep 10000
    volumeMounts:  # 容器 1 挂载卷到 /cache
    - mountPath: /cache
      name: cache-volume

  - image: nginx:1.23  # 第二个容器(nginx)
    name: web
    volumeMounts:  # 容器 2 挂载同一个卷到 /usr/share/nginx/html(nginx 网页目录)
    - mountPath: /usr/share/nginx/html
      name: cache-volume

  volumes:  # 定义 emptyDir 卷
  - name: cache-volume
    emptyDir:
      medium: Memory  # 存储在内存中(临时缓存,速度快)
      sizeLimit: 100Mi  # 限制大小 100Mi

# 创建 Pod
[root@master volumes]# kubectl apply -f empty.yml
# 查看 Pod 状态(两个容器都运行)
[root@master volumes]# kubectl get pods
NAME      READY   STATUS    RESTARTS   AGE
testpod   2/2     Running   0          40s

# 进入第一个容器(busyboxplus),创建文件到共享卷
[root@master volumes]# kubectl exec -it pods/testpod -c busyboxplus /bin/sh
/ # echo rch is very good > /cache/index.html  # 在共享卷中创建 index.html

# 访问 nginx 验证(第二个容器挂载了同一个卷,可读取文件)
/ # curl localhost  # 访问 nginx(默认 80 端口)
rch is very good  # 成功读取到共享卷中的文件

结果:emptyDir 卷被两个容器共享,一个容器写入的文件,另一个容器可直接读取,数据存储在内存中(速度快,适合临时数据)。

思路:如果一个 Pod 里的多个容器需要交换数据(比如一个容器生成文件,另一个容器处理),用 emptyDir 最方便,临时存数据,Pod 删了数据也不用留。

3.3 hostpath卷

**功能:**将节点主机的文件 / 目录挂载到 Pod 中,数据在 Pod 删除后仍保存在节点上,适合需要持久化到节点本地的场景。

hostPath 的一些用法

  • 运行一个需要访问 Docker 引擎内部机制的容器,挂载 /var/lib/docker 路径。

  • 在容器中运行 cAdvisor(监控) 时,以 hostPath 方式挂载 /sys。

  • 允许 Pod 指定给定的 hostPath 在运行 Pod 之前是否应该存在,是否应该创建以及应该以什么方式存在

hostPath的安全隐患

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

示例:

# 生成Pod的yaml模板
[root@master volumes]# kubectl run testweb --image nginx:1.23 --dry-run=client -o yaml > hostpath.yml
# 编辑yaml,配置hostPath卷
[root@master volumes]# vim hostpath.yml 
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: testweb
  name: testweb
spec:
  containers:
  - image: nginx:1.23
    name: testweb
    volumeMounts:  # 挂载到nginx的网页目录
    - mountPath: /usr/share/nginx/html
      name: cache-col      
  volumes:
    - name: cache-col
      hostPath:
        path: /data  # 节点上的路径(不存在会自动创建,因type: DirectoryOrCreate)
        type: DirectoryOrCreate  # 类型:目录不存在则创建
# 创建Pod
[root@master volumes]# kubectl apply -f hostpath.yml 
pod/testweb created
# 查看Pod运行的节点(此处为node1)
[root@master volumes]# kubectl get pods -o wide
NAME      READY   STATUS    RESTARTS   AGE   IP            NODE    NOMINATED NODE   READINESS GATES
testweb   1/1     Running   0          11s   10.244.1.17   node1   <none>           <none>

# 访问nginx(初始403,因/data目录为空)
[root@master volumes]# curl 10.244.1.17
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.23.4</center>
</body>
</html>
# 在节点node1上的/data目录创建index.html
[root@node1 ~]# echo rch > /data/index.html
# 再次访问,返回新内容(数据持久化到节点)
[root@master volumes]# curl 10.244.1.17
rch

结果:hostPath 卷将节点的/data目录挂载到 nginx 容器,节点上创建的文件会被容器读取,且 Pod 删除后/data目录仍在节点上(数据持久化)。适合需要本地持久化的场景(如日志存储)。

3.4 nfs卷

挂载远程 NFS 服务器的共享目录到 Pod,实现跨节点数据共享,数据在 Pod / 节点故障后仍可保留。

对比一下:

  • emptyDir: Pod内共享(同一个Pod内的多个容器)
  • hostPath: 节点内共享(同一个节点上的多个Pod可以共享,但不同节点上的Pod访问的是各自节点上的路径)
  • nfs: 跨节点共享(集群内所有节点上的Pod都可以共享)

NFS 卷允许将一个现有的 NFS 服务器上的目录挂载到 Kubernetes 中的 Pod 中。这对于在多个 Pod 之间共享数据或持久化存储数据非常有用

例如,如果有多个容器需要访问相同的数据集,或者需要将容器中的数据持久保存到外部存储,NFS 卷可以提供一种方便的解决方案。

3.4.1 部署一台nfs共享主机并在所有k8s节点中安装nfs-utils

# 在NFS服务器(假设IP:192.168.2.61)安装nfs-utils
[root@harbor ~]# yum install nfs-utils -y
# 启动NFS服务并设置开机自启
[root@harbor ~]# systemctl enable --now nfs-server.service 
Created symlink /etc/systemd/system/multi-user.target.wants/nfs-server.service → /usr/lib/systemd/system/nfs-server.service.
# 创建共享目录
[root@harbor ~]# mkdir /rch
# 配置共享权限(/etc/exports):允许所有节点读写
[root@harbor ~]# vim /etc/exports
/rch	*(rw,sync,no_root_squash)  # *表示所有IP,rw读写,sync同步,no_root_squash保留root权限

# 设置目录权限
[root@harbor ~]# chmod 777 /rch/
# 刷新共享配置
[root@harbor ~]# exportfs -rv
exporting *:/rch
# 验证共享(在master/node节点执行)
[root@master ~]# showmount -e 192.168.2.61
Export list for 192.168.2.61:
/rch *
# 所有K8s节点(master、node1、node2)安装nfs-utils(确保能挂载NFS)
[root@master ~]# yum install nfs-utils -y

# 复制hostPath的yaml作为模板,修改为nfs卷
[root@master volumes]# ls
empty.yml  hostpath.yml
[root@master volumes]# cp hostpath.yml nfs.yml
[root@master volumes]# vim nfs.yml 
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: testweb
  name: testweb
spec:
  containers:
  - image: nginx:1.23
    name: testweb
    volumeMounts:  # 挂载到nginx网页目录
    - mountPath: /usr/share/nginx/html
      name: cache-col
  volumes:
    - name: cache-col
      nfs:  # 卷类型为nfs
        server: 192.168.2.61  # NFS服务器IP
        path: /rch  # NFS共享目录
# 创建Pod
[root@master volumes]# kubectl apply -f nfs.yml 
# 查看Pod运行的节点(此处为node1)
[root@master volumes]# kubectl get pods -o wide
NAME      READY   STATUS    RESTARTS   AGE   IP            NODE    NOMINATED NODE   READINESS GATES
testweb   1/1     Running   0          91s   10.244.1.33   node1   <none>           <none>
# 验证挂载配置
[root@master volumes]# kubectl describe pods testweb 
    Mounts:
      /usr/share/nginx/html from cache-col (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-9jm4r (ro)
Volumes:
  cache-col:
    Type:      NFS (an NFS mount that lasts the lifetime of a pod)
    Server:    192.168.2.61
    Path:      /rch
    ReadOnly:  false

# 访问nginx(初始403,因NFS目录为空)
[root@master volumes]# curl 10.244.1.33
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.23.4</center>
</body>
</html>
# 在NFS服务器的/rch目录创建index.html
[root@harbor ~]# cd /rch/
[root@harbor rch]# echo rch is vrey good > index.html
# 容器内查看文件(已同步)
[root@master volumes]# kubectl exec -it pods/testweb /bin/bash
root@testweb:/# cd /usr/share/nginx/html/
root@testweb:/usr/share/nginx/html# cat index.html 
rch is vrey good
# 访问验证
[root@master volumes]# curl 10.244.1.33
rch is vrey good

结果:NFS 卷挂载后,无论 Pod 调度到集群中的哪个节点(如 node1、node2),都能访问 NFS 服务器(192.168.2.61)的/rch目录。在 NFS 服务器创建的文件会被所有挂载该 NFS 的 Pod 共享,实现了跨节点数据共享,且数据独立于 Pod 和节点存在,即使 Pod 或节点故障,数据仍保留在 NFS 服务器。

思路:如果需要多个节点上的 Pod 共享数据,而且数据要长期保留,就用 NFS 卷。先搭一个 NFS 服务器,把一个目录共享出来,然后在 K8s 的 Pod 里配置挂载这个 NFS 目录。这样不管 Pod 跑在哪个节点,访问的都是 NFS 服务器上的同一个目录,数据不会因为 Pod 或节点挂了而丢失。

3.5 PersistentVolume持久卷

3.5.1 静态持久卷pv与静态持久卷声明pvc

将存储资源抽象为集群中的一种资源(PV),由管理员预先创建或动态供给,用户通过 PVC 申请存储资源,实现存储与 Pod 的解耦。

3.5.1.1 PersistentVolume(持久卷,简称PV)
  • pv是集群内由管理员提供的网络存储的一部分。

  • PV也是集群中的一种资源。是一种volume插件,

  • 但是它的生命周期却是和使用它的Pod相互独立的。

  • PV这个API对象,捕获了诸如NFS、ISCSI、或其他云存储系统的实现细节

  • pv有两种提供方式:静态和动态

    • 静态PV:集群管理员创建多个PV,它们携带着真实存储的详细信息,它们存在于Kubernetes API中,并可用于存储使用

    • 动态PV:当管理员创建的静态PV都不匹配用户的PVC时,集群可能会尝试专门地供给volume给PVC。这种供给基于StorageClass

3.5.1.2 PersistentVolumeClaim(持久卷声明,简称PVC)
  • 是用户的一种存储请求

  • 它和Pod类似,Pod消耗Node资源,而PVC消耗PV资源

  • Pod能够请求特定的资源(如CPU和内存)。PVC能够请求指定的大小和访问的模式持久卷配置

  • PVC与PV的绑定是一对一的映射。没找到匹配的PV,那么PVC会无限期得处于unbound未绑定状态

3.5.1.3 PV 与 PVC 的区别总结
对比项PV(持久卷)PVC(持久卷声明)
创建者集群管理员普通用户
作用提供具体存储资源(含底层细节)声明存储需求(大小、访问模式)
生命周期独立于 Pod,由管理员管理与使用它的 Pod 关联,但独立于 Pod 生命周期
绑定关系等待被 PVC 绑定主动匹配并绑定符合条件的 PV
3.5.1.4 volumes访问模式
  • ReadWriteOnce – 该volume只能被单个节点以读写的方式映射

  • ReadOnlyMany – 该volume可以被多个节点以只读方式映射

  • ReadWriteMany – 该volume可以被多个节点以读写的方式映射

  • 在命令行中,访问模式可以简写为:

    • RWO - ReadWriteOnce

      • ROX - ReadOnlyMany

      • RWX – ReadWriteMany

3.5.1.5 volumes回收策略
  • Retain:保留,需要手动回收

  • Recycle:回收,自动删除卷中数据(在当前版本中已经废弃)

  • Delete:删除,相关联的存储资产,如AWS EBS,GCE PD,Azure Disk,or OpenStack Cinder卷都会被删除

注意:

[!NOTE]

只有NFS和HostPath支持回收利用

AWS EBS,GCE PD,Azure Disk,or OpenStack Cinder卷支持删除操作。

3.5.1.6 回收策略区别总结
策略数据处理方式适用场景支持的存储类型
Retain保留数据,需手动清理重要数据(如需要备份后再删除)所有存储类型
Recycle自动删除数据(已废弃)临时数据,无需保留仅 NFS、HostPath
Delete自动删除存储资产及数据临时测试、无需持久化的数据AWS EBS、GCE PD、Azure Disk 等
3.5.1.7 volumes状态说明
  • Available 卷是一个空闲资源,尚未绑定到任何申领

  • Bound 该卷已经绑定到某申领

  • Released 所绑定的申领已被删除,但是关联存储资源尚未被集群回收

  • Failed 卷的自动回收操作失败

3.5.1.8 静态pv实例:
#在nfs主机中建立实验目录(用于绑定PV)
[root@harbor rch]# mkdir pv{1..3}  # 创建pv1、pv2、pv3三个目录
[root@harbor rch]# ls
index.html  pv1  pv2  pv3

[root@master ~]# mkdir pv  # 创建存放PV配置的目录
[root@master ~]# cd pv/
[root@master pv]# vim pv.yml  # 编辑PV配置文件
apiVersion: v1
kind: PersistentVolume  # 资源类型为PersistentVolume
metadata:
  name: pv1  # PV名称
spec:
  capacity:
    storage: 5Gi  # 存储容量5Gi
  volumeMode: Filesystem  # 卷模式为文件系统
  accessModes:
  - ReadWriteOnce  # 访问模式:单节点读写
  persistentVolumeReclaimPolicy: Retain  # 回收策略:保留(需手动处理)
  storageClassName: nfs  # 存储类名称(需与PVC的storageClassName匹配)
  nfs:  # 存储类型为NFS
    path: /rch/pv1  # NFS服务器上的目录
    server: 192.168.2.61  # NFS服务器IP
---  # 分隔多个资源
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv2
spec:
  capacity:
    storage: 10Gi  # 容量10Gi
  volumeMode: Filesystem
  accessModes:
  - ReadOnlyMany  # 访问模式:多节点只读
  persistentVolumeReclaimPolicy: Retain
  storageClassName: nfs
  nfs:
    path: /rch/pv2
    server: 192.168.2.61
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv3
spec:
  capacity:
    storage: 20Gi  # 容量20Gi
  volumeMode: Filesystem
  accessModes:
  - ReadWriteMany  # 访问模式:多节点读写
  persistentVolumeReclaimPolicy: Retain
  storageClassName: nfs
  nfs:
    path: /rch/pv3
    server: 192.168.2.61
[root@master pv]# kubectl apply -f pv.yml  # 创建PV
persistentvolume/pv1 created
persistentvolume/pv2 created
persistentvolume/pv3 created
[root@master pv]# kubectl get pv  # 查看PV状态(未绑定,状态为Available)
NAME   CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
pv1    5Gi        RWO            Retain           Available           nfs            <unset>                          5s
pv2    10Gi       ROX            Retain           Available           nfs            <unset>                          5s
pv3    20Gi       RWX            Retain           Available           nfs            <unset>                          5s

建立pvc,pvc是pv使用的申请,需要保证和pod在一个namesapce中

[root@master pv]# vim pvc.yml  # 编辑PVC配置文件
apiVersion: v1
kind: PersistentVolumeClaim  # 资源类型为PersistentVolumeClaim
metadata:
  name: pvc1  # PVC名称
spec:
  storageClassName: nfs  # 存储类名称(需与PV的storageClassName匹配)
  accessModes:
    - ReadWriteOnce  # 访问模式(需与PV的accessModes匹配)
  resources:
    requests:  # 请求的存储资源
      storage: 1Gi  # 请求1Gi(不能大于匹配的PV容量)
---     
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc2
spec:
  storageClassName: nfs
  accessModes:
    - ReadOnlyMany
  resources:
    requests:
      storage: 5Gi  # 请求5Gi(小于pv2的10Gi)
---     
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc3
spec:
  storageClassName: nfs
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 10Gi  # 请求10Gi(小于pv3的20Gi)
[root@master pv]# kubectl apply -f pvc.yml  # 创建PVC
persistentvolumeclaim/pvc1 created
persistentvolumeclaim/pvc2 created
persistentvolumeclaim/pvc3 created
[root@master pv]# kubectl get pvc  # 查看PVC状态(已绑定到对应PV)
NAME   STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
pvc1   Bound    pv1      5Gi        RWO            nfs            <unset>                 4s
pvc2   Bound    pv2      10Gi       ROX            nfs            <unset>                 4s
pvc3   Bound    pv3      20Gi       RWX            nfs            <unset>                 4s
[root@master pv]# kubectl get pv  # 查看PV状态(已绑定到PVC)
NAME   CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM          STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
pv1    5Gi        RWO            Retain           Bound    default/pvc1   nfs            <unset>                          6m31s
pv2    10Gi       ROX            Retain           Bound    default/pvc2   nfs            <unset>                          6m31s
pv3    20Gi       RWX            Retain           Bound    default/pvc3   nfs            <unset>                          6m31s

在pod中使用pvc

[root@master pv]# cp ../volumes/nfs.yml nginx-pv.yml  # 复制模板
[root@master pv]# vim nginx-pv.yml  # 编辑Pod配置,使用PVC
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: pv
  name: nginxpv
spec:
  containers:
  - image: nginx:1.23
    name: nginxpv
    volumeMounts:
    - mountPath: /usr/share/nginx/html  # 容器内挂载路径
      name: cache-col  # 关联的卷名称
  volumes:
    - name: cache-col  # 卷名称
      persistentVolumeClaim:  # 卷类型为PVC
        claimName: pvc1  # 关联的PVC名称(需与已创建的PVC名称一致)
[root@master pv]# kubectl apply -f nginx-pv.yml  # 创建Pod
[root@master pv]# kubectl get pods  # 查看Pod状态
NAME      READY   STATUS    RESTARTS   AGE
nginxpv   1/1     Running   0          7s
[root@master pv]# kubectl exec -it pods/nginxpv /bin/bash  # 进入容器
root@nginxpv:/# cd /usr/share/nginx/html/  # 切换到挂载目录
root@nginxpv:/usr/share/nginx/html# ls  # 初始为空
[root@harbor rch]# echo rch > pv1/index.html  # 在NFS的pv1目录创建文件(pv1绑定pv1,对应pvc1)
root@nginxpv:/usr/share/nginx/html# ls  # 容器内可见文件
index.html
root@nginxpv:/usr/share/nginx/html# exit  # 退出容器
exit
[root@master pv]# kubectl get pods -o wide  # 查看Pod运行节点
NAME      READY   STATUS    RESTARTS   AGE     IP            NODE    NOMINATED NODE   READINESS GATES
nginxpv   1/1     Running   0          2m45s   10.244.1.34   node1   <none>           <none>
testweb   1/1     Running   0          38m     10.244.1.33   node1   <none>           <none>
[root@master pv]# curl 10.244.1.34  # 访问Pod的IP,返回文件内容
rch

#需要手动删除,如果删除pvc,策略为回收,那么会删除掉pv里面的数据
[root@master pv]# kubectl delete -f nginx-pv.yml  # 删除Pod
pod "nginxpv" deleted
[root@master pv]# kubectl get pvc  # PVC仍绑定(PV生命周期独立于Pod)
NAME   STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
pvc1   Bound    pv1      5Gi        RWO            nfs            <unset>                 9m16s
pvc2   Bound    pv2      10Gi       ROX            nfs            <unset>                 9m16s
pvc3   Bound    pv3      20Gi       RWX            nfs            <unset>                 9m16s

结果:管理员创建了 3 个静态 PV(pv1、pv2、pv3),分别关联 NFS 服务器的/rch/pv1、/rch/pv2、/rch/pv3目录,并设置了不同的容量、访问模式和回收策略。用户通过 PVC(pvc1、pvc2、pvc3)申请存储资源,K8s 自动将 PVC 与匹配的 PV 绑定(容量、访问模式、存储类需匹配)。Pod 通过引用 PVC 使用存储资源,即使 Pod 被删除,PVC 和 PV 仍保持绑定,数据保留在 NFS 服务器。

思路:PV 就像管理员提前准备好的 “存储盘”,标明了大小、能被怎么访问(读写权限)、用的哪种存储(比如 NFS)。PVC 是用户的 “ storage 申请单”,说明需要多大空间、怎么访问。K8s 会自动找一个符合条件的 PV 和 PVC 绑定,用户的 Pod 直接用 PVC 就行,不用关心底层存储细节。这样存储管理和使用就分开了,方便维护。

3.5.1.9 静态 PV 与 PVC 的区别总结:
  • PV 是管理员创建的 “存储资源”,包含具体存储细节(如 NFS 路径、容量、访问模式);
  • PVC 是用户的 “存储请求”,声明需要的容量、访问模式等;
  • 关系:PVC 必须与 PV 的存储类、访问模式匹配才能绑定,绑定后 PVC 代表对 PV 的使用权,Pod 通过 PVC 使用存储。

四 存储类storageclass

定义存储类,实现动态供给 PV,当用户创建 PVC 时,K8s 根据 StorageClass 自动创建匹配的 PV,无需管理员手动创建,简化存储管理。

官网: https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner

4.1 StorageClass说明

  • StorageClass提供了一种描述存储类(class)的方法,不同的class可能会映射到不同的服务质量等级和备份策略或其他策略等。

  • 每个 StorageClass 都包含 provisioner、parameters 和 reclaimPolicy 字段, 这些字段会在StorageClass需要动态分配 PersistentVolume 时会使用到

4.2 StorageClass的属性

属性说明:https://kubernetes.io/zh/docs/concepts/storage/storage-classes/

Provisioner(存储分配器):用来决定使用哪个卷插件分配 PV,该字段必须指定。可以指定内部分配器,也可以指定外部分配器。外部分配器的代码地址为: kubernetes-incubator/external-storage,其中包括NFS和Ceph等。

Reclaim Policy(回收策略):通过reclaimPolicy字段指定创建的Persistent Volume的回收策略,回收策略包括:Delete 或者 Retain,没有指定默认为Delete。

4.3 存储分配器NFS Client Provisioner

源码地址:https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner

  • NFS Client Provisioner是一个automatic provisioner,使用NFS作为存储,自动创建PV和对应的PVC,本身不提供NFS存储,需要外部先有一套NFS存储服务。

  • PV以 n a m e s p a c e − {namespace}- namespace−{pvcName}-${pvName}的命名格式提供(在NFS服务器上)

  • PV回收的时候以 archieved- n a m e s p a c e − {namespace}- namespace−{pvcName}-${pvName} 的命名格式(在NFS服务器上)

4.4 部署NFS Client Provisioner

4.4.1 创建sa并授权

为 NFS Client Provisioner 创建专用服务账户,并赋予操作 PV、PVC、StorageClass 等资源的权限,确保其能正常工作。

[root@master pv]# vim rbac.yml  # 编辑RBAC配置(权限控制)
apiVersion: v1
kind: Namespace  # 创建命名空间(隔离资源)
metadata:
  name: nfs-client-provisioner
---
apiVersion: v1
kind: ServiceAccount  # 创建服务账户(用于Pod运行身份)
metadata:
  name: nfs-client-provisioner
  namespace: nfs-client-provisioner
---
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"]  # 允许操作PV
  - apiGroups: [""]
    resources: ["persistentvolumeclaims"]
    verbs: ["get", "list", "watch", "update"]  # 允许操作PVC
  - apiGroups: ["storage.k8s.io"]
    resources: ["storageclasses"]
    verbs: ["get", "list", "watch"]  # 允许操作StorageClass
  - apiGroups: [""]
    resources: ["events"]
    verbs: ["create", "update", "patch"]  # 允许创建事件
---
kind: ClusterRoleBinding  # 集群角色绑定(将角色权限绑定到服务账户)
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: run-nfs-client-provisioner
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    namespace: nfs-client-provisioner
roleRef:
  kind: ClusterRole
  name: nfs-client-provisioner-runner
  apiGroup: rbac.authorization.k8s.io
---
kind: Role  # 命名空间级角色
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: leader-locking-nfs-client-provisioner
  namespace: nfs-client-provisioner
rules:
  - apiGroups: [""]
    resources: ["endpoints"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]  # 允许操作endpoints(用于 leader选举)
---
kind: RoleBinding  # 命名空间角色绑定
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: leader-locking-nfs-client-provisioner
  namespace: nfs-client-provisioner
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    namespace: nfs-client-provisioner
roleRef:
  kind: Role
  name: leader-locking-nfs-client-provisioner
  apiGroup: rbac.authorization.k8s.io
  
[root@master pv]# kubectl apply -f rbac.yml  # 应用RBAC配置
namespace/nfs-client-provisioner created
serviceaccount/nfs-client-provisioner created
clusterrole.rbac.authorization.k8s.io/nfs-client-provisioner-runner created
clusterrolebinding.rbac.authorization.k8s.io/run-nfs-client-provisioner created
role.rbac.authorization.k8s.io/leader-locking-nfs-client-provisioner created
rolebinding.rbac.authorization.k8s.io/leader-locking-nfs-client-provisioner created
[root@master pv]# kubectl -n nfs-client-provisioner get sa  # 查看创建的服务账户
NAME                     SECRETS   AGE
default                  0         12s
nfs-client-provisioner   0         12s
  • 创建了独立命名空间nfs-client-provisioner,避免与其他资源冲突。
  • 通过 ClusterRole 和 ClusterRoleBinding 赋予 SA 操作 PV、PVC 等集群资源的权限;通过 Role 和 RoleBinding 赋予 SA 在命名空间内操作 endpoints 的权限(用于 leader 选举,确保高可用)。
4.4.2 部署应用

部署 NFS Client Provisioner 应用,使其连接到 NFS 服务器,准备为 PVC 动态创建 PV。

[root@master pv]# vim deployment.yml  # 编辑部署配置
apiVersion: apps/v1
kind: Deployment  # 部署NFS Client Provisioner
metadata:
  name: nfs-client-provisioner
  labels:
    app: nfs-client-provisioner
  namespace: nfs-client-provisioner  # 指定命名空间
spec:
  replicas: 1  # 副本数1
  strategy:
    type: Recreate  # 重建策略(更新时删除旧Pod再创建新Pod)
  selector:
    matchLabels:
      app: nfs-client-provisioner  # 匹配标签
  template:
    metadata:
      labels:
        app: nfs-client-provisioner  # Pod标签
    spec:
      serviceAccountName: nfs-client-provisioner  # 使用前面创建的服务账户
      containers:
        - name: nfs-client-provisioner
          image: sig-storage/nfs-subdir-external-provisioner:v4.0.2  # 镜像
          volumeMounts:
            - name: nfs-client-root  # 挂载NFS共享目录到容器内
              mountPath: /persistentvolumes
          env:  # 环境变量
            - name: PROVISIONER_NAME  # 分配器名称(需与StorageClass中的provisioner一致)
              value: k8s-sigs.io/nfs-subdir-external-provisioner
            - name: NFS_SERVER  # NFS服务器IP
              value: 192.168.2.61
            - name: NFS_PATH  # NFS共享目录
              value: /rch
      volumes:
        - name: nfs-client-root  # 卷名称
          nfs:  # 挂载NFS服务器的共享目录
            server: 192.168.2.61
            path: /rch

#现在还运行不了,因为没有镜像,先加载镜像
[root@master pv]# ls
deployment.yml  nfs-subdir-external-provisioner-4.0.2.tar  nginx-pv.yml  pvc.yml  pv.yml  rbac.yml
[root@master pv]# docker load -i nfs-subdir-external-provisioner-4.0.2.tar  # 加载本地镜像
1a5ede0c966b: Loading layer [==================================================>]  3.052MB/3.052MB
ad321585b8f5: Loading layer [==================================================>]  42.02MB/42.02MB
Loaded image: registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2
# 标签镜像并推送到私有仓库(供集群拉取)
[root@master pv]# docker tag registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2 rch.hjn.com/sig-storage/nfs-subdir-external-provisioner:v4.0.2
[root@master pv]# docker push rch.hjn.com/sig-storage/nfs-subdir-external-provisioner:v4.0.2 
The push refers to repository [rch.hjn.com/sig-storage/nfs-subdir-external-provisioner]
ad321585b8f5: Pushed 
1a5ede0c966b: Pushed 
v4.0.2: digest: sha256:f741e403b3ca161e784163de3ebde9190905fdbf7dfaa463620ab8f16c0f6423 size: 739
  • 要在harbor中建立一个公开得的名为sig-storage的项目,才能上传

image-20251031143342413

[root@master pv]# kubectl apply -f deployment.yml  # 部署应用
deployment.apps/nfs-client-provisioner created
[root@master pv]# kubectl -n nfs-client-provisioner get pods  # 查看Pod状态(运行中)
NAME                                      READY   STATUS    RESTARTS   AGE
nfs-client-provisioner-55c94bc8ff-457kg   1/1     Running   0          35s
  • Deployment 定义了 provisioner 的 Pod 模板,使用前面创建的 SA,挂载 NFS 服务器的/rch目录到容器的/persistentvolumes。
  • 环境变量指定了 provisioner 名称(用于关联 StorageClass)、NFS 服务器地址和路径,确保 provisioner 能正确连接 NFS。
  • 镜像通过私有仓库分发,避免节点拉取镜像失败。
4.4.3 创建存储类

定义动态创建 PV 的规则(关联 provisioner、设置回收策略等),使 PVC 能自动触发 PV 创建。

[root@master pv]# vim class.yaml  # 编辑StorageClass配置
apiVersion: storage.k8s.io/v1
kind: StorageClass  # 资源类型为StorageClass
metadata:
  name: nfs-client  # 存储类名称
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner  # 分配器名称(与Deployment中的PROVISIONER_NAME一致)
parameters:
  archiveOnDelete: "false"  # 删除时不归档(直接删除目录)
  
[root@master pv]# kubectl apply -f class.yaml  # 创建存储类
storageclass.storage.k8s.io/nfs-client created
[root@master pv]# kubectl get  storageclasses.storage.k8s.io  # 查看存储类
NAME         PROVISIONER                                   RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
nfs-client   k8s-sigs.io/nfs-subdir-external-provisioner   Delete          Immediate           false                  12s
  • StorageClass 的provisioner需与 provisioner 部署中的PROVISIONER_NAME一致,确保关联正确。
  • archiveOnDelete: "false"表示删除 PVC 时直接删除对应的 NFS 目录(默认会归档为archieved-*)。
  • 回收策略默认为 Delete(删除 PVC 时自动删除 PV 和存储数据)。
4.4.4 创建pvc

创建 PVC 时指定使用nfs-client存储类,provisioner 会自动检测并创建符合条件的 PV(容量、访问模式匹配),同时在 NFS 服务器上生成对应目录。

[root@master pv]# vim auto-pvc.yml  # 编辑PVC配置(使用存储类动态申请)
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: test-claim  # PVC名称
spec:
  storageClassName: nfs-client  # 指定存储类(触发动态供给)
  accessModes:
    - ReadWriteMany  # 访问模式
  resources:
    requests:
      storage: 1G  # 请求1G存储
      
[root@master pv]# kubectl apply -f auto-pvc.yml  # 创建PVC
persistentvolumeclaim/test-claim created
[root@master pv]# kubectl get pvc  # 查看PVC(已绑定,动态创建了PV)
NAME         STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
test-claim   Bound    pvc-64def5c4-083d-4f1c-9497-297f20b85c46   1G         RWX            nfs-client     <unset>                 6s
# 查看NFS服务器目录(自动创建了以PVC命名的目录)
[root@harbor rch]# tree .
.
├── default-test-claim-pvc-64def5c4-083d-4f1c-9497-297f20b85c46  # 动态创建的目录
├── index.html
├── pv1
│   └── index.html
├── pv2
└── pv3
# 删除PVC(触发回收,目录被删除)
[root@master pv]# kubectl delete  -f auto-pvc.yml 
persistentvolumeclaim "test-claim" deleted
[root@harbor rch]# tree .  # 动态创建的目录已删除
.
├── index.html
├── pv1
│   └── index.html
├── pv2
└── pv3
  • PVC 指定storageClassName: nfs-client后,provisioner 自动创建 PV(名称格式为pvc-<uuid>),并在 NFS 服务器生成目录(default-test-claim-<pvName>)。
  • 删除 PVC 后,由于回收策略为 Delete 且archiveOnDelete=false,PV 和 NFS 目录被自动删除,实现资源自动回收。
4.4.5 创建测试pod

验证动态生成的 PV/PVC 能否正常被 Pod 使用,数据是否正确存储到 NFS 服务器。

[root@master pv]# kubectl run testpod --image busyboxplus --dry-run=client -o yaml > pod.yml  # 生成Pod模板
[root@master pv]# vim pod.yml  # 编辑Pod配置,使用动态PVC
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: testpod
  name: testpod
spec:
  containers:
  - image: busyboxplus
    name: testpod
    command:  # 容器启动命令:创建文件后退出
    - /bin/sh
    - -c
    - "touch /mnt/SUCCESS && exit 0 || exit 1"
    volumeMounts:
      - name: nfs-pvc  # 关联卷
        mountPath: "/mnt"  # 挂载路径
  restartPolicy: "Never"  # 失败不重启
  volumes:
    - name: nfs-pvc  # 卷名称
      persistentVolumeClaim:
        claimName: test-claim  # 关联动态创建的PVC
        
[root@master pv]# kubectl apply -f pod.yml  # 创建Pod(需先重新创建test-claim PVC)
[root@harbor rch]# ls  # 查看NFS目录(Pod创建的文件在动态目录中)
default-test-claim-pvc-e8f98ba1-23e4-4ad8-9108-ca85b2bdcb75  index.html  pv1  pv2  pv3
[root@harbor rch]# cd default-test-claim-pvc-e8f98ba1-23e4-4ad8-9108-ca85b2bdcb75/
[root@harbor default-test-claim-pvc-e8f98ba1-23e4-4ad8-9108-ca85b2bdcb75]# ls
SUCCESS  # Pod创建的文件
  • Pod 通过 PVCtest-claim挂载动态生成的 PV,在容器的/mnt目录创建SUCCESS文件。
  • 该文件自动同步到 NFS 服务器的对应目录,验证动态存储正常工作。
4.4.6 设置默认存储类

将nfs-client设为默认存储类,后续创建 PVC 时可不指定storageClassName,自动使用默认存储类动态生成 PV。

  • 在未设定默认存储类时pvc必须指定使用类的名称

  • 在设定存储类后创建pvc时可以不用指定storageClassName

[root@master pv]# kubectl edit storageclasses.storage.k8s.io  # 编辑存储类,设为默认
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"  # 设定为默认存储类
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"storage.k8s.io/v1","kind":"StorageClass","metadata":{"annotations":{},"name":"nfs-client"},"parameters":{"archiveOnDelete":"false"},"provisioner":"k8s-sigs.io/nfs-subdir-external-provisioner"}
  creationTimestamp: "2025-10-31T06:37:46Z"
  name: nfs-client
  resourceVersion: "134004"
  uid: d2a1b5f6-c1f2-4e46-a069-e55e9cb75193
parameters:
  archiveOnDelete: "false"
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner
reclaimPolicy: Delete
volumeBindingMode: Immediate


[root@master pv]# vim class.yaml  # 也可直接在YAML中添加注解
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"  # 设为默认
  name: nfs-client
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner
parameters:
  archiveOnDelete: "false"

测试

[root@master pv]# vim auto-pvc.yml  # 编辑PVC,不指定存储类(使用默认)
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: test-claim
spec:                 # 不指定storageClassName,使用默认存储类
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1G
      
[root@master pv]# kubectl get pvc  # 查看PVC(自动使用默认存储类nfs-client)
NAME   STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
pvc1   Bound    pv1      5Gi        RWO            nfs            <unset>                 60m
pvc2   Bound    pv2      10Gi       ROX            nfs            <unset>                 60m
pvc3   Bound    pv3      20Gi       RWX            nfs            <unset>                 60m
[root@master pv]# kubectl apply -f auto-pvc.yml 
persistentvolumeclaim/test-claim created
[root@master pv]# kubectl get pvc
NAME         STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
pvc1         Bound    pv1                                        5Gi        RWO            nfs            <unset>                 60m
pvc2         Bound    pv2                                        10Gi       ROX            nfs            <unset>                 60m
pvc3         Bound    pv3                                        20Gi       RWX            nfs            <unset>                 60m
test-claim   Bound    pvc-97e747cd-c34d-401b-8caa-8ab1f24408b0   1G         RWX            nfs-client     <unset>                 2s

结果:通过部署 NFS Client Provisioner 和 StorageClass,实现了 PV 的动态供给。当创建 PVC 时,K8s 自动创建对应的 PV,并在 NFS 服务器上生成以 PVC 命名的目录。删除 PVC 时,动态创建的 PV 和 NFS 目录也会被自动删除(因回收策略为 Delete)。设置默认存储类后,PVC 可省略storageClassName配置,简化用户操作。

思路:StorageClass 就像一个 “自动售货机”,管理员提前设置好 “售货机” 的规则(用哪个存储、怎么回收),用户想要存储时,只需说要多大空间、怎么访问,“售货机” 就会自动分配一个 PV 给用户(PVC),不用管理员手动创建 PV 了。这样大大减少了管理员的工作量,适合存储需求频繁变化的场景。

4.4.7 StorageClass 与静态 PV 的区别总结:
  • 静态 PV:管理员手动创建 PV,用户 PVC 需与 PV 的容量、访问模式等匹配才能绑定;
  • StorageClass:动态供给 PV,管理员定义存储类规则,用户创建 PVC 时自动生成匹配的 PV,无需手动创建 PV;
  • 适用场景:静态 PV 适合存储需求固定的场景,StorageClass 适合存储需求动态变化、需自动化管理的场景。

五 statefulset控制器

管理有状态服务(如数据库、分布式系统),保证 Pod 的稳定网络标识和存储状态,支持有序部署、扩展和更新。

5.1 功能特性

  • Statefulset是为了管理有状态服务的问提设计的

  • StatefulSet将应用状态抽象成了两种情况:

  • 拓扑状态:应用实例必须按照某种顺序启动。新创建的Pod必须和原来Pod的网络标识一样

  • 存储状态:应用的多个实例分别绑定了不同存储数据。

  • StatefulSet给所有的Pod进行了编号,编号规则是: ( s t a t e f u l s e t 名称 ) − (statefulset名称)- (statefulset名称)−(序号),从0开始。

  • Pod被删除后重建,重建Pod的网络标识也不会改变,Pod的拓扑状态按照Pod的“名字+编号”的方式固定下来,并且为每个Pod提供了一个固定且唯一的访问入口,Pod对应的DNS记录。

5.2 StatefulSet的组成部分

  • Headless Service:用来定义pod网络标识,生成可解析的DNS记录

  • volumeClaimTemplates:创建pvc,指定pvc名称大小,自动创建pvc且pvc由存储类供应。

  • StatefulSet:管理pod的名称

5.3 构建方法

先创建无头服务(Headless Service)为 Pod 提供固定网络标识,再定义 StatefulSet(关联无头服务、指定存储模板),系统会按顺序创建 Pod 并为每个 Pod 分配独立存储,最后验证 Pod 的网络标识和存储是否持久。

环境准备(确认默认存储类)

[root@master statefulset]# kubectl get storageclasses.storage.k8s.io  # 确认默认存储类(用于动态生成PVC)
NAME                   PROVISIONER                                   RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
nfs-client (default)   k8s-sigs.io/nfs-subdir-external-provisioner   Delete          Immediate           false                  97m

创建无头服务

为 StatefulSet 的 Pod 提供固定网络标识(DNS 记录),使 Pod 即使重建,网络地址仍可通过名称解析。

[root@master statefulset]# kubectl create service clusterip rch --tcp=80:80 --clusterip="None" --dry-run=client -o yaml > rch.yml  # 生成无头服务模板(--clusterip=None 表示无头服务)
[root@master statefulset]# vim rch.yml  # 编辑无头服务配置
apiVersion: v1
kind: Service
metadata:
  labels:
    app: rch  # 标签
  name: rch  # 服务名称
spec:
  clusterIP: None  # 无头服务(无集群IP)
  ports:
  - port: 80  # 服务端口
    protocol: TCP
    targetPort: 80  # 容器端口
  selector:
    app: rch  # 匹配标签为app=rch的Pod
  type: ClusterIP

[root@master statefulset]# kubectl apply -f rch.yml  # 创建无头服务
[root@master statefulset]# kubectl describe svc rch  # 查看服务详情(无集群IP,Endpoints为空)
Name:              rch
Namespace:         default
Labels:            app=rch
Annotations:       <none>
Selector:          app=rch
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                None
IPs:               None
Port:              <unset>  80/TCP
TargetPort:        80/TCP
Endpoints:         <none>
Session Affinity:  None
Events:            <none>	 
  • 无头服务(clusterIP: None)不分配集群 IP,而是为每个匹配的 Pod 生成 DNS 记录(格式:pod名称.服务名称.命名空间.svc.cluster.local)。
  • 服务的selector: app=rch用于关联 StatefulSet 创建的 Pod(标签为app=rch)。

建立statefulset

创建有状态 Pod,按顺序启动,为每个 Pod 分配固定名称和独立存储,并关联无头服务。

[root@master statefulset]# vim statefulset.yml  # 编辑StatefulSet配置
apiVersion: apps/v1
kind: StatefulSet  # 资源类型为StatefulSet
metadata:
  name: web  # StatefulSet名称
spec:
  serviceName: "rch"  # 关联的无头服务名称(用于网络标识)
  replicas: 3  # 副本数3
  selector:
    matchLabels:
      app: rch  # 匹配标签
  template:
    metadata:
      labels:
        app: rch  # Pod标签(与服务selector匹配)
    spec:
      containers:
      - name: nginx
        image: nginx:1.23  # 容器镜像
        volumeMounts:
        - name: www  # 关联的卷名称(与volumeClaimTemplates中的name一致)
          mountPath: /usr/share/nginx/html  # 容器内挂载路径

  volumeClaimTemplates:  # PVC模板(自动为每个Pod创建PVC)
  - metadata:
      name: www  # PVC名称(与volumeMounts中的name对应)
    spec:
      storageClassName: nfs-client  # 存储类(使用默认存储类动态生成PV)
      accessModes:
      - ReadWriteOnce  # 访问模式
      resources:
        requests:
          storage: 1Gi  # 每个PVC请求1Gi存储
[root@master statefulset]# kubectl apply -f statefulset.yml  # 创建StatefulSet
statefulset.apps/web created
[root@master statefulset]# kubectl get pods  # 查看Pod(按顺序创建web-0、web-1、web-2)
NAME      READY   STATUS    RESTARTS   AGE
testweb   1/1     Running   0          144m
web-0     1/1     Running   0          24s
web-1     1/1     Running   0          23s
web-2     1/1     Running   0          20s
[root@master statefulset]# kubectl get pvc  # 查看PVC(自动为每个Pod创建www-web-0、www-web-1、www-web-2)
NAME         STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
www-web-0    Bound    pvc-9f347cd9-42cd-47f7-83d7-52d7826d4fd8   1Gi        RWO            nfs-client     <unset>                 2m21s
www-web-1    Bound    pvc-5f6fbaf6-afe8-4202-83e0-6a5118d489aa   1Gi        RWO            nfs-client     <unset>                 55s
www-web-2    Bound    pvc-60d99ae4-22a2-4df2-9b3f-9748d7a8dffa   1Gi        RWO            nfs-client     <unset>                 52s
# 查看NFS服务器目录(每个PVC对应一个目录)
[root@harbor rch]# ls
default-test-claim-pvc-97e747cd-c34d-401b-8caa-8ab1f24408b0  index.html
default-www-web-0-pvc-9f347cd9-42cd-47f7-83d7-52d7826d4fd8   pv1
default-www-web-1-pvc-5f6fbaf6-afe8-4202-83e0-6a5118d489aa   pv2
default-www-web-2-pvc-60d99ae4-22a2-4df2-9b3f-9748d7a8dffa   pv3
  • StatefulSet 的serviceName: "rch"关联无头服务,确保 Pod 的 DNS 记录为web-0.rch、web-1.rch等。
  • volumeClaimTemplates为每个 Pod 创建独立 PVC(命名格式:模板名称-StatefulSet名称-序号),通过默认存储类动态生成 PV,实现存储隔离。
  • Pod 按序号顺序启动(0→1→2),确保拓扑状态有序。

测试(验证网络标识和存储持久性)

在每个 Pod 的存储目录中写入标识文件,验证通过 DNS 可访问每个 Pod,且 Pod 重建后数据和网络标识不变。

# 在NFS服务器为每个Pod的存储目录写入标识文件
[root@harbor rch]# cd default-www-web-0-pvc-9f347cd9-42cd-47f7-83d7-52d7826d4fd8/
[root@harbor default-www-web-0-pvc-9f347cd9-42cd-47f7-83d7-52d7826d4fd8]# echo web0 > index.html  # web-0的标识
[root@harbor rch]# echo web1 > default-www-web-1-pvc-5f6fbaf6-afe8-4202-83e0-6a5118d489aa/index.html  # web-1的标识
[root@harbor rch]# echo web2 > default-www-web-2-pvc-60d99ae4-22a2-4df2-9b3f-9748d7a8dffa/index.html  # web-2的标识

# 查看无头服务(已关联3个Pod的Endpoints)
[root@master statefulset]# kubectl describe svc rch
Name:              rch
Namespace:         default
Labels:            app=rch
Annotations:       <none>
Selector:          app=rch
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                None
IPs:               None
Port:              <unset>  80/TCP
TargetPort:        80/TCP
Endpoints:         10.244.1.36:80,10.244.2.22:80,10.244.2.23:80  # 3个Pod的IP
Session Affinity:  None
Events:            <none>

# 进入测试Pod,验证DNS解析和访问
[root@master statefulset]# kubectl attach pods/testpod -it
If you don't see a command prompt, try pressing enter.
/ # nslookup rch.default.svc.cluster.local  # 解析无头服务的DNS
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      rch.default.svc.cluster.local
Address 1: 10.244.1.36 web-1.rch.default.svc.cluster.local  # web-1的DNS记录
Address 2: 10.244.2.22 web-0.rch.default.svc.cluster.local  # web-0的DNS记录
Address 3: 10.244.2.23 web-2.rch.default.svc.cluster.local  # web-2的DNS记录

/ # curl web-0.rch  # 通过DNS访问web-0
web0  # 正确返回标识
/ # curl web-1.rch  # 访问web-1
web1
/ # curl web-2.rch  # 访问web-2
web2

# 测试缩容(从3个副本缩为2个,删除序号最大的web-2)
[root@master statefulset]# kubectl scale statefulset web --replicas 2
statefulset.apps/web scaled
[root@master statefulset]# kubectl get pods
NAME      READY   STATUS    RESTARTS      AGE
testpod   1/1     Running   3 (39s ago)   6m12s
web-0     1/1     Running   0             12m
web-1     1/1     Running   0             12m  # web-2已删除

# 验证web-2不可访问
/ # curl web-2.rch
curl: (6) Couldn't resolve host 'web-2.rch'

# 测试扩容(恢复3个副本,重建web-2,网络标识和存储不变)
[root@master statefulset]# kubectl scale statefulset web --replicas 3
statefulset.apps/web scaled
/ # curl web-2.rch  # 重建后仍可通过原DNS访问
web2  # 数据仍保留(存储未删除)

# 测试删除所有Pod后重建(网络标识和数据不变)
[root@master statefulset]# kubectl scale statefulset web --replicas 0  # 缩容到0
statefulset.apps/web scaled
[root@master statefulset]# kubectl scale statefulset web --replicas 3  # 扩容到3
statefulset.apps/web scaled
/ # curl web-0.rch
web0  # 数据和DNS均正常
/ # curl web-1.rch
web1
/ # curl web-2.rch
web2
  • 每个 Pod 通过web-<序号>.rch的 DNS 名称可访问,验证了固定网络标识。
  • 缩容后重建的 Pod(如 web-2)仍使用原 DNS 名称,且存储数据(index.html)未丢失,验证了存储持久性。
  • StatefulSet 确保了有状态服务的拓扑有序性和数据一致性。

StatefulSet 与 Deployment 的区别总结

对比项StatefulSetDeployment
适用场景有状态服务(数据库、分布式系统)无状态服务(Web 应用、API)
Pod 名称固定(<名称>-< 序号 >)随机生成(<名称>-< 随机串 >)
网络标识固定 DNS 记录(基于 Pod 名称)无固定标识(通过 Service 访问)
存储每个 Pod 独立 PVC/PV共享存储卷(如 emptyDir)
部署顺序有序(0→1→2…)无序(并行创建)

更多推荐