内容:

  1. Kubernetes Dashboard

  2. Kubernetes 动态卷供应

  3. Kubernetes StatefulSet

Kubernetes 集群 Dashboard

kuboard(自学)

kuboard 介绍

Kuboard 是一款专为 Kubernetes 设计的免费管理界面,兼容 Kubernetes 版本 1.13 及以上。Kuboard 每周发布一个 beta 版本,最长每月发布一个正式版本,经过两年的不断迭代和优化,已经具备多集群管理、权限管理、监控套件、日志套件等丰富的功能,并且有 1000+ 的企业将 Kuboard 应用于其生产环境。Kuboard 自 2019年8月发布第一个版本以来,得到了众多用户的认可,目前已经获得了 10000+ GitHub Star。

Kuboard 的定位和 Dashboard 是相似的,主要的区别 在于:

  • Kuboard 关注微服务参考架构的视角对界面进行组织,参考 Kuboard 简介

  • Kuboard 中,不需要手工编写 YAML 文件,进一步降低 K8S 使用难度,提高便捷性。

  • Kuboard 可以导出整个微服务架构的部署信息,并在新的名称空间/集群导入配置信息。

  • Kuboard 的一个发展方向是,提供内建的 监控套件(目前的全局监控套件成熟度比较高)。

kuboard 特点

相较于 Kubernetes Dashboard 等其他 Kubernetes 管理界面,Kuboard 的主要特点有:

  • 多种认证方式:Kuboard 可以使用内建用户库、gitlab / github 单点登录或者 LDAP 用户库进行认证,避免管理员将 ServiceAccount 的 Token 分发给普通用户而造成的麻烦。使用内建用户库时,管理员可以配置用户的密码策略、密码过期时间等安全设置。

  • 多集群管理:管理员可以将多个 Kubernetes 集群导入到 Kuboard 中,并且通过权限控制,将不同集群/名称空间的权限分配给指定的用户或用户组。

  • 微服务分层展示:在 Kuboard 的名称空间概要页中,以经典的微服务分层方式将工作负载划分到不同的分层,更加直观地展示微服务架构的结构,并且可以为每一个名称空间自定义名称空间布局。

  • 工作负载的直观展示:Kuboard 中将 Deployment 的历史版本、所属的 Pod 列表、Pod 的关联事件、容器信息合理地组织在同一个页面中,可以帮助用户最快速的诊断问题和执行各种相关操作。

  • 工作负载编辑:Kuboard 提供了图形化的工作负载编辑界面,用户无需陷入繁琐的 YAML 文件细节中,即可轻松完成对容器的编排任务。支持的 Kubernetes 对象类型包括:Node、Namespace、Deployment、StatefulSet、DaemonSet、Secret、ConfigMap、Service、Ingress、StorageClass、PersistentVolumeClaim、LimitRange、ResourceQuota、ServiceAccount、Role、RoleBinding、ClusterRole、ClusterRoleBinding、CustomResourceDefinition、CustomResource 等各类常用 Kubernetes 对象,

  • 存储类型支持:在 Kuboard 中,可以方便地对接 NFS、CephFS 等常用存储类型,并且支持对 CephFS 类型的存储卷声明执行扩容和快照操作。

  • 丰富的互操作性:可以提供许多通常只在 kubectl 命令行界面中才提供的互操作手段,例如:

    • Top Nodes / Top Pods

  • 容器的日志、终端

    • 容器的文件浏览器(支持从容器中下载文件、上传文件到容器)

    • KuboardProxy(在浏览器中就可以提供 kubectl proxy 的功能)

  • 套件扩展:Kuboard 提供了必要的套件库,使得用户可以根据自己的需要扩展集群的管理能力。当前提供的套件有:

    • 资源层监控套件,基于 Prometheus / Grafana 提供 K8S 集群的监控能力,可以监控集群、节点、工作负载、容器组等各个级别对象的 CPU、内存、网络、磁盘等资源的使用情况;

  • 日志聚合套件,基于 Grafana / Loki / Promtail 实现日志聚合;

    • 存储卷浏览器,查看和操作存储卷中的内容;

  • 告警配置:可以通过界面直接配置资源层监控套件发送告警消息:

    • 支持邮件、微信发送告警消息;

  • 支持告警路由配置;

    • 支持告警规则配置等;

  • 操作审计:Kuboard 支持操作审计的功能:

    • 审计用户通过 Kuboard 界面和 Kuboard API 执行的操作;

    • 自定义审计规则;

在线演示

在线演示环境中,您具备 只读 权限,只能体验 Kuboard 的一部分功能。

地址:https://demo.kuboard.cn 用 户:demo 密 码:demo123

kuboard 版本

Kuboard v2.x 版本说明

Kuboard v2.x 支持 Kubernetes 单集群管理。

版本支持:

Kuboard v3.x 版本说明

Kuboard v3.x 支持 Kubernetes 多集群管理。如果您从 Kuboard v1.0.x 或者 Kuboard v2.0.x 升级到 Kuboard,请注意:

  • 您可以同时使用 Kuboard v3.x 和 Kuboard v2.0.x;

  • Kuboard v3.x 支持 amd64 (x86) 架构和 arm68 (armv8) 架构的 CPU;

版本支持:

kuboard 安装和配置

本次实验环境kubernetes版本是1.32.0,使用kuboard v3.x版本。

安装方式
  1. 以 独立容器 方式运行

  2. 在 Kubernetes 中运行

  3. 以 static pod 方式运行

基于如下原因,建议您以 独立容器 方式 Kuboard:

  • 结构更清晰(Kuboard 作为多个集群的管理界面应该独立于任何集群之外);

  • 登录 Kuboard 时使用不同的认证方式;

  • 问题排查更简单;

独立容器 方式运行
安装计划

在正式安装 kuboard v3 之前,需做好一个简单的部署计划的设计,在本例中,各组件之间的连接方式,如下图所示:

  • 假设用户通过 http://外网IP:80 访问 Kuboard v3;

  • 安装在 Kubernetes 中的 Kuboard Agent 通过 内网IP 访问 Kuboard 的 Web 服务端口 80 和 Kuboard Agent Server 端口 10081。

安装过程
# 创建目录
root@master30:~# mkdir /kuboard-data

# 创建容器
root@master30:~# nerdctl run -d --name=kuboard \
-p 80:80/tcp \
-p 10081:10081/tcp \
-e KUBOARD_ENDPOINT="http://10.1.8.30:80" \
-e KUBOARD_AGENT_SERVER_TCP_PORT="10081" \
-v /kuboard-data:/data \
eipwork/kuboard:v3

# 10.1.8.30是物理机的ip
卸载
root@master30:~# nerdctl container rm kuboard --force
root@master30:~# rm -fr /kuboard-data
static pod 方式运行(自学)
安装
# 在master节点上执行
root@master30:~# curl -fsSL https://addons.kuboard.cn/kuboard/kuboard-static-pod.sh -o kuboard.sh
root@master30:~# sh kuboard.sh
current ip address is 10.1.8.30
create file /root/kuboard-sa.yaml

kubectl apply -f /root/kuboard-sa.yaml
namespace/kuboard created
serviceaccount/kuboard-admin created
clusterrolebinding.rbac.authorization.k8s.io/kuboard-admin-crb created
serviceaccount/kuboard-viewer created
clusterrolebinding.rbac.authorization.k8s.io/kuboard-viewer-crb created

create file /etc/kubernetes/manifests/kuboard.yaml

restart kubelet

检查状态 待 kuboard-v3-master30.laoma.cloud 的容器组变为 Running 状态后,则安装成功,可以通过 http://10.1.8.30 访问 kuboard 界面

No resources found in kuboard namespace.

# 验证
root@master30:~# kubectl get all -n kuboard 
NAME                                READY   STATUS    RESTARTS   AGE
pod/kuboard-v3-laoma30.redhat.fun   1/1     Running   0          45s
卸载
# 删除静态文件
root@master30:~# rm -f /etc/kubernetes/manifests/kuboard.yaml

# 删除数据目录
root@master30:~# rm -fr /usr/share/kuboard

# 删除命名空间
root@master30:~# kubectl delete ns kuboard

访问过程

在浏览器输入 http://your-host-ip:80 ,访问 Kuboard v3.x 的界面。

登录信息:

  • 用户名: admin

  • 密 码: Kuboard123

添加集群

这里选择:.kubeconfig

导入凭据文件(复制~/.kube/config内容到红色方框中)

导入后效果:

选择访问集群时所使用的身份:使用 ServiceAccount kuboard-admin

参考资料

安装手册:安装 Kubernetes 多集群管理工具 - Kuboard v3 | Kuboard

Kubernetes Dashboard

1. 定位

Kubernetes Dashboard 是 K8s 官方原生 Web 可视化面板,项目开源地址:https://github.com/kubernetes/dashboard/

2. 核心功能通俗总结

  1. 可视化部署容器应用(不用敲 kubectl 命令)

  2. 故障排查:查看 Pod 日志、容器崩溃、资源报错

  3. 全集群资源管理:Deployment、Pod、Service、Job、DaemonSet、ConfigMap 等增删改查

  4. 集群监控:节点、Pod 资源指标(CPU / 内存)

  5. 运维操作:扩缩容 Deployment、滚动更新、重启 Pod、灰度发布

  6. 统一展示集群所有报错事件

3. 组件构成(部署后两个核心 Pod)

  • kubernetes-dashboard:主面板 Web 服务,提供 HTTPS 登录界面

  • dashboard-metrics-scraper:指标采集器,给面板提供 CPU、内存监控数据

# 下载资源yaml文件
[root@master30 ~ 19:55:12]# wget https://raw.githubusercontent.com/kubernetes/dashboard/v2.6.1/aio/deploy/recommended.yaml


# 查看 yaml 内镜像版本
[root@master30 ~ 20:50:06]# grep image: recommended.yaml 
          image: kubernetesui/dashboard:v2.6.1
          image: kubernetesui/metrics-scraper:v1.0.8



#- 作用:从 github 拉取官方一键部署清单recommended.yaml
#- 清单内置:命名空间、ServiceAccount、ClusterRole、ClusterRoleBinding、Deployment、Service、Metrics 采集器全套资源,开箱即用
#- 版本:v2.6.1,aio=all in one 一体化部署文件

# 执行部署:执行后自动创建独立命名空间kubernetes-dashboard,隔离面板相关资源
[root@master30 ~ 20:50:38]# kubectl  apply -f recommended.yaml 
namespace/kubernetes-dashboard created
serviceaccount/kubernetes-dashboard created
service/kubernetes-dashboard created
secret/kubernetes-dashboard-certs created
secret/kubernetes-dashboard-csrf created
secret/kubernetes-dashboard-key-holder created
configmap/kubernetes-dashboard-settings created
role.rbac.authorization.k8s.io/kubernetes-dashboard created
clusterrole.rbac.authorization.k8s.io/kubernetes-dashboard created
rolebinding.rbac.authorization.k8s.io/kubernetes-dashboard created
clusterrolebinding.rbac.authorization.k8s.io/kubernetes-dashboard created
deployment.apps/kubernetes-dashboard created
service/dashboard-metrics-scraper created
deployment.apps/dashboard-metrics-scraper created

验证

#验证命名空间,查看是否创建了命名空间kubenetes-dashboard
[root@master30 ~ 20:51:31]# kubectl  get  ns
NAME                   STATUS   AGE
default                Active   6d23h
kube-node-lease        Active   6d23h
kube-public            Active   6d23h
kube-system            Active   6d23h
kubernetes-dashboard   Active   47s
zy                     Active   6d23h

#切换到 dashboard 命名空间
[root@master30 ~ 20:52:18]# kubectl  config  set-context --current  --namespace=kubernetes-dashboard
Context "kubernetes-admin@kubernetes" modified.

[root@master30 ~ 20:54:07]# kubectl  get deployments.apps 
NAME                        READY   UP-TO-DATE   AVAILABLE   AGE
dashboard-metrics-scraper   1/1     1            1           3m10s
kubernetes-dashboard        1/1     1            1           3m10s



#输出两个 Deployment:
- kubernetes-dashboard:面板主程序
- dashboard-metrics-scraper:指标采集服务
字段说明:
- READY 1/1:每个 Deployment 正常运行 1 个 Pod,就绪
- UP-TO-DATE:副本版本同步完成
- AVAILABLE:服务正常可用


#查看内部 Service
[root@master30 ~ 20:54:43]# kubectl  get service
NAME                        TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
dashboard-metrics-scraper   ClusterIP   10.102.191.211   <none>        8000/TCP   4m12s
kubernetes-dashboard        ClusterIP   10.96.43.158     <none>        443/TCP    4m12s


#两个 ClusterIP 类型 Service:
1. kubernetes-dashboard-web:面板主服务,内部端口 8000(底层 HTTPS)
2. kubernetes-dashboard-metrics-scraper:指标服务,端口 8000
- 默认ClusterIP:仅集群内部 Pod 能访问,外部机器无法打开页面


#查看服务账号 ServiceAccount
[root@master30 ~ 20:55:23]# kubectl  get serviceaccounts 
NAME                   SECRETS   AGE
default                0         4m45s
kubernetes-dashboard   0         4m45s


# ServiceAccount(SA):K8s 内部 Pod 身份凭证,Pod 运行时用 SA 申请 API Server 权限
# kubernetes-dashboard:面板专用账号,后续用来绑定权限

#查看 dashboard SA 详情
[root@master30 ~ 20:55:56]# kubectl  describe  serviceaccounts kubernetes-dashboard 
Name:                kubernetes-dashboard
Namespace:           kubernetes-dashboard
Labels:              k8s-app=kubernetes-dashboard
Annotations:         <none>
Image pull secrets:  <none>
Mountable secrets:   <none>
Tokens:              <none>     #- Tokens: <none>:默认没有生成登录 token,这就是登录面板需要手动创建管理员 token的原因
Events:              <none>




# 创建了clusterrolebindings/kubernetes-dashboard
[root@master30 ~ 20:58:02]# kubectl  get clusterrolebindings.rbac.authorization.k8s.io   |grep dashboard
kubernetes-dashboard                                            ClusterRole/kubernetes-dashboard                          


# sa/kubernetes-dashboard具有ClusterRole/kubernetes-dashboard
[root@master30 ~ 20:58:04]# kubectl  describe  clusterrolebindings.rbac.authorization.k8s.io  kubernetes-dashboard 
Name:         kubernetes-dashboard
Labels:       <none>
Annotations:  <none>
Role:
  Kind:  ClusterRole
  Name:  kubernetes-dashboard
Subjects:
  Kind            Name                  Namespace
  ----            ----                  ---------
  ServiceAccount  kubernetes-dashboard  kubernetes-dashboard

#核心两段:
#1. Role 绑定:ClusterRole/kubernetes-dashboard(权限规则集合)
#2. Subjects 主体:绑定对象是ServiceAccount: kubernetes-dashboard
#含义:给 dashboard 这个服务账号分配 ClusterRole 的权限




