Kubernetes Dashboard、动态卷供应、StatefulSet

Kubernetes 集群 Dashboard

Kubernetes Dashboard

介绍

项目地址:https://github.com/kubernetes/dashboard/

Kubernetes Dashboard 是 Kubernetes 的官方 Web UI。

Kubernetes Dashboard 具备的功能:

  • 向 Kubernetes 集群部署容器化应用
  • 诊断容器化应用的问题
  • 管理集群的资源
  • 查看集群上所运行的应用程序
  • 创建、修改Kubernetes 上的资源(例如 Deployment、Job、DaemonSet等)
  • 展示集群上发生的错误

例如:您可以伸缩一个 Deployment、执行滚动更新、重启一个 Pod 或部署一个新的应用程序。

部署

bash 复制代码
# 下载资源yaml文件
root@master30~ 14:10:07# wget https://raw.githubusercontent.com/kubernetes/dashboard/v2.6.1/aio/deploy/recommended.yaml

# 查看images
root@master30~ 14:10:22# grep image: recommended.yaml
          image: kubernetesui/dashboard:v2.6.1
          image: kubernetesui/metrics-scraper:v1.0.8


# 执行部署
root@master30~ 14:11:53# kubectl apply -f recommended.yaml

验证

bash 复制代码
root@master30~ 14:12:05# kubens
auth
default
ingress-nginx
kube-node-lease
kube-public
kube-system
kubernetes-dashboard
metallb-system
quota
scheduler


root@master30~ 14:14:18# kubens kubernetes-dashboard
root@master30~ 14:14:57# kubectl get all
NAME                                             READY   STATUS    RESTARTS   AGE
pod/dashboard-metrics-scraper-795895d745-rn8qv   1/1     Running   0          71s
pod/kubernetes-dashboard-6c8b589b99-sfp9x        1/1     Running   0          71s

NAME                                TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/dashboard-metrics-scraper   ClusterIP   10.96.166.241   <none>        8000/TCP   71s
service/kubernetes-dashboard        ClusterIP   10.103.0.21     <none>        443/TCP    72s

NAME                                        READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/dashboard-metrics-scraper   1/1     1            1           71s
deployment.apps/kubernetes-dashboard        1/1     1            1           71s

NAME                                                   DESIRED   CURRENT   READY   AGE
replicaset.apps/dashboard-metrics-scraper-795895d745   1         1         1       71s
replicaset.apps/kubernetes-dashboard-6c8b589b99        1         1         1       71s

root@master30~ 14:25:39# kubectl get sa
NAME                   SECRETS   AGE
default                0         11m
kubernetes-dashboard   0         11m

root@master30~ 14:25:44# 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>
Events:              <none>

# 创建了clusterrolebindings/kubernetes-dashboard
root@master30~ 14:26:11# kubectl get clusterrolebindings.rbac.authorization.k8s.io |grep dashboard
kubernetes-dashboard                                            ClusterRole/kubernetes-dashboard                                                   14m


# sa/kubernetes-dashboard具有ClusterRole/kubernetes-dashboard
root@master30~ 14:26:25# 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


# ClusterRole/kubernetes-dashboard权限如下:
root@master30~ 14:26: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]


# deployments.apps/kubernetes-dashboard以sa/kubernetes-dashboard身份运行,所以deployments.apps/kubernetes-dashboard部署的pod只可以查看资源nodes.metrics.k8s.io和pods.metrics.k8s.io
root@master30~ 14:27:09# kubectl get deployments.apps kubernetes-dashboard -o yaml|grep ' serviceAccount'
      serviceAccount: kubernetes-dashboard
      serviceAccountName: kubernetes-dashboard

修改 service/kubernetes-dashboard 类型为 NodePort,从外部访问。

bash 复制代码
root@master30~ 14:17:33# kubectl edit svc kubernetes-dashboard
spec:
......
  type: NodePort

root@master30~ 14:18:51# kubectl get svc
NAME                        TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)         AGE
dashboard-metrics-scraper   ClusterIP   10.96.166.241   <none>        8000/TCP        4m47s
kubernetes-dashboard        NodePort    10.103.0.21     <none>        443:32204/TCP   4m48s

使用ingress配置访问,参考:

yaml 复制代码
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-dashboard
namespace: kubernetes-dashboard
annotations:
# 关键:透传 TLS,Ingress 不解密
nginx.ingress.kubernetes.io/ssl-passthrough: "true"
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
spec:
ingressClassName: nginx
rules:
  - host: dashboard.hgq.cloud
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service: 
            name: kubernetes-dashboard
            port: 
              number: 443

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

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

访问 Dashboard

访问 https://10.1.8.30:32204

新版本

