kubernetes的存储

一 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 能拉取私有镜像(否则会因未认证而拉取失败)。
建立私有仓库并上传镜像

# 登录私有仓库(示例仓库 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的项目,才能上传

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

所有评论(0)