# 查看 dashboard 默认权限
[root@master30 ~ 20:58:47]# kubectl  describe  clusterroles kubernetes-dashboard 
Name:         kubernetes-dashboard
Labels:       k8s-app=kubernetes-dashboard
Annotations:  <none>
PolicyRule:
  Resources             Non-Resource URLs  Resource Names  Verbs
  ---------             -----------------  --------------  -----
  nodes.metrics.k8s.io  []                 []              [get list watch]
  pods.metrics.k8s.io   []                 []              [get list watch]



#关键重点(必懂)
#默认权限只能查看节点、Pod 监控指标,没有管理集群资源的权限:不能创建 Deployment、查看 Pod、删除资源,只能看监控。所以生产 / 实验环境必须新建管理员 ClusterRoleBinding,绑定 dashboard SA 才能完整操作集群。

#查看 Deployment 使用的服务账号
[root@master30 ~ 21:01:06]# kubectl  get deployments.apps  kubernetes-dashboard  -o yaml | grep ' serviceAccount'
      serviceAccount: kubernetes-dashboard
      serviceAccountName: kubernetes-dashboard

#含义:dashboard Pod 运行时,身份是kubernetes-dashboard这个 SA,权限完全继承上面 ClusterRole 的规则。

修改 service/kubernetes-dashboard 类型为 NodePort(节点端口暴露),从外部访问。

三种 Service 类型区别:

  1. ClusterIP:仅集群内部访问(默认)

  2. NodePort:每个节点开放一个宿主机端口,外部机器通过节点IP:端口访问

  3. LoadBalancer:云厂商负载均衡

[root@master30 ~ 21:01:13]# kubectl  edit  svc kubernetes-dashboard 
service/kubernetes-dashboard edited
spec:
......
  type: NodePort       # 原本默认ClusterIP,改成NodePort

#查看修改后的 Service
[root@master30 ~ 21:03:37]# kubectl  get svc kubernetes-dashboard 
NAME                   TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)         AGE
kubernetes-dashboard   NodePort   10.96.43.158   <none>        443:31042/TCP   12m


#端口解读:
#- 443:集群内部 Service 端口(HTTPS)
#- 30518:宿主机 NodePort 端口(范围 30000-32767 随机分配)
#访问地址:https://节点IP:30518,示例:https://10.1.8.30:31042
#注意:Dashboard 强制 HTTPS,不能用 http 访问,浏览器会提示证书不安全,直接忽略继续即可。

外部访问方案 2:Ingress 域名方式(生产推荐)

使用ingress配置访问,参考:

apiVersion: networking.k8s.io/v1  # Ingress稳定版API
kind: Ingress                     # 资源类型:Ingress ingress网关
metadata:
name: ingress-dashboard         # ingress名称
namespace: kubernetes-dashboard # 和dashboard同命名空间
annotations:
 # 1. SSL透传:Ingress不终止HTTPS,原始加密流量直接转发给dashboard
 nginx.ingress.kubernetes.io/ssl-passthrough: "true"
 # 2. 后端协议是HTTPS,不是http
 nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
spec:
ingressClassName: nginx         # 使用nginx-ingress控制器
rules:
    - host: dashboard.laoma.cloud # 自定义访问域名
      http:
        paths:
        - path: /                 # 匹配域名下所有路径
          pathType: Prefix        # 前缀匹配
          backend:
            service: 
              name: kubernetes-dashboard # 后端service名称
              port: 
                number: 443              # service内部HTTPS端口

客户端配置dashboard.laoma.cloud域名到ingress服务地址,实验环境是10.1.8.40

后续直接访问https://dashboard.laoma.cloud

访问 Dashboard

访问 https://10.1.8.30:31042(注意一定要手动加上https://)