Kubernetes Dashboard 新版本只支持使用 Bearer Token登录

Kubernetes Dashboard 默认部署时,只配置了最低权限的 RBAC。因此,我们要创建一个名为 admin-user 的 ServiceAccount,再创建一个 ClusterRolebinding,将其绑定到 Kubernetes 集群中默认初始化的 cluster-admin 这个 ClusterRole。

bash 复制代码
root@master30~ 14:19:05# cat > kubernetes-dashboard-admin-user.yaml <<'EOF'
apiVersion: v1
kind: ServiceAccount
metadata:
  name: admin-user
  namespace: kubernetes-dashboard
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: admin-user
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: admin-user
  namespace: kubernetes-dashboard
EOF
yaml 复制代码
root@master30~ 14:19:58# kubectl apply -f kubernetes-dashboard-admin-user.yaml

# 创建 Bearer Token
root@master30~ 14:20:05# kubectl -n kubernetes-dashboard create token admin-user
eyJhbGciOiJSUzI1NiIsImtpZCI6IllwMXBpWm9vSnpQUVN5MkRtZVJDSUJQUnNiOXozdDNGUWJTWFEwdEtwb0EifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzgyOTc2ODEyLCJpYXQiOjE3ODI5NzMyMTIsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiMDI5OGU3NTctOGFhMi00M2JmLTk2YzMtMzAwN2VhNTYwZjQ4Iiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsInNlcnZpY2VhY2NvdW50Ijp7Im5hbWUiOiJhZG1pbi11c2VyIiwidWlkIjoiOGNjYWIzMGUtOGRmOC00Zjk3LTg3ZmQtMGQ3MmFmMTFkZTg5In19LCJuYmYiOjE3ODI5NzMyMTIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDprdWJlcm5ldGVzLWRhc2hib2FyZDphZG1pbi11c2VyIn0.DR-z85-HlvBg6p9eJpfHzEPXf3z-RthDK6aJB3ZfvO29NbJlWs4JA63iJLwAyDntFnUoTl2zH58pMscnbF0U5yXy7kB0KrMUNnxPef5SsPdDetY6UDuA4dBt1os0Q5AXDDnqWgnz3OreYNrXJaDMgYgodtDKLgH9EjDPyChwHeDdGNV-hrwFIurIc1ey0_9gDaIPata9agX4gmtZhzao302ZZJb5_MRyDzd9JLra1Ci7O5y_8NHq3tfwmfuJr6u1-qoetvjH869Il799ff5z2yBaaB8kdntl3p6DwnYm__Zx1GxLdfnAI4cd32TmCbidGC2G1zuVS09xa2V8zphP7w

提示:Token默认有效期1小时,过期后自动删除。下一次登录需要生成新的Token。

旧版本

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

获取token过程如下:

bash 复制代码
# 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 使用案例

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

bash 复制代码
# Kubernetes Dashboard 以deployment形式运行
root@master30~ 14:20:12# kubectl get deployments.apps
NAME                        READY   UP-TO-DATE   AVAILABLE   AGE
dashboard-metrics-scraper   1/1     1            1           16m
kubernetes-dashboard        1/1     1            1           16m

# 以 serviceAccount: kubernetes-dashboard 身份运行
root@master30~ 14:31:08# kubectl get deployments.apps kubernetes-dashboard -o yaml|grep serviceAc
      serviceAccount: kubernetes-dashboard
      serviceAccountName: kubernetes-dashboard

# serviceAccount: kubernetes-dashboard 绑定了Role/kubernetes-dashboard 
root@master30~ 14:31:36# kubectl get rolebindings.rbac.authorization.k8s.io
NAME                   ROLE                        AGE
kubernetes-dashboard   Role/kubernetes-dashboard   17m


# Role/kubernetes-dashboard 具备能力如下:
root@master30~ 14:32:02# 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]

  
  
