Kubernetes 存储
Kubernetes 存储
- 一、存储分类
- 二、ConfigMap(保存配置数据)
- 2.1 自动化运维平台
- 2.2 ConfigMap 定义
- 2.3 创建 ConfigMap
- 2.3.1 以文件方式创建
- 2.3.2 第二种方式创建
- 2.4 环境变量(env)
- 2.4.1 将 cm 的数据注入到容器内部,从而去调用
- 2.4.2 将 ConfigMap 变成容器的启动命令
- 2.4.3 将 configmap 变成文件去使用
- 2.5 热更新
- 2.6 解决热更新问题 (annotations)
- 2.7 不可改变(immutable)
- 三、Secret(存储敏感数据)
- 3.1 Opaque
- 3.2 Opaque - ENV
- 3.3 Opaque - Volume
- 3.4 热更新
- 3.5 不可更改(immutable)
- 四、Downward API(将 Pod 的信息带入到容器中)
- 4.1 env(通过环境变量将 downward API 传递到 pod 内部)
- 4.2 volume(通过卷绑定同步修改操作)
- 五、volume(在不同的 Pod 之间进行数据共享)
- 5.1 emptyDir(临时的)
- 5.2 hostPath(将主机某个路径共享到容器内部去使用)
- 六、持久卷PV/持久卷声明PVC
- 6.1 PVC 与 Pod 间的关系
- 6.2 关联条件
- 6.3 回收策略
- 6.4 PV 状态
- 6.5 PVC 保护(确保由 pod 正在使用的 PVC 不会从系统中移除)
- 6.6 StatefulSet 部署
- 6.6.1 部署 PV (PersistentVolume)
- 6.6.2 创建服务并使用 PV(StatefulSet)==Service
- 6.6.3 强制删除pv
- 6.6.4 回收策略
- 七、StorageClass
- 搭建 NFS 服务器
- 自动补全
[root@master 4]# cd
[root@master ~]# ls
4 6 calico-images-v3.30.2.tar k8s-images-v1.33.2.tar
5 anaconda-ks.cfg calico-v3.30.2.yaml
[root@master ~]# mkdir 7
[root@master ~]# cd 7
[root@master 7]# ls
[root@master 7]# mkdir 1
[root@master 7]# cd 1
[root@master 1]#
一、存储分类
存储各类特性
- 元数据
- configMap:用于保存配置数据(明文保存)
- Secret:用于保存敏感性数据(编码)
- Downward API:容器在运行时从 Kubernetes API服务器获取有关它们自身的信息
- 真实数据
- Volume:用于存储临时或者持久性数据
- PersistentVolume:申请制的持久化存储
共享适合文件多且小的场景,注入适合文件少且小的场景
二、ConfigMap(保存配置数据)
官方文档:ConfigMap
2.1 自动化运维平台
在非 Kubernetes 的环境中,也就是传统环境中,一般可能在大的公司里都会听过自动化运维平台,其中有一个核心的功能:配置中心
- 首先会在每一个节点上安装一个叫 agent 端,agent 端要做的作用就是监听当前的目标配置中心的配置选项是否发生更新动作
- 如果有的话, agent 端要从远程的配置中心去下载最新的配置文件,替换当前的。再去触发 nginx 实现重载。让当前的配置生效。
2.2 ConfigMap 定义
ConfigMap API 给我们提供了向容器中注入配置信息的机制,ConfigMap 可以被用来保存单个属性,也可以用来保存整个配置文件或者 JSON 二进制等对象
但凡跟配置挂钩的信息,都可以保存在 configMap 里,再提供给 pod 去使用。
ConfigMap 是一种注入
的机制,而不是共享的机制
让多个不同服务之间的文件达到一致性的两种思想:
- 注入:用一个新的数据将原来的文件内部去替换掉,保证多个 pod 之间的文件是一样的;一次注入后多次读取不再消耗网络 IO
- 共享:比如通过 NFS 这种共享服务;每一次在读取文件的时候都会发生网络 IO
共享适合文件多且小的场景,而注入的话更适用于文件少且小的场景
configMap 本身的原理还是比较简单的,就是存储信息再给 pod 内部去使用
保存配置参数,.file文件内容必须是key=value键值对,或者一行内容
2.3 创建 ConfigMap
创建命令:
#1. 可以在使用的时候注入至 pod 内部变成环境变量
kubectl create configmap cm的名称 --from-file=源文件名称
# file 文件内容要求每行必须是一对 key=value 形式
或者
#2.不能变成环境变量,只能再把它还原成文件去使用
kubectl create configmap cm的名称 --from-literal=key=value --from-literal=key=value
file 文件内容要求每行必须是一对 key=value 形式
eg.
[root@master 1]# vim chengke.file
[root@master 1]# cat chengke.file
name=zhangsan
password=123
2.3.1 以文件方式创建
- 首先创建一个 .file 文件
- 通过命令创建 configmap 对象用于保存数据:
kubectl create configmap info-config --from-file=chengke.file
- 查看当前 configmap 内部数据的方式
kubectl describe cm configmap的名字
kubectl get cm configmap的名字 -o yaml
[root@master 1]# kubectl create configmap info-config --from-file=chengke.file
configmap/info-config created
[root@master 1]# kubectl get pod
No resources found in default namespace.
[root@master 1]# kubectl get cm
NAME DATA AGE
info-config 1 10s
kube-root-ca.crt 1 6d5h #别删
- 将查看的东西保存成资源清单的话,可以加一个
--dry-run -o yaml
来将结果保存到文件中
[root@master 1]# kubectl get cm info-config --dry-run=client -o yaml > 2.cm.yaml
2.3.2 第二种方式创建
有些简短的或精简的数据或配置参数需要去创建,就使用 from literal ,通过命令的方式去创建对应的 configmap 资源对象
[root@master 1]# kubectl create configmap literal-config --from-literal=name=chengke --from-literal=password=123
configmap/literal-config created
[root@master 1]# kubectl get configmap
NAME DATA AGE
info-config 1 5m47s
kube-root-ca.crt 1 6d5h
literal-config 2 7s
[root@master 1]# kubectl get cm literal-config -o yaml
apiVersion: v1
data:name: chengkepassword: "123"
kind: ConfigMap
metadata:creationTimestamp: "2025-07-15T08:09:57Z"name: literal-confignamespace: defaultresourceVersion: "207195"uid: a03d869b-7f0a-4e3a-bcea-c8b133c6c73f
- 将查看的东西保存成资源清单的话,可以加一个
--dry-run -o yaml
来将结果保存到文件中
[root@master 1]# kubectl get cm info-config --dry-run=client -o yaml > 2.cm.yaml
2.4 环境变量(env)
把这些 configmap 的资源对象变成 pod 内部能够调用的数据
2.4.1 将 cm 的数据注入到容器内部,从而去调用
将 ConfigMap 的数据注入到容器内部变成环境变量
,从而去调用
- 清单文件:
[root@master 1]# kubectl delete cm info-config
configmap "info-config" deleted
[root@master 1]# vim 2.pod.yaml
[root@master 1]# cat 2.pod.yaml
apiVersion: v1
kind: ConfigMap
metadata:name: literal-confignamespace: default
data:name: chengkepassword: "123"
--- #多个资源清单通过 --- 分隔线进行组合
apiVersion: v1
kind: ConfigMap
metadata:name: env-confignamespace: default
data:log_level: INFO
--- #多个资源清单通过 --- 分隔线进行组合
apiVersion: v1
kind: Pod
metadata:name: cm-env-pod
spec:containers:- name: test-containerimage: harbor.registry.com/library/myapp:v1.0command: ['/bin/sh', '-c', 'env'] #原本的值替换为了env,在它启动时直接打印pod内部的环境变量env: # 定义了一个env,这是给容器内部添加环境变量- name: USERNAME # 第一个环境变量valueFrom: # 值来源configMapKeyRef: # 来源于一个 configMap的keyname: literal-config # 当前的configMap的名称,要与前面创建的名称一致key: name # 当前的key叫name,也是就将 name对应的值赋给USERNAME- name: PASSWORD # 第二个环境变量valueFrom:configMapKeyRef:name: literal-configkey: passwordenvFrom: # 第二种定义环境变量方式
# 是直接将当前的configMap引入进来,key和value都会被注入到当前系统内部,就不能替换了- configMapRef:name: env-configrestartPolicy: Never # 重启策略,永不
因为当前把启动命令替换成了打印 env,打印完成环境变量以后,它的前台就不存在了。所以就会死亡,需要要给它一个重启策略,让它就别再重启了,因为结果已经拿到了。
也需要注意一下,当前的重启策是跟当前的 mainc 处于同一个层级的。不是属于mainc的子对象。
- 创建文件:
[root@master 1]# kubectl create -f 2.pod.yaml
configmap/literal-config created
configmap/env-config created
pod/cm-env-pod created
[root@master 1]# kubectl get pod
NAME READY STATUS RESTARTS AGE
cm-env-pod 0/1 Completed 0 2s
Completed 代表曾经就绪过
- 通过查看日志信息来确认:
[root@master 1]# kubectl logs cm-env-pod
........
USERNAME=chengke
.......
PASSWORD=123
# 刚才设置的 USERNAME 和 PASSWORD 环境变量都已经创建好了
第一种使用方式:将 configMap 的数据注入到容器内部,变成环境变量去调用
2.4.2 将 ConfigMap 变成容器的启动命令
将 ConfigMap 变成容器的启动命令( configMap 别忘了还能当参数,当启动参数)
- 资源清单文件:
[root@master 7]# cat 3.pod.yaml
apiVersion: v1
kind: Pod
metadata:name: cm-command-podnamespace: default
spec:containers:- name: cm-command-pod-containerimage: harbor.registry.com/library/myapp:v1.0command: ['/bin/sh', '-c', "echo $(USERNAME) $(PASSWORD)"]env:- name: USERNAMEvalueFrom:configMapKeyRef:name: literal-configkey: name- name: PASSWORDvalueFrom:configMapKeyRef:name: literal-configkey: passwordrestartPolicy: Never
[root@master 7]# kubectl create -f 3.pod.yaml
pod/cm-command-pod created
[root@master 7]# kubectl get pod
NAME READY STATUS RESTARTS AGE
cm-command-pod 0/1 Completed 0 9m31s
- 通过查看日志确认是否可用
[root@master 1]# kubectl logs cm-command-pod
chengke 123
基于把环境变量变成启动命令参数的方式去调用
2.4.3 将 configmap 变成文件去使用
将 configmap 还原成文件,即 key 会变成文件名,value 会变成文件内容
[root@master 7]# vim 4.pod.yaml
[root@master 7]# cat 4.pod.yaml
apiVersion: v1
kind: Pod
metadata:name: cm-volume-podnamespace: default
spec:containers:- name: cm-volume-pod-containerimage: harbor.registry.com/library/myapp:1.0volumeMounts: #作用是卷绑定- name: ocnfig-volumemountPath: /etc/configvolumes: #声明一个卷- name: config-volume #要把一个卷名叫 config-volume 的卷挂载到当前容器的 /etc/config 目录下configMap: #是基于 configMap 的方式去做这个卷name: literal-configrestartPolicy: Never
卷类似于在 docker 里的卷的含义,相当于脱离当前容器生命周期以外的存储方式,卷绑定:就是告诉它要把哪个卷挂载到哪个目录
[root@master 7]# kubectl create -f 4.pod.yaml
pod/cm-volume-pod created
[root@master 7]# kubectl get pod
NAME READY STATUS RESTARTS AGE
cm-command-pod 0/1 Completed 0 9m31s
cm-env-pod 0/1 Completed 0 20m
cm-volume-pod 1/1 Running 0 28s
[root@master 7]# kubectl exec -it cm-volume-pod -- /bin/sh
# 由于已经将数据挂载到了 /etc/config 目录下,进入这个目录:
/ # cd /etc/config/
/etc/config # ls
name password
/etc/config # ls -l
total 0 #软链接,只有软链接这个数字才不会变(链接文件,为了热更新)
lrwxrwxrwx 1 root root 11 Jul 15 17:20 name -> ..data/name
lrwxrwxrwx 1 root root 15 Jul 15 17:20 password -> ..data/password
/etc/config # cat ..data/name
chengke
/etc/config # cat ..data/password
123
/etc/config #
相当于是直接把 configmap 对象放在当前的容器内部变成了文件
ConfigMap(注入:一个文件去覆盖原有的文件(支持热更新)
软链接:为了支持热更新
热更新:通过覆盖原有的配置文件,不需服务去重启 reload,实现服务更新
2.5 热更新
[root@master ~]# ls
4 5 6 7
[root@master ~]# cd 7
[root@master 7]# ls
1 2.pod.yaml 3.pod.yaml 4.pod.yaml
[root@master 7]# mkdir 5
[root@master 7]# cd 5
[root@master 5]# cat default.conf default_server表示默认服务
server {listen 80 default_server;server_name example.com www.example.com;location / {root /usr/share/nginx/html;index index.html index.htm;}
}
default_server 表示默认服务,也就是如果没有匹配到域名的话,都用当前的 server 去进行我们的替代
- 资源清单文件:
[root@master 5]# cat deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:name: hotupdate-deploylabels:app: hotupdate-deploy
spec:replicas: 5selector:matchLabels:app: hotupdate-deploytemplate:metadata:labels:app: hotupdate-deployspec:containers:- image: nginx:1.27.4name: nginxvolumeMounts:- name: config-volumemountPath: /etc/nginx/conf.d/volumes:- name: config-volumeconfigMap:name: default-nginx
可以把这一个 configmap 注入到当前 deployment 控制器所创建的三个 pod 中,变成默认的配置文件
- 去修改 nginx 的配置文件,然后去观察有没有发生变化。以此去确认是不是发生了热更新的关联。
[root@master 5]# kubectl create configmap default-nginx --from-file=default.conf
configmap/default-nginx created
[root@master 5]# kubectl get cm default-nginx -o yaml
apiVersion: v1
data: #如下内容已被添加进来default.conf: "server {\n\tlisten\t80\tdefault_server;\n\tserver_name\texample.comwww.example.com;\n\tlocation / {\n\t\troot\t/usr/share/nginx/html;\n\t\tindex\tindex.htmlindex.htm;\n\t}\n}\n"
kind: ConfigMap
metadata:creationTimestamp: "2025-07-17T02:23:48Z"name: default-nginxnamespace: defaultresourceVersion: "221527"uid: 8f8b0952-b37a-4215-81e6-ea4d8416e63d
- 创建一个 deployment 控制器来使用这个 configmap 对象
[root@master 5]# kubectl create -f deployment.yaml
deployment.apps/hotupdate-deploy created
[root@master 5]# kubectl get pod
NAME READY STATUS RESTARTS AGE
hotupdate-deploy-5c4f488685-grb8k 1/1 Running 0 5s
hotupdate-deploy-5c4f488685-mg42b 1/1 Running 0 5s
hotupdate-deploy-5c4f488685-p7qfq 1/1 Running 0 5s
hotupdate-deploy-5c4f488685-pk8j6 1/1 Running 0 5s
hotupdate-deploy-5c4f488685-zmhbs 1/1 Running 0 5s
[root@master 5]# kubectl get deploy
NAME READY UP-TO-DATE AVAILABLE AGE
hotupdate-deploy 5/5 5 5 13s
[root@master 5]# kubectl exec -it hotupdate-deploy-5c4f488685-grb8k -- /bin/sh
# ls
bin docker-entrypoint.d home media proc sbin tmp
boot docker-entrypoint.sh lib mnt root srv usr
dev etc lib64 opt run sys var
# cd /etc/nginx/conf.d
# ls
default.conf
# ls -l
total 0
lrwxrwxrwx. 1 root root 19 Jul 17 02:24 default.conf -> ..data/default.conf
# 挂载目录下有一个叫 default.conf 的文件
# cat default.conf
server {listen 80 default_server;server_name example.com www.example.com;location / {root /usr/share/nginx/html;index index.html index.htm;}
}
#说明是挂载成功了# 写一个循环
while true; do cat dufault.conf; sleep 2; done;
编写一个循环来不断的查看这个文件的内容,从而全款观察它的热更新功能:
- 复制会话去修改 configmap 对象
kubectl edit cm default-nginx
,将端口 80 改为 8080
[root@master 5]# kubectl edit cm default-nginx
configmap/default-nginx edited
- 保存退出,然后再验证是否修改成功
[root@master 5]# kubectl get cm default-nginx -o yaml
apiVersion: v1
data: #已经修改成功了default.conf: "server {\n\tlisten\t8080\tdefault_server;\n\tserver_name\texample.comwww.example.com;\n\tlocation / {\n\t\troot\t/usr/share/nginx/html;\n\t\tindex\tindex.htmlindex.htm;\n\t}\n}\n"
kind: ConfigMap
metadata:creationTimestamp: "2025-07-17T02:23:48Z"name: default-nginxnamespace: defaultresourceVersion: "211427"uid: 8f8b0952-b37a-4215-81e6-ea4d8416e63d
建议把可能会后期修改的配置文件抽象在 configmap 对象里,然后再挂载回 pod 内部。
这样做的好处就是一旦后续需要修改,不需要重新封装镜像,不需要重新写控制器。只需要通过 edit 等方式修改 configmap 对象本身,就可以修改所有的配置文件
2.6 解决热更新问题 (annotations)
nginx 服务器本身不支持热更新,需要我们手动去热更新
修改 pod 的 annotations
的方式强制触发滚动更新
kubectl patch deployment hotupdate-deploy --patch '{"spec":{"template":{"metadata":{"annotations":{"version/config":"8888"}}}}}'执行上面的代码后,就会触发滚动更新了
# version/config 的值可以是任意字符串,只要发生变化就会更新
更新 ConfigMap 后:
使用该 ConfigMap 挂载的 Env 不会同步更新
使用该 ConfigMap 挂载的 Volume 中的数据需要一段时间(实测大概10秒)才能同步更新
2.7 不可改变(immutable)
并不是所有的情况都希望 configmap 的配置信息允许被改变。
如果任何人都可以随意改变,那么极有可能会把一些重要的服务给搞坏,也可能产生误操作。为了避免产生这类问题,可以通过设定一个值让它变成不可修改的状态
Kubernetes 给不可变的 ConfigMap 和 Secret 提供了一种可选配置,可以设置各个 Secret 和 ConfigMap 为不可变
好处:
- 防止意外(或非预期的)更新导致应用程序中断
- 通过将 configmap 标记为不可变来
关闭 kube-apiserver 对其的监视
,从而显著降低 kube-apiserver 的负载,提升集群性能
使用方法:在资源文件中增加 immutable
,它的值设置为 true 即可
apiVersion: v1
metadata:name: default-nginx
immutable: true #immutable:永恒的,不可改变的
kind: ConfigMap
ConfigMap 如果修改为无可改变状态,是不允许回退的,是不可逆的。
解决的办法是删除此 ConfigMap,然后重新创建一个没有 immutable 标记的 ConfigMap
三、Secret(存储敏感数据)
官方文档:Secret
Secret 对象类型用来保存敏感信息,例如密码、OAuth 令牌和 SSH 密钥。将这些信息放在 Secret 中比放在 Pod 的定义或者 容器镜像 中来说更加安全(非明文)和灵活(Secret 依然可以做到热更新)
特点(安全-相对的)-- (令牌:通行证)
- Kubernetes 通过仅仅将 Secret 分发到需要访问 Secret 的 Pod 所在的机器节点来保障其安全性
- Secret 只会存储在节点的内存中,永不写入物理存储,这样从节点删除 secret 时就不需要擦除磁盘数据
- 从 Kubernetes1.7 版本开始,etcd 会以加密形式存储 Secret,一定程度的保证了 Secret 安全性
configmap 的 key 和 value 都是明码的
secret 是需要解码的,基于 base64 编码(base64 编码;base64 -d 解码)
类型:
内置类型 | 用法 |
---|---|
Opaque(默认) | 用户定义的任意数据 |
kubernetes.io/service-account-token | 服务账号令牌 |
kubernetes.io/dockercfg | ~/.dockercfg 文件的序列化形式 |
kubernetes.io/dockerconfigjson | ~/.docker/config.json 文件的序列化形式 |
kubernetes.io/basic-auth | 用于基于身份认证的凭据 |
kubernetes.io/ssh-auth | 用于 SSH 身份认证的凭据 |
kubernetes.io/tls | 用于 TLS 客户端或者服务器端的数据 |
bootstrap.kubernetes.io/token | 启动引导令牌数据 |
3.1 Opaque
当 Secret 配置文件中未作显式设定时,默认的 Secret 类型是 Opaque。当使用 kubectl 来创建一个Secret 时,会使用 generic 子命令
来标明要创建的是一个 Opaque 类型 Secret
- 首先生成对应的密码:
# 编码
[root@master 5]# echo -n "admin" | base64 #编码
YWRtaW4=
# 解码
[root@master 5]# echo "YWRtaW4=" | base64 -d #解码
admin[root@master 5]# [root@master 5]# echo -n "39528\$vdg7Jb" | base64 #注意$符号需要转义字符\
Mzk1MjgkdmRnN0pi
[root@master 5]# echo "Mzk1MjgkdmRnN0pi" | base64 -d
39528$vdg7Jb
- 定义 scret 资源清单文件,创建 secret 对象
Secret 中的 value 值必须经过 base64 编码。因为当 pod 被使用时,它会自动解码
[root@master 7]# vim 6.secret.yaml
[root@master 7]# cat 6.secret.yaml
apiVersion: v1
kind: Secret #secret中的 value值必须经过 base64编码
metadata:name: mysecretnamespace: default
type: Opaque
data:username: YWRtaW4=password: Mzk1MjgkdmRnN0pi# create:命令式,第一次使用,把定义的与没有定义的都创建
# apply:声明式,都可以使用,将我们写的和变更的都创建和更新
[root@master 7]# kubectl apply -f 6.secret.yaml
secret/mysecret created
[root@master 7]# kubectl get secret
NAME TYPE DATA AGE
mysecret Opaque 2 9s
[root@master 7]# kubectl get secret mysecret -o yaml
apiVersion: v1
data:password: Mzk1MjgkdmRnN0piusername: YWRtaW4=
kind: Secret
metadata:annotations:kubectl.kubernetes.io/last-applied-configuration: |{"apiVersion":"v1","data":{"password":"Mzk1MjgkdmRnN0pi","username":"YWRtaW4="},"kind":"Secret","metadata":{"annotations":{},"name":"mysecret","namespace":"default"},"type":"Opaque"}creationTimestamp: "2025-07-17T03:15:52Z"name: mysecretnamespace: defaultresourceVersion: "226326"uid: 936c934c-985d-4562-a67b-0a5896b0e9a1
type: Opaque
[root@master 7]# kubectl describe secret mysecret
Name: mysecret
Namespace: default
Labels: <none>
Annotations: <none>Type: OpaqueData
====
password: 12 bytes
username: 5 bytes[root@master 7]# kubectl describe secret mysecret
Name: mysecret
Namespace: default
Labels: <none>
Annotations: <none>Type: OpaqueData
====
password: 12 bytes
username: 5 bytes
# 当前的数据里面显示的是有 username 和 password,并且告诉有几个字节,但是并没有把数据实际的打印出来
获取 Secret 的真实值:
- 先查看资源清单文件的内容:
kubectl get secret secret的名称 -o yaml
; - 然后根据文件内容的 data 中的值(加密后的值),通过 echo 和管道符进行解码:
echo -n "被加密的值" | base64 -d
这个数据并不是非常安全的
3.2 Opaque - ENV
- 使用方式(以环境方式挂载-env)不支持热更新
以环境方式挂载
[root@master 7]# cat 7.deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:name: secret-opaque-env-deploynamespace: defaultlabels:app: secret-opaque-env
spec:replicas: 5selector:matchLabels:app: op-se-env-podtemplate:metadata:labels:app: op-se-env-podspec:containers:- name: myapp-containerimage: harbor.registry.com/library/myapp:1.0ports:- containerPort: 80env:- name: TEST_USERvalueFrom:secretKeyRef:name: mysecretkey: username- name: TEST_PASSWORDvalueFrom:secretKeyRef:name: mysecretkey: password[root@master 7]# kubectl apply -f 7.deployment.yaml
deployment.apps/secret-opaque-env-deploy created
[root@master 7]# kubectl get pod
NAME READY STATUS RESTARTS AGE
secret-opaque-env-deploy-7d8746b588-7qcfb 1/1 Running 0 7s
secret-opaque-env-deploy-7d8746b588-cz7gp 1/1 Running 0 7s
secret-opaque-env-deploy-7d8746b588-l7kqw 1/1 Running 0 7s
secret-opaque-env-deploy-7d8746b588-n77zr 1/1 Running 0 7s
secret-opaque-env-deploy-7d8746b588-wctwz 1/1 Running 0 7s
[root@master 7]#
[root@master 7]# kubectl exec -it secret-opaque-env-deploy-7d8746b588-wctwz -- /bin/sh
/ # env #通过 evn 关键字,可查看所有环境变量
KUBERNETES_SERVICE_PORT=443
KUBERNETES_PORT=tcp://10.0.0.1:443
HOSTNAME=secret-opaque-env-deploy-7d8746b588-wctwz
SHLVL=1
HOME=/root
TEST_PASSWORD=39528$vdg7Jb #是解码后的结果
TEST_USER=admin #是解码后的结果
TERM=xterm
KUBERNETES_PORT_443_TCP_ADDR=10.0.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_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT_443_TCP=tcp://10.0.0.1:443
KUBERNETES_SERVICE_HOST=10.0.0.1
PWD=/
所有的数据在创建的时候 value 需要通过 base64 位编码,但是在挂载使用的时候,它会把这个 value 值进行自动的解码。这个解码是自动的,并且不可选的
3.3 Opaque - Volume
- 创建可通过卷访问 secret 数据的 Pod(支持热更新)
[root@master 7]# cp 7.deployment.yaml 8.deployment.yaml
[root@master 7]# vim 8.deployment.yaml
[root@master 7]# cat 8.deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:name: secret-opaque-env-deploynamespace: defaultlabels:app: secret-opaque-env
spec:replicas: 5selector:matchLabels:app: op-se-env-podtemplate:metadata:labels:app: op-se-env-podspec:containers:- name: myapp-containerimage: harbor.registry.com/library/myapp:1.0volumeMounts: # 卷绑定- name: secret-volumemountPath: "/data" # 将secret-volume绑定在根下的data目录readOnly: truevolumes:- name: secret-volumesecret: # 这个卷基于secret提供defaultMode: 256 #权限设置(八进制转换256--400|420--644)secretName: mysecret #没有再往下定义某个特定的键值对,就默认把secret下的所有键值对挂载
权限设置:在spec.template.spe.volume.secret.defaultMode: 256(八进制,加的权限是400)另外,比如比较常用的权限叫 644,它是 420 的八进制
volumes:- name: secret-volumesecret: # 这个卷基于secret提供defaultMode: 256 #权限设置(八进制转换256--400|420--644)secretName: mysecret
部分键 key 值进行对应路径挂载
volumes:- name: secret-volumesecret: # 这个卷基于secret提供defaultMode: 256 secretName: mysecretitems:- key: usernamepath: /data
不是把 secret 对象的所有的数据都挂载到后面的容器里,而是挑了一个健值。这个键值的名字叫 username,把它挂载到 /data 目录下
[root@master 7]# kubectl apply -f 8.deployment.yaml
deployment.apps/secret-opaque-env-deploy created
[root@master 7]# kubectl get pod
NAME READY STATUS RESTARTS AGE
secret-opaque-env-deploy-8f88784bc-6989n 1/1 Running 0 9s
secret-opaque-env-deploy-8f88784bc-8k2fz 1/1 Running 0 9s
secret-opaque-env-deploy-8f88784bc-fn6fv 1/1 Running 0 9s
secret-opaque-env-deploy-8f88784bc-kb4tl 1/1 Running 0 9s
secret-opaque-env-deploy-8f88784bc-wkkrj 1/1 Running 0 9s
[root@master 7]# kubectl exec -it secret-opaque-env-deploy-8f88784bc-wkkrj -- /bin/sh
/ # cd /data
/data # ls
password username
/data # ls -l
total 0
lrwxrwxrwx 1 root root 15 Jul 17 11:37 password -> ..data/password
lrwxrwxrwx 1 root root 15 Jul 17 11:37 username -> ..data/username
# 查看时是解码后的结果
以单独卷声明的方式挂载的话,不支持热更新。
做链接文件:防止后面文件要更新
- 映射 Secret 键到特定文件路径
defaultMode: 256items:- key: usernamepath: my-group/my-username
[root@master 7]# cp 9.deployment.yaml 10.deployment.yaml
[root@master 7]# vim 10.deployment.yaml [root@master 7]# kubectl get pod
NAME READY STATUS RESTARTS AGE
secret-opaque-env-deploy-6cbbb799b4-54lcf 1/1 Running 0 31s
secret-opaque-env-deploy-6cbbb799b4-6tbr6 1/1 Running 0 31s
secret-opaque-env-deploy-6cbbb799b4-p724n 1/1 Running 0 31s
secret-opaque-env-deploy-6cbbb799b4-xgxbm 1/1 Running 0 31s
secret-opaque-env-deploy-6cbbb799b4-xlnsc 1/1 Running 0 31s
[root@master 7]# kubectl exec -it secret-opaque-env-deploy-6cbbb799b4-xlnsc -- /bin/sh
/ # cd /data/
/data # ls
my-group
/data # cd my-group/
/data/..2025_07_17_06_10_38.1492058393/my-group # ls
my-username
/data/..2025_07_17_06_10_38.1492058393/my-group # ls -l
total 4
-r-------- 1 root root 5 Jul 17 14:10 my-username
/data/..2025_07_17_06_10_38.1492058393/my-group # cat my-username
admin/data/..2025_07_17_06_10_38.1492058393/my-group #
3.4 热更新
当已经存储于卷中被使用的 Secret 被更新时,被映射的键也将终将被更新
- 将用户name值从 admin 改为 chengke
- 资源清单去改
- edit 去改 kubectl edit secret 名称
[root@master 5]# echo -n "chengke" | base64
Y2hlbmdrZQ==[root@master 7]# kubectl delete -f 10.deployment.yaml
deployment.apps "secret-opaque-env-deploy" deleted
[root@master 7]# kubectl apply -f 8.deployment.yaml
deployment.apps/secret-opaque-env-deploy created
[root@master 7]# kubectl get pod
NAME READY STATUS RESTARTS AGE
secret-opaque-env-deploy-8f88784bc-fp222 1/1 Running 0 4s
secret-opaque-env-deploy-8f88784bc-gm89z 1/1 Running 0 4s
secret-opaque-env-deploy-8f88784bc-gmqb4 1/1 Running 0 4s
secret-opaque-env-deploy-8f88784bc-kkjlx 1/1 Running 0 4s
secret-opaque-env-deploy-8f88784bc-vn8lc 1/1 Running 0 4s
[root@master 7]# kubectl exec -it secret-opaque-env-deploy-8f88784bc-vn8lc -- /
bin/sh
/ # cd /data/
/data # ls
password username
/data # ls -l
total 0
lrwxrwxrwx 1 root root 15 Jul 17 14:12 password -> ..data/password
lrwxrwxrwx 1 root root 15 Jul 17 14:12 username -> ..data/username/data # cat ..data/username
/data # cat username
chengke/data #
# username 值已经变成 chengke 了
3.5 不可更改(immutable)
Kubernetes 给不可变的 Secret 和 ConfigMap 提供了一种可选配置,可以设置各个 Secret 和 ConfigMap为不可变的。对于大量使用 Secret 的集群(至少有成千上万各不相同的 Secret 供 Pod 挂载)禁止变更它们的数据有下列好处:
- 止意外(或非预期的)更新导致应用程序中断
- 通过将 Secret 标记为不可变来关闭 kube-apiserver 对其的监视,从而显著降低 kube-apiserver 的负载提升集群性能。
四、Downward API(将 Pod 的信息带入到容器中)
官方文档:Downward API
作用:允许容器在运行时从 Kubernetes API 服务器获取有关它们自身的信息。这些信息可以作为容器内部的环境变量或文件注入到容器中,以便容器可以获取有关其运行环境的各种信息,如 Pod 名称、命名空间、标签等(可以是环境变量的方式,也可以是卷的方式,也可以是卷子路径的方式)
将 Pod 和容器字段 暴露 给 运行中的容器:环境变量和由特殊卷类型承载的文件。 这两种暴露 Pod 和容器字段的方法统称为 Downward API
将 Pod 和容器字段 暴露给运行中的容器
- 环境变量( secret、devlop:env )
- 由特殊卷类型承载的文件
可以使用 fieldRef
传递来自可用的 Pod 级字段的信息。在 API 层面,一个 Pod 的 spec 总是定义了至少一个 Container。 你可以使用 resourceFieldRef
传递来自可用的 Container 级字段的信息
4.1 env(通过环境变量将 downward API 传递到 pod 内部)
[root@master 7]# vim 11.pod.yaml
[root@master 7]# cat 11.pod.yaml
apiVersion: v1
kind: Pod
metadata: name: download-api-env-example
spec:containers:- name: my-containerimage: harbor.registry.com/library/myapp:1.0env:- name: POD_NAMEvalueFrom:fieldRef:fieldPath: metadata.name- name: POD_NAMESPACEvalueFrom:fieldRef:fieldPath: metadata.namespace- name: POD_IPvalueFrom:fieldRef:fieldPath: status.podIP- name: CPU_REQUESTvalueFrom:resourceFieldRef:resource: requests.cpu- name: CPU_LIMITvalueFrom:resourceFieldRef:resource: limits.cpu- name: MEMORY_REQUESTvalueFrom:resourceFieldRef:resource: requests.memory- name: MEMORY_LIMITvalueFrom:resourceFieldRef:resource: limits.memoryrestartPolicy: Never
[root@master 7]# kubectl create -f 11.pod.yaml
pod/download-api-env-example created
[root@master 7]# kubectl get pod
NAME READY STATUS RESTARTS AGE
download-api-env-example 1/1 Running 0 4s# 进入这个容器内部并查看 env 环境变量
[root@master 7]# kubectl exec -it download-api-env-example -- /bin/sh
/ # env
POD_IP=10.224.104.24
KUBERNETES_SERVICE_PORT=443
KUBERNETES_PORT=tcp://10.0.0.1:443
CPU_REQUEST=0
HOSTNAME=download-api-env-example
SHLVL=1
HOME=/root
MEMORY_REQUEST=0
TERM=xterm
POD_NAME=download-api-env-example
KUBERNETES_PORT_443_TCP_ADDR=10.0.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
CPU_LIMIT=4
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT_443_TCP=tcp://10.0.0.1:443
POD_NAMESPACE=default
KUBERNETES_SERVICE_HOST=10.0.0.1
PWD=/
MEMORY_LIMIT=1721077760
/ #
在资源清单中定义的变量都包含在了环境变量中
4.2 volume(通过卷绑定同步修改操作)
apiVersion: v1
kind: Pod
metadata:name: downward-api-volume-example
spec:containers:- name: my-containerimage: harbor.registry.com/library/myapp:1.0resources: # 对当前的容器做资源限制,如果容器不做资源限制,默认可以使用所有,即当前机器的最大资源。limits: # 最大的可用资源cpu: "1"memory: "512Mi"requests: # 初始值资源cpu: "0.5"memory: "256Mi"volumeMounts:- name: downward-api-volumemountPath: /etc/podinfovolumes:- name: downward-api-volumedownwardAPI:items:- path: "annotations"fieldRef:fieldPath: metadata.annotationsrestartPolicy: Never
卷的绑定方式是可以热更新的,只要 Pod 发生一些修改,都会被反馈到文件内部的变化
- volume相较于evn的优势
- 会保持热更新的特性
- 传递一个容器的资源字段到另一个容器中
五、volume(在不同的 Pod 之间进行数据共享)
官方文档:volume
volume 解决 Pod 内部容器如果出现了重建而导致的文件丢失的问题
5.1 emptyDir(临时的)
当 Pod 被分配给节点时,首先创建 emptyDir 卷,并且只要该 Pod 在该节点上运行,该卷就会存在。
Pod 中的容器可以读取和写入 emptyDir 卷中的相同文件,尽管该卷可以挂载到每个容器中的相同或不同路径上。
当出于任何原因从节点中删除 Pod 时,emptyDir 中的数据将被永久删除。在容器崩溃时是安全的(删除就会删掉里面的数据,这时是不可靠的)
emptyDir 的用法有:
- 暂存空间,例如用于基于磁盘的合并排序、用作长时间计算崩溃恢复时的检查点
- Web 服务器容器提供数据时,保存内容管理器容器提取的文件
借助 emptyDir 可以在容器或者 pod 内部的不同容器间进行数据共享
- 内存共享
在 linux 系统里会有一个内存目录,它是由内存去提供的,因此它的速度是极快的,延迟是极低的。所以当需要性能要求非常高时就可以采用这处方式来提供
[root@master 7]# vim 15.pod.yaml
[root@master 7]# cat 15.pod.yaml
apiVersion: v1
kind: Pod
metadata:name: volume-emptydir-memnamespace:
spec:containers:- name: myappimage: harbor.registry.com/library/myapp:1.0ports:- containerPort: 80resources:limits: # 限制最大值cpu: "1"memory: "1024Mi"requests: # 限制初始值cpu: "1"memory: "1024Mi"volumeMounts:- name: mem-volumemountPath: /datavolumes:- name: mem-volumeemptyDir:medium: Memory # 定义媒介类型sizeLimit: 500Mi # 定义了内存最大大小,注意,这个值是不能超过 pod定义的内存限制[root@master 7]# kubectl create -f 15.pod.yaml
pod/volume-emptydir-mem created
[root@master 7]# kubectl get pod
NAME READY STATUS RESTARTS AGE
curl 1/1 Running 1 (16h ago) 17h
download-api-env-example 0/1 Completed 0 18h
downward-api-volume-example 0/1 Completed 0 18h
volume-emptydir-mem 1/1 Running 0 5s
volume-emptydir-pod 2/2 Running 2 (16h ago) 16h
这种方式一般用于对响应速度要求高且数据不大的场景
5.2 hostPath(将主机某个路径共享到容器内部去使用)
hostPath 卷将主机节点的文件系统中的文件或目录挂载到集群中(将主机某个路径共享到容器内部去使用)
hostPath 用途如下:
- 运行需要访问 Docker 内部的容器;使用 /var/lib/docker 的 hostPath
- 在容器中运行 cAdvisor:使用 /dev/cgroups 的 hostPath
- 允许 pod 指定给定的 hostPath 是否应该在 pod 运行之前存在,是否应该创建,以及它应该以什么形式存在
[root@master 7]# vim 16.pod.yaml
[root@master 7]# cat 16.pod.yaml
apiVersion: v1
kind: Pod
metadata:name: hostpath-podnamespace: default
spec:containers:- name: myappimage: harbor.registry.com/library/myapp:1.0volumeMounts:- name: test-volumemountPath: /test-podvolumes:- name: test-volumehostPath:path: /testtype: Directory[root@master 7]# kubectl create -f 16.pod.yaml
pod/hostpath-pod created
[root@master 7]# kubectl get pod
NAME READY STATUS RESTARTS AGE
hostpath-pod 0/1 ContainerCreating 0 7s
[root@master 7]# kubectl describe pod hostpath-pod
Name: hostpath-pod
Namespace: default
Priority: 0
Service Account: default
Node: node2/192.168.86.13
Start Time: Fri, 18 Jul 2025 10:05:56 +0800
Labels: <none>
Annotations: <none>
Status: Pending
IP:
IPs: <none>
Containers:myapp:Container ID: Image: harbor.registry.com/library/myapp:1.0Image ID: Port: <none>Host Port: <none>State: WaitingReason: ContainerCreatingReady: FalseRestart Count: 0Environment: <none>Mounts:/test-pod from test-volume (rw)/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-7xf6t (ro)
Conditions:Type StatusPodReadyToStartContainers False Initialized True Ready False ContainersReady False PodScheduled True
Volumes:test-volume:Type: HostPath (bare host directory volume)Path: /testHostPathType: Directorykube-api-access-7xf6t:Type: Projected (a volume that contains injected data from multiple sources)TokenExpirationSeconds: 3607ConfigMapName: kube-root-ca.crtOptional: falseDownwardAPI: true
QoS Class: BestEffort
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300snode.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:Type Reason Age From Message---- ------ ---- ---- -------Normal Scheduled 19s default-scheduler Successfully assigned default/hostpath-pod to node2 #基于默认调度器,并且成功调度一了 node2 节点上Warning FailedMount 4s (x6 over 19s) kubelet MountVolume.SetUp failed for volume "test-volume" : hostPath type check failed: /test is not a directory #由 kubelet 发起的,而失败的原因是 /test 目录不存在
#事件一:基于默认调度器,并且成功调度一了 node2 节点上
#事件二:由 kubelet 发起的,而失败的原因是 /test 目录不存在
由于要求的类型是 Directory,表示是必须是一个目录。而现在这个目录是不存在的,因此就会报错
[root@node2 ~]# mkdir /test
# 先删除
[root@master 7]# kubectl delete -f 16.pod.yaml
pod "hostpath-pod" deleted
# 再创建
[root@master 7]# kubectl create -f 16.pod.yaml
pod/hostpath-pod created
[root@master 7]# kubectl get pod
NAME READY STATUS RESTARTS AGE
hostpath-pod 1/1 Running 0 2s[root@master 7]# kubectl exec -it hostpath-pod -- /bin/sh
/ # cd /test-pod/
/test-pod # ls
/test-pod # ls -l
total 0[root@node2 ~]# echo "hahaha" > /test/1 #去节点写一个数据/test-pod # ls
1
/test-pod # ll
/bin/sh: ll: not found
/test-pod # ls -l
total 4
-rw-r--r-- 1 root root 7 Jul 18 10:10 1
/test-pod #
这就是将主机某个路径共享到容器内部去使用
六、持久卷PV/持久卷声明PVC
官方文档:PV/PVC
volume :解决的是 Pod 内部容器如果出现了重建而导致的文件丢失的问题
- Persistent Volume(持久卷,简称PV):是 Kubernetes 中对存储资源的抽象,它独立于使用存储的 Pod。PV 是集群中的一块存储,可以由管理员预先配置或者通过存储类(Storage Class)动态分配。它类似于传统存储中的磁盘卷
- Persistent Volume Claim(持久卷声明,简称PVC):是用户对存储的请求。Pod 通过 PVC 来请求所需的存储资源,而不是直接访问 PV,实现了 Pod 和 PV 的解耦。PVC 就像是用户向存储系统 “下单”,请求一定规格的存储,比如指定存储大小、访问模式等,一定规格的存储指的就是PV
6.1 PVC 与 Pod 间的关系
当 PVC 看到有多个 PV 出现的时,它会根据两个步骤来进行选择:
- 一是预选(看是不是满足基本要求);
- 二是优选(在满足要求的 PV 中选一个最好的)
- 经过一系列匹配以后,PVC 就找到了最适合它的 PV 并进行绑定关联
6.2 关联条件
最好一致匹配(大小,PV不小于PVC)、模式匹配(PVC的类和PV的类必须一致)、读写策略完全匹配
- 容量:PV 的值不小于 PVC 要求,可以大于,最好一致
- 读写策略:完全匹配
- 单节点读写 - ReadWriteOnce - RWO:这种模式下,存储卷可以被单个节点以读写方式挂载。
例如,一个有状态的应用(如 MySQL 数据库)可能需要这种模式,因为数据库文件需要在一个地方进行读写操作,以保证数据的一致性。 - 多节点只读 - ReadOnlyMany - ROX:存储卷可以被多个节点以只读方式挂载。
适用于存储配置文件等只读数据,比如多个 Web 服务器节点可以同时挂载一个包含网站配置的存储卷,但只能读取其中的配置信息。 - 多节点读写 - ReadWriteMany - RWX:存储卷能够被多个节点以读写方式挂载。
在一些分布式文件系统(如 CephFS)中可以支持这种模式,适用于多个节点需要同时读写共享数据的场景,比如集群中的共享存储。
- 单节点读写 - ReadWriteOnce - RWO:这种模式下,存储卷可以被单个节点以读写方式挂载。
- 存储类:PV 的类与 PVC 的类必须一致,不存在包容降级关
6.3 回收策略
当 Pod 被杀死,PVC 被删除后,PV 有以下三种回收策略:
- Retain(保留):当 PVC 被删除后,PV 仍然保留数据,需要管理员手动清理。
这种策略适用于数据很重要,不能轻易删除的情况。
例如,存储了重要数据库备份的 PV,在 PVC 不再使用后,管理员可能需要先检查备份数据是否还需要,再决定是否删除 PV。会被标记为不可就绪状态,并且不会被别的 PVC 绑定,需要手动回收 - Recycle(回收):(已弃用)旧版本的 Kubernetes 有回收策略,会对存储卷进行格式化后重新使用,但是由于安全和效率等问题已经不推荐使用。会自动执行擦除命令来删除持久数据(rm -rf /thevolume/* ),从而可被其它 PVC 再次绑定
- Delete(删除):当 PVC 被删除后,PV 及其数据也会被自动删除。
这在一些临时存储场景下比较有用,比如用于存储临时测试数据的存储卷。
当前,只有 NFS 和 HostPath 支持回收策略
AWS EBS、GCE PD、AzureDisk 和 Cinder 卷支持删除策略
6.4 PV 状态
一个 PV 的生命周期中,可能会处于4中不同的阶段:
- Available(可用):表示可用状态,还未被任何 PVC 绑定。
- Bound(已绑定):表示 PVC 已经被 PVC 绑定。
- Released(已释放):PVC 被删除,但是资源还未被集群重新声明。
- Failed(失败):表示该 PV 的自动回收失败
6.5 PVC 保护(确保由 pod 正在使用的 PVC 不会从系统中移除)
PVC 保护的目的是确保由 Pod 正在使用的 PVC 不会从系统中移除,因为如果被移除的话可能会导致数据丢失。
注意:当 Pod 状态为 Pending 并且 Pod 已经分配给节点或 Pod 为 Running 状态时,PVC 处于活动状态
当启用 PVC 保护功能时,如果用户删除了一个 Pod 正在使用的 PVC,则该 PVC 不会被立即删除。PVC 的删除将被推迟,直到 PVC 不再被任何 Pod 使用。
6.6 StatefulSet 部署
- RC(ReplicationController):管理 Pod,让 Pod 数量与指定的数量一致
- RS(ReplicaSet):管理 Pod,确保一定数量的 Pod 正在运行,支持扩缩容和镜像版本的升降级
- Deployment:利用 RS 管理 Pod,适用于 无状态应用,能够管理 Pod 的副本,确保指定数量的 Pod 处于运行状态
- 支持扩缩容(kubectl scale)
- 自动扩缩容(kubectl autoscale)
- 滚动更新和回滚(kubectl rollout undo deployment/资源对象名称 --to-revision=2)
- DaemonSet:让每个节点有且只有一个 Pod 运行
适用于需要在每个节点上运行的服务,如日志收集和监控 - Job:执行一次性任务,任务执行完后就退出
- CronJob:周期性任务的执行
- StateFulSet:是在 Deployment 的基础上扩展出来的控制器,专门用于管理 有状态应用 的控制器
-
StatefulSet 控制器需要对数据进行持久化操作
-
StatefulSet控制器需要与无头服务(service 中 clusterIP:None 配合使用)
-
官方文档:StatefulSet 控制器
特点:
- 稳定的存储卷,Pod 的数据可以共享
- 有序创建,有序回收,有序删除
- 稳定的网络访问模式
StatefulSet 控制器的服务的访问方式:
pod_name.service_name.namespace_name.svc.clusterIP.local
- 案例:搭建一个 NFS 服务器,然后再把 NFS 服务器封装成 PV,然后再被 PVC 所调用并使用(正常情况下我们是需要有一个独立的机器去部署这一个 NFS 服务器)
- 三台主机中都安装 nfs
- 在 master 上创建共享目录
- 打开共享文件/etc/exports
/nfsdata *(rw,no_root_squash,no_all_squash,sync)
- 多创建一些目录
showmount -e 192.168.10.11
:查看共享目录 - node 节点测试
# 1. 首先在三台主机中都安装nfs
# 2. 然后在master上创建共享目录
[root@master 7]# mkdir /nfsdata
[root@master 7]# chmod 666 /nfsdata/
[root@master 7]# chown nobody /nfsdata# 3. 接着打开共享文件
[root@master 7]# vim /etc/exports
[root@master 7]# cat /etc/exports
/nfsdata *(rw,no_root_squash,no_all_squash,sync)# 4. 为了更贴近真实环境,多创建一些目录
[root@master 7]# mkdir /nfsdata/{1,2}
[root@master 7]# tree /nfsdata/
/nfsdata/
├── 1
└── 22 directories, 0 files
[root@master 7]# echo 1 > /nfsdata/1/index.html
[root@master 7]# echo 2 > /nfsdata/2/index.html
[root@master 7]# vim /etc/exports
[root@master 7]# cat /etc/exports
/nfsdata *(rw,no_root_squash,no_all_squash,sync)
/nfsdata/1 *(rw,no_root_squash,no_all_squash,sync)
/nfsdata/2 *(rw,no_root_squash,no_all_squash,sync)
# 这样就可以基于 NFS 服务器封装出来 3 个 PV
[root@master 7]# systemctl restart nfs-server
# 启动成功后可执行如下命令查看共享目录:
[root@master 7]# showmount -e 192.168.86.11
Export list for 192.168.86.11:
/nfsdata/2 *
/nfsdata/1 *
/nfsdata *
- 测试:在 node 上去创建一个 /nfstest 目录,然后将它挂载到共享的目录上
mount -t nfs 192.168.86.11:/nfsdata/1 /nfstest
node节点:
[root@node1 ~]# mkdir /nfstest
[root@node1 ~]# mount -t nfs 192.168.86.11:/nfsdata/1 /nfstest
[root@node1 ~]# cd /nfstest/
[root@node1 nfstest]# ls
index.html
[root@node1 nfstest]# cat index.html
1
[root@node1 nfstest]# echo "haha" > index.html
[root@node1 nfstest]# cat index.html
haha
[root@node2 ~]# mkdir /nfstest
[root@node2 ~]# mount -t nfs 192.168.86.11:/nfsdata/2 /nfstest
[root@node2 ~]# ls
[root@node2 ~]# cd /nfstest/
[root@node2 nfstest]# ls
index.html
[root@node2 nfstest]# cat index.html
2
[root@node2 nfstest]# echo "hehe" > index.html
[root@node2 nfstest]# cat index.html
hehe[root@master 7]# cat /nfsdata/1/index.html
haha
[root@master 7]# cat /nfsdata/2/index.html
hehe# node 节点解除挂载: umount /nfstest
6.6.1 部署 PV (PersistentVolume)
将 NFS 转换为 PV,便于后续使用
命令行会显示绑定到 PV 的 PVC 的名称
[root@master 7]# vim 17.pv.yaml
[root@master 7]# cat 17.pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:name: nfspv1
spec:capacity: #PV容量storage: 1Gi #PV存储空间accessModes: #访问方式- ReadWriteOnce #单节点读写persistentVolumeReclaimPolicy: Delete #回收策略storageClassName: nfs #存储类的名字nfs:path: /nfsdata/1 #共享的路径server: 192.168.86.11 #NFS服务器的服务地址
---
apiVersion: v1
kind: PersistentVolume
metadata:name: nfspv2
spec:capacity:storage: 0.9GiaccessModes:- ReadWriteOncepersistentVolumeReclaimPolicy: DeletestorageClassName: nfsnfs:path: /nfsdata/2server: 192.168.86.11
---[root@master 7]# kubectl create -f 17.pv.yaml
persistentvolume/nfspv1 created
persistentvolume/nfspv2 created
[root@master 7]# kubectl get pv
# PV容量 当前读写的模式 回收策略 分配给哪个PVC 存储类
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE
nfspv1 1Gi RWO Delete Available nfs <unset> 41s
nfspv2 966367641600m RWO Delete Available nfs <unset> 41s
6.6.2 创建服务并使用 PV(StatefulSet)==Service
对于无状态服务它不提供数据的持久化,而有状态服务需要提供数据的持久化
- 下面通过 StatefulSet 控制器来提供有状态服务,资源清单如下:
[root@master 7]# mkdir /nfsdata/3
[root@master 7]# mkdir /nfsdata/4[root@master 7]# vim 18.pvc.yaml
[root@master 7]# cat 18.pvc.yaml
# 定义了一个无头服务的 serivce,用于供给给 StatefulSet 控制器去使用
apiVersion: v1
kind: Service
metadata:name: nginxlabels:app: nginx
spec:ports:- port: 80name: webclusterIP: None # 指定为ClusterIP,但是没有 VIPselector:app: nginx
# Service中:
# 由于底层是IPVS方式,对于IPVS来说,它的命令是 ipvsadm -A 来添另一个新的集群 -t 来指定集群的地址
# 现在没有VIP,所以无法创建负载均衡集群 ---
apiVersion: apps/v1
kind: StatefulSet
metadata:name: web
spec:selector:matchLabels:app: nginxserviceName: "nginx"replicas: 3template:metadata:labels:app: nginxspec:containers:- name: nginximage: harbor.registry.com/library/myapp:1.0ports:- containerPort: 80name: webvolumeMounts:- name: www mountPath: /usr/local/nginx/htmlvolumeClaimTemplates: # 定义PVC模板- metadata:name: www # 模板的名字spec:accessModes: [ "ReadWriteOnce" ] # 单节点读写storageClassName: "nfs" # 存储类resources: # 请求的容量为1Grequests:storage: 1Gi
# 在 StatefulSet 控制器中定义了一个创建 pvc 的模板
# 并且会创建 3 个 PVC。而这三个 PVC 又会去自动匹配满足条件的 PV ,从而建立关联
clusterIP: None 代表的是无头服务
-
kubectl get svc
可以看到 ClusterIP 是空的,因此它的背后是没有负载均衡集群的 -
查看 endpoints 信息:
kubectl get endpoints
(可以看到匹配到这个端点值是有 IP 的)
这个端点值作用:用于写 DNS 记录 -
通过命令:
dig -t A nginx.default.svc.cluster.local. @10.0.0.10
解析验证
A 记录解析完毕以后,有好几个结果:- 当前 statefulset 控制器的 pod 地址(web-0、web-1、web-2)
# 名字与之前的 Pod 名称不一样,直接使用的是控制器的名字
[root@master 7]# kubectl get pod
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 3m10s
web-1 1/1 Running 0 3m9s
web-2 1/1 Running 0 54s
所以 无头服务会将标签选择器所匹配到的 Pod 添加到 SVC 的 DNS 解析结果中
6.6.3 强制删除pv
kubectl patch pv xxx -p '{"metadata":{"finalizers":null}}'
特性:
- 有序生成,最后回收
- 数据的一致性(Retain:保留,删掉Pod后,PV不会被删除,新创建一个Pod后,数据会自动共享给新的Pod)
6.6.4 回收策略
如果 PVC 删除后,PV并没有被解绑(BOUND),即(Release/Failed),那么这个 PV 就不能再被其他 PVC 绑定使用,如果想要这个 PV 能够真正释放出来,即状态变为 Avalable,则有两种处理方式
- 删除 StatefulSet 控制器的资源清单(
kubectl delete -f xxx.yaml
) - 查看 PVC 信息:kubectl get pvc
- 如果 PVC 还是存在,手动删除:
kubectl delete pvc --all
已释放状态(Released)下的 PV 是没有办法去能够自动被关联或者使用的。因此,要想能被绑定,就必须把它们变为活跃状态
方法:
- 删除后现创建,删除以后,再通过清单来重新创建新的 PV 对象
- 清除绑定记录,
kubectl edit pv pv名称
,删除 claimRef-kind:PersistentVolumeClaim
七、StorageClass
StorageClass 是一种资源对象,用于定义持久卷(Persistent Volumes)的动态供给(DynamicProvisioning)策略。StorageClass 允许管理员定义不同类型的存储,并指定如何动态创建持久卷以供应用程序使用。
这使得 kubernetes 集群中的存储管理更加灵活和自动化。
官方文档:StorageClass
nfs-client-provisioner 自动化了根据需要创建持久卷的过程
搭建 NFS 服务器
使用已有的 NFS 服务器,在 /etc/exports 文件中增加一个共享目录
[root@master 7]# mkdir /nfsdata/share
[root@master 7]# vim /etc/exports
[root@master 7]# cat /etc/exports
/nfsdata/1 *(rw,no_root_squash,no_all_squash,sync)
/nfsdata/2 *(rw,no_root_squash,no_all_squash,sync)
/nfsdata/3 *(rw,no_root_squash,no_all_squash,sync)
/nfsdata/4 *(rw,no_root_squash,no_all_squash,sync)
/nfsdata/share *(rw,no_root_squash,no_all_squash,sync)
[root@master 7]# ll -d /nfsdata/share/
drwxr-xr-x. 2 root root 6 Jul 18 15:30 /nfsdata/share/
# 修改所属者权限
[root@master 7]# chown -R nobody /nfsdata/share/
[root@master 7]# ll -d /nfsdata/share/
drwxr-xr-x. 2 nobody root 6 Jul 18 15:30 /nfsdata/share/
[root@master 7]# systemctl restart nfs-server.service
[root@master 7]# systemctl enable nfs-server.service
Created symlink /etc/systemd/system/multi-user.target.wants/nfs-server.service → /usr/lib/systemd/system/nfs-server.service.
[root@master 7]# showmount -e 192.168.86.11
Export list for 192.168.86.11:
/nfsdata/share *
/nfsdata/4 *
/nfsdata/3 *
/nfsdata/2 *
/nfsdata/1 *
- 创建名字空间:
[root@master 7]# kubectl create namespace nfs-storageclass
namespace/nfs-storageclass created
[root@master 7]# kubectl get ns
NAME STATUS AGE
default Active 9d
kube-node-lease Active 9d
kube-public Active 9d
kube-system Active 9d
nfs-storageclass Active 4s
[root@master 7]# kubectl get namespace nfs-storageclass -o yaml #看资源清单怎么写
apiVersion: v1 #需要写的部分
kind: Namespace #需要写的部分
metadata: #需要写的部分creationTimestamp: "2025-07-18T07:33:24Z"labels:kubernetes.io/metadata.name: nfs-storageclassname: nfs-storageclassresourceVersion: "295595"uid: c82301e3-3a7c-42c0-b133-4bc0797c8771
spec: #以下都不需要写finalizers:- kubernetes
status:phase: Active
- 部署 nfs-client-provisioner
- 创建接口对象(Deployment)
- 创建 ServiceAccount,包括角色和角色绑定(ServiceAccount、ClusterRole、ClusterRoleBinding、Role、RoleBinding)
- 创建存储类(StorageClass)
- 创建相关对象
在两个node节点导入:docker load -i nfs-subdir-external-provisioner-v4.0.2.tar
[root@node2 ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
.................
k8s.dockerproxy.com/sig-storage/nfs-subdir-external-provisioner v4.0.2 932b0bface75 4 years ago 43.8MB[root@master 7]# mkdir 18
[root@master 7]# cd 18
[root@master 18]# vim deployment.yaml
# 1.创建接口对象(Deployment)
[root@master 18]# cat deployment.yaml
kind: Deployment
apiVersion: apps/v1
metadata:name: nfs-client-provisionernamespace: nfs-storageclass
spec:replicas: 1selector:matchLabels:app: nfs-client-provisionerstrategy:type: Recreatetemplate:metadata:labels:app: nfs-client-provisionerspec:serviceAccountName: nfs-client-provisionercontainers:- name: nfs-client-provisioner#image: registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2image: k8s.dockerproxy.com/sig-storage/nfs-subdir-external-provisioner:v4.0.2image: harbor.registry.com/library/nfs-subdir-external-provisioner:v4.0.2volumeMounts:- name: nfs-client-rootmountPath: /persistentvolumesenv:- name: PROVISIONER_NAMEvalue: k8s-sigs.io/nfs-subdir-external-provisioner- name: NFS_SERVER#value: <YOUR NFS SERVER HOSTNAME>value: 192.168.10.11- name: NFS_PATHvalue: /nfsdata/sharevolumes:- name: nfs-client-rootnfs:# server: <YOUR NFS SERVER HOSTNAME>server: 192.168.10.11# share nfs pathpath: /nfsdata/share
# 2.创建 ServiceAccount,包括角色和角色绑定
[root@master 18]# cat rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:name: nfs-client-provisioner# replace with namespace where provisioner is deployednamespace: nfs-storageclass
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:name: nfs-client-provisioner-runner
rules:
- apiGroups: [""]resources: ["nodes"]verbs: ["get", "list", "watch"]
- apiGroups: [""]resources: ["persistentvolumes"]verbs: ["get", "list", "watch", "create", "delete"]
- apiGroups: [""]resources: ["persistentvolumeclaims"]verbs: ["get", "list", "watch", "update"]
- apiGroups: ["storage.k8s.io"]resources: ["storageclasses"]verbs: ["get", "list", "watch"]
- apiGroups: [""]resources: ["events"]verbs: ["create", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:name: run-nfs-client-provisioner
subjects:- kind: ServiceAccountname: nfs-client-provisioner# replace with namespace where provisioner is deployednamespace: nfs-storageclass
roleRef:kind: ClusterRolename: nfs-client-provisioner-runnerapiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:name: leader-locking-nfs-client-provisioner# replace with namespace where provisioner is deployednamespace: nfs-storageclass
rules:
- apiGroups: [""]resources: ["endpoints"]verbs: ["get", "list", "watch", "create", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:name: leader-locking-nfs-client-provisioner# replace with namespace where provisioner is deployednamespace: nfs-storageclass
subjects:- kind: ServiceAccountname: nfs-client-provisioner# replace with namespace where provisioner is deployednamespace: nfs-storageclass
roleRef:kind: Rolename: leader-locking-nfs-client-provisionerapiGroup: rbac.authorization.k8s.io# 3.创建存储类
[root@master 18]# vim storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:name: nfs-clientnamespace: nfs-storageclass
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner
parameters:pathPattern: ${.PVC.namespace}/${.PVC.name}onDelete: delete# 4.创建相关对象
[root@master 18]# kubectl apply -f ../18/
deployment.apps/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
storageclass.storage.k8s.io/nfs-client created
[root@master 18]# kubectl get pod -n nfs-storageclass
NAME READY STATUS RESTARTS AGE
nfs-client-provisioner-cdf554dd7-rw4lv 1/1 Running 0 45s
[root@master 18]# kubectl get pod -n nfs-storageclass -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nfs-client-provisioner-cdf554dd7-rw4lv 1/1 Running 0 72s 10.224.104.30 node2 <none> <none>
# 分布在了node2上
- 测试 Pod
# 动态 PVC 清单文件如下:
[root@master 7]# vim 19.pod.yaml
[root@master 7]# cat 19.pod.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:name: test-claimannotations:
spec:accessModes:- ReadWriteManyresources:requests:storage: 1MistorageClassName: nfs-client
---
apiVersion: v1
kind: Pod
metadata:name: test-pod
spec:containers:- name: test-podimage: harbor.registry.com/library/myapp:1.0volumeMounts:- name: nfs-pvcmountPath: "/usr/local/nginx/html"restartPolicy: Nevervolumes:- name: nfs-pvcpersistentVolumeClaim:claimName: test-claim
[root@master 7]# kubectl get pod
NAME READY STATUS RESTARTS AGE
pod-demo 1/1 Running 1 (21m ago) 81m
[root@master 7]# kubectl delete pod pod-demo
pod "pod-demo" deleted[root@master 7]# kubectl create -f 19.pod.yaml
persistentvolumeclaim/test-claim created
pod/test-pod created
[root@master 7]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.0.0.1 <none> 443/TCP 6h2m
[root@master 7]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
test-claim Bound pvc-db8056be-a623-4032-8e65-7cc2bfda924c 1Mi RWX nfs-client <unset> 29s[root@master 7]# kubectl exec -it test-pod -- /bin/sh
/ # cd /usr/local/nginx/html/
/usr/local/nginx/html # ls
hostname.html
/usr/local/nginx/html # cat hostname.html
test-pod[root@master 7]# tree /nfsdata/share/
/nfsdata/share/
└── default└── test-claim└── hostname.html2 directories, 1 file
[root@master 7]# cat /nfsdata/share/default/test-claim/hostname.html
test-pod
自动补全
- 先在系统中安装 bash-completion 软件
- 修改当前用户家目录下的隐藏文件 .bashrc
添加如下内容:source <(kubectl completion bash)
- 添加好后保存退出,然后执行如下命令让配置生效
source .bashrc
[root@master ~]# vim .bashrc
[root@master ~]# cat .bashrc
# .bashrc# Source global definitions
if [ -f /etc/bashrc ]; then. /etc/bashrc
fi# User specific environment
if ! [[ "$PATH" =~ "$HOME/.local/bin:$HOME/bin:" ]]
thenPATH="$HOME/.local/bin:$HOME/bin:$PATH"
fi
export PATH# Uncomment the following line if you don't like systemctl's auto-paging feature:
# export SYSTEMD_PAGER=# User specific aliases and functionsalias rm='rm -i'
alias cp='cp -i'
alias mv='mv -i'source <(kubectl completion bash)[root@master ~]# source .bashrc