不加的后果:(原因:你浏览器地址栏只输了 http://10.1.8.30:31042(明文 HTTP 访问),但 Dashboard 后端强制只支持 HTTPS 加密访问,拒绝明文 HTTP 请求,直接抛出这个提示页面。)

新版本
  1. 新版 Dashboard 不再支持 kubeconfig 证书登录,只能 Bearer Token 令牌登录

  2. Dashboard 安装自带的默认账号权限极低,只能看自身命名空间资源,没法操作集群所有节点、Pod、Deployment、存储、权限等,日常运维不够用。

  3. 解决思路:

    1. 新建一个专用服务账号 ServiceAccount(集群内置身份账号)

    2. 绑定集群最高管理员权限 cluster-admin

    3. 用这个账号生成临时 Token,拿 Token 登录 Dashboard 获得全集群操作权限

#文件分两块,用 --- 分割,一次创建两种资源:ServiceAccount + ClusterRoleBinding
[root@master30 ~ 21:03:58]# cat > kubernetes-dashboard-admin.yaml <<'EOF'
# 第一段:创建服务账号
apiVersion: v1
kind: ServiceAccount
metadata:
  name: kubernetes-dashboard-admin  # 账号名称
  namespace: kubernetes-dashboard   # 必须放在dashboard所在命名空间
------------------------------------------------------------------
# 第二段:全局权限绑定
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding         #集群级绑定,作用整个集群所有命名空间;如果是 RoleBinding 只能限制单个命名空间。
metadata:
  name: kubernetes-dashboard-admin
roleRef:        #roleRef:绑定哪个权限角色
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin  # 集群内置最高权限角色,拥有所有增删改查权限
subjects:            #subjects:授权给谁:类型:ServiceAccount;账号名 + 所在命名空间,对应下面创建的账号
- kind: ServiceAccount
  name: kubernetes-dashboard-admin
  namespace: kubernetes-dashboard
EOF
[root@master30 ~ 21:19:05]# kubectl  apply  -f kubernetes-dashboard-admin.yaml 
serviceaccount/kubernetes-dashboard-admin created
clusterrolebinding.rbac.authorization.k8s.io/kubernetes-dashboard-admin created
#输出结果解析:
#- serviceaccount/xxx created 账号创建成功
#- clusterrolebinding/xxx created 权限绑定成功

#查看是否创建了 ServiceAccount(账号本体)
[root@master30 ~ 09:17:48]# kubectl get sa -n kubernetes-dashboard
NAME                         SECRETS   AGE     
default                      0         12h        #每个命名空间天生自带
kubernetes-dashboard         0         12h        #安装 dashboard 自动生成,权限极低,只能访问本命名空间资源,无法查看集群节点、其他命名空间业务资源,生产 / 日常运维基本不用它登录。
kubernetes-dashboard-admin   0         12h        #你手动创建的,绑定了 cluster-admin 超级权限,用来生成 token 登录 Dashboard 管理全集群。

#解释:
#get sa:sa 是 ServiceAccount 简写,查询服务账号列表
#-n kubernetes-dashboard:指定 dashboard 命名空间
#输出里出现 kubernetes-dashboard-admin 代表账号创建成功。



#kubectl describe 资源类型 资源名 [-n 命名空间](clusterrolebinding 是集群级资源,不需要加 -n)
[root@master30 ~ 09:47:42]# kubectl describe clusterrolebinding kubernetes-dashboard-admin
Name:         kubernetes-dashboard-admin
Labels:       <none>
Annotations:  <none>
Role:
  Kind:  ClusterRole
  Name:  cluster-admin
Subjects:
  Kind            Name                        Namespace
  ----            ----                        ---------
  ServiceAccount  kubernetes-dashboard-admin  kubernetes-dashboard


#解释:
#describe:查看资源详细信息
#clusterrolebinding:集群级权限绑定资源
#重点看两块内容:
#1.RoleRef 部分 Name: cluster-admin(确认绑的是超级管理员)
#2.Subjects 部分名称、命名空间和我们创建的 SA 一致。




# 创建 Bearer Token:整条命令作用:给指定命名空间下的指定服务账号,生成一段临时 Bearer 登录令牌,用来登录 Kubernetes Dashboard。
[root@master30 ~ 21:19:52]# kubectl  -n kubernetes-dashboard  create token kubernetes-dashboard-admin 
eyJhbGciOiJSUzI1NiIsImtpZCI6Ik1oT2h1X3JrbkJxMjJHcmFmUXVIUk4zUl9xNTlBZnJqbThvMzhSV3FyVlEifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzgzODY2MDI1LCJpYXQiOjE3ODM4NjI0MjUsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiNDA2M2UxM2MtYjFkOS00MTk4LTkyNDctZDkzMmE3NjA0YmYwIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsInNlcnZpY2VhY2NvdW50Ijp7Im5hbWUiOiJrdWJlcm5ldGVzLWRhc2hib2FyZC1hZG1pbiIsInVpZCI6IjhmY2NmMGE0LTgxZTUtNGY0OC1iNTM5LTA3NDc0OTA5YzI5ZCJ9fSwibmJmIjoxNzgzODYyNDI1LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZXJuZXRlcy1kYXNoYm9hcmQ6a3ViZXJuZXRlcy1kYXNoYm9hcmQtYWRtaW4ifQ.ZtbvyTfw3KdSJMsIr5e0kkMp3z5oau0I58OeGWNyYDx0vvQuzoehTkVncv3wZ6lGYJ48KS9YAZWOLN-GVwnYhz0eGnuBPsIYaOcgWzS9V3U7o_4CLlhR_QokMfUsKasODHQzeHettA_IpfNL9kKYJyrLUIBu-m0mRVeMMXvUNta7q7m2k-82R55_9j0ZsraxqzAfisov-RcUUrMVfprH5WOPcm2rzgrY0PMdg0kmP_WshJY5AFH4y5x5XHgQ0KUnCR7i9QhMWcyyywuM-VznX4eHdQip7IHnt9PaWKxtewPe_iC6R8-ue8fM-VDYS52M3J5MxZFpIRU8xFSj3ERnDA


#重点说明:
#1. -n kubernetes-dashboard 指定账号所在命名空间
#2. create token:k8s 1.24+ 新版命令,专门生成 ServiceAccount 临时令牌
#3. 输出一长串字符串就是 Bearer Token,复制粘贴到 Dashboard 登录框即可进入页面
#4. 有效期默认 24 小时,到期令牌直接失效,需要重新执行这条命令生成新令牌登录
#5. kubernetes-dashboard-admin:目标资源名称:我们前面 YAML 创建的 ServiceAccount(服务账号)名字。命令会找到这个账号,基于账号身份签发认证令牌。


1. 集群自带的 Dashboard 默认账号是什么?
你安装 Dashboard 时,集群会自动创建一个默认的 dashboard serviceAccount。
但是!官方为了安全,给这个默认账号设置了【极低只读权限】
只能看 kubernetes-dashboard 这一个命名空间
看不了业务 Pod、看不了节点、改不了任何资源、删不了、部署不了
99% 的运维操作都会报错 forbidden

2. 为什么要特意加 kubernetes-dashboard-admin 这个账号?
(1)不冲突
不改动系统默认账号,新建一个独立账号,安全、不破坏集群原生配置。
(2)专门提权
新账号是空权限的,我们手动给它绑定了 cluster-admin 超级权限
让 Dashboard 拥有整个集群所有命名空间的读写删改全部权限。
(3)适配新版登录规则
新版 Dashboard 只能 Token 登录,自定义管理员账号,才能生成有权限的 Token。
如果你用默认账号生成 Token,登录进去也是残废权限界面。

3. 一句话总结
默认账号:官方锁死最低权限 → 没法干活
kubernetes-dashboard-admin:我们自建的超级管理员账号 → 全盘可控,正常运维

4. 额外关键点(你疑惑的重点)
你 yaml 里的名字可以随便改,不一定要叫 kubernetes-dashboard-admin
你叫 dashboard-root、k8s-admin 都行,只是一个自定义账号名
关键不是名字,是后面那行 ClusterRoleBinding 绑定了 cluster-admin 权限

输入token登录进来:

旧版本

之前的一些版本,默认已经为sa账户kubernetes-dashboard 创建了token。

获取token过程如下:

# sa/kubernetes-dashboard使用的token是kubernetes-dashboard-token-hm26j
root@master30:~# kubectl describe sa kubernetes-dashboard 
Name:                kubernetes-dashboard
Namespace:           kubernetes-dashboard
Labels:              k8s-app=kubernetes-dashboard
Annotations:         <none>
Image pull secrets:  <none>
Mountable secrets:   kubernetes-dashboard-token-hm26j
Tokens:              kubernetes-dashboard-token-hm26j
Events:              <none>

# 获取token内容
root@master30:~# kubectl describe secrets kubernetes-dashboard-token-hm26j 
Name:         kubernetes-dashboard-token-hm26j
Namespace:    kubernetes-dashboard
Labels:       <none>
Annotations:  kubernetes.io/service-account.name: kubernetes-dashboard
              kubernetes.io/service-account.uid: d4b7d7ec-a76b-46c5-9dc5-629217cd07ba

Type:  kubernetes.io/service-account-token

Data
====
token:      eyJhbGciOiJSUzI1NiIsImtpZCI6IkNVVmdmMGNUckVGZFFPUVJ0dTkxZGtEc2hoOTlGZ2ktWjI2RmtnRWI5dFEifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJrdWJlcm5ldGVzLWRhc2hib2FyZC10b2tlbi1obTI2aiIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6ImQ0YjdkN2VjLWE3NmItNDZjNS05ZGM1LTYyOTIxN2NkMDdiYSIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDprdWJlcm5ldGVzLWRhc2hib2FyZDprdWJlcm5ldGVzLWRhc2hib2FyZCJ9.nBy6Hpv3a5jM-LWzqi95i2jkdLT8bx-jnTOuM5eroxGjpUq8OFOIFgky-0LUlJ-4dlFLdh3FZ2RV8SwQwQoctaay2p-xGfsB1IWNJkySYlGDy9-iMnH6Lc3J_Kb085rgqvY1nLPbSIhBlkueHUFsG9ii52gnGZf2RqroNBtsl_t2ofWfNDJRE7dm51B-WM_rSnNgTChFygRSoMXpzP7jG64dTrvQyxqHKRwr3zMNeYUlleHECUx0Z525hkdFv5B-gPV6ev_2cRS0BR2TM0V2qr1TKqM6G56E7CDkHiu6S2B1mrLdz5E9IKQCPFvxGVM4O4-bia4MHdmJo_INFD57pA
ca.crt:     1066 bytes
namespace:  20 bytes

登录页面

使用上面的 token 登录

选择Pods

分析 SA 使用案例

整体实验背景:

官方recommended.yaml部署 Dashboard 时,会自动创建专属命名空间级 ServiceAccount(SA):kubernetes-dashboard,配套同命名空间内的RoleRoleBinding,仅授予面板自身运行必需的最小权限无任何集群业务资源查看 / 管理权限。 设计目的:遵循最小权限安全原则,防止 Dashboard 被入侵后,攻击者拿到集群最高权限。

Kubernetes Dashboard 默认部署时,只配置了最低权限的 RBAC。接下来分析默认的权限配置。

# Kubernetes Dashboard 以deployment形式运行
[root@master30 ~ 21:20:25]# kubectl  get deployments.apps 
NAME                        READY   UP-TO-DATE   AVAILABLE   AGE
dashboard-metrics-scraper   1/1     1            1           75m
kubernetes-dashboard        1/1     1            1           75m
#- 两个 Deployment 分工:
  #1. kubernetes-dashboard:Web 面板主程序 Pod
  #2. dashboard-metrics-scraper:指标采集 Pod,给面板提供 CPU / 内存监控数据


# 查看 Deployment 绑定的 ServiceAccount
[root@master30 ~ 22:06:53]# kubectl  get deployments.apps  kubernetes-dashboard  -o yaml | grep serviceAc
---------------
      serviceAccount: kubernetes-dashboard
      serviceAccountName: kubernetes-dashboard
#核心含义
#kubernetes-dashboard创建的所有 Pod,运行身份为命名空间内的 ServiceAccount:kubernetes-dashboard。Pod 内部会自动挂载该 SA 的认证 Token,所有调用集群 API 的请求,都会携带这个 Token 鉴权。


# 查看权限绑定关系(RoleBinding)
[root@master30 ~ 22:06:56]# kubectl  get rolebindings.rbac.authorization.k8s.io 
NAME                   ROLE                        AGE
kubernetes-dashboard   Role/kubernetes-dashboard   83m

#逻辑关系
#RoleBinding = 权限绑定桥梁:
#- 绑定主体:ServiceAccount kubernetes-dashboard
#- 绑定权限模板:同命名空间内Role/kubernetes-dashboard
#- 生效范围:仅 kubernetes-dashboard 这一个命名空间,跨命名空间资源无任何权限
#重点区分:这里是Role(命名空间权限),不是ClusterRole(全局集群权限),天然无法访问集群其他 ns 资源。



# 查看默认 Role 的完整权限(核心安全分析)
[root@master30 ~ 22:15:08]# kubectl  describe  role kubernetes-dashboard 
Name:         kubernetes-dashboard
Labels:       k8s-app=kubernetes-dashboard
Annotations:  <none>
PolicyRule:
  Resources       Non-Resource URLs  Resource Names                     Verbs
  ---------       -----------------  --------------                     -----
  secrets         []                 [kubernetes-dashboard-certs]       [get update delete]
  secrets         []                 [kubernetes-dashboard-csrf]        [get update delete]
  secrets         []                 [kubernetes-dashboard-key-holder]  [get update delete]
  configmaps      []                 [kubernetes-dashboard-settings]    [get update]
  services/proxy  []                 [dashboard-metrics-scraper]        [get]
  services/proxy  []                 [heapster]                         [get]
  services/proxy  []                 [http:dashboard-metrics-scraper]   [get]
  services/proxy  []                 [http:heapster:]                   [get]
  services/proxy  []                 [https:heapster:]                  [get]
  services        []                 [dashboard-metrics-scraper]        [proxy]
  services        []                 [heapster]                         [proxy]

  
#关键结论(企业安全重点)
#1. 该 Role完全没有Pod、Deployment、Service、PVC、Node、Namespace 等业务资源的读写 / 查看权限;
#2. 权限全部限制在kubernetes-dashboard命名空间内,且仅能操作面板专属资源;
#3. 用这个默认 SA 登录 Dashboard 后,看不到集群任何业务 Pod、服务、数据库,只能打开空白面板。  
  
# 生成 SA 登录 Token
#注意和上面的 kubectl  -n kubernetes-dashboard  create token kubernetes-dashboard-admin 这条命令区分,一个是使用自己创建的kubernetes-dashboard-admin服务账号,绑定了cluster-admin超级管理员权限,登录面板能操作集群所有命名空间、节点、资源,无权限拦截;和Dashboard 安装时系统自带的默认服务账号kubernetes-dashboard,仅拥有kubernetes-dashboard命名空间内少量只读权限,查看节点、其他业务命名空间资源会报forbidden权限不足。
[root@master30 ~ 22:15:26]# kubectl   -n kubernetes-dashboard  create  token kubernetes-dashboard
eyJhbGciOiJSUzI1NiIsImtpZCI6Ik1oT2h1X3JrbkJxMjJHcmFmUXVIUk4zUl9xNTlBZnJqbThvMzhSV3FyVlEifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzgzODY5MzU5LCJpYXQiOjE3ODM4NjU3NTksImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiMWJhZDFjZWEtNzdiMC00YmYyLTg4NGUtNWE0MjBjMzQ3N2UzIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsInNlcnZpY2VhY2NvdW50Ijp7Im5hbWUiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsInVpZCI6IjExM2E4Y2I0LTYzZWYtNDFmMC04NWNmLThiMGUzM2VjNjI5MyJ9fSwibmJmIjoxNzgzODY1NzU5LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZXJuZXRlcy1kYXNoYm9hcmQ6a3ViZXJuZXRlcy1kYXNoYm9hcmQifQ.CmKoswvcQm5JrO9z2Xpf8mWhWwZk3LRIbUcPuV32tigimseovOVBdoMufjJTpURXnc7RHoLsHjteiBCV9UGxkIQsCOCgnNDKd0oNeDMMuWExOUB3uiOZyFvF_ZIsDWWnO07F0nz9-ZwoS4fTbdxCRwyUgNwQCEaXFYOp2jAukNBzmBquA6zl20CuamPOa5gyGovO5irbigXCjgyu0ufz0VS1FSzirfGX23Lvel7hoIbIaoV4Hf3ZjidsghXoDE28pyfZCCM-mlhXTN1_blGWt_Pp11OIoSEmhLM3Q1PCn3P_tA-NBEkWC2ozmPJoDU0cn1RDdvTr-sSx-Q2FVt-RaA

使用 serviceAccount: kubernetes-dashboard Token 登录

无法看到Pods。

为什么:
Kubernetes Dashboard RBAC权限对比实验解析
分两部分讲:1、为什么之前能看到资源,现在报错;2、这个实验核心想证明什么

一、先解释「前后两种登录结果差异」
情况 1:你之前能正常查看集群所有资源
你之前操作时,手动执行过授权操作:创建了 ClusterRoleBinding,把 kubernetes-dashboard 这个 SA 绑定了集群管理员权限 cluster-admin。
# 之前你加过的管理员绑定
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: dashboard-full-admin
subjects:
- kind: ServiceAccount
  name: kubernetes-dashboard
  namespace: kubernetes-dashboard
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
此时 SA 拥有全集群所有命名空间的增删改查权限,登录面板自然能看到 Pod、Service、SA、节点等全部资源。

情况 2:现在页面提示权限禁止、空白无资源
本次实验使用官方原生推荐 yaml 的默认权限配置,没有额外添加管理员 ClusterRoleBinding:
1. 权限载体是 Role(命名空间级权限,不是 ClusterRole);

2. Role 仅允许操作面板自身专属证书、配置、监控代理,没有 list ServiceAccounts、list Namespaces、list Pod 等任何资源查看权限;

3. 你用默认 SA 生成的 Token 登录,API Server 校验权限后直接拒绝你的查询请求,弹出红色forbidden禁止报错,页面显示无资源。
简单区分:
- 加了 cluster-admin 绑定 → 全权限,什么都能看
- 纯默认 yaml 不做任何授权 → 最低权限,绝大多数资源都禁止访问

-------------------------------------------------------------------------------

二、这个实验想要说明的 4 个核心知识点(企业运维 / 面试重点)
1. 遵循「最小权限安全原则」是官方默认设计
K8s 官方部署 Dashboard 时,不会给任何集群管理权限,只分配面板自身运行必须的最小权限。
企业价值:
如果 Dashboard 对外开放,一旦页面存在漏洞被攻击者拿到登录 Token,攻击者也只能操作面板自身的证书配置,无法入侵业务 Pod、数据库、节点,极大缩小安全攻击面。
2. 区分两种权限资源:Role(命名空间内) vs ClusterRole(全局集群)
- 默认配置用 Role + RoleBinding:权限锁死在kubernetes-dashboard命名空间,仅能操作面板内部专属资源;
- 想要查看全集群资源,必须手动创建 ClusterRoleBinding 绑定全局 ClusterRole,权限跨所有命名空间生效。
实验直观对比出两者权限生效范围的巨大差异。
3. ServiceAccount 的权限完全由绑定的 RBAC 规则决定
Pod 本身没有权限,一切权限来源于 SA 绑定的 Role/ClusterRole:
  (1). 同一个 SA,不加集群绑定 = 只能维持面板基础运行;
  (2). 同一个 SA,追加 cluster-admin 绑定 = 拥有集群全部操作权限;
  证明:Token 的权限 = 对应 ServiceAccount 绑定的 RBAC 权限,Token 只是身份凭证,权限由   RBAC 管控。
4. 生产环境必须手动精细化授权,不能依赖默认配置
  (1). 测试环境:临时绑定 cluster-admin 方便运维调试;
  (2). 生产环境:禁止直接绑定最高权限 cluster-admin,需要自定义只读 / 运维专用 ClusterRole,按需开放查看、修改权限;
  (3). 默认配置仅适合验证面板能否启动,完全无法用于日常集群管理。

---
三、一句话总结实验目的
通过有无管理员 RBAC 绑定两种登录效果对比,直观验证 K8s RBAC 权限控制逻辑,理解官方默认最小权限的安全设计,区分命名空间级 Role 与集群级 ClusterRole 的权限边界。

核心知识点总结:

  1. ServiceAccount 是 Pod 的内置身份,Pod 访问集群 API 必须依赖 SA 鉴权;

  2. 默认 Dashboard 仅绑定命名空间内窄权限 Role,遵循最小权限原则,安全隔离;

  3. kubectl create token生成 SA 临时登录凭证,用于网页登录鉴权;

  4. 默认 SA 无业务资源权限,必须新增 ClusterRoleBinding 授予全局权限才能管理集群。

Kubernetes 动态卷供应

动态卷介绍

之前创建存储卷(PV)都需要管理员手动操作,这类卷被称为静态卷。那么 Kubernetes 能不能 “智能” 管理卷呢?比如需要用卷时自动创建,不用时自动清理?

答案是可以的 —— 这就是动态卷供应的能力。它彻底省去了集群管理员提前手动配置存储的工作,用户只要提出存储需求,系统就会自动创建对应的存储卷。

动态卷供应流程

  1. 管理员先创建 “存储模板”(StorageClass),指定用哪个 “造卷工具”(Provisioner,制备器)来创建卷;

  2. 用户创建 “存储申请单”(PVC)时,指定要用哪个 “存储模板”;

  3. 系统收到申请后,会让模板绑定的 “造卷工具” 自动创建一个 PV,并且把 PV 和用户的 PVC 绑定,用户直接用 PVC 就行。

StorageClass

StorageClass 就像管理员定义的 “存储套餐”—— 不同套餐对应不同的存储类型、服务质量(比如读写速度)、备份策略等。Kubernetes 不关心套餐叫什么,只负责按套餐规则造卷。

  • 命名很重要:用户创建 PVC 时,要通过这个名字选择对应的存储套餐;

  • 创建后不能改:StorageClass 一旦创建,名字和参数就没法修改了,要改只能重新建。

核心配置

每个 “存储套餐” 都包含以下核心信息,用来指导系统创建 PV:

  • provisioner:造卷工具(制备器),指定用哪个工具创建 PV,必须设置。

    Kubernetes 自带一些 Provisioner,名字以 “kubernetes.io” 开头(比如 AWS EBS、Azure Disk);也可以使用第三方提供的(比如 NFS 没有内置工人,需要自己装)Provisioner,按 Kubernetes 规则运行即可。

    存储类型 是否内置 说明
    AWSElasticBlockStore ✅ 是 亚马逊云硬盘
    AzureFile/AzureDisk ✅ 是 微软云文件 / 硬盘
    Ceph RBD ✅ 是 Ceph 块存储
    Glusterfs ✅ 是 分布式文件系统
    NFS ❌ 否 网络文件系统,需装外部工人
    本地存储(Local) ❌ 否 节点本地硬盘,需装外部工人
    iSCSI/CephFS ❌ 否 需外部工人
  • reclaimPolicy:PV 回收策略,决定当绑定它的 PVC 被删除后,这块磁盘存储怎么处理。(PVC:Pod 申请存储的申请单;PV:集群真实的存储盘。流程:PVC 绑定 PV → Pod 挂载 PV 使用 → 删除 PVC → 触发 reclaimPolicy 执行对应操作。)

    • Delete(默认):PVC 删除后,PV 也自动删,对应的后端存储(比如云硬盘、NFS 目录)也会清掉;

    • Retain:PVC 删除后,PV 保留下来,后端存储的数据也不删,管理员可以手动处理。

  • volumeBindingMode:PVC 创建出来后,指定 PVC 与P V 绑定时机。

    • Immediate(立即绑定):创建 PVC 就造卷,不管 Pod 会不会用;适合存储能被所有节点访问的场景(比如云硬盘);

    • WaitForFirstConsumer(等使用者):先不造卷,等第一个用这个 PVC 的 Pod 创建后,根据 Pod 要跑的节点造卷;适合本地存储、只能被特定节点访问的存储。

  • mountOptions:指定 PV 挂载时附加的 “额外设置”,比如权限、调试模式。

    注意:如果存储不支持这些选项,造卷会失败;如果选项填错,PV 挂载会失败。

  • parameters:比如存储类型(SSD/HDD)、副本数等,不同工具参数不一样。

基础示例

 apiVersion: storage.k8s.io/v1
 kind: StorageClass
 metadata:
   name: standard  # 套餐名,用户PVC要填这个
 provisioner: kubernetes.io/aws-ebs  # 用AWS EBS的造卷工具
 parameters:
   type: gp2  # 存储类型为gp2(AWS的通用型SSD)
 reclaimPolicy: Retain  # PVC 不用了保留,不删除
 allowVolumeExpansion: true  # 允许扩容
 mountOptions:
   - debug  # 挂载时开启调试模式
 volumeBindingMode: Immediate  # 立即创建并绑定PV

部署 Local Path provisioner

一:是什么、解决什么问题:

Local Path Provisioner(简称 LPP)是 Rancher 开源、K8s 集群常用的本地磁盘动态存储插件

痛点(不用它的麻烦)

原生 hostPath / local PV 必须手动预先创建 PV,还要手动写节点亲和,PVC 不能自动生成 PV,运维繁琐。

LPP 核心能力
  1. 动态供给 PV:只需要创建 PVC,插件自动在对应节点生成本地目录 + 自动创建 PV,不用手动写 PV 清单;

  2. 自动调度约束:WaitForFirstConsumer 模式,先等 Pod 调度到某个节点,再在该节点创建存储目录,不会出现 Pod 和存储跨节点

  3. 统一管理节点本地磁盘目录,自动创建、自动清理目录;

  4. 性能高:走本地硬盘,无网络存储延迟,适合测试、轻量单机 / 小规模集群。

缺点(必须清楚)

存储绑定节点:Pod 只能跑在创建 PV 的节点;节点宕机 / 下线,本地数据直接无法访问,不适合生产核心数据库

二:部署本地路径制备器

 整套实验完整工作流程总结
 1. 部署 Local Path 插件:创建 ns、权限账号、控制器、存储模板、磁盘操作脚本;
 2. 创建 PVC 申请本地存储,此时 PVC 处于等待状态;
 3. 启动 Nginx Pod 挂载 PVC,Pod 调度到某 worker 节点;
 4. 插件监听到 Pod 绑定 PVC,在对应节点/opt/local-path-provisioner生成存储目录,自动创建 PV,PVC 绑定成功;
 5. 容器读写挂载目录,数据持久化保存在宿主机本地硬盘;
 6. 删除 PVC 后,自动执行清理脚本,本地目录全部删除,数据清空。
 # 下载官方模板(下不了,自己复制地址去浏览器下载完传上去)
 [root@master30 ~ 09:52:52]# wget https://github.com/rancher/local-path-provisioner/blob/v0.0.35/deploy/local-path-storage.yaml
 ​
 # 查看资源配置
 [root@master30 ~ 10:31:31]# cat local-path-storage.yaml 
 apiVersion: v1
 kind: Namespace
 metadata:
   name: local-path-storage
 ​
 ---
 apiVersion: v1
 kind: ServiceAccount
 metadata:
   name: local-path-provisioner-service-account
   namespace: local-path-storage
 ​
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: Role
 metadata:
   name: local-path-provisioner-role
   namespace: local-path-storage
 rules:
   - apiGroups: [""]
     resources: ["pods"]
     verbs: ["get", "list", "watch", "create", "patch", "update", "delete"]
 ​
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRole
 metadata:
   name: local-path-provisioner-role
 rules:
   - apiGroups: [""]
     resources: ["nodes", "persistentvolumeclaims", "configmaps", "pods", "pods/log"]
     verbs: ["get", "list", "watch"]
   - apiGroups: [""]
     resources: ["persistentvolumes"]
     verbs: ["get", "list", "watch", "create", "patch", "update", "delete"]
   - apiGroups: [""]
     resources: ["events"]
     verbs: ["create", "patch"]
   - apiGroups: ["storage.k8s.io"]
     resources: ["storageclasses"]
     verbs: ["get", "list", "watch"]
 ​
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: RoleBinding
 metadata:
   name: local-path-provisioner-bind
   namespace: local-path-storage
 roleRef:
   apiGroup: rbac.authorization.k8s.io
   kind: Role
   name: local-path-provisioner-role
 subjects:
   - kind: ServiceAccount
     name: local-path-provisioner-service-account
     namespace: local-path-storage
 ​
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
 metadata:
   name: local-path-provisioner-bind
 roleRef:
   apiGroup: rbac.authorization.k8s.io
   kind: ClusterRole
   name: local-path-provisioner-role
 subjects:
   - kind: ServiceAccount
     name: local-path-provisioner-service-account
     namespace: local-path-storage
 ​
 ---
 apiVersion: apps/v1
 kind: Deployment
 metadata:
   name: local-path-provisioner
   namespace: local-path-storage
 spec:
   replicas: 1
   selector:
     matchLabels:
       app: local-path-provisioner
   template:
     metadata:
       labels:
         app: local-path-provisioner
     spec:
       serviceAccountName: local-path-provisioner-service-account
       containers:
         - name: local-path-provisioner
           image: rancher/local-path-provisioner:v0.0.35
           imagePullPolicy: IfNotPresent
           command:
             - local-path-provisioner
             - --debug
             - start
             - --config
             - /etc/config/config.json
           volumeMounts:
             - name: config-volume
               mountPath: /etc/config/
           env:
             - name: POD_NAMESPACE
               valueFrom:
                 fieldRef:
                   fieldPath: metadata.namespace
             - name: CONFIG_MOUNT_PATH
               value: /etc/config/
       volumes:
         - name: config-volume
           configMap:
             name: local-path-config
 ​
 ---
 apiVersion: storage.k8s.io/v1
 kind: StorageClass
 metadata:
   name: local-path
 provisioner: rancher.io/local-path
 volumeBindingMode: WaitForFirstConsumer
 reclaimPolicy: Delete
 ​
 ---
 kind: ConfigMap
 apiVersion: v1
 metadata:
   name: local-path-config
   namespace: local-path-storage
 data:
   config.json: |-
     {
             "nodePathMap":[
             {
                     "node":"DEFAULT_PATH_FOR_NON_LISTED_NODES",
                     "paths":["/opt/local-path-provisioner"]
             }
             ]
     }
   setup: |-
     #!/bin/sh
     set -eu
     mkdir -m 0777 -p "$VOL_DIR"
   teardown: |-
     #!/bin/sh
     set -eu
     rm -rf "$VOL_DIR"
   helperPod.yaml: |-
     apiVersion: v1
     kind: Pod
     metadata:
       name: helper-pod
     spec:
       priorityClassName: system-node-critical
       tolerations:
         - key: node.kubernetes.io/disk-pressure
           operator: Exists
           effect: NoSchedule
       containers:
       - name: helper-pod
         image: busybox
         imagePullPolicy: IfNotPresent
 ​

local-path-storage.yaml 核心由 5 类资源 组成,每部分各司其职:

资源类型 名称 核心作用
Namespace local-path-storage 隔离 Local Path Provisioner 相关资源
ConfigMap local-path-config 定义本地存储路径规则(如默认 /opt/local-path-provisioner
ServiceAccount + RBAC local-path-provisioner 赋予 Provisioner 操作 PV/PVC 的权限
Deployment local-path-provisioner 运行 Local Path Provisioner 核心程序(动态创建本地 PV)
StorageClass local-path 提供给 PVC 调用的「本地存储模板」
 # 资源默认部署在命名空间:local-path-storage;执行kubectl apply -f local-path-storage.yaml一次性创建全部9种资源:ns、sa、role、clusterrole、rolebinding、clusterrolebinding、deployment、sc、configmap,代表所有依赖一次性就绪。
 [root@master30 ~ 10:39:58]# kubectl  apply -f local-path-storage.yaml 
 namespace/local-path-storage created
 serviceaccount/local-path-provisioner-service-account created
 role.rbac.authorization.k8s.io/local-path-provisioner-role created
 clusterrole.rbac.authorization.k8s.io/local-path-provisioner-role created
 rolebinding.rbac.authorization.k8s.io/local-path-provisioner-bind created
 clusterrolebinding.rbac.authorization.k8s.io/local-path-provisioner-bind created
 deployment.apps/local-path-provisioner created
 storageclass.storage.k8s.io/local-path created
 configmap/local-path-config created
 ​
 ​
 # 确认资源状态
 [root@master30 ~ 10:40:30]# kubectl  get sc local-path 
 NAME         PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
 local-path   rancher.io/local-path   Delete          WaitForFirstConsumer   false                  28s
 ​
 ​
 #1.PROVISIONER: rancher.io/local-path:存储供给者标识,PVC 靠这个匹配插件;
 #2.RECLAIMPOLICY: Delete:删除 PVC 自动删除 PV + 本地磁盘目录;
 #3.VOLUMEBINDINGMODE: WaitForFirstConsumer:核心特性,不会提前绑定 PV,必须等 Pod 调度到节点后,才在对应节点创建存储目录,杜绝 Pod 和存储跨节点。
 ​
 ​
 #Pod 状态Running代表存储插件控制器正常运行,持续监听集群存储事件。
 [root@master30 ~ 10:40:58]# kubectl  get all -n  local-path-storage 
 NAME                                          READY   STATUS    RESTARTS   AGE
 pod/local-path-provisioner-74b5c4bf98-p6zkv   1/1     Running   0          52s
 ​
 NAME                                     READY   UP-TO-DATE   AVAILABLE   AGE
 deployment.apps/local-path-provisioner   1/1     1            1           52s
 ​
 NAME                                                DESIRED   CURRENT   READY   AGE
 replicaset.apps/local-path-provisioner-74b5c4bf98   1         1         1       52s
 ​

三:验证部署

 # 设置默认命名空间
 [root@master30 ~ 10:41:37]# kubectl  config  set-context  --current --namespace=default
 Context "kubernetes-admin@kubernetes" modified.
 ​
1. 创建 pvc(存储申请单)
 [root@master30 ~ 10:42:58]# cat > local-path-pvc.yaml <<'EOF'
 apiVersion: v1
 kind: PersistentVolumeClaim
 metadata:
   name: local-path-pvc
   namespace: default
 spec:
   accessModes: ["ReadWriteOnce"]
   resources:
     requests:
       storage: 5Gi
   storageClassName: local-path  # 指定 Local Path 的 StorageClass
 EOF
 ​
 [root@master30 ~ 10:45:26]# kubectl  apply  -f local-path-pvc.yaml 
 persistentvolumeclaim/local-path-pvc created
 ​
 #查看pvc状态
 [root@master30 ~ 10:45:29]# kubectl  get pvc
 NAME             STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
 local-path-pvc   Pending                                      local-path     <unset>                 16s
 ​
 #执行后 PVC 状态为Pending
 #现象原理:开启了WaitForFirstConsumer模式,没有 Pod 使用这个 PVC 时,插件不会创建 PV,PVC 一直处于待绑定状态,避免提前在随机节点生成磁盘。
2. 创建 Deployment 挂载 PVC(触发 PV 自动创建)
 [root@master30 ~ 10:45:45]# cat > local-path-deployment.yaml <<'EOF'
 apiVersion: apps/v1
 kind: Deployment
 metadata:
   labels:
     app: webapp
   name: webapp
   namespace: default
 spec:
   replicas: 2      #副本数 2
   selector:
     matchLabels:
       app: webapp
   template:
     metadata:
       labels:
         app: webapp
     spec:
       containers:
       - image: nginx   #镜像 nginx
         name: nginx
         volumeMounts:
         - name: webapp-data
           mountPath: /usr/share/nginx/html       #挂载 PVC 到容器/usr/share/nginx/html;
       volumes:
       - name: webapp-data
         persistentVolumeClaim:
           claimName: local-path-pvc
 EOF
 ​
 ​
 ​
 ​
 [root@master30 ~ 10:46:04]# kubectl  apply -f  local-path-deployment.yaml 
 deployment.apps/webapp created
 ​
 # 查看 pod 状态
 [root@master30 ~ 10:46:25]# kubectl  get pods -o  wide 
 NAME                      READY   STATUS    RESTARTS   AGE   IP               NODE                NOMINATED NODE   READINESS GATES
 webapp-6d46b54487-dv8d2   1/1     Running   0          8s    10.224.215.138   worker31.zy.cloud   <none>           <none>
 webapp-6d46b54487-kjqbs   1/1     Running   0          8s    10.224.215.139   worker31.zy.cloud   <none>           <none>
 #apply 后生成 2 个 Pod,全部调度到 worker31 节点;
 #底层逻辑:本地存储绑定节点,同一个 PVC 只能对应一个节点本地目录,所以所有副本只能调度到存储所在节点;
 #缺点:节点故障后存储无法迁移,不适合高可用业务。
 ​
 # 查看 pvc 状态
 [root@master30 ~ 10:46:33]# kubectl  get pvc
 NAME             STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
 local-path-pvc   Bound    pvc-77c5234c-7280-4c74-90e7-ec5d29215199   5Gi        RWO            local-path     <unset>                 3m7s
 #Pod 正常运行后,PVC 状态自动变为Bound:
 #插件感知到 Pod 调度到 worker31,自动在该节点创建本地目录、生成对应 PV,完成 PVC 与 PV 绑定。
 ​
 # 写入测试文件(数据读写验证,确认本地磁盘持久化)
 [root@master30 ~ 10:48:36]# kubectl  exec  webapp-6d46b54487-dv8d2  -- bash -c 'echo Hello world from nginx > /usr/share/nginx/html/index.html'
 ​
 ​
 # 到宿主机 worker31 查看目录:
 #路径格式固定:/opt/local-path-provisioner/【PV名称】_【命名空间】_【PVC名称】/
 #文件真实存在,证明容器写入的数据保存在节点本地物理磁盘,实现持久化。
 [root@worker31 ~ 09:15:05]# ls /opt/local-path-provisioner/pvc-77c5234c-7280-4c74-90e7-ec5d29215199_default_local-path-pvc/
 index.html
 [root@worker31 ~ 10:51:02]# cat /opt/local-path-provisioner/pvc-77c5234c-7280-4c74-90e7-ec5d29215199_default_local-path-pvc/index.html 
 Hello world from nginx
 ​
3. 清理资源
 # 删除 deployment
 [root@master30 ~ 11:27:59]# kubectl  delete deployments.apps  webapp 
 deployment.apps "webapp" deleted
 ​
 # 删除 pvc
 [root@master30 ~ 11:27:20]# kubectl delete pvc local-path-pvc 
 ​
 # 关键现象:宿主机本地目录直接消失
 [root@worker31 ~ 11:28:37]# ls /opt/local-path-provisioner/pvc-77c5234c-7280-4c74-90e7-ec5d29215199_default_local-path-pvc/
 ls: cannot access '/opt/local-path-provisioner/pvc-77c5234c-7280-4c74-90e7-ec5d29215199_default_local-path-pvc/': No such file or directory
 ​
 #原理;删除 PVC 后插件自动启动 helper 临时 Pod,执行rm -rf $VOL_DIR清空整个存储目录,数据全部销毁,对应 Delete 回收策略特性。
4:实验核心知识点总结
  1. WaitForFirstConsumer 绑定模式 无 Pod 使用 PVC 时不创建 PV,Pod 调度完成后才在对应节点生成本地存储,不会出现存储和 Pod 跨节点无法挂载的问题。

  2. 本地存储强绑定节点 同一个 PVC 生成的 PV 固定在某一台 worker 节点,应用所有副本只能跑在该节点,节点宕机数据不可访问。

  3. reclaimPolicy: Delete 风险点 删除 PVC 会连带删除本地磁盘所有数据,测试环境可用;生产数据库、重要业务必须修改为Retain策略,删除 PVC 后保留数据目录。

  4. 完整工作链路 业务创建 PVC(指定 local-path 存储类)→ 创建 Pod 挂载 PVC → 插件监听 Pod 调度节点 → 节点自动生成本地目录 + PV → 容器读写宿主机本地磁盘 → 删除 PVC 自动清空本地目录。

  5. 适用 / 不适用场景 ✅ 适用:测试集群、单机缓存、开发环境、追求本地磁盘低延迟; ❌ 不适用:核心数据库、需要集群故障迁移、多节点高可用业务。

6.为什么同一个 PVC 生成的 PV 固定在某一台 worker 节点,应用所有副本只能跑在该节点,节点宕机数据不可访问?

 1. 核心前提:Local Path 是节点本地硬盘
 普通云存储 / 分布式存储(ceph、云盘)是集群共享存储,所有节点都能读写;
 但 Local Path 用的是单个 worker 机器本地磁盘文件夹 /opt/local-path-provisioner,文件只存在这一台机器硬盘里,别的机器看不到。
 ​
 2. 什么叫同一个 PVC 的 PV 固定一台 worker
 你实验里创建了 1 个 PVC:local-path-pvc
 - 启动 Nginx Pod 时,调度器把 Pod 分到了 worker31;
 - Local Path 插件立刻只在 worker31 上创建存储目录、生成 PV;
 - 这个 PV 永久绑定 worker31,不会跑到其他节点(worker30、worker32 等)。
 这个 PVC 对应的存储,永远只存在 worker31 本地。
 ​
 3. 为什么应用所有副本只能跑在该节点
 你的 Deployment 副本数是 2,两个 Pod 都必须读取同一个本地文件夹。
 - 如果其中一个 Pod 调度到 worker32:worker32 本地没有这个 PVC 对应的存储目录,容器启动会直接报错、挂载失败;
 - K8s 调度器识别本地 PV 绑定节点,自动约束:所有副本只能调度到 PV 所在 worker31。
 所以你执行 kubectl get pods -o wide 看到两个 Pod 都在 worker31。
 ​
 4. 节点宕机数据不可访问(最大缺陷)
 分两种场景理解:
 场景 1:worker31 机器关机 / 故障、断电
 - PV、数据全部存在 worker31 本地硬盘;
 - worker31 离线后,整个集群没有任何节点能读取这份存储;
 - 所有依赖这个 PVC 的 Pod 全部无法启动,业务直接瘫痪,直到 worker31 恢复开机。
 场景 2:想把业务迁移到其他节点
 集群没有自动数据同步机制,worker30/32 上没有这份数据,就算手动删除 Pod 重新调度,Pod 也不会跑到其他节点,就算强制调度也挂载失败、丢失数据。
 ​
 5. 和共享存储对比区分,更好理解
 1. 分布式共享存储(Ceph / 云硬盘)
 盘是集群公共资源,Pod 可以在任意节点漂移,节点宕机,其他节点照样挂载磁盘读取数据,高可用。
 2. Local Path 本地存储
 盘属于单台机器私有资源,锁死节点;节点故障 = 存储失联,业务中断。
 ​
 6. 结合你实验的现象总结
 1. PVC 创建后无 Pod:Pending,不创建存储;
 2. Pod 调度到 worker31:仅 worker31 生成本地目录,PV 绑定该节点;
 3. 多副本 Pod 全部锁死在 worker31,不能分散到多节点;
 4. 一旦 worker31 宕机,Nginx 读取不到index.html,页面完全打不开,数据无法访问。
 ​
 7.补充使用建议
 只适合测试、开发环境、临时缓存;
 生产数据库、核心业务绝对不能用 Local Path,节点故障会直接丢业务服务。

部署 NFS provisioner

 实验整体流程总结
 1. 搭建基础 NFS 共享存储服务端,开放集群读写权限
 2. 所有节点安装 NFS 客户端依赖
 3. 创建独立命名空间隔离存储组件
 4. 配置 RBAC 权限,给 provisioner 操作 PV/PVC 的集群权限
 5. 部署 nfs-subdir-external-provisioner,监听 PVC 事件自动创建子目录 PV
 6. 创建 StorageClass 绑定制备器,定义存储回收、扩容策略
 7. 创建 PVC 指定存储类,自动生成 PV,验证 NFS 底层生成独立目录
 8. 部署 Nginx 挂载 PVC,测试持久化数据读写
 9. 清理应用与 PVC,验证存储回收策略,区分集群 PV 对象与底层真实文件生命周期

部署 NFS 服务

 # 安装 NFS server
 [root@master30 ~ 15:00:09]# apt install  -y nfs-kernel-server 
 ​
 # 创建共享目录并赋予 777 权限,直接设置目录权限:所有用户可读、可写、可执行,解决容器挂载 NFS 后文件权限、root 用户映射报错问题
 [root@master30 ~ 11:28:20]# mkdir -m 777 /shares/
 ​
 ​
 # 配置共享,允许所有客户端访问
 root@master30 ~ 15:42:12]#  cat << EOF > /etc/exports
 /shares *(rw,sync,no_root_squash,no_all_squash,insecure)
 EOF
 ​
 #配置字段拆解:
   - /shares:要对外共享的本地目录
   - *:允许所有客户端 IP挂载(生产环境建议限制集群网段如10.1.8.0/24)
   - rw:读写权限;ro = 只读
   - sync:同步写入,数据落盘后再返回成功,数据更安全;async 异步性能高但丢电丢数据
   - no_root_squash:客户端 root 用户不被压缩为匿名 nfsnobody,容器内 root 能正常读写文件(k8s 必备)
   - no_all_squash:普通客户端用户也不映射匿名用户,保留原有 UID/GID
   - insecure:允许客户端使用大于 1024 的随机端口挂载,k8s pod 随机端口必须开启,否则挂载失败
 ​
 ​
 # 重启 NFS 服务生效配置:修改 exports 必须重启 nfs-server 才能加载新共享规则
 [root@master30 ~ 15:42:12]# systemctl  restart  nfs-server.service 
 ​
 # 客户端安装:所有 k8s 节点必须安装,否则 pod 无法挂载 NFS 存储,会出现卷挂载失败报错
 [root@worker31 ~ 11:28:38]# apt install -y nfs-common 
 [root@worker32 ~ 09:15:08]# apt install -y nfs-common 
 ​

部署 NFS provisioner

NFS没有内置制备器,这里我们自定义外部分配器。

 # 资源部署到命名空间:kube-storage
 [root@master30 ~ 15:42:37]# kubectl  create  ns kube-storage
 namespace/kube-storage created
 [root@master30 ~ 15:45:51]# kubectl  config  set-context  --current --namespace  kube-storage
 Context "kubernetes-admin@kubernetes" modified.
1. 创建 RBAC 权限

为什么要创建RBAC权限:NFS provisioner pod 需要权限去集群创建 / 删除 PV、监听 PVC 事件,无权限会无法自动创建 PV。

创建 nfs-rbac.yaml 文件,赋予 Provisioner 操作 PV/PVC 的权限。

 [root@master30 ~ 15:46:03]# cat > nfs-rbac.yaml <<'EOF'
 # 1. 服务账号:给provisioner pod使用
 apiVersion: v1
 kind: ServiceAccount
 metadata:
   name: nfs-client-provisioner
   namespace: kube-storage
 ---
 # 2. 集群角色:定义允许操作的资源动作
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRole
 metadata:
   name: nfs-client-provisioner-runner
 rules:
   - 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"]
   - apiGroups: [""]
     resources: ["endpoints"]
     verbs: ["get", "list", "watch", "create", "update", "patch"]
 ---
 # 3. 集群角色绑定:把角色权限授予服务账号
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
 metadata:
   name: run-nfs-client-provisioner
 subjects:
   - kind: ServiceAccount
     name: nfs-client-provisioner
     namespace: kube-storage
 roleRef:
   kind: ClusterRole
   name: nfs-client-provisioner-runner
   apiGroup: rbac.authorization.k8s.io
 EOF
 ​
 ​
 #逐块解释
 #1. ServiceAccount
 Pod 运行身份,provisioner 容器启动时指定serviceAccountName,所有集群操作都用该账号鉴权。
 #2. ClusterRole(集群角色,全局权限,跨命名空间生效)
   - persistentvolumes:增删查看 PV(动态创建删除存储卷)
   - persistentvolumeclaims:监听 PVC 创建事件、更新 PVC 绑定状态
   - storageclasses:读取存储类配置
   - events:生成事件记录 PV/PVC 操作日志
   - endpoints:内部通信所需资源权限
 #3. ClusterRoleBinding
 绑定规则:将 ClusterRole 的全套权限授予 kube-storage 命名空间下的 sa,全局生效。
 ​
 [root@master30 ~ 15:47:23]#  kubectl apply -f nfs-rbac.yaml
 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
 ​

2. 部署 NFS Provisioner 应用

该程序核心功能:监听集群 PVC 创建事件,自动在 NFS 共享目录新建独立子目录,自动生成 PV 绑定 PVC,实现动态存储

 [root@master30 ~ 15:49:48]# cat > nfs-provisioner.yaml <<'EOF'
 apiVersion: apps/v1
 kind: Deployment
 metadata:
   name: nfs-client-provisioner
   namespace: kube-storage
 spec:
   replicas: 1       #- replicas:1:单副本运行,provisioner 不支持多副本高可用
   strategy:
     type: Recreate    #- strategy: Recreate:更新 pod 时先删除旧 pod 再新建,避免同时读写 NFS 目录冲突
   selector:
     matchLabels:
       app: nfs-client-provisioner
   template:
     metadata:
       labels:
         app: nfs-client-provisioner
     spec:
       serviceAccountName: nfs-client-provisioner      #指定前面创建的 RBAC 服务账号
       volumes:
       - name: nfs-client-root
         nfs:
           server: 10.1.8.30  # 同上 NFS 服务器 IP
           path: /shares        # NFS共享根目录
       containers:
       - name: nfs-client-provisioner
         image: registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2
         volumeMounts:
         - name: nfs-client-root
           mountPath: /persistentvolumes
         env:
         - name: PROVISIONER_NAME
           value: fuseim.pri/ifs
         - name: NFS_SERVER
           value: 10.1.8.30  # 替换为你的 NFS 服务器 IP
         - name: NFS_PATH
           value: /shares  # 核心:共享路径改为 /shares
 EOF
 ​
 #  部署 NFS Provisioner 应用
 root@master30 ~ 15:51:00]# kubectl apply -f nfs-provisioner.yaml
 deployment.apps/nfs-client-provisioner created
 ​
 # 查看 NFS Provisioner 应用
 [root@master30 ~ 15:51:17]# kubectl  get deployments.apps -n kube-storage 
 NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
 nfs-client-provisioner   1/1     1            1           106s