# 创建 serviceAccount: kubernetes-dashboard Token
root@master30~ 14:32:20# kubectl -n kubernetes-dashboard create token kubernetes-dashboard
eyJhbGciOiJSUzI1NiIsImtpZCI6IllwMXBpWm9vSnpQUVN5MkRtZVJDSUJQUnNiOXozdDNGUWJTWFEwdEtwb0EifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzgyOTc3NTYxLCJpYXQiOjE3ODI5NzM5NjEsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiYjkyNzYyMzEtMWU0MS00MWE5LTg2ODktYWVlOTgwZmMzMWE0Iiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsInNlcnZpY2VhY2NvdW50Ijp7Im5hbWUiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsInVpZCI6Ijk4MTRmZTZmLTZhNmMtNDU4MC1iM2IxLTRkMjNlMWU4MmRiOSJ9fSwibmJmIjoxNzgyOTczOTYxLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZXJuZXRlcy1kYXNoYm9hcmQ6a3ViZXJuZXRlcy1kYXNoYm9hcmQifQ.X9U1ogJKv8Buwb8GWgG7Sv-cflIhBSFaW4B9avNwcmsZGVIx01PwKUtKjcBt9NC4oQmpZaQYSVX5foPcOdWYVIXS2vCX5fwjJ4xs4z5iZmDjDzzem1yjnbTzGu5nN23qSpr2q6zfLZkParJhY-HXJRfNoNALhRwcJ6GU8orbFCPL3CfevlrpWmCqYtrKqSpWEF3fEqrLRXXIKiW3SyahjGN5Re0F79ULD798sruHhhdn-aL_8Kz8TzODyEPhQUcw9mLSTJeiBvQhlWs47T-gY346o5SLy52iO37kMKFs9_V1Z0_JyFXdMkCR4pf5oeP7qo27uKywqrFv4Ku_9ck8FA

使用 serviceAccount: kubernetes-dashboard Token 登录

无法看到Pods。

Kubernetes 动态卷供应

动态卷介绍

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

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

动态卷供应流程

  1. 管理员先创建 "存储模板 "(StorageClass),指定用哪个 "造卷工具"(Provisioner,制备器)来创建卷;
  2. 用户创建 "存储申请单"(PVC)时,指定要用哪个 "存储模板";
  3. 系统收到申请后,会让模板绑定的 "造卷工具" 自动创建一个 PV,并且把 PV 和用户的 PVC 绑定,用户直接用 PVC 就行。

详细流程:

  1. 创建 PVC(用户发起)。
  2. 匹配 StorageClass (Kubernetes 根据 storageClassName 查找或使用默认 StorageClass)。
  3. Provisioner 读取 parameters(但这只是准备阶段)。
  4. 🌐 调用后端存储 API 创建真实卷这是最关键的一步 !Provisioner 带着 parameters 参数,实际调用 AWS、Ceph 或 NFS 服务器的 API,在底层物理/逻辑存储中划出一块真实的存储空间)。
  5. 📄 自动生成对应的 PV 对象(底层存储创建成功后,Provisioner 会在 K8s 集群中创建一个 PV 资源对象,用来描述刚才创建好的那块存储)。
  6. 🔗 绑定 (Kubernetes 控制平面将新生成的 PV 与用户的 PVC 进行一一绑定,状态变为 Bound)。

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 回收策略,指定 PV 不用怎么处理 PV。

    • Delete(默认):PVC 删除后,PV 也自动删,对应的后端存储(比如云硬盘、NFS 目录)也会清掉;
    • Retain:PVC 删除后,PV 保留下来,后端存储的数据也不删,管理员可以手动处理。
  • volumeBindingMode:PVC 创建出来后,指定 PVC 与P V 绑定时机。

    • Immediate(立即绑定):创建 PVC 就造卷,不管 Pod 会不会用;适合存储能被所有节点访问的场景(比如云硬盘);
    • WaitForFirstConsumer(等使用者):先不造卷,等第一个用这个 PVC 的 Pod 创建后,根据 Pod 要跑的节点造卷;适合本地存储、只能被特定节点访问的存储。
  • mountOptions:指定 PV 挂载时附加的 "额外设置",比如权限、调试模式。

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

  • parameters :给底层存储驱动(Provisioner,供应器) 传递自定义参数,用于控制动态创建 PV 的行为、存储介质特性。

基础示例

yaml 复制代码
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 是 K8s 官方推荐的本地存储动态卷插件,适合需要使用节点本地磁盘的场景。

部署本地路径制备器

bash 复制代码
# 下载官方模板
root@master30~ 15:14:36# wget https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml

# 查看资源配置
root@master30~ 15:14:47# cat local-path-storage.yaml
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
#WaitForFirstConsumer即不提前创建 PV,等第一个使用 PVC 的 Pod 调度完成后,再创建本地 PV 并绑定到 Pod 所在节点。
#对比默认 Immediate:创建 PVC 立刻生成 PV,本地 PV 会随机绑定一个节点,后续 Pod 只能跑该节点
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

这份 YAML 中各个组件的关系可以总结为以下链条:

ServiceAccount (服务账户)RBAC (权限)Deployment (Provisioner 进程)ConfigMap (配置)

同时,StorageClass (存储类) 通过 provisioner 字段指向 Deployment 中的 Provisioner。

具体对应关系如下:

资源 A 关联方式 资源 B 作用说明
StorageClass (local-path) provisioner: rancher.io/local-path Deployment (容器镜像中的程序) StorageClass 声明由谁(哪个驱动)来创建卷,Deployment 中的程序就是干活的"工人"。
Deployment (Pod) serviceAccountName ServiceAccount Provisioner 容器运行时使用的身份,决定了它有没有权限操作 K8s 资源。
ServiceAccount RoleBinding + ClusterRoleBinding Role + ClusterRole 赋予 Provisioner 创建 PV修改 PVC读取 Node 等核心权限(否则无法实现动态制备)。
Deployment (Pod) volumes 挂载 ConfigMap (local-path-config) 将 ConfigMap 里的 config.jsonsetupteardown 脚本挂进容器,告诉 Provisioner 在宿主机的哪个目录/opt/local-path-provisioner)创建本地数据。

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 调用的「本地存储模板」
bash 复制代码
# 资源默认部署在命名空间:local-path-storage
root@master30~ 15:15:37# kubectl apply -f local-path-storage.yaml

# 确认资源状态
root@master30~ 15:20:05# kubectl get sc local-path
NAME         PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
local-path   rancher.io/local-path   Delete          WaitForFirstConsumer   false                  4m14s


root@master30~ 15:23:02# kubectl get all -n local-path-storage
NAME                                          READY   STATUS    RESTARTS   AGE
pod/local-path-provisioner-5b47679ddd-85dqg   1/1     Running   0          4m22s

NAME                                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/local-path-provisioner   1/1     1            1           4m22s

NAME                                                DESIRED   CURRENT   READY   AGE
replicaset.apps/local-path-provisioner-5b47679ddd   1         1         1       4m22s

验证部署

bash 复制代码
# 设置默认命名空间
root@master30~ 15:27:28# kubens default
1. 创建 pvc
bash 复制代码
root@master30~ 15:23:10# 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~ 15:23:36# kubectl apply -f local-path-pvc.yaml

#根据StorageClass中的volumeBindingMode的WaitForFirstConsumer策略,由于此时没有 Pod 使用它 → 不会生成 PV,所以PVC 一直 Pending
root@master30~ 15:23:46# kubectl get pvc
NAME             STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
local-path-pvc   Pending                                      local-path     <unset>                 6s

# 等待pod关联才会创建本地目录
2. 创建 deployment
bash 复制代码
root@master30~ 15:24:07# cat > local-path-deployment.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: webapp
  name: webapp
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: webapp
  template:
    metadata:
      labels:
        app: webapp
    spec:
      containers:
      - image: nginx
        name: nginx
        volumeMounts:
        - name: webapp-data
          mountPath: /usr/share/nginx/html
      volumes:
      - name: webapp-data
        persistentVolumeClaim:
          claimName: local-path-pvc
EOF

root@master30~ 15:24:17# kubectl apply -f local-path-deployment.yaml

# 查看 pod 状态
root@master30~ 15:30:57# kubectl get pods -o wide
NAME                      READY   STATUS    RESTARTS   AGE    IP              NODE                 NOMINATED NODE   READINESS GATES
webapp-6d46b54487-lgdfl   1/1     Running   0          7m5s   10.224.128.50   worker31.hgq.cloud   <none>           <none>
webapp-6d46b54487-mm475   1/1     Running   0          7m5s   10.224.128.51   worker31.hgq.cloud   <none>           <none>
# 所有节点都调度到 worker31,因为只用worker31可以提供这个卷。

# 查看 pvc 状态
root@master30~ 15:31:45# kubectl get pvc
NAME                                   STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
persistentvolumeclaim/local-path-pvc   Bound    pvc-ba1e2bed-7b55-4251-8c9f-3486d8719e0b   5Gi        RWO            local-path     <unset>                 8m12s

# 写入测试文件
root@master30~ 15:27:38# kubectl exec webapp-6d46b54487-lgdfl -- bash -c 'echo Hello World From Nginx > /usr/share/nginx/html/index.html'

# 验证本地路径
root@worker31~ 15:32:06# ls /opt/local-path-provisioner/pvc-ba1e2bed-7b55-4251-8c9f-3486d8719e0b_default_local-path-pvc/
index.html

root@worker31~ 15:32:56# cat /opt/local-path-provisioner/pvc-ba1e2bed-7b55-4251-8c9f-3486d8719e0b_default_local-path-pvc/index.html
Hello World From Nginx
总结⭐️
1. 先看懂两个关键配置
① StorageClass 字段
复制代码
volumeBindingMode: WaitForFirstConsumer

含义:不提前创建 PV,等第一个使用 PVC 的 Pod 调度完成后,再创建本地 PV 并绑定到 Pod 所在节点

对比默认 Immediate:创建 PVC 立刻生成 PV,本地 PV 会随机绑定一个节点,后续 Pod 只能跑该节点。

