Kubernetes 集群 Dashboard
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配置访问,参考:
yamlapiVersion: 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
访问 Dashboard

新版本
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 能不能 "智能" 管理卷呢?比如需要用卷时自动创建,不用时自动清理?
答案是可以的 ------ 这就是动态卷供应的能力。它彻底省去了集群管理员提前手动配置存储的工作,用户只要提出存储需求,系统就会自动创建对应的存储卷。
动态卷供应流程
- 管理员先创建 "存储模板 "(StorageClass),指定用哪个 "造卷工具"(Provisioner,制备器)来创建卷;
- 用户创建 "存储申请单"(PVC)时,指定要用哪个 "存储模板";
- 系统收到申请后,会让模板绑定的 "造卷工具" 自动创建一个 PV,并且把 PV 和用户的 PVC 绑定,用户直接用 PVC 就行。
详细流程:
- 创建 PVC(用户发起)。
- 匹配 StorageClass (Kubernetes 根据
storageClassName查找或使用默认 StorageClass)。- Provisioner 读取
parameters(但这只是准备阶段)。- 🌐 调用后端存储 API 创建真实卷 (这是最关键的一步 !Provisioner 带着
parameters参数,实际调用 AWS、Ceph 或 NFS 服务器的 API,在底层物理/逻辑存储中划出一块真实的存储空间)。- 📄 自动生成对应的 PV 对象(底层存储创建成功后,Provisioner 会在 K8s 集群中创建一个 PV 资源对象,用来描述刚才创建好的那块存储)。
- 🔗 绑定 (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.json、setup、teardown 脚本挂进容器,告诉 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. 完整流程拆解
-
先创建 PVC
local-path-pvc- SC 是
WaitForFirstConsumer,此时没有 Pod 消费 PVC → Provisioner 不会生成 PV,PVC 状态 Pending。
- SC 是
-
创建副本数 2 的 Nginx Deployment,挂载该 PVC
- 调度器先调度第一个 Pod ,随机选中节点
worker31启动; - Pod 就绪、开始消费 PVC,触发 local-path-provisioner 工作;
- 程序在
worker31本地/opt/local-path-provisioner生成对应目录,创建 PV; - PV 写入节点亲和:仅允许在
worker31挂载,PVC 状态变为 Bound。
- 调度器先调度第一个 Pod ,随机选中节点
-
调度第二个 Pod 副本
- Pod 模板复用同一个 PVC,PVC 绑定的 PV 限定只能跑
worker31; - 调度器只能把第二个 Pod 也调度到
worker31,无法分配到其他 worker 节点。
- Pod 模板复用同一个 PVC,PVC 绑定的 PV 限定只能跑
部署 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) |
总结
- 无状态核心:实例"无记忆",可随意替换/扩容,是云原生优先推荐的应用形态;
- 有状态核心:实例"有记忆",依赖持久化和固定身份,需通过 StatefulSet 等组件保障稳定性;
- 最佳实践:尽可能将应用拆分为"无状态业务层 + 有状态数据层",既提升扩展能力,又保障数据安全。
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 可保留,数据不丢失)。、