3. 创建 NFS StorageClass

StorageClass 是 k8s 存储模板,PVC 指定 storageClassName 即可触发对应 provisioner 自动创建 PV。

 [root@master30 ~ 15:53:03]# cat > nfs-storageclass.yaml <<'EOF'
 apiVersion: storage.k8s.io/v1
 kind: StorageClass
 metadata:
   name: nfs-storage
   namespace: kube-storage
 provisioner: fuseim.pri/ifs  # 必须和 Provisioner 名称一致
 parameters:
   onDelete: retain  # 存储底层:删除PVC后保留NFS真实业务目录
   archiveOnDelete: "false"
 # 按需设置回收策略:Delete:回收策略,删除 PVC 时自动删除对应 PV;Retain 代表保留 PV 和 NFS 数据
 #reclaimPolicy: Retain
 reclaimPolicy: Delete         # 集群层面:删除PVC后清除集群PV资源,注意和onDelete:retain区分
 allowVolumeExpansion: true
 volumeBindingMode: Immediate
 EOF
 ​
 # 创建 StorageClass
 [root@master30 ~ 15:53:32]# kubectl apply -f nfs-storageclass.yaml
 storageclass.storage.k8s.io/nfs-storage created
 ​
 # 查看 StorageClass
 root@master30 ~ 15:53:43]# kubectl get sc
 NAME          PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
 nfs-storage   fuseim.pri/ifs          Delete          Immediate              true                   17s
 ​