② Local Path Provisioner 本地卷特性

本地磁盘 PV 本质是节点绑定资源 ,生成的 PV 自带 nodeAffinity 节点亲和规则,限定只能在创建 PV 的那个节点挂载使用。

PVC 一旦和该 PV 绑定,所有使用此 PVC 的 Pod,强制只能调度到该节点,不能跨节点。

2. 完整流程拆解
  1. 先创建 PVC

    复制代码
    local-path-pvc
    • SC 是WaitForFirstConsumer,此时没有 Pod 消费 PVC → Provisioner 不会生成 PV,PVC 状态 Pending。
  2. 创建副本数 2 的 Nginx Deployment,挂载该 PVC

    • 调度器先调度第一个 Pod ,随机选中节点 worker31 启动;
    • Pod 就绪、开始消费 PVC,触发 local-path-provisioner 工作;
    • 程序在worker31本地 /opt/local-path-provisioner 生成对应目录,创建 PV;
    • PV 写入节点亲和:仅允许在worker31挂载,PVC 状态变为 Bound。
  3. 调度第二个 Pod 副本

    • Pod 模板复用同一个 PVC,PVC 绑定的 PV 限定只能跑worker31
    • 调度器只能把第二个 Pod 也调度到worker31,无法分配到其他 worker 节点。

部署 NFS provisioner

部署 NFS 服务

bash 复制代码
# 安装 NFS server
root@master30~ 18:10:01# apt install -y nfs-kernel-server

# 安创建NFS目录 修改创建文件夹的权限
root@master30~ 18:10:01# mkdir -m 777 /shares/

# 配置共享,允许所有客户端访问
root@master30~ 18:10:22# cat << EOF > /etc/exports
/shares *(rw,sync,no_root_squash,no_all_squash,insecure)
EOF


# 重启 nfs server
root@master30~ 18:10:31# systemctl restart nfs-server.service

# 客户端安装
root@worker31~ 18:11:19# apt install -y nfs-common
root@worker32~ 18:11:24# apt install -y nfs-common

部署 NFS provisioner

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

bash 复制代码
# 资源部署到命名空间:kube-storage
root@master30~ 18:11:02# kubectl create ns kube-storage
root@master30~ 18:11:54# kubens kube-storage
1. 创建ServiceAccount 与 RBAC(权限绑定)
  • 作用 :让 Provisioner 容器有权限调用 K8s API 来创建 PV、修改 PVC 状态、读取 Node 信息等。这一步确实必须首先创建,否则 Provisioner 启动后会因权限不足疯狂报错。

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

yaml 复制代码
root@master30~ 18:12:00# cat > nfs-rbac.yaml <<'EOF'
apiVersion: v1
kind: ServiceAccount
metadata:
  name: nfs-client-provisioner
  namespace: kube-storage
---
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"]
---
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

root@master30~ 18:12:18# kubectl apply -f nfs-rbac.yaml
2. 部署 NFS Provisioner(制备器) 应用
yaml 复制代码
root@master30~ 18:12:47# cat > nfs-provisioner.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nfs-client-provisioner
  namespace: kube-storage
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: nfs-client-provisioner
  template:
    metadata:
      labels:
        app: nfs-client-provisioner
    spec:
      serviceAccountName: nfs-client-provisioner
      volumes:
      - name: nfs-client-root
        nfs:
          server: 10.1.8.30  # 同上 NFS 服务器 IP
          path: /shares  # 核心:同步改为 /shares
      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~ 18:13:00# kubectl apply -f nfs-provisioner.yaml

# 查看 NFS Provisioner 应用
root@master30~ 18:14:04# kubectl get deployments.apps nfs-client-provisioner
NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
nfs-client-provisioner   1/1     1            1           47s
3. 创建 NFS StorageClass存储模板
yaml 复制代码
root@master30~ 18:14:11# 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"
# 按需设置回收策略
#reclaimPolicy: Retain
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: Immediate
EOF

# 创建 StorageClass
root@master30~ 18:14:22# kubectl apply -f nfs-storageclass.yaml

# 查看 StorageClass
root@master30~ 18:15:15# kubectl get sc
NAME          PROVISIONER      RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
nfs-storage   fuseim.pri/ifs   Delete          Immediate           true                   32s

验证部署

在default命名空间测试。

bash 复制代码
root@master30~ 18:15:19# kubens default
1. 创建 pvc
bash 复制代码
root@master30~ 18:15:33# cat > nfs-pvc.yaml <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: webclaim
  namespace: default
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: "nfs-storage"
EOF

# 创建 pvc
root@master30~ 18:15:45# kubectl apply -f nfs-pvc.yaml