验证部署

在default命名空间测试。

 [root@master30 ~ 15:55:01]# kubectl config set-context --current --namespace default
 Context "kubernetes-admin@kubernetes" modified.

1. 创建 pvc
 [root@master30 ~ 15:55:24]# cat > nfs-pvc.yaml <<'EOF'
 apiVersion: v1
 kind: PersistentVolumeClaim
 metadata:
   name: webclaim
   namespace: default
 spec:
   accessModes:
     - ReadWriteOnce
   resources:
     requests:
       storage: 5Gi
   storageClassName: "nfs-storage"    #指定使用我们创建的 NFS 存储类,触发自动制备 PV
 EOF
 ​
 # 创建 pvc,查看绑定状态
 [root@master30 ~ 15:55:36]# kubectl apply -f nfs-pvc.yaml 
 persistentvolumeclaim/webclaim created
 ​
 # 查看 pvc 状态:PVC 状态变为 Bound:代表 provisioner 自动创建 PV 并完成绑定
 [root@master30 ~ 15:55:54]# kubectl get pvc
 NAME       STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
 webclaim   Bound    pvc-f8dd12bb-4dc7-4e18-9923-0b2301705839   5Gi        RWO            nfs-storage    <unset>                 5s
 ​
 # 查看 pv 状态:PV 名称为 UUID 格式,对应 NFS 服务器 /shares 下自动生成的独立文件夹
 [root@master30 ~ 15:55:59]# kubectl get pv
 NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM              STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
 pvc-f8dd12bb-4dc7-4e18-9923-0b2301705839   5Gi        RWO            Delete           Bound    default/webclaim   nfs-storage    <unset>                          11s
 ​
 ​
 ​
 # 查看 nfs 共享目录
 [root@master30 ~ 15:56:05]# ls /shares/
 default-webclaim-pvc-f8dd12bb-4dc7-4e18-9923-0b2301705839
 ​
 #输出类似 default-webclaim-pvc-xxx
 #命名规则:命名空间-pvc名称-pvc唯一ID,每个 PVC 隔离独立目录,数据互不干扰
 ​
 ​
 # 写入测试网页文件(NFS 服务端操作),直接在 NFS 底层目录写入静态页面,验证容器能读取共享存储数据
 [root@master30 ~ 15:57:20]# echo Hello world From nginx > /shares/default-webclaim-pvc-f8dd12bb-4dc7-4e18-9923-0b2301705839/index.html
 ​

2. 创建 deployment(部署 Nginx 应用挂载 PVC)
 [root@master30 ~ 15:57:59]# cat > nfs-deploy.yaml << EOF
 apiVersion: apps/v1
 kind: Deployment
 metadata:
   labels:
     app: webapp
   name: webapp
 spec:
   replicas: 2          #启动 2 个 Nginx 副本,共享同一份 NFS 存储
   selector:
     matchLabels:
       app: webapp
   template:
     metadata:
       labels:
         app: webapp
     spec:
       containers:
       - image: nginx
         name: nginx
         volumeMounts:
         - name: webapp-data
           mountPath: /usr/share/nginx/html         #将 PVC 挂载到 Nginx 网页目录/usr/share/nginx/html
       volumes:
       - name: webapp-data
         persistentVolumeClaim:
           claimName: webclaim          #绑定刚才创建的 PVC
 EOF
 ​
 ​
 [root@master30 ~ 15:58:14]# kubectl apply -f nfs-deploy.yaml
 deployment.apps/webapp created
 ​
 #- kubectl expose:为 deployment 创建 ClusterIP 内部服务
 #--port:service 端口;--target-port:容器内 nginx 端口 80
 [root@master30 ~ 15:58:26]# kubectl  expose  deployment  webapp  --target-port 80 --port 80 --protocol TCP
 service/webapp exposed
 ​
 ​
 #查看 service 分配的集群内网 IP
 [root@master30 ~ 15:59:00]# kubectl  get  svc webapp 
 NAME     TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
 webapp   ClusterIP   10.104.143.59   <none>        80/TCP    12s
 ​
 ​
 # 访问测试
 [root@master30 ~ 15:59:12]# curl 10.104.143.59
 Hello world From nginx
 #输出Hello World From Nginx,证明 pod 成功读取 NFS 共享目录的文件,动态存储可用。

3. 清理资源
 # 删除 deployment
 [root@master30 ~ 15:59:30]# kubectl  delete  deployments.apps  webapp 
 deployment.apps "webapp" deleted
 ​
 # 删除 pvc,pv也将一起删除
 [root@master30 ~ 15:59:47]# kubectl  delete pvc webclaim 
 persistentvolumeclaim "webclaim" deleted
 [root@master30 ~ 15:59:55]# kubectl  get pv
 No resources found
 ​
 # 查看后端存储中数据,仍然保留
 [root@master30 ~ 16:00:00]# ls /shares/default-webclaim-pvc-f8dd12bb-4dc7-4e18-9923-0b2301705839/
 index.html
 ​
 #文件夹和 index.html 文件仍然存在,原因:StorageClass 参数onDelete: retain控制删除 PVC 不清除 NFS 真实数据,避免误删业务文件;如需自动清理可修改参数。

保留 NFS provisioner ,下一章 statefulset 使用。

Kubernetes StatefulSet

环境准备

 [root@master30 ~ 16:00:18]# kubectl  create  ns statefulset
 namespace/statefulset created
 [root@master30 ~ 16:13:18]# kubectl  config  set-context  --current  --namespace statefulset
 Context "kubernetes-admin@kubernetes" modified.

无状态应用 vs 有状态应用

维度 无状态应用(Stateless) 有状态应用(Stateful)
核心特征 不保存任何请求/会话数据,每次请求独立 依赖持久化的会话/数据,请求间有依赖关系
数据存储 数据仅在请求期间存在,不落地/仅落地外部存储 数据需持久化(本地/分布式存储),依赖固定存储
实例一致性 所有实例完全相同,可随意扩缩容/替换 实例有身份(如编号),启动/扩缩容有顺序要求
网络标识 共享 IP/域名,无固定网络标识 有固定网络标识(如 Headless Service + DNS)

总结

  1. 无状态核心:实例“无记忆”,可随意替换/扩容,是云原生优先推荐的应用形态;

  2. 有状态核心:实例“有记忆”,依赖持久化和固定身份,需通过 StatefulSet 等组件保障稳定性;

  3. 最佳实践:尽可能将应用拆分为“无状态业务层 + 有状态数据层”,既提升扩展能力,又保障数据安全。

StatefulSet 介绍

StatefulSet 定义

StatefulSet 是 Kubernetes 中专门用于管理有状态应用的工作负载资源,与用于管理无状态应用的 Deployment 相比,其核心优势在于为每个 Pod 分配固定且唯一的身份标识(包括名称、网络标识、存储关联),即便 Pod 发生重启、迁移或重建,其身份信息和绑定的存储资源也不会改变,确保有状态应用的稳定性和连续性。

适用场景:数据库(MySQL、PostgreSQL)、分布式中间件(Redis 集群、ZooKeeper)、消息队列(Kafka)等依赖节点身份和数据持久化的服务。

StatefulSet 特性

  • 固定身份标识:每个 Pod 拥有唯一的序号(从 0 开始递增),hostname 固定为「StatefulSet 名称-序号」(如 mysql-0),同时通过 Headless Service 提供固定 DNS 解析(如 mysql-0.mysql.default.svc.cluster.local),便于集群内节点间稳定通信和服务发现。

  • 有序操作

    • 部署/扩容:按序号从 0 到 N-1 依次创建 Pod,必须等待前一个 Pod 处于 Ready 状态后,再创建下一个,确保集群启动顺序符合业务依赖;

      • 缩容/删除:按序号从 N-1 到 0 依次删除 Pod,前一个 Pod 彻底删除(Terminating 状态结束)后,再删除下一个,避免数据丢失或集群异常;

      • 更新:默认按序号从 N-1 到 0 滚动更新,确保更新过程中集群始终保持可用,避免服务中断。

  • 稳定存储:通过 PersistentVolumeClaim(PVC)模板 为每个 Pod 自动创建独立的 PVC,PVC 名称与 Pod 序号绑定(格式:模板名-StatefulSet名称-序号)。即使 Pod 被删除,PVC 及绑定的 PersistentVolume(PV)仍会保留,重新创建的 Pod 会自动复用原 PVC 和 PV,实现数据不丢失。

  • Headless Service 依赖:StatefulSet 必须关联一个 Headless Service(ClusterIP 设为 None),该服务不提供负载均衡功能,仅负责为每个 Pod 提供固定的 DNS 解析,是 Pod 间稳定通信的核心前提。

StatefulSet VS Deployment

控制器 StatefulSet Deployment
适用场景 有状态应用(数据库、分布式集群、消息队列) 无状态应用(Web 服务、API 接口、静态服务)
网络 Headless Service(固定 DNS 解析) ClusterIP/LoadBalancer(共享 IP)
存储 必须 PVC + PV(固定存储卷绑定) 可选 PVC(无数据持久化要求)
扩缩容 按实例编号顺序扩缩容(如从 0→1→2) 无顺序,瞬间完成
更新策略 有序更新(如从最后一个实例开始) 滚动更新/重建,无顺序
重启/重建 实例重启后需恢复原有数据/身份 实例重启后无影响

StatefulSet 关键组件

  • StatefulSet 控制器:Kubernetes 核心控制器之一,负责管理 Pod 的全生命周期,确保 Pod 按序号规则创建、更新、删除,维护 Pod 身份和存储的稳定性。

  • Headless Service:核心作用是为 Pod 提供固定 DNS 解析,无 ClusterIP,仅负责将 Pod 名称映射为 DNS 记录,支持 Pod 间通过 hostname 稳定通信。

  • PVC 模板:定义每个 Pod 所需的存储规格(如存储容量、访问模式),StatefulSet 会根据模板为每个 Pod 自动创建对应的 PVC,无需手动创建。

  • PV(PersistentVolume):实际的存储资源,与 PVC 一一绑定,为 Pod 提供持久化存储支持,可选用本地存储、云存储(AWS EBS、阿里云 EBS)、分布式存储(Ceph)等类型。

StatefulSet 实践操作

所有命令均在 statefulset 命名空间执行。

部署 NFS 服务器

 # 安装 NFS server
 [root@master30 ~ 16:00:18]# apt install -y nfs-kernel-server
 ​
 # 安创建NFS目录 修改创建文件夹的权限
 [root@master30 ~ 16:00:51]# mkdir -m 777 -p /shares
 ​
 # 配置共享,允许所有客户端访问
 root@master30:~# cat << EOF >> /etc/exports
 /shares *(rw,sync,no_root_squash,no_all_squash,insecure)
 EOF
 ​
 # 重启 nfs server
 [root@master30 ~ 16:14:50]#  systemctl restart nfs-server.service
 ​
 # 客户端安装
 [root@worker31 ~ 16:15:16]# apt install -y nfs-common
 [root@worker32 ~ 16:15:25]#  apt install -y nfs-common

示例 1:部署 Nginx 集群

需求:部署 3 节点 Nginx 集群,实现数据持久化、固定网络标识、有序部署,满足高可用。

步骤 1:创建 StatefulSet

创建 nginx-statefulset.yaml,通过 PVC 模板自动创建存储。

 [root@master30 ~ 16:15:12]# cat > nginx-statefulset.yaml <<'EOF'
 apiVersion: apps/v1
 kind: StatefulSet
 metadata:
   name: nginx
 spec:
   selector:
     matchLabels:
       app: nginx
   serviceName: "nginx"
   replicas: 3
   minReadySeconds: 10
   template:
     metadata:
       labels:
         app: nginx
     spec:
       terminationGracePeriodSeconds: 10
       containers:
       - name: nginx
         image: docker.io/library/nginx:latest
         ports:
         - containerPort: 80
           name: nginx
         volumeMounts:
         - name: nginx-data
           mountPath: /usr/share/nginx/html
   volumeClaimTemplates:
   - metadata:
       name: nginx-data
     spec:
       accessModes: [ "ReadWriteOnce" ]
       # 使用上一章动态卷置备nfs Provider
       storageClassName: "nfs-storage"
       resources:
         requests:
           storage: 10Gi
 EOF
 ​
 # 创建 statefulset
 [root@master30 ~ 16:16:16]#  kubectl apply -f nginx-statefulset.yaml
 statefulset.apps/nginx created
 ​
 ​
 # 查看 pods
 [root@master30 ~ 16:16:35]# watch kubectl get pods
 NAME      READY   STATUS    RESTARTS   AGE
 nginx-0   1/1     Running   0          10m
 nginx-1   1/1     Running   0          10m
 nginx-2   1/1     Running   0          9m41s
 # 预期输出:Pod 按 nginx-0、nginx-1、nginx-2顺序创建
 # 先创建 nginx-0,处于 Running 状态
 # 等待20s,再创建 nginx-1,处于 Running 状态
 # 等待20s,再创建 nginx-2,处于 Running 状态
 # 最终 3 个 Pod 均为 Running 状态。
 ​
 # 查看 statefulset
 [root@master30 ~ 16:17:33]# kubectl  get sts
 NAME    READY   AGE
 nginx   3/3     75s
 ​
 # 查看 pvc 创建和pv绑定过程(可选)
 [root@master30 ~ 16:18:41]# kubectl  get pvc
 NAME                 STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
 nginx-data-nginx-0   Bound    pvc-8593d195-a5e7-4458-8155-324d3a9680d5   10Gi       RWO            nfs-storage    <unset>                 4m21s
 nginx-data-nginx-1   Bound    pvc-4042fca6-e55f-4bb4-85d4-207fbfb80f2f   10Gi       RWO            nfs-storage    <unset>                 4m1s
 nginx-data-nginx-2   Bound    pvc-84db5793-9338-48cc-8aa5-28fd07b69abc   10Gi       RWO            nfs-storage    <unset>                 3m41s
 ​
步骤 2:准备 nfs 存储
 [root@master30 ~ 16:20:56]# kubectl get pvc|awk '{print $1,$3}'
 NAME VOLUME
 nginx-data-nginx-0 pvc-8593d195-a5e7-4458-8155-324d3a9680d5
 nginx-data-nginx-1 pvc-4042fca6-e55f-4bb4-85d4-207fbfb80f2f
 nginx-data-nginx-2 pvc-84db5793-9338-48cc-8aa5-28fd07b69abc
 ​
 ​
 [root@master30 ~ 16:21:28]# echo nginx-data-nginx-0 > /shares/statefulset-nginx-data-nginx-0-pvc-8593d195-a5e7-4458-8155-324d3a9680d5/index.html
 [root@master30 ~ 16:22:29]# echo nginx-data-nginx-1 > /shares/statefulset-nginx-data-nginx-1-pvc-4042fca6-e55f-4bb4-85d4-207fbfb80f2f/index.html
 [root@master30 ~ 16:23:01]# echo nginx-data-nginx-2 > /shares/statefulset-nginx-data-nginx-2-pvc-84db5793-9338-48cc-8aa5-28fd07b69abc/index.html
 ​
步骤 3:创建 Headless Service

创建 nginx-service.yaml,为后续 StatefulSet 管理的 Pod 提供固定名称。

 [root@master30 ~ 16:23:17]# cat > nginx-service.yaml <<'EOF'
 apiVersion: v1
 kind: Service
 metadata:
   name: nginx
   namespace: statefulset
 spec:
   selector:
     app: nginx
   clusterIP: None
   ports:
   - port: 80
     targetPort: 80
     name: nginx-port
 EOF
 ​
 # 创建服务
 [root@master30 ~ 16:24:03]#  kubectl apply -f nginx-service.yaml
 service/nginx created
 ​
 # 没有 CLUSTER-IP 
 [root@master30 ~ 16:24:17]# kubectl get svc
 NAME    TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
 nginx   ClusterIP   None         <none>        80/TCP    3s
步骤 4:验证部署
 # 通过pod名称验证 pod 内容
 [root@master30 ~ 16:24:20]# kubectl  run test --image nginx -it -- bash
 If you don't see a command prompt, try pressing enter.
 ​
 # pod名称后要加service后缀
 root@test:/# curl nginx-0.nginx
 nginx-data-nginx-0
 root@test:/# curl nginx-1.nginx
 nginx-data-nginx-1
 root@test:/# curl nginx-2.nginx
 nginx-data-nginx-2
 ​
 ​
 # 删除 nginx-2,等待重新创
 [root@master30 ~ 16:27:45]# kubectl  delete pod nginx-2
 pod "nginx-2" deleted
 [root@master30 ~ 16:28:00]# kubectl  get pods
 NAME       READY   STATUS              RESTARTS   AGE
 nginx-0    1/1     Running             0          49m
 nginx-1    1/1     Running             0          49m
 nginx-2    0/1     ContainerCreating   0          3s
 # 新创建的pod的 IP 可能变化
 ​
 # 验证 nginx-2 内容
 [root@master30 ~ 16:28:08]# kubectl  exec test  -it -- bash
 root@test:/# curl nginx-2.nginx
 nginx-data-nginx-2
 ​
 # 预期输出:数据仍存在,说明 PV/PVC 实现了数据持久化,Pod 重建后可复用存储。

思考:如何为statefulset提供一个统一的IP入口呢?

答案:再创建一个普通的ClusterIP类型的service,暴漏statefulset。

 [root@master30 ~ 16:29:23]# cat > nginx-service-www.yaml <<EOF
 apiVersion: v1
 kind: Service
 metadata:
   labels:
     app: www
   name: www
 spec:
   ports:
   - name: 80-80
     port: 80
     protocol: TCP
     targetPort: 80
   selector:
     app: nginx
   type: ClusterIP
 EOF
 ​
 [root@master30 ~ 16:29:37]# kubectl apply -f nginx-service-www.yaml
 service/www created
 [root@master30 ~ 16:29:48]# kubectl get svc www 
 NAME   TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
 www    ClusterIP   10.107.127.191   <none>        80/TCP    10s
 ​
 ​
 # 通过svc访问后端pod
 [root@master30 ~ 16:29:58]# curl 10.107.127.191
 nginx-data-nginx-0
 [root@master30 ~ 16:30:22]# curl 10.107.127.191
 nginx-data-nginx-2
 [root@master30 ~ 16:30:23]# curl 10.107.127.191
 nginx-data-nginx-1
 ​
步骤 5:清理环境
 [root@master30 ~ 16:30:25]# kubectl  delete sts nginx 
 statefulset.apps "nginx" deleted
 [root@master30 ~ 16:32:04]# kubectl  delete svc www nginx 
 service "www" deleted
 service "nginx" deleted
 [root@master30 ~ 16:32:22]# rm -rf /shares/statefulset-nginx-data-nginx-{0..2}-*
 [root@master30 ~ 16:32:47]# kubectl  delete pod test 
 pod "test" deleted
 ​

示例 2:部署 Etcd 集群

需求:部署 3 节点 etcd 集群,实现数据持久化、固定网络标识、有序部署,满足高可用。

步骤 1:准备 NFS 存储
 # 准备应用目录和文件
 [root@master30 ~ 16:33:26]# mkdir -m 777 -p /shares/etcd/data-{0..2}
步骤 2:创建 Headless Service