# 查看 pvc 状态
root@master30~ 18:16:02# kubectl get pvc
NAME       STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
webclaim   Bound    pvc-3ebc8a8a-92b6-4ac3-a432-c1b932b45831   5Gi        RWO            nfs-storage    <unset>                 10s


# 查看 pv 状态
root@master30~ 18:16:12# kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM              STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
pvc-3ebc8a8a-92b6-4ac3-a432-c1b932b45831   5Gi        RWO            Delete           Bound    default/webclaim   nfs-storage    <unset>  

# 查看 nfs 共享目录
root@master30~ 18:16:24# ls /shares/
default-webclaim-pvc-3ebc8a8a-92b6-4ac3-a432-c1b932b45831

# 准备 web 主页
root@master30~ 18:16:36# echo hello world from nginx > /shares/default-webclaim-pvc-3ebc8a8a-92b6-4ac3-a432-c1b932b45831/index.html
2. 创建 deployment
bash 复制代码
root@master30~ 18:17:24# cat > nfs-deploy.yaml << EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: webapp
  name: webapp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: webapp
  template:
    metadata:
      labels:
        app: webapp
    spec:
      containers:
      - image: nginx
        name: nginx
        volumeMounts:
        - name: webapp-data
          mountPath: /usr/share/nginx/html
      volumes:
      - name: webapp-data
        persistentVolumeClaim:
          claimName: webclaim
EOF

root@master30~ 18:17:48# kubectl apply -f nfs-deploy.yaml

root@master30~ 18:18:02# kubectl expose deployment webapp --port=80 --target-port=80 --protocol TCP
root@master30~ 18:18:54# kubectl get svc webapp
NAME     TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
webapp   ClusterIP   10.103.140.81   <none>        80/TCP    15s


# 访问测试
root@master30~ 18:19:10# curl 10.103.140.81
hello world from nginx
3. 清理资源
bash 复制代码
# 删除 deployment
root@master30~ 18:19:22# kubectl delete deployments.apps webapp

# 删除 pvc,pv也将一起删除
root@master30~ 18:19:46# kubectl delete pvc webclaim
persistentvolumeclaim "webclaim" deleted
root@master30~ 18:19:58# kubectl get pv
No resources found

# 查看后端存储中数据,仍然保留
root@master30~ 18:20:03# ls /shares/default-webclaim-pvc-3ebc8a8a-92b6-4ac3-a432-c1b932b45831/
index.html

Kubernetes StatefulSet

环境准备

bash 复制代码
root@master30~ 18:21:06# kubectl create ns statefulset
root@master30~ 18:21:28# kubens statefulset

无状态应用 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 服务器

bash 复制代码
# 安装 NFS server
root@master30~ 18:44:44# apt install -y nfs-kernel-server

# 安创建NFS目录 修改创建文件夹的权限
root@master30~ 18:44:44# mkdir -m 777 /shares/

# 配置共享,允许所有客户端访问
root@master30~ 18:55:59# cat << EOF >> /etc/exports
/shares *(rw,sync,no_root_squash,no_all_squash,insecure)
EOF


# 重启 nfs server
root@master30~ 18:56:57# systemctl restart nfs-server.service

# 客户端安装
root@worker31~ 19:03:33# apt install -y nfs-common
root@worker32~ 19:03:35# apt install -y nfs-common

示例 1:部署 Nginx 集群

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

步骤 1:准备 NFS 存储
bash 复制代码
# 准备应用目录和文件
root@master30~ 18:57:13# mkdir -m 777 -p /shares/nginx/data-{0..2}
root@master30~ 19:05:59# echo Hello World From nginx-0 > /shares/nginx/data-0/index.html
root@master30~ 19:06:19# echo Hello World From nginx-1 > /shares/nginx/data-1/index.html
root@master30~ 19:06:27# echo Hello World From nginx-2 > /shares/nginx/data-2/index.html
步骤 2:创建 Headless Service

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

yaml 复制代码
root@master30~ 19:06:30# 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~ 19:07:24# kubectl apply  -f nginx-service.yaml


# 没有 CLUSTER-IP 
root@master30~ 19:07:43# kubectl get svc
NAME    TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
nginx   ClusterIP   None         <none>        80/TCP    4s
步骤 3:创建 PV

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

yaml 复制代码
root@master30~ 19:07:47# cat > nginx-pv.yaml <<'EOF'
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nginx-pv-0
spec:
  capacity:
    storage: 10Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: nginx-storage
  nfs:
    server: 10.1.8.30
    path: /shares/nginx/data-0
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nginx-pv-1
spec:
  capacity:
    storage: 10Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: nginx-storage
  nfs:
    server: 10.1.8.30
    path: /shares/nginx/data-1
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nginx-pv-2
spec:
  capacity:
    storage: 10Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: nginx-storage
  nfs:
    server: 10.1.8.30
    path: /shares/nginx/data-2