创建 etcd-service.yaml,为后续 StatefulSet 管理的 Pod 提供固定名称。

 [root@master30 ~ 16:37:47]# cat > etcd-service.yaml <<'EOF'
 apiVersion: v1
 kind: Service
 metadata:
   name: etcd
   namespace: statefulset
   labels:
     app: etcd
 spec:
   ports:
   - port: 2379
     name: client
   - port: 2380
     name: peer
   clusterIP: None
   selector:
     app: etcd
 EOF
 ​
 # 创建服务
 [root@master30 ~ 16:38:35]# kubectl apply -f etcd-service.yaml
 service/etcd created
 ​
 ​
 # 没有 CLUSTER-IP 
 [root@master30 ~ 16:38:48]# kubectl get svc
 NAME   TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)             AGE
 etcd   ClusterIP   None         <none>        2379/TCP,2380/TCP   12s
 ​
步骤 3:创建 PV

创建 3个 PV,使用 NFS 存储(服务器地址10.1.8.30,共享目录/shares/),指定存储容量和回收策略。

 [root@master30 ~ 16:39:00]# cat > etcd-pv.yaml <<'EOF'
 apiVersion: v1
 kind: PersistentVolume
 metadata:
   name: etcd-pv-0
 spec:
   capacity:
     storage: 10Gi
   accessModes:
   - ReadWriteOnce
   persistentVolumeReclaimPolicy: Retain
   storageClassName: etcd-storage
   nfs:
     server: 10.1.8.30
     path: /shares/etcd/data-0
 ---
 apiVersion: v1
 kind: PersistentVolume
 metadata:
   name: etcd-pv-1
 spec:
   capacity:
     storage: 10Gi
   accessModes:
   - ReadWriteOnce
   persistentVolumeReclaimPolicy: Retain
   storageClassName: etcd-storage
   nfs:
     server: 10.1.8.30
     path: /shares/etcd/data-1
 ---
 apiVersion: v1
 kind: PersistentVolume
 metadata:
   name: etcd-pv-2
 spec:
   capacity:
     storage: 10Gi
   accessModes:
   - ReadWriteOnce
   persistentVolumeReclaimPolicy: Retain
   storageClassName: etcd-storage
   nfs:
     server: 10.1.8.30
     path: /shares/etcd/data-2
 EOF
 ​
 # 创建 pv
 [root@master30 ~ 16:39:56]# kubectl apply -f etcd-pv.yaml
 persistentvolume/etcd-pv-0 created
 persistentvolume/etcd-pv-1 created
 persistentvolume/etcd-pv-2 created
 [root@master30 ~ 16:40:09]# kubectl get pv
 NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM                            STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
 etcd-pv-0                                  10Gi       RWO            Retain           Available                                    etcd-storage   <unset>                          4s
 etcd-pv-1                                  10Gi       RWO            Retain           Available                                    etcd-storage   <unset>                          4s
 etcd-pv-2                                  10Gi       RWO            Retain           Available                                    etcd-storage   <unset>                          4s
 pvc-4042fca6-e55f-4bb4-85d4-207fbfb80f2f   10Gi       RWO            Delete           Bound       statefulset/nginx-data-nginx-1   nfs-storage    <unset>                          23m
 pvc-84db5793-9338-48cc-8aa5-28fd07b69abc   10Gi       RWO            Delete           Bound       statefulset/nginx-data-nginx-2   nfs-storage    <unset>                          22m
 pvc-8593d195-a5e7-4458-8155-324d3a9680d5   10Gi       RWO            Delete           Bound       statefulset/nginx-data-nginx-0   nfs-storage    <unset>                          23m
 ​

建议:使用动态卷制备。

步骤 4:创建 StatefulSet

创建 etcd-statefulset.yaml,通过 PVC 模板自动创建存储。

 [root@master30 ~ 16:40:13]# cat > etcd-statefulset.yaml <<'EOF'
 apiVersion: apps/v1
 kind: StatefulSet
 metadata:
   name: etcd
   namespace: statefulset
 spec:
   serviceName: etcd
   replicas: 3
   selector:
     matchLabels:
       app: etcd
   template:
     metadata:
       labels:
         app: etcd
     spec:
       containers:
       - name: etcd
         image: registry.aliyuncs.com/google_containers/etcd:3.5.10-0
         command:
         - /usr/local/bin/etcd
         ports:
         - containerPort: 2379
           name: client
         - containerPort: 2380
           name: peer
         env:
         - name: ETCD_NAME
           valueFrom:
             fieldRef:
               fieldPath: metadata.name
         - name: ETCD_DATA_DIR
           value: /var/lib/etcd
         - name: ETCD_INITIAL_ADVERTISE_PEER_URLS
           value: "http://$(ETCD_NAME).etcd.statefulset.svc.cluster.local:2380"
         - name: ETCD_LISTEN_PEER_URLS
           value: "http://0.0.0.0:2380"
         - name: ETCD_LISTEN_CLIENT_URLS
           value: "http://0.0.0.0:2379"
         - name: ETCD_ADVERTISE_CLIENT_URLS
           value: "http://$(ETCD_NAME).etcd.statefulset.svc.cluster.local:2379"
         - name: ETCD_INITIAL_CLUSTER
           value: "etcd-0=http://etcd-0.etcd.statefulset.svc.cluster.local:2380,etcd-1=http://etcd-1.etcd.statefulset.svc.cluster.local:2380,etcd-2=http://etcd-2.etcd.statefulset.svc.cluster.local:2380"
         - name: ETCD_INITIAL_CLUSTER_TOKEN
           value: "etcd-token"
         - name: ETCD_INITIAL_CLUSTER_STATE
           value: "new"
         volumeMounts:
         - name: etcd-data
           mountPath: /var/lib/etcd
         resources:
           requests:
             cpu: 100m
             memory: 256Mi
           limits:
             cpu: 500m
             memory: 512Mi
   volumeClaimTemplates:
   - metadata:
       name: etcd-data
     spec:
       accessModes: [ "ReadWriteOnce" ]
       storageClassName: "etcd-storage"
       resources:
         requests:
           storage: 10Gi
 EOF
 ​
 # 创建 statefulset
 [root@master30 ~ 16:40:50]# kubectl apply -f etcd-statefulset.yaml
 statefulset.apps/etcd created
 ​
 # 查看 pods
 [root@master30 ~ 16:41:11]# kubectl get pods
 NAME     READY   STATUS              RESTARTS   AGE
 etcd-0   0/1     ContainerCreating   0          5s
 [root@master30 ~ 16:41:16]# kubectl get pods
 NAME     READY   STATUS    RESTARTS   AGE
 etcd-0   1/1     Running   0          19s
 etcd-1   1/1     Running   0          9s
 etcd-2   0/1     Pending   0          1s
 [root@master30 ~ 16:41:30]# kubectl get pods
 NAME     READY   STATUS    RESTARTS   AGE
 etcd-0   1/1     Running   0          26s
 etcd-1   1/1     Running   0          16s
 etcd-2   1/1     Running   0          8s
 ​
 ​
 # 预期输出:Pod 按 etcd-0、etcd-1、etcd-2顺序创建
 # 先创建 etcd-0,处于 Running 状态
 # 再创建 etcd-1,处于 Running 状态
 # 再创建 etcd-2,处于 Running 状态
 # 最终 3 个 Pod 均为 Running 状态。
 ​
 # 查看 statefulset
 [root@master30 ~ 16:41:37]# kubectl get sts
 NAME   READY   AGE
 etcd   3/3     43s
 ​
步骤 5:验证部署

验证集群状态

 # 验证集群状态:建立集群
 [root@master30 ~ 16:41:54]# kubectl  exec  etcd-2  -- etcdctl member list 
 8747632212a8465, started, etcd-2, http://etcd-2.etcd.statefulset.svc.cluster.local:2380, http://etcd-2.etcd.statefulset.svc.cluster.local:2379, false
 881e7c876792f042, started, etcd-0, http://etcd-0.etcd.statefulset.svc.cluster.local:2380, http://etcd-0.etcd.statefulset.svc.cluster.local:2379, false
 b6d311da39fc1138, started, etcd-1, http://etcd-1.etcd.statefulset.svc.cluster.local:2380, http://etcd-1.etcd.statefulset.svc.cluster.local:2379, false
 ​
 ​
 # 删除 etcd-2,等待重新创
 [root@master30 ~ 16:44:26]# kubectl  delete pod etcd-2 
 pod "etcd-2" deleted
 ​
 # 验证集群状态:删了 etcd-2 Pod,但 member list 里节点信息完全没变
 [root@master30 ~ 16:44:49]# kubectl  exec  etcd-0 -- etcdctl member list
 8747632212a8465, started, etcd-2, http://etcd-2.etcd.statefulset.svc.cluster.local:2380, http://etcd-2.etcd.statefulset.svc.cluster.local:2379, false
 881e7c876792f042, started, etcd-0, http://etcd-0.etcd.statefulset.svc.cluster.local:2380, http://etcd-0.etcd.statefulset.svc.cluster.local:2379, false
 b6d311da39fc1138, started, etcd-1, http://etcd-1.etcd.statefulset.svc.cluster.local:2380, http://etcd-1.etcd.statefulset.svc.cluster.local:2379, false
 ​
 ​
 #1. 先分清两个概念(核心关键点)
 ① StatefulSet 的 Pod
 etcd-2 是 StatefulSet 管理的 Pod,执行 kubectl delete pod etcd-2:
 - K8s 会立刻新建一个同名 Pod etcd-2,网络标识、Pod hostname 完全不变;
 - 只是底层容器进程重启,etcd 集群成员配置没有任何改动。
 ② etcd 集群 Member(成员记录)
 etcdctl member list 展示的是 etcd 数据库内持久保存的集群成员清单,存在 etcd 数据盘里,不是实时 Pod 状态。
 - 只有执行 etcdctl member remove <节点ID>,才会从集群里彻底删掉这条成员记录;
 - 单纯删 Pod、重启 Pod,不会修改 etcd 内部的成员列表。
 ​
 #2. 你操作的完整流程拆解
 1. 初始状态
 3 个 etcd 成员:etcd-0 /etcd-1 /etcd-2,全部 started,集群健康。
 2. 执行 kubectl delete pod etcd-2
 StatefulSet 控制器检测副本数不足,马上调度创建新 Pod etcd-2:
 - Pod 名称、hostname、svc 域名 etcd-2.etcd.statefulset.svc.cluster.local 和原来完全一致;
 - 新 Pod 启动后,直接使用原有 etcd 成员 ID 加入集群。
 ​
 #3. 再次执行 etcdctl member list
 etcd 内部成员记录没被删除,所以三条记录原样输出,etcd-2 那条依然存在、状态 started。
 3. 为什么不会消失?
 1. StatefulSet 有稳定网络身份,删除 Pod 只是重启实例,不是集群缩容;
 2. etcd 的 member 列表独立于 Pod 生命周期,只记录集群注册过的节点;
 3. 只要新建 Pod 能连上原有域名、原有数据目录,就复用原来的 member ID。

验证数据

 [root@master30 ~ 16:45:49]# kubectl  exec  etcd-0 -- etcdctl put /users/user1/name zy
 OK
 ​
 #输出 OK,代表数据写入成功、集群已完成数据持久化。
 ​
 ​
 #- kubectl exec etcd-0:进入集群中 etcd-0 节点 的容器内部执行命令
 #- --:kubectl 参数分隔符,后面内容为容器内执行的原生命令
 #- etcdctl put:etcd 原生写入指令,用于新增/更新 kv 键值对数据
 #- /users/user1/name:etcd 自定义键(key),支持类文件系统层级路径格式,方便分类管理数据
 #- laowang:对应 key 的值(value),即存储的具体数据
 ​
 ​
 #- etcdctl get:etcd 原生读取指令,根据指定 key 查询对应存储值
 [root@master30 ~ 17:52:07]# kubectl  exec  etcd-2 -- etcdctl get  /users/user1/name zy
 /users/user1/name
 zy
 ​
 #核心底层逻辑(重点)
 #你仅在 etcd-0 节点写入数据,但 etcd 是分布式一致性集群:写入数据后,集群自动通过 Raft 协议同步数据到 etcd-1、etcd-2 所有节点,三个节点的底层数据完全一致,不存在单节点数据隔离。

步骤 6:清理环境
 [root@master30 ~ 17:53:20]# kubectl delete sts etcd
 statefulset.apps "etcd" deleted
 [root@master30 ~ 17:53:33]# kubectl delete svc etcd 
 service "etcd" deleted
 [root@master30 ~ 17:53:37]# kubectl delete pvc etcd-data-etcd-{0..2}
 persistentvolumeclaim "etcd-data-etcd-0" deleted
 persistentvolumeclaim "etcd-data-etcd-1" deleted
 persistentvolumeclaim "etcd-data-etcd-2" deleted
 [root@master30 ~ 17:53:41]# kubectl delete pv etcd-pv-{0..2}
 persistentvolume "etcd-pv-0" deleted
 persistentvolume "etcd-pv-1" deleted
 persistentvolume "etcd-pv-2" deleted
 [root@master30 ~ 17:53:44]# rm -fr /shares/etcd
 ​

示例 3:部署 Redis 集群

实验整体架构与核心目标
1. 需求定位

搭建1 主 2 从 Redis 哨兵高可用集群,不是 Redis Cluster 分片集群,是主从 + 哨兵故障自动转移架构; 依托 K8s StatefulSet 固定网络身份、Headless Service 稳定域名、NFS 持久存储实现:

  1. 固定 Pod 域名:redis-0/1/2.redis.statefulset.svc.cluster.local,哨兵可稳定识别节点

  2. AOF 持久化,宕机数据不丢失

  3. 主节点故障哨兵自动选举新主,自动切换读写

  4. 资源配额限制,生产级容器资源管控

  5. PV/PVC 绑定 NFS 存储,数据持久落盘宿主机共享存储

2. 组件分工
组件 作用
Headless Service(redis) 无 ClusterIP,为 StatefulSet 提供固定 DNS 域名,Pod 之间通过域名互通
PV(redis-pv-0/1/2) 预创建 NFS 存储卷,对应 3 个独立数据目录,静态供给 PVC
StatefulSet redis 有序创建 Pod redis-0、redis-1、redis-2,有序删除;绑定固定 PVC
Redis-Server 主从复制:redis-0 默认主,1、2 自动设为从;开启 AOF 持久化、访问密码
Redis-Sentinel 3 节点哨兵集群,quorum=2,主节点故障自动故障转移
NFS 存储 所有 Redis 数据落地共享存储,Pod 重建数据不丢失
3. 关键区分(容易踩坑)
  • 本实验:Redis 主从 + Sentinel 高可用(主从复制,故障自动切主,无 hash 分片)

  • Redis Cluster 集群:16384 槽分片,多主多从,数据分散存储,本实验不涉及分片槽位

需求:部署 3 节点 Redis 集群(1 主 2 从),实现固定网络标识、数据持久化、集群自动组网,支持数据分片与故障自动切换,适用于高并发分布式缓存场景。

步骤 1:准备 NFS 存储
 # 准备应用目录
 [root@master30 ~ 17:53:49]# mkdir  -m 777 -p /shares/redis/data-{0..2}
 ​
 1. -m 777:NFS 客户端读写权限放开,避免 Redis 容器权限不足无法写入 AOF/RDB
 2. data-0/1/2:3 个独立目录,分别对应 3 个 PV,每个 Redis 节点独占存储目录,数据隔离
 3. 前置依赖:服务器已部署 NFS 服务,共享路径/shares/redis对外开放
步骤 2:创建 Headless Service

创建 redis-service.yaml,为后续 StatefulSet 管理的 Pod 提供固定名称。

 [root@master30 ~ 18:13:14]#  cat > redis-service.yaml <<'EOF'
 apiVersion: v1
 kind: Service
 metadata:
   name: redis
   namespace: statefulset
   labels:
     app: redis
 spec:
   ports:
   - port: 6379    #- 6379:Redis 主从服务端口,数据读写、主从复制
     name: redis 
   - port: 26379   #- 26379:Sentinel 哨兵端口,哨兵之间互相通信、监控主节点
     name: sentinel
   clusterIP: None     #核心配置:无头服务,不分配集群 IP,K8s 自动为每个 StatefulSet Pod 生成唯一 DNS 域名:
   selector:
     app: redis
 EOF
 ​
 [root@master30 ~ 18:13:30]# kubectl apply -f redis-service.yaml
 service/redis created
 [root@master30 ~ 18:13:50]# kubectl get svc
 NAME    TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)              AGE
 redis   ClusterIP   None         <none>        6379/TCP,26379/TCP   4s
 ​
步骤 3:创建 PV

创建 3 个 PV,创建 redis-pv.yaml

 [root@master30 ~ 18:13:54]# cat > redis-pv.yaml <<'EOF'
 apiVersion: v1
 kind: PersistentVolume
 metadata:
   name: redis-pv-0
 spec:
   capacity:
     storage: 10Gi
   accessModes:
   - ReadWriteOnce
   persistentVolumeReclaimPolicy: Retain
   storageClassName: redis-storage
   nfs:
     server: 10.1.8.30
     path: /shares/redis/data-0
 ---
 apiVersion: v1
 kind: PersistentVolume
 metadata:
   name: redis-pv-1
 spec:
   capacity:
     storage: 10Gi
   accessModes:
   - ReadWriteOnce
   persistentVolumeReclaimPolicy: Retain
   storageClassName: redis-storage
   nfs:
     server: 10.1.8.30
     path: /shares/redis/data-1
 ---
 apiVersion: v1
 kind: PersistentVolume
 metadata:
   name: redis-pv-2
 spec:
   capacity:
     storage: 10Gi
   accessModes:
   - ReadWriteOnce
   persistentVolumeReclaimPolicy: Retain
   storageClassName: redis-storage
   nfs:
     server: 10.1.8.30
     path: /shares/redis/data-2
 EOF
 ​
 #1. 每个 PV 绑定独立 NFS 路径:/shares/redis/data-0 ~ data-2,一一对应 redis-0/1/2
 #2. storageClassName: redis-storage:与 StatefulSet 里 PVC 存储类匹配,实现自动绑定
 #3. accessModes: ReadWriteOnce:NFS 单节点读写(本实验每个 Pod 独占一个 NFS 目录,无需多写)
 #4. persistentVolumeReclaimPolicy: Retain:删除 PVC 后 PV 保留,数据不自动删除,防止误删丢失数据
 #5. 容量 10Gi:为 Redis 缓存 + 持久化文件预留存储空间
 ​
 ​
 ​
 ​
 [root@master30 ~ 18:14:09]# kubectl apply -f redis-pv.yaml
 persistentvolume/redis-pv-0 created
 persistentvolume/redis-pv-1 created
 persistentvolume/redis-pv-2 created
 [root@master30 ~ 18:14:30]#  kubectl get pv
 NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM                            STORAGECLASS    VOLUMEATTRIBUTESCLASS   REASON   AGE
 redis-pv-0                                 10Gi       RWO            Retain           Available                                    redis-storage   <unset>                          3s
 redis-pv-1                                 10Gi       RWO            Retain           Available                                    redis-storage   <unset>                          3s
 redis-pv-2                                 10Gi       RWO            Retain           Available                                    redis-storage   <unset>                          3s
 ​
步骤 4:创建 StatefulSet

创建 redis-statefulset.yaml,部署 3 个 Redis 节点集群,实现集群自动组网。

 [root@master30 ~ 18:14:33]# cat > redis-statefulset.yaml <<'EOF'
 apiVersion: apps/v1
 kind: StatefulSet
 metadata:
   name: redis
   namespace: statefulset
 spec:
   serviceName: redis
   replicas: 3
   selector:
     matchLabels:
       app: redis
   template:
     metadata:
       labels:
         app: redis
     spec:
       containers:
       - name: redis
         image: redis:7.0-alpine
         ports:
         - containerPort: 6379
           name: redis
         - containerPort: 26379
           name: sentinel
         command: ["/bin/sh", "-c"]
         args:
         - |
           NODE_ID=$(hostname | awk -F'-' '{print $2}')
           cat > /data/redis.conf << EOF
           bind 0.0.0.0
           protected-mode no
           port 6379
           dir /data
           appendonly yes
           requirepass "redis123"
           masterauth "redis123"
           EOF
           if [ "$NODE_ID" != "0" ]; then
             echo "replicaof redis-0.redis.statefulset.svc.cluster.local 6379" >> /data/redis.conf
           fi
           redis-server /data/redis.conf &
           
           # 等待主节点域名解析成功
           until nslookup redis-0.redis.statefulset.svc.cluster.local; do
             echo "Waiting for DNS resolution..."
             sleep 2
           done
           
           cat > /data/sentinel.conf << EOF
           bind 0.0.0.0
           protected-mode no
           port 26379
           sentinel resolve-hostnames yes
           sentinel announce-hostnames yes
           sentinel monitor mymaster redis-0.redis.statefulset.svc.cluster.local 6379 2
           sentinel auth-pass mymaster redis123
           sentinel down-after-milliseconds mymaster 5000
           sentinel failover-timeout mymaster 10000
           EOF
           redis-sentinel /data/sentinel.conf
         volumeMounts:
         - name: redis-data
           mountPath: /data
         resources:
           requests:
             cpu: 100m
             memory: 128Mi
           limits:
             cpu: 500m
             memory: 512Mi
   volumeClaimTemplates:
   - metadata:
       name: redis-data
     spec:
       accessModes: [ "ReadWriteOnce" ]
       storageClassName: "redis-storage"
       resources:
         requests:
           storage: 10Gi
 EOF
 ​
 ​
 [root@master30 ~ 18:16:58]#  kubectl apply -f redis-statefulset.yaml
 statefulset.apps/redis created
 ​
 [root@master30 ~ 18:25:16]# kubectl  get  pods 
 NAME      READY   STATUS    RESTARTS   AGE
 redis-0   1/1     Running   0          36s
 redis-1   1/1     Running   0          24s
 redis-2   1/1     Running   0          14s
 ​
 ​
 [root@master30 ~ 18:25:45]# kubectl get sts
 NAME    READY   AGE
 redis   3/3     55s
 ​
步骤 5:验证部署

验证主从状态(redis-0 为主,2 个从节点)

 #验证主从复制 info replication
 [root@master30 ~ 18:26:04]# kubectl  exec -it redis-0 -- redis-cli -a redis123  info replication
 Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
 # Replication
 role:master
 connected_slaves:2
 slave0:ip=10.224.170.15,port=6379,state=online,offset=18757,lag=0
 slave1:ip=10.224.170.16,port=6379,state=online,offset=18584,lag=1
 master_failover_state:no-failover
 master_replid:b6f81a444581d1616f718971d474f2bad0dfe459
 master_replid2:0000000000000000000000000000000000000000
 master_repl_offset:19101
 second_repl_offset:-1
 repl_backlog_active:1
 repl_backlog_size:1048576
 repl_backlog_first_byte_offset:1
 repl_backlog_histlen:19101
 ​

验证哨兵状态(能识别主节点)

 #哨兵状态验证 sentinel master mymaster
 [root@master30 ~ 18:28:19]# kubectl  exec  -it redis-0 -n statefulset -- redis-cli -p 26379 sentinel master mymaster
  1) "name"
  2) "mymaster"
  3) "ip"
  4) "redis-0.redis.statefulset.svc.cluster.local"
  5) "port"
  6) "6379"
  7) "runid"
  8) "e485d2b2a949be1302b9fb60e9bc7256fd72e93c"
  9) "flags"
 10) "master"
 11) "link-pending-commands"
 12) "0"
 13) "link-refcount"
 14) "1"
 15) "last-ping-sent"
 16) "0"
 17) "last-ok-ping-reply"
 18) "33"
 19) "last-ping-reply"
 20) "33"
 21) "down-after-milliseconds"
 22) "5000"
 23) "info-refresh"
 24) "2758"
 25) "role-reported"
 26) "master"
 27) "role-reported-time"
 28) "173403"
 29) "config-epoch"
 30) "0"
 31) "num-slaves"
 32) "2"
 33) "num-other-sentinels"
 34) "2"
 35) "quorum"
 36) "2"
 37) "failover-timeout"
 38) "10000"
 39) "parallel-syncs"
 40) "1"
 ​
 #- num-other-sentinels:2:当前节点能发现另外 2 个哨兵,集群完整
 #- quorum:2:仲裁数量,满足故障切换条件
 #- num-slaves:2:哨兵识别 2 台从节点
 #- ip 字段:主节点使用域名而非 IP,得益于resolve-hostnames yes
 ​

验证集群数据分片与主从同步:

 # 在 redis-0(主节点)设置测试数据,主节点 set 写入 key,从节点 get 读取,证明主从复制链路正常,数据实时同步。
 [root@master30 ~ 18:29:32]# kubectl  exec -it redis-1 -- redis-cli -a redis123 get test-key "redis-cluster-test"
 OK
 ​
 ​
 ## 在 redis-1(从节点)查看测试数据
 [root@master30 ~ 18:29:58]# kubectl  exec -it redis-1 -- redis-cli -a redis123 get test-key 
 "redis-cluster-test"
 ​

验证集群故障切换。

 # 删除一个主节点(如 redis-0),模拟节点故障
 root@master30:~# kubectl delete pod redis-0
 ​
 # 等待 redis-0 重新创建,查看集群状态:redis-0 的 role 变更为 slave
 [root@master30 ~ 18:32:10]# kubectl  exec  -it redis-0 -- redis-cli -a redis123 info replication
 Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
 # Replication
 role:slave
 master_host:10.224.170.15
 master_port:6379
 master_link_status:up
 master_last_io_seconds_ago:0
 master_sync_in_progress:0
 slave_read_repl_offset:106962
 slave_repl_offset:106962
 slave_priority:100
 slave_read_only:1
 replica_announced:1
 connected_slaves:0
 master_failover_state:no-failover
 master_replid:66ac381d832ca07c34a2a327a745e0f65dbd9890
 master_replid2:0000000000000000000000000000000000000000
 master_repl_offset:106962
 second_repl_offset:-1
 repl_backlog_active:1
 repl_backlog_size:1048576
 repl_backlog_first_byte_offset:105654
 repl_backlog_histlen:1309
 ​
 ​
 #1. Pod 被删除,StatefulSet 控制器自动重建 redis-0;
 #2. redis-0 宕机,3 个哨兵 5 秒后判定主节点主观下线;
 #3. 超过 quorum=2,触发故障转移,在 redis-1/redis-2 中选举新主;
 #4. 新主取消 replicaof,自动接受写入;
 #5. 重建后的 redis-0 启动后,哨兵推送新主地址,redis-0 自动变为从节点;
 #6. 执行info replication可见role:slave,master_host 为新主 PodIP,证明切换生效。
步骤 6:清理环境
 [root@master30 ~ 18:32:46]#  kubectl delete svc redis
 service "redis" deleted
 [root@master30 ~ 18:33:07]# kubectl delete sts redis
 statefulset.apps "redis" deleted
 [root@master30 ~ 18:33:10]# kubectl delete pvc redis-data-redis-{0..2} 
 persistentvolumeclaim "redis-data-redis-0" deleted
 persistentvolumeclaim "redis-data-redis-1" deleted
 persistentvolumeclaim "redis-data-redis-2" deleted
 [root@master30 ~ 18:33:41]# kubectl delete pv redis-pv-{0..2} 
 persistentvolume "redis-pv-0" deleted
 persistentvolume "redis-pv-1" deleted
 persistentvolume "redis-pv-2" deleted
 [root@master30 ~ 18:33:44]# rm -fr /shares/redis/data-*/*
 ​

步骤七:实验核心知识点总结
1. StatefulSet 对比 Deployment 优势(本实验刚需)
  1. 有序创建 / 删除 Pod,保证主节点先启动

  2. 稳定网络身份(固定域名),哨兵必须依赖稳定标识监控主节点

  3. 独立持久存储,每个 Pod 独占 PVC/PV,重建不丢失数据 Deployment 无固定域名,Pod IP 随机变化,无法用于 Redis 哨兵集群。

2. Headless Service 核心价值

普通 Service 分配统一 ClusterIP,流量负载均衡; Headless 关闭 ClusterIP,直接返回所有 Pod A 记录,实现 Pod 点对点域名互通,是中间件集群标准搭配。

3. Redis 哨兵高可用核心条件
  1. 哨兵数量≥3,quorum = 哨兵数量 / 2 向上取整(3 哨兵 quorum=2)

  2. 开启域名解析支持sentinel resolve-hostnames yes(K8s 环境必开)

  3. 主从统一密码requirepass/masterauth,否则同步 / 监控失败

  4. 持久化 AOF 开启,故障切换后数据完整

4. NFS 持久化作用

Pod 重建、节点驱逐、故障切换时,容器销毁但 NFS 目录数据留存,Redis 重启后加载原有 AOF 文件,不会丢失缓存数据。

StatefulSet 总结

  • 生产环境中,敏感信息(如 MySQL 根密码、Redis 密码)需使用 Kubernetes Secret 管理,禁止明文写在 YAML 文件中,避免安全风险。

  • StatefulSet 缩容时,严格按序号从大到小删除 Pod,若需保留数据,建议提前备份 PV 中的数据,避免误操作导致数据丢失。

  • 修改 StatefulSet 的 PVC 模板(如调整存储容量、修改 storageClassName)后,直接执行 kubectl apply 不会生效,需删除现有 PVC 和 Pod,重新创建(数据会保留,因 PV 为 Retain 策略)。

  • 存储选型建议:当前已改为NFS存储(共享路径/shares/,适配WordPress+MySQL 5.7部署),测试环境可直接使用该NFS配置;生产环境建议选用高可用NFS集群或分布式存储(如Ceph),避免NFS单点故障,确保WordPress和MySQL 5.7数据高可用。

  • StatefulSet 不支持动态修改副本数以外的核心配置(如 serviceName、PVC 模板),若需修改,需删除 StatefulSet 后重新创建(Pod 和 PVC 可保留,数据不丢失)。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