EOF

# 创建 pv
root@master30~ 19:09:02# kubectl apply -f nginx-pv.yaml

# 查看 pv
root@master30~ 19:09:22# kubectl get pv
NAME         CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS    VOLUMEATTRIBUTESCLASS   REASON   AGE
nginx-pv-0   10Gi       RWO            Retain           Available           nginx-storage   <unset>                          11s
nginx-pv-1   10Gi       RWO            Retain           Available           nginx-storage   <unset>                          11s
nginx-pv-2   10Gi       RWO            Retain           Available           nginx-storage   <unset>                          11s

建议:使用动态卷制备。

步骤 4:创建 StatefulSet

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

yaml 复制代码
root@master30~ 19:09:33# 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
  # 持久化存储:为每个 Pod 自动创建一个独立的PVC
  volumeClaimTemplates:
  - metadata:
      name: nginx-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "nginx-storage"
      resources:
        requests:
          storage: 10Gi
EOF

# 创建 statefulset
root@master30~ 19:09:52# kubectl apply -f nginx-statefulset.yaml

# 查看 pods
root@master30~ 19:10:38# kubectl get pods
NAME      READY   STATUS    RESTARTS   AGE
nginx-0   1/1     Running   0          55s
nginx-1   1/1     Running   0          35s
nginx-2   1/1     Running   0          15s


# 预期输出:Pod 按 nginx-0、nginx-1、nginx-2顺序创建
# 先创建 nginx-0,处于 Running 状态
# 再创建 nginx-1,处于 Running 状态
# 再创建 nginx-2,处于 Running 状态
# 最终 3 个 Pod 均为 Running 状态。

# 查看 statefulset
root@master30~ 19:11:05# kubectl get statefulsets.apps
NAME    READY   AGE
nginx   3/3     69s
步骤 5:验证部署
bash 复制代码
# 验证 pod 域名解析
root@master30~ 19:11:19# kubectl run test-pod --image busybox --image-pull-policy=IfNotPresent -- sleep 3600
root@master30~ 19:48:40# kubectl exec test-pod -- wget nginx-0.nginx
Connecting to nginx-0.nginx (10.224.96.227:80)
saving to 'index.html'
index.html           100% |********************************|    25  0:00:00 ETA
'index.html' saved

nginx-2.nginx 是 Kubernetes Headless Service 为 StatefulSet 的每个 Pod 生成的标准 DNS 记录格式

它完整的写法是:$(Pod名称).$(Headless Service名称).$(命名空间).svc.cluster.local

bash 复制代码
root@master30/shares/nginx 19:40:53# kubectl get pvc
NAME                 STATUS   VOLUME       CAPACITY   ACCESS MODES   STORAGECLASS    VOLUMEATTRIBUTESCLASS   AGE
nginx-data-nginx-0   Bound    nginx-pv-0   10Gi       RWO            nginx-storage   <unset>                 34m
nginx-data-nginx-1   Bound    nginx-pv-2   10Gi       RWO            nginx-storage   <unset>                 33m
nginx-data-nginx-2   Bound    nginx-pv-1   10Gi       RWO            nginx-storage   <unset>                 33m

# 验证 pod 内容
root@master30~ 19:48:47# curl 10.224.96.227
Hello World From nginx-0

# 删除 nginx-0,等待重新创
root@master30~ 19:49:40# kubectl delete pods nginx-0
root@master30~ 19:50:18# kubectl get pods
NAME       READY   STATUS              RESTARTS   AGE
nginx-0    0/1     ContainerCreating   0          2s
nginx-1    1/1     Running             0          39m
nginx-2    1/1     Running             0          55s

# 验证 pod 域名解析:IP地址变更
root@master30~ 19:50:40# kubectl exec test-pod -- sh -c "rm index.html;wget nginx-0.nginx"
Connecting to nginx-0.nginx (10.224.96.234:80)
saving to 'index.html'
index.html           100% |********************************|    25  0:00:00 ETA
'index.html' saved


# 验证 pod 内容
root@master30~ 19:51:08# curl 10.224.96.234
Hello World From nginx-0

# 预期输出:数据仍存在,说明 PV/PVC 实现了数据持久化,Pod 重建后可复用存储。

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

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

步骤 6:清理环境
bash 复制代码
root@master30~ 19:51:25# kubectl delete sts nginx
root@master30~ 19:51:45# kubectl delete svc nginx
root@master30~ 19:51:56# kubectl delete pvc nginx-data-nginx-{0..2}
root@master30~ 19:52:09# kubectl delete pv nginx-pv-{0..2}
root@master30~ 19:52:39# kubectl delete pods test-pod --force
root@master30~ 19:52:42# rm -rf /shares/nginx/

示例 2:部署 Etcd 集群

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

步骤 1:准备 NFS 存储
bash 复制代码
# 准备应用目录和文件
root@master30:~# mkdir -m 777 -p /shares/etcd/data-{0..2}
步骤 2:创建 Headless Service

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

yaml 复制代码
root@master30~ 19:31:56# 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~ 19:32:38# kubectl apply -f etcd-service.yaml

# 没有 CLUSTER-IP 
root@master30~ 19:35:42# kubectl get svc
NAME   TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)             AGE
etcd   ClusterIP   None         <none>        2379/TCP,2380/TCP   3s
步骤 3:创建 PV

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

yaml 复制代码
root@master30~ 19:36:44# 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~ 19:36:48# kubectl apply -f etcd-pv.yaml
root@master30~ 19:47:29# 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>                          15s
etcd-pv-1   10Gi       RWO            Retain           Available           etcd-storage   <unset>                          15s
etcd-pv-2   10Gi       RWO            Retain           Available           etcd-storage   <unset>                          15s

建议:使用动态卷制备。

步骤 4:创建 StatefulSet

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

yaml 复制代码
root@master30~ 19:47:44# 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~ 19:48:33# kubectl apply -f etcd-statefulset.yaml

# 查看 pods
root@master30~ 19:51:17# kubectl get pods
NAME     READY   STATUS    RESTARTS   AGE
etcd-0   1/1     Running   0          56s
etcd-1   1/1     Running   0          30s
etcd-2   1/1     Running   0          13s


# 预期输出:Pod 按 etcd-0、etcd-1、etcd-2顺序创建
# 先创建 etcd-0,处于 Running 状态
# 再创建 etcd-1,处于 Running 状态
# 再创建 etcd-2,处于 Running 状态
# 最终 3 个 Pod 均为 Running 状态。

# 查看 statefulset
root@master30~ 19:51:28# kubectl get sts
NAME   READY   AGE
etcd   3/3     74s
步骤 5:验证部署

验证集群状态

bash 复制代码
# 验证集群状态:建立集群
root@master30~ 19:51:46# 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


# 删除 etcd-2,等待重新创
root@master30~ 19:53:14# kubectl get pods
NAME     READY   STATUS    RESTARTS   AGE
etcd-0   1/1     Running   0          2m45s
etcd-1   1/1     Running   0          2m19s
etcd-2   1/1     Running   0          36s

# 验证集群状态:保持一直,etcd-2的ID也没有
root@master30~ 19:53:17# 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

验证数据

bash 复制代码
root@master30~ 19:53:47# kubectl exec etcd-0 -- etcdctl put /users/user1/name lisi
root@master30~ 19:54:51# kubectl exec etcd-2 -- etcdctl get /users/user1/name
/users/user1/name
lisi
步骤 6:清理环境
bash 复制代码
root@master30~ 19:55:12# kubectl delete sts etcd
root@master30~ 19:55:30# kubectl delete svc etcd
root@master30~ 19:55:35# kubectl delete pvc etcd-data-etcd-{0..2}
root@master30~ 19:55:45# rm -rf /shares/etcd/

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 可保留,数据不丢失)。、

相关推荐
云烟成雨TD5 小时前
Kubernetes 系列【14】对象元数据:标签、标签选择器、命名空间、字段选择器
云原生·容器·kubernetes
极客先躯5 小时前
高级java每日一道面试题-2026年04月05日-实战篇[Docker]-如何排查容器 OOM 的问题?
java·运维·docker·容器·内存溢出·高级面试
SLD_Allen6 小时前
K8s+Ray:大模型时代AI Workload调度的通用范式
人工智能·容器·kubernetes
Skilce8 小时前
基于GitLab CI/CD + Kubernetes的自动化部署平台搭建
ci/cd·kubernetes·gitlab
极客先躯8 小时前
高级java每日一道面试题-2026年04月04日-实战篇[Docker]-如何排查容器存储挂载失败的问题?
java·运维·docker·容器·高级面试
宇晨T9 小时前
从零搭建企业级Web服务:Shell脚本 + LNMP + Docker 实战指南
前端·docker·容器
努力进修20 小时前
Docker+cpolar 实战:打造灵活可控的远程办公系统
运维·docker·容器
jieyucx1 天前
Docker 入门第一阶段:建立正确认知与初体验
运维·docker·容器
only_Klein1 天前
k8sgpt集成Prometheus Grafana
gpt·kubernetes·prometheus