Kubernetes 授权

学习参考:API 访问控制

环境准备

root@master30:~# kubectl create ns auth
root@master30:~# kubectl config set-context --current --namespace auth

Kubernetes API 访问控制

学习参考:Kubernetes API 访问控制

当用户使用User服务账号访问Kubernetes API时,每个请求都会经过多阶段的访问控制之后才会被接受,包括身份认证、鉴权以及准入控制(Admission Control)。

认证管理

Kubernetes 中的用户

Kubernetes 集群有两类用户:

  • 普通用户,Kubernetes 中普通用户不是由 kubernetes 直接提供,而是身份认证插件提供,Kubernetes 并不包含用来代表普通用户账号的对象。 普通用户的信息无法通过 API 调用添加到集群中,Kubernetes 认为:能够提供由集群的证书机构签名的合法证书的用户是通过身份认证的用户。 基于这样的机制,Kubernetes 使用证书中的 ‘subject’ 的通用名称(Common Name)字段 (例如,“/CN=bob”)来确定用户名。 接下来,基于角色访问控制(RBAC)子系统会确定用户是否有权针对某资源执行特定的操作。
  • 服务账号,Kubernetes 中服务账号是 Kubernetes API 所管理的用户。它们被绑定到特定的名字空间, 或者由 API 服务器自动创建,或者通过 API 调用创建。服务账号与一组以 Secret 保存的凭据相关,这些凭据会被挂载到 Pod 中,从而允许集群内的进程访问 Kubernetes API。

每个 API 请求必须包含一个用户:普通用户相关或者服务账号,客户端也可以发起匿名请求。这意味着集群内外的每个进程在向 API 服务器发起请求时都必须通过身份认证,否则会被视作匿名用户。

创建账户

以下探讨使用 **X509客户证书 **插件管理用户。

客户端准备

客户端要想访问集群,必须安装与集群版本一致的kubectl工具。

root@client:~# apt install -y kubectl=1.30.2-1.1

准备申请材料

# 创建私钥
root@client:~# openssl genrsa -out laoma.key 2048

# 根据私钥,创建请求证书
root@client:~# openssl req -new -key laoma.key -out laoma.csr -subj '/CN=laoma/O=kubernets'
# 参数说明:
## C,Country,代表国家
## ST,STate,代表省份
## L,Location,代表城市
## O,Organization,代表组织,公司
## OU,Organization Unit,代表部门
## CN,Common Name,代表服务器域名
## emailAddress,代表联系人邮箱地址。

# 其他示例:
root@client:~# openssl req -new -key servera.key -out servera.csr -subj "/C=CHINA/ST=JS/L=NJ/O=LM/OU=DEVOPS/CN=servera.lab.example.com/emailAddress=laoma@lab.example.com"

# 客户端将自己请求证书发给kubernetes管理员
root@client:~# scp laoma.csr root@master30:
创建用户凭据

管理员使用以下资源文件创建用户的kubeconfig。

# 使用ca.crt和ca.key签名laoma.csr,得到laoma.crt证书
root@master30:~# openssl x509 -req -in laoma.csr -CA /etc/kubernetes/pki/ca.crt -CAkey /etc/kubernetes/pki/ca.key -CAcreateserial -out laoma.crt -days 1095

# 导出kubeconfig模版
root@master30:~# kubectl config view > config.tpl

# 将kubeconfig模板、laoma.crt和kubernetes的ca证书发给客户端
root@master30:~# scp config.tpl laoma.crt /etc/kubernetes/pki/ca.crt root@client:~
创建 kubeconfig

kubeconfig创建方法:

**方法一:**使用kubectl config命令创建。

修改模版config.tpl,结果如下:

apiVersion: v1
clusters:
- cluster:
    server: https://10.1.8.30:6443
  name: kubernetes
contexts:
- context:
    cluster: kubernetes
    namespace: default
    user: laoma
  name: laoma@kubernetes
current-context: laoma@kubernetes
kind: Config
users:
- name: laoma
# 设置 cluster
root@client:~# mv config.tpl config
root@client:~# kubectl config set-cluster kubernetes --kubeconfig=config --certificate-authority=ca.crt --embed-certs 

# --kubeconfig指定kubeconfig文件
# --server指定kubernetes服务器认证地址
# --certificate-authority选项指定ca证书
# --embed-certs选项作用是将ca.crt的内容添加到config文件中,
# 如果没有该选项,则添加 --certificate-authority 选项指定的路径,也就是ca.crt

# 设置credentials
root@client:~# kubectl config set-credentials laoma --kubeconfig=config --client-key=laoma.key --client-certificate=laoma.crt --embed-certs

# --client-key 指定用户私钥
# --client-certificate 指定服务器为用户生成的证书

# 设置context
root@client:~# kubectl config set-context laoma --kubeconfig=config --namespace=default --cluster=kubernetes --user=laoma
# --namespace指定Namespace
# --cluster指定集群
# --user指定用户

# 授权用户laoma集群管理员角色,后续详细讲解角色管理
root@master30:~# kubectl create clusterrolebinding laoma-admin --clusterrole=cluster-admin --user=laoma

# 验证结果
root@client:~# kubectl get nodes --kubeconfig=config 
NAME                  STATUS   ROLES           AGE   VERSION
master30.laoma.cloud   Ready    control-plane   40h   v1.30.2
worker31.laoma.cloud   Ready    <none>          40h   v1.30.2
worker32.laoma.cloud   Ready    <none>          40h   v1.30.2

**方法二:**使用文本编辑器创建(不推荐)。

# 获取文件ca.crt base64编码,填充到certificate-authority-data
root@client:~# cat ca.crt | base64 | tr -d '\n'

# 获取文件laoma.key base64编码,填充到client-key-data
root@client:~# cat laoma.key | base64 | tr -d '\n'

# 获取文件laoma.crt base64编码,填充到client-certificate-data
root@client:~# cat laoma.crt | base64 | tr -d '\n'

# 使用上面的base64编码修改配置文件对应值
root@master30:~# kubectl config view
apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: DATA+OMITTED
    server: https://10.1.8.30:6443
  name: kubernetes
contexts:
- context:
    cluster: kubernetes
    user: kubernetes-laoma
  name: kubernetes-laoma@kubernetes
current-context: kubernetes-admin@kubernetes
kind: Config
preferences: {}
users:
- name: kubernetes-laoma
  user:
    client-certificate-data: REDACTED
    client-key-data: REDACTED

删除账户

# 通过删除证书进行删除账户即可,为了后续操作方便,该账户不删除

# k8s删除用户csr资源后,客户端用户仍可以访问集群。
# 解释:用户认证功能由x509提供,与k8s无关。
# 如果不予许用户登录,应该从x509认证机制方面下手,例如ca.crt将相应客户端csr加入黑名单。

# 删除权限
root@master30:~# kubectl delete clusterrolebindings.rbac.authorization.k8s.io laoma-admin

# 验证权限
root@client:~# kubectl get nodes
Error from server (Forbidden): nodes is forbidden: User "laoma" cannot list resource "nodes" in API group "" at the cluster scope

授权管理

学习参考:鉴权使用 RBAC 鉴权

鉴权概述

Kubernetes API 服务器对 API 请求进行鉴权。 它根据所有策略评估所有请求属性来决定允许或拒绝请求。 一个 API 请求的所有部分都必须被某些策略允许才能继续。 这意味着默认情况下拒绝权限。

当系统配置了多个鉴权模块时,Kubernetes 将按顺序使用每个模块。

  • 如果任何鉴权模块批准或拒绝请求,则立即返回该决定,并且不会与其他鉴权模块协商。
  • 如果所有模块对请求没有意见,则拒绝该请求。 被拒绝响应返回 HTTP 状态代码 403。

鉴权模式

鉴权模式定义认证成功的用户对集群的操作权限,由kube-apiserver配置文件指定:

root@master30:~# cat /etc/kubernetes/manifests/kube-apiserver.yaml |grep mode
    - --authorization-mode=Node,RBAC

我们这里使用RBACNode模式。

支持的模式:

  • Node,是节点专用的鉴权模式,根据调度到 kubelet 上运行的 Pod 为 kubelet 授予权限。 要了解有关使用节点鉴权模式的更多信息,请参阅节点鉴权

  • ABAC,基于属性的访问控制(ABAC)定义了一种访问控制范型,通过使用将属性组合在一起的策略, 将访问权限授予用户。策略可以使用任何类型的属性(用户属性、资源属性、对象,环境属性等)。 要了解有关使用 ABAC 模式的更多信息,请参阅 ABAC 模式

  • RBAC,基于角色的访问控制(RBAC) 是一种基于企业内个人用户的角色来管理对计算机或网络资源的访问的方法。要了解有关使用 RBAC 模式的更多信息,请参阅 RBAC 模式

    • 被启用之后,RBAC(基于角色的访问控制)使用 rbac.authorization.k8s.io API 组来驱动鉴权决策,从而允许管理员通过 Kubernetes API 动态配置权限策略。
    • 要启用 RBAC,请使用 --authorization-mode = RBAC 启动 API 服务器。
  • Webhook —— WebHook 是一个 HTTP 回调:发生某些事情时调用的 HTTP POST; 通过 HTTP POST 进行简单的事件通知。 实现 WebHook 的 Web 应用程序会在发生某些事情时将消息发布到 URL。 要了解有关使用 Webhook 模式的更多信息,请参阅 Webhook 模式

  • AlwaysAllow,允许用户所有请求。

# 前面我们删除了用户laoma权限,更改为AlwaysAllow模式,测试laoma用户权限。
root@master30:~# vim /etc/kubernetes/manifests/kube-apiserver.yaml
......
# 在spec.containers下的command中添加参数- --basic-auth-file=/etc/kubernets/pki/aa.csv
spec:
  containers:
  - command:
    - --authorization-mode=AlwaysAllow
......

# 更改完成后,kubernetes会自动重启pod/kube-apiserver,等该pod状态为running再次验证。
root@client:~# kubectl get nodes
NAME                   STATUS   ROLES           AGE   VERSION
master30.laoma.cloud   Ready    control-plane   12d   v1.30.2
worker31.laoma.cloud   Ready    <none>          12d   v1.30.2
worker32.laoma.cloud   Ready    <none>          12d   v1.30.2
  • AlwaysDeny,拒绝用户所有请求,不管用户是否具有权限,但不限制admin用户。

role 管理

kubernetes 方便管理权限,将一组特定权限赋予角色,然后将角色赋予用户,那么用户将继承该角色具有的权限。

角色分类
  • role,namespace 角色,限定用户访问特定namespace。role绑定给用户,称之为rolebinding

  • clusterrole,cluster 角色,可以管理集群,包括所有namespace中资源。clusterrole绑定给用户,称之为clusterrolebinding

权限由kubernetes系统预定义的,clusterroles/admin中包涵系统中全部权限列表。

root@master30:~# kubectl describe clusterroles admin
Name:         admin
Labels:       kubernetes.io/bootstrapping=rbac-defaults
Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
PolicyRule:
  Resources                                       Non-Resource URLs  Resource Names  Verbs
  ---------                                       -----------------  --------------  -----
  rolebindings.rbac.authorization.k8s.io          []                 []              [create delete deletecollection get list patch update watch]
  roles.rbac.authorization.k8s.io                 []                 []              [create delete deletecollection get list patch update watch]
  configmaps                                      []                 []              [create delete deletecollection patch update get list watch]
  endpoints                                       []                 []              [create delete deletecollection patch update get list watch]
......

输出说明:

  • Resources:代表系统中资源类型,例如Secret,Configmap等。

  • Resource Names:代表特定资源。如果Resources是Secret,那么这里就指特定Secret。

  • Non-Resource URLs: 被称为非资源URL或虚拟URL对象,是k8s中所需要的特殊动作(不需要关注)。

  • Verbs:代表针对资源执行的动作,包括操作get、list、create、delete、update、edit、watch、exec。

    • get,用于获得特定资源信息,例如,针对pod,可以执行GET /api/v1/namespaces/{namespace}/pods/{podname}
root@client:~# kubectl get pod -n kube-system 
Error from server (Forbidden): pods is forbidden: User "laoma" cannot list resource "pods" in API group "" in the namespace "kube-system"

root@client:~# kubectl get pod kube-proxy-8kp8w -n kube-system 
NAME               READY   STATUS    RESTARTS   AGE
kube-proxy-8kp8w   1/1     Running   4          52d
  • list,用户查看某一类型资源清单,例如针对pod,可以执行GET /api/v1/namespaces/{namespace}/pods
root@client:~# kubectl get pod -n kube-system 
NAME                                        READY   STATUS    RESTARTS   AGE
calico-kube-controllers-6dfcd885bf-lq4n5    1/1     Running   4          52d
calico-node-44cf4                           1/1     Running   4          52d
calico-node-48sm4                           1/1     Running   4          52d
.....
    
    root@client:~# kubectl get pod kube-proxy-8kp8w -n kube-system 
    Error from server (Forbidden): pods "kube-proxy-8kp8w" is forbidden: User "laoma" cannot get resource "pods" in API group "" in the namespace "kube-system"

更多API信息参考:https://kubernetes.io/docs/reference/kubernetes-api/

创建 role
root@master30:~# kubectl create role -h
Create a role with single rule.

Usage:
  kubectl create role NAME --verb=verb --resource=resource.group/subresource
[--resource-name=resourcename] [--dry-run=server|client|none] [options]

示例1:可以对项目中所有pods执行get、list、watch操作

root@master30:~# kubectl create role pod-role --verb=get,list,watch --resource=pods -n default --dry-run=client -o yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  creationTimestamp: null
  name: pod-role
  namespace: default
rules:
- apiGroups:
  - ""
  resources:
  - pods
  verbs:
  - get
  - list
  - watch
root@master30:~# kubectl create role pod-role --verb=get,list,watch --resource=pods -n default

root@master30:~# kubectl get roles pod-role -n default
NAME         CREATED AT
pod-role   2021-08-24T10:24:36Z

root@master30:~# kubectl describe roles pod-role -n default
Name:         pod-role
Labels:       <none>
Annotations:  <none>
PolicyRule:
  Resources  Non-Resource URLs  Resource Names  Verbs
  ---------  -----------------  --------------  -----
  pods       []                 []              [get list watch]

示例2:访问特定资源pods/readablepod

root@master30:~# kubectl create role pod-role --verb=get --resource=pods \
--resource-name=readablepod --resource-name=anotherpod

示例3:可以对项目中所有replicasets执行get list watch操作

root@master30:~# kubectl create role foo --verb=get,list,watch --resource=replicasets
修改 role
# 增加create权限
root@master30:~# kubectl edit roles -n default pod-role
......
rules:
- apiGroups:
  - ""
  resources:
  - pods
  verbs:
# 在verbs下添加相应权限
  - list
  - get
  - watch
  - create
apiGroups
  • 角色的 rules 属性中 apiGroups 默认为空

  • pod、service 资源的 apiVersion 是v1,apiGroups为""。

  • Deployment、DaemonSet 资源的 apiVersion 是apps/v1,apiGroups为"apps"。如下:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  creationTimestamp: null
  name: deployments-role
  namespace: default
rules:
- apiGroups:
  - "apps"
  resources:
  - deployments
  verbs:
  - get
  - list
  - watch

**示例1:**定义角色,无法操作deployments,因为apiGroups未指定apps。

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  creationTimestamp: null
  name: deployments-role
  namespace: default
rules:
- apiGroups:
  - ""
  resources:
  - deployments
  verbs:
  - get
  - list
  - watch

**示例2:**定义一个可以 scale deployments 的角色。

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  creationTimestamp: null
  name: deployments-role
  namespace: default
rules:
- apiGroups:
  - "apps"
  resources:
  - deployments
  # 额外添加以下资源
  - deployments/scale
  verbs:
  - get
  - list
  - watch
  # 额外添加以下权限
  - patch

**示例3:**定义一个对不同类型资源赋予不同权限的角色。

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  creationTimestamp: null
  name: all-role
  namespace: default
rules:
- apiGroups:
  - ""
  resources:
  - pods
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - "apps"
  resources:
  - deployments
  - deployments/scale
  verbs:
  - get
  - list
  - watch
  - patch

常见apiGroups

Type apiVersion apiGroups
Pod、Service、PersistentVolume、PersistentVolumeClaim v1 “”
Deployment、DaemonSet、StatefulSets apps/v1 apps
Job batch/v1 batch
CronJob batch/v1beta1 batch
Role RoleBinding ClusterRole ClusterRoleBinding rbac.authorization.k8s.io/v1 rbac.authorization.k8s.io
NetworkPolicy networking.k8s.io/v1 networking.k8s.io

每种类型资源的 apiVersion 都可以通过以下命令查询:

root@master30:~# kubectl explain deployment|grep VERSION
VERSION:  apps/v1

root@master30:~# kubectl explain networkpolicy|grep VERSION
VERSION:  networking.k8s.io/v1
# 或者
root@master30:~/auth# kubectl api-resources |grep -i deploy
deployments                       deploy       apps/v1                                true         Deployment
role 绑定

将 role 绑定给用户。

root@master30:~# kubectl create rolebinding default-pod-laoma -n default --role=pod-role --user=laoma --dry-run=client -o yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  creationTimestamp: null
  name: default-pod-laoma
  namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: pod-role
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: User
  name: laoma
# 绑定 ns/default 中角色 pod-role 给 laoma
root@master30:~# kubectl create rolebinding default-pod-laoma -n default --role=pod-role --user=laoma
# 角色绑定完成后,角色的权限发生变化,用户获得的权限也会跟着动态变化。

root@master30:~# kubectl get rolebindings -n default default-pod-laoma 
NAME                    ROLE              AGE
default-pod-laoma   Role/pod-role   2m15s

root@master30:~# kubectl describe rolebindings -n default default-pod-laoma 
Name:         default-pod-laoma
Labels:       <none>
Annotations:  <none>
Role:
  Kind:  Role
  Name:  pod-role
Subjects:
  Kind  Name   Namespace
  ----  ----   ---------
  User  laoma 
 
# 验证
root@client:~# kubectl run web --image=hub.laoma.cloud/library/httpd --image-pull-policy=IfNotPresent -n default

root@client:~# kubectl get pod -n default
NAME   READY   STATUS    RESTARTS   AGE
web    1/1     Running   0          2m30s


root@client:~# kubectl get pod -n default -w
NAME   READY   STATUS    RESTARTS   AGE
web    1/1     Running   0          2m42s
role 回收
root@master30:~# kubectl delete rolebindings default-pod-laoma -n kube-system 
rolebinding.rbac.authorization.k8s.io "default-pod-laoma" deleted

# 再次验证
root@client:~# kubectl get pod -n default
Error from server (Forbidden): pods is forbidden: User "laoma" cannot list resource "pods" in API group "" in the namespace "default"
role 删除
root@master30:~# kubectl delete roles pod-role -n default
实践 1:角色管理
  1. 在 auth 命名空间,创建角色 pod-reader,针对pod,具备权限get、list、watch
  2. 赋予 laoma 用户 auth 命名空间 角色 pod-reader
  3. 管理员身份在 auth 命名空间创建 deployment
  4. 回收 laoma 用户 集群管理员角色 cluster-admin(如果存在)
  5. laoma 用户验证:查看deployment和pod
  6. 清理资源:回收用户角色,删除 deployment

clusterrole 管理

常见 clusterrole

kubernetes系统中已经预定义了很多clusterrole,常见的clusterrole如下:

  • view,对系统中几乎所有的对象都有get、list和watch权限。
  • edit,对系统中几乎所有的对象都有get、list和watch权限。其中部分对象额外具有 create、delete、deletecollection、patch、update 权限。
  • admin,对系统中大部分的对象具有所有权限。
  • cluster-admin,对系统中所有的对象具有所有权限。
创建 clusterrole
root@master30:~# kubectl create clusterrole -h
Usage:
  kubectl create clusterrole NAME --verb=verb --resource=resource.group
[--resource-name=resourcename] [--dry-run=server|client|none] [options]

示例1:创建一个可以get、list、watch所有项目中pods的clusterrole

root@master30:~# kubectl create clusterrole pod-role --verb=get,list,watch --resource=pods --dry-run=client -o yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  creationTimestamp: null
  name: pod-role
rules:
- apiGroups:
  - ""
  resources:
  - pods
  verbs:
  - get
  - list
  - watch
root@master30:~# kubectl create clusterrole pod-role --verb=get,list,watch --resource=pods

root@master30:~# kubectl get clusterrole pod-role
NAME       CREATED AT
pod-role   2021-08-25T14:31:21Z

root@master30:~# kubectl describe clusterrole pod-role
Name:         pod-role
Labels:       <none>
Annotations:  <none>
PolicyRule:
  Resources  Non-Resource URLs  Resource Names  Verbs
  ---------  -----------------  --------------  -----
  pods       []                 []              [get list watch]

示例2:创建一个可以get、list、watch所有项目中pods/readablepod和pods/anotherpod的clusterrole

root@master30:~# kubectl create clusterrole pod-reader --verb=get --resource=pods --resource-name=readablepod --resource-name=anotherpod

示例3:创建一个可以get、list、watch所有项目中pods和pods/status的clusterrole

root@master30:~# kubectl create clusterrole foo --verb=get,list,watch --resource=pods,pods/status
修改 clusterrole
root@master30:~# kubectl edit clusterrole pod-role
......
rules:
- apiGroups:
  - ""
  resources:
  - pods
  verbs:
  - get
  - list
  - watch
  # 添加create权限
  - create
clusterrole 绑定

将clusterrole绑定给用户。

root@master30:~# kubectl create clusterrolebinding laoma-pod-role --clusterrole=pod-role --user=laoma --dry-run=client -o yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  creationTimestamp: null
  name: laoma-pod-role
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: pod-role
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: User
  name: laoma
root@master30:~# kubectl create clusterrolebinding laoma-pod-role --clusterrole=pod-role --user=laoma

root@master30:~# kubectl get clusterrolebinding laoma-pod-role
NAME             ROLE                   AGE
laoma-pod-role   ClusterRole/pod-role   35s

root@master30:~# kubectl describe clusterrolebinding laoma-pod-role
Name:         laoma-pod-role
Labels:       <none>
Annotations:  <none>
Role:
  Kind:  ClusterRole
  Name:  pod-role
Subjects:
  Kind  Name   Namespace
  ----  ----   ---------
  User  laoma

# client节点使用laoma用户测试权限
root@client:~# kubectl get pod -n kube-system
NAME                                        READY   STATUS    RESTARTS   AGE
calico-kube-controllers-6dfcd885bf-lq4n5    1/1     Running   6          53d
calico-node-44cf4                           1/1     Running   6          53d
......

root@client:~# kubectl get pod -n default 
NAME   READY   STATUS    RESTARTS   AGE
web    1/1     Running   1          15h
clusterrole 回收
root@master30:~# kubectl delete clusterrolebinding laoma-pod-role
删除 clusterrole
root@master30:~# kubectl delete clusterrole pod-reader
实践 2:使用现有集群角色
  1. 赋予 laoma 用户集群角色 admin,指定在auth命名空间
  2. laoma 用户在 auth 命名空间:创建、查看和删除 deployment
  3. laoma 用户在 default 命名空间:创建、查看和删除 deployment
  4. 回收 laoma 用户权限

结果:绑定集群角色的时候,限定特定命名空间是没有意义的,仍然针对集群级别所有命名空间生效。

实践 3:自定义集群角色
  1. 创建 clusterrole 名称 cluster-pod-deploy-reader,能够查看pod和deployment
  2. 赋予给 laoma 用户
  3. laoma 用户在 auth 命名空间中:查看pod、deployment
  4. laoma 用户在 kube-system 命名空间中:查看pod、deployment
  5. 删除集群角色绑定和集群角色

服务账户

学习参考:管理服务账号配置服务账号

服务账户概述

Service Account,即服务账户,pod 使用 Service Account 身份运行容器。赋予Service Account相应角色,则使用该Service Account身份运行的pod中进程将具有对应Service Account的权限,进而有权限管理Kubernetes集群。

在每个namespace中都有一个名称为default的Service Account,创建的pod都会以 Service Account-default身份运行。

root@master30:~# kubectl get sa
NAME      SECRETS   AGE
default   1         53d

# 创建一个pod,验证Service Account信息
root@master30:~# kubectl run web --image=hub.laoma.cloud/library/httpd --image-pull-policy=IfNotPresent
root@master30:~# kubectl get pod web -o yaml|grep serviceAccount
  serviceAccount: default
  serviceAccountName: default
      - serviceAccountToken:

服务账号与用户账号比较

Kubernetes 区分用户账号和服务账号的概念,主要基于以下原因:

类别 用户账号 服务账号
使用主体 针对人 针对Pod, Pod 中的应用程序通过服务账号访问集群。
范围 集群范围,集群中的用名称必须唯一。 命名空间范围,两个不同的名字空间可以包含具有相同名称的服务账号。
轻量程度 重量,通过认证模块配置。 轻量,用户为了具体的任务按需创建服务账号。将服务账户与用户分离开来, 使工作负载更易于遵从权限最小化原则。

服务账号令牌卷机制

默认情况下,Kubernetes 添加一个投射卷到 Pod, 此卷包括了访问 Kubernetes API 的令牌。

root@master30:~# kubectl get pod web -o yaml
...
    volumeMounts:
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: kube-api-access-sjffr
      readOnly: true
...
  volumes:
  - name: kube-api-access-sjffr
    projected:
      sources:
        - serviceAccountToken:
            path: token # 必须与应用所预期的路径匹配
        - configMap:
            items:
              - key: ca.crt
                path: ca.crt
            name: kube-root-ca.crt
        - downwardAPI:
            items:
              - fieldRef:
                  apiVersion: v1
                  fieldPath: metadata.namespace
                path: namespace

该清单片段定义了由三个数据源组成的投射卷。在当前场景中,每个数据源也代表该卷内的一条独立路径。这三个数据源是:

  1. serviceAccountToken 数据源,包含 kubelet 从 kube-apiserver 获取的令牌。 kubelet 使用 TokenRequest API 获取有时间限制的令牌**。为 TokenRequest 服务的这个令牌会在 Pod 被删除或定义的生命周期(默认为 1 小时)结束之后过期。**该令牌绑定到特定的 Pod, 并将其 audience(受众)设置为与 kube-apiserver 的 audience 相匹配。 这种机制取代了之前基于 Secret 添加卷的机制,之前 Secret 代表了针对 Pod 的 ServiceAccount 但不会过期。

    注意:没有特定的机制可以使通过 TokenRequest 签发的令牌无效。 如果你不再信任为某个 Pod 绑定的服务账号令牌, 你可以删除该 Pod。删除 Pod 将使其绑定的服务账号令牌过期。

  2. **configMap 数据源。**ConfigMap 包含一组证书颁发机构数据。 Pod 可以使用这些证书来确保自己连接到集群的 kube-apiserver(而不是连接到中间件或意外配置错误的对等点上)。

  3. downwardAPI 数据源,用于查找包含 Pod 的名字空间的名称, 并使该名称信息可用于在 Pod 内运行的应用程序代码。

Pod 内挂载这个特定卷的所有容器都可以访问上述信息。

root@master30:~# kubectl exec -it web -- bash
root@web:/usr/local/apache2# ls /var/run/secrets/kubernetes.io/serviceaccount
ca.crt	namespace  token
root@web:/usr/local/apache2# cat \
/var/run/secrets/kubernetes.io/serviceaccount/token 
eyJhbGciOiJSUzI1NiIsI......
root@web:/usr/local/apache2# cat /var/run/secrets/kubernetes.io/serviceaccount/name pace 
auth

服务账户管理

SA 创建
root@master30:~# kubectl create sa sa1
root@master30:~# kubectl get sa sa1
NAME   SECRETS   AGE
sa1    1         9s
SA 授权
root@master30:~# kubectl create clusterrolebinding auth-sa1-cluster-admin --clusterrole=cluster-admin --serviceaccount=auth:sa1
clusterrolebinding.rbac.authorization.k8s.io/auth-sa1-cluster-admin created
# 选项--serviceaccoun指明Service Account时,格式为namespace:Service Account Name
SA 使用
# 删除 pod 重新创建
root@master30:~# kubectl delete pod web
root@master30:~# kubectl run web --image=hub.laoma.cloud/library/httpd --image-pull-policy=IfNotPresent --dry-run=client -o yaml > web.yaml
root@master30:~# vim web.yaml
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: web
  name: web
spec:
  containers:
  - image: httpd
    imagePullPolicy: IfNotPresent
    name: web
    resources: {}
  dnsPolicy: ClusterFirst
  restartPolicy: Always
  # 添加以下任一记录
  serviceAccount: sa1
  serviceAccountName: sa1

status: {}
root@master30:~# kubectl apply -f web.yaml
root@master30:~# kubectl get pod web -o yaml
......
spec:
......
  serviceAccount: sa1
  serviceAccountName: sa1

重要:虽然 pod/web 中 httpd 进程具有 clusterrole/cluster-admin 角色,但是它只是用来提供httpd服务,不会做其他操作。

回收权限
root@master30:~# kubectl delete clusterrolebindings auth-sa1-cluster-admin 
clusterrolebinding.rbac.authorization.k8s.io "auth-sa1-cluster-admin" deleted
使用案例

集群 dashboard 中 pod 使用服务账户管理集群。

手动管理服务账号 token

  • v1.22 之前的 Kubernetes 版本会自动创建凭据访问 Kubernetes API。 这种机制是:先创建令牌 Secret,然后将其挂载到正运行的 Pod 中。
  • 在包括 Kubernetes v1.28 在内最近的几个版本中,使用 TokenRequest API 直接获得 API 凭据, 并使用投射卷挂载到 Pod 中。这种方法获得的令牌具有绑定的生命周期, 当挂载的 Pod 被删除时这些令牌将自动失效。
创建临时令牌
root@master30:~# kubectl create sa sa1
root@master30:~# kubectl create token sa1 
eyJhbGciOiJSUzI1NiIsImt......

# 注意:服务账号属性中是看不到关联的token的。
root@master30:~# kubectl get sa sa1 -o yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  creationTimestamp: "2023-11-01T04:05:03Z"
  name: sa1
  namespace: laoma
  resourceVersion: "11548"
  uid: 4944f75b-43ae-43fc-abfd-f2d20b7375f1
创建永久令牌

创建一个带有特殊注解 kubernetes.io/service-account.name 的 Secret 对象。一旦你手动创建一个 Secret 并将其关联到 ServiceAccount, Kubernetes 控制平面就会自动将令牌填充到该 Secret 中。

root@master30:~# vim sa1-token.yaml
apiVersion: v1
kind: Secret
metadata:
  name: sa1-secret
  annotations:
    kubernetes.io/service-account.name: sa1
type: kubernetes.io/service-account-token
root@master30:~# kubectl apply -f sa1-token.yaml

当你删除一个与某 Secret 相关联的 ServiceAccount 时,Kubernetes 的控制面会自动清理该 Secret 中长期有效的令牌。

root@master30:~# kubectl delete sa sa1 
root@master30:~# kubectl get secrets 
No resources found in laoma namespace.

说明: 尽管存在手动创建长久 ServiceAccount 令牌的机制,但还是推荐使用 TokenRequest 获得短期的 API 访问令牌。

思考

**问题:**Kubernetes 为什么不提供用户管理和身份认证功能?

Kubernetes 采用 RBAC 模型 管理用户(User)和用户组(Group)权限。但是Kubernetes中找不到真正的用户以及用户组信息,甚至连对应的User Resource以及Group Resource都没有,Kubernetes无法列出可信任的User列表以及Group列表。

换句话说 Kubernetes 并没有提供用户管理和身份认证功能,除了Service Account外,所有的用户信息都依赖外部的用户管理系统来存储,因此通过api-serever根本无法列出User和Group。

**解答:**这其实挺符合UNIX设计哲学的,即 Do One Thing and Do It Well

Kubernetes只专注于做应用编排,其他的功能则提供接口集成,除了认证和鉴权,我们发现网络、存储也都如此。

这样做的好处也显而易见,用户账户信息与Kubernetes集群松耦合,便于集成企业已有的身份认证系统,如AD、LDAP、Keycloak等。

环境清理

root@master30:~# kubectl delete ns auth

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 单集群管理。

版本支持:

image-20230215115931658
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;

版本支持:

image-20230215120112579

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。
img
安装过程
# 创建目录
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内容到红色方框中)

image-20230215143057258

导入后效果:

image-20230215143241263

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

参考资料

安装手册:https://kuboard.cn/install/v3/install.html

Kubernetes Dashboard

介绍

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

Kubernetes Dashboard 是 Kubernetes 的官方 Web UI。

Kubernetes Dashboard 具备的功能:

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

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

部署

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

# 查看images
root@master30:~# grep image: recommended.yaml 
          image: kubernetesui/dashboard:v2.6.1
          image: kubernetesui/metrics-scraper:v1.0.8
root@master30:~# sed -i 's#image: kubernetesui#image: hub.laoma.cloud/kubernetesui#g' recommended.yaml 

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

验证

root@master30:~# kubens
default
kube-node-lease
kube-public
kube-system
kubernetes-dashboard

root@master30:~# kubens kubernetes-dashboard
root@master30:~# kubectl get deployments.apps 
NAME                        READY   UP-TO-DATE   AVAILABLE   AGE
dashboard-metrics-scraper   1/1     1            1           2m25s
kubernetes-dashboard        1/1     1            1           2m25s

root@master30:~# kubectl get service
NAME                                   TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
kubernetes-dashboard-metrics-scraper   ClusterIP   10.98.116.192   <none>        8000/TCP   3m54s
kubernetes-dashboard-web               ClusterIP   10.107.63.216   <none>        8000/TCP   3m54s

root@master30:~# kubectl get serviceaccounts 
NAME                   SECRETS   AGE
default                1         23h
kubernetes-dashboard   1         23h

root@master30:~# kubectl describe serviceaccounts kubernetes-dashboard 
Name:                kubernetes-dashboard
Namespace:           kubernetes-dashboard
Labels:              app.kubernetes.io/part-of=kubernetes-dashboard
Annotations:         <none>
Image pull secrets:  <none>
Mountable secrets:   <none>
Tokens:              <none>
Events:              <none>

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

# sa/kubernetes-dashboard具有ClusterRole/kubernetes-dashboard
root@master30:~# 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:~# 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:~# kubectl get deployments.apps kubernetes-dashboard -o yaml|grep ' serviceAccount'
      serviceAccount: kubernetes-dashboard
      serviceAccountName: kubernetes-dashboard

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

root@master30:~# kubectl edit svc kubernetes-dashboard
spec:
......
  type: NodePort

root@master30:~# kubectl get svc kubernetes-dashboard
NAME                   TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)         AGE
kubernetes-dashboard   NodePort   10.111.32.116   <none>        443:30518/TCP   4m45s

使用ingress配置访问,参考:

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.laoma.cloud
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service: 
            name: kubernetes-dashboard
            port: 
              number: 443

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

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

访问 Dashboard

访问 https://10.1.8.30:30518

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

新版本

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

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

root@master30:~# cat > kubernetes-dashboard-admin.yaml <<'EOF'
apiVersion: v1
kind: ServiceAccount
metadata:
  name: kubernetes-dashboard-admin
  namespace: kubernetes-dashboard
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: kubernetes-dashboard-admin
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: kubernetes-dashboard-admin
  namespace: kubernetes-dashboard
EOF
root@master30:~# kubectl apply -f kubernetes-dashboard-admin.yaml

# 创建 Bearer Token
root@master30:~# kubectl -n kubernetes-dashboard create token kubernetes-dashboard-admin
eyJhbGciOiJSUzI1NiIsImtpZCI6ImhLWEhaTWQtNGZTek5zUFUyUmNobzN1V2ZlOG1iWkwzSU5kZDZLbU1WUTAifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNjc2NDU4ODI3LCJpYXQiOjE2NzY0NTUyMjcsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsInNlcnZpY2VhY2NvdW50Ijp7Im5hbWUiOiJhZG1pbi11c2VyIiwidWlkIjoiYmViNGI3MWMtNmI5MS00ODk4LThiMTMtZWQwZWE3N2JiMmQ5In19LCJuYmYiOjE2NzY0NTUyMjcsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDprdWJlcm5ldGVzLWRhc2hib2FyZDphZG1pbi11c2VyIn0.kBwU3g3Euqj5jMKD9AYu-OQW2yc9pc2hb7uaDi2PO3AdkOBVMkaIiXnWEUC98hpFyXUWGSiaZWPsylJHXMpWz_X-J03nd7zZegUhPbzVxPh7vHyaOaILmqATlakAHMHbwcEQDygBQPBNbPNd-hD-rzaVNkmr2oARNGVRaX7fMNRkuvcbji0-s6IO5yotKkWaGXkIAtwmMTgtgcGnOpVsB97UYjPgWz6RMBL9ZCZIVXHxE_FIl_If7ewhObyaijZIbWeZ21job5PPvb1H1VAHOZoDXtFK_lZdIRIK6e6MYYU4mfYqTcOyiEG8c5SxCOaD4xchWGdzFcfZfn52VQJ7hQ

提示:Token默认有效期24小时,过期后自动删除。下一次登录需要生成新的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 使用案例

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

# Kubernetes Dashboard 以deployment形式运行
root@master30:~# kubectl get deployments.apps 
NAME                        READY   UP-TO-DATE   AVAILABLE   AGE
dashboard-metrics-scraper   1/1     1            1           146m
kubernetes-dashboard        1/1     1            1           146m

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

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

# Role/kubernetes-dashboard 具备能力如下:
root@master30:~# 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:~# kubectl -n kubernetes-dashboard create token kubernetes-dashboard 
eyJhbGciOiJSUzI1NiIsImtpZCI6Im9jekNIUUFRZUpKYzA3VXdmRHJvT3ZhaU1fMFNxVkh6NEJZa2lyUnhaaXcifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzc4MDUwMjI4LCJpYXQiOjE3NzgwNDY2MjgsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiMTY0YzllOTItMzUwMS00Y2M2LWEwNjctOGZiNDI4OTQ4MDIwIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsInNlcnZpY2VhY2NvdW50Ijp7Im5hbWUiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsInVpZCI6IjU3ZDAwNjUxLTZmYWQtNDJmOC04ZmYxLTM3YmRmZGRiY2Y3ZCJ9fSwibmJmIjoxNzc4MDQ2NjI4LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZXJuZXRlcy1kYXNoYm9hcmQ6a3ViZXJuZXRlcy1kYXNoYm9hcmQifQ.EehmwsmNT6OVbazZs1pUOr_7kletmrpduE-8p4m2Y49c_hWXKs3ic7u3DMxw4jNJgbPfnvBiq2nrIObknsXBi6BKXL5XLDSlwsZobYUd0Uw0op2YOZZ3O_B4A-sSOiD-qD1eFMw2RYyR6aRuycWLngvYvyN3pfFCsd1GS_Gh4rAetsPtW4QzlFf230HL8Az2fU-M7A-0_kkt_ujpgMXQpyPSxV5qgyffjnDkOo-llDR-j_QsyyMt12k1zSjzN5ApcGQtHn9y8vbIS0xoqHulmU38kOjnKNpeQJ41nwKnf53q2I2MDoatO5r-hnD_TGx6xcckW0qFNrpje38XgRPHTQ

使用 serviceAccount: kubernetes-dashboard Token 登录

image-20230215180431064

无法看到Pods。

image-20210827081857816

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 回收策略,指定 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:比如存储类型(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 是 K8s 官方推荐的本地存储动态卷插件,适合需要使用节点本地磁盘的场景。

部署本地路径制备器

# 下载官方模板
root@master30:~# wget https://github.com/rancher/local-path-provisioner/blob/v0.0.35/deploy/local-path-storage.yaml

# 查看资源配置
root@master30:~# 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
root@master30:~# kubectl apply -f local-path-storage.yaml

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

root@master30:~# kubectl get all -n local-path-storage 
NAME                                          READY   STATUS    RESTARTS   AGE
pod/local-path-provisioner-74b5c4bf98-955sk   1/1     Running   0          63s

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

NAME                                                DESIRED   CURRENT   READY   AGE
replicaset.apps/local-path-provisioner-74b5c4bf98   1         1         1       63s

验证部署

# 设置默认命名空间
root@master30:~# kubectl config set-context --current --namespace default
1. 创建 pvc
root@master30:~# 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:~# kubectl apply -f local-path-pvc.yaml
root@master30:~# kubectl get pvc
NAME             STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
local-path-pvc   Pending                                      local-path     <unset>                 6s

# 等待pod关联才会创建本地目录
2. 创建 deployment
root@master30:~# 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:~# kubectl apply -f local-path-deployment.yaml

# 查看 pod 状态
root@master30:~# kubectl get pods -o wide
NAME                      READY   STATUS    RESTARTS   AGE    IP             NODE                   NOMINATED NODE   READINESS GATES
webapp-6d46b54487-6nqmh   1/1     Running   0          114s   10.224.19.21   worker31.laoma.cloud   <none>           <none>
webapp-6d46b54487-kzkd5   1/1     Running   0          114s   10.224.19.22   worker31.laoma.cloud   <none>           <none>
# 所有节点都调度到 worker31,因为只用worker31可以提供这个卷。

# 查看 pvc 状态
root@master30:~# kubectl get pvc
NAME             STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
local-path-pvc   Bound    pvc-e992833c-af6a-4d4d-aff5-fc544394db6d   5Gi        RWO            local-path     <unset>                 4m41s

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

# 验证本地路径
root@worker31:~# ls /opt/local-path-provisioner/pvc-e992833c-af6a-4d4d-aff5-fc544394db6d_default_local-path-pvc/
index.html
root@worker31:~# cat /opt/local-path-provisioner/pvc-e992833c-af6a-4d4d-aff5-fc544394db6d_default_local-path-pvc/index.html 
Hello World From Nginx
3. 清理资源
# 删除 deployment
root@master30:~# kubectl delete deployment webapp

# 删除 pvc
root@master30:~# kubectl delete pvc local-path-pvc 

# 本地目录也被删除
root@worker31:~# ls /opt/local-path-provisioner/pvc-e992833c-af6a-4d4d-aff5-fc544394db6d_default_local-path-pvc/
ls: cannot access '/opt/local-path-provisioner/pvc-e992833c-af6a-4d4d-aff5-fc544394db6d_default_local-path-pvc/': No such file or directory

部署 NFS provisioner

部署 NFS 服务

# 安装 NFS server
root@master30:~# apt install -y nfs-kernel-server

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

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

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

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

部署 NFS provisioner

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

# 资源部署到命名空间:kube-storage
root@master30:~# kubectl create ns kube-storage
root@master30:~# kubectl config set-context --current --namespace kube-storage
1. 创建 RBAC 权限

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

root@master30:~# 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:~# kubectl apply -f nfs-rbac.yaml
2. 部署 NFS Provisioner 应用
root@master30:~# 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:~# kubectl apply -f nfs-provisioner.yaml

# 查看 NFS Provisioner 应用
root@master30:~# kubectl get deployments.apps -n kube-storage 
NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
nfs-client-provisioner   1/1     1            1           1m
3. 创建 NFS StorageClass
root@master30:~# 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:~# kubectl apply -f nfs-storageclass.yaml

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

验证部署

在default命名空间测试。

root@master30:~# kubens default
1. 创建 pvc
root@master30:~# 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:~# kubectl apply -f nfs-pvc.yaml 

# 查看 pvc 状态
root@master30:~# kubectl get pvc
NAME       STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
webclaim   Bound    pvc-6a020ff4-6b7f-4ef4-90fc-3dc94d3e8ddc   5Gi        RWO           default    <unset>                 3s

# 查看 pv 状态
root@master30:~# kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                   STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
pvc-6a020ff4-6b7f-4ef4-90fc-3dc94d3e8ddc   5Gi        RWO            Delete           Bound    default/webclaim  default    <unset>                          65s

# 查看 nfs 共享目录
root@master30:~# ls /shares/
default-webclaim-pvc-6a020ff4-6b7f-4ef4-90fc-3dc94d3e8ddc

# 准备 web 主页
root@master30:~/nfs-storage# echo Hello World From Nginx > /shares/default-webclaim-pvc-6a020ff4-6b7f-4ef4-90fc-3dc94d3e8ddc/index.html
2. 创建 deployment
root@master30:~# 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:~# kubectl apply -f nfs-deploy.yaml

root@master30:~# kubectl expose deployment webapp --target-port 80 --port 80 --protocol TCP
root@master30:~# kubectl get svc webapp 
NAME     TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
webapp   ClusterIP   10.109.140.205   <none>        80/TCP    5m11s

# 访问测试
root@master30:~# curl 10.109.140.205
Hello World From Nginx
root@master30:~# curl 10.109.140.205
Hello World From Nginx
3. 清理资源
# 删除 deployment
root@master30:~# kubectl delete deployments.apps webapp

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

# 查看后端存储中数据,仍然保留
root@master30:~# ls  /shares/default-webclaim-pvc-6a020ff4-6b7f-4ef4-90fc-3dc94d3e8ddc/
index.html

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

课后作业

完成以下动态卷制备:

  • cephfs
  • RBD

Kubernetes StatefulSet

环境准备

root@master30:~# kubectl create ns statefulset
root@master30:~# kubectl config set-context --current --namespace 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 服务器

# 安装 NFS server
root@master30:~# apt install -y nfs-kernel-server

# 安创建NFS目录 修改创建文件夹的权限
root@master30:~# 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:~# systemctl restart nfs-server.service

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

示例 1:部署 Nginx 集群

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

步骤 1:创建 StatefulSet

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

root@master30:~# 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:~# kubectl apply -f nginx-statefulset.yaml

# 查看 pods
root@master30:~# 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:~# kubectl get sts
NAME    READY   AGE
nginx   3/3     10m

# 查看 pvc 创建和pv绑定过程(可选)
root@master30 ~# watch "kubectl get pvc| awk '{print $1,$3}'"
NAME VOLUME
nginx-data-nginx-0 pvc-8eb06bf1-a5de-4e94-bf83-514941f73c9f
nginx-data-nginx-1 pvc-e853eccd-8060-4326-8c45-0b88c34a3d97
nginx-data-nginx-2 pvc-27d944f7-1e62-4352-9023-3874e6867ef9
步骤 2:准备 nfs 存储
root@master30 ~# kubectl get pvc|awk '{print $1,$3}'
NAME VOLUME
nginx-data-nginx-0 pvc-8eb06bf1-a5de-4e94-bf83-514941f73c9f
nginx-data-nginx-1 pvc-e853eccd-8060-4326-8c45-0b88c34a3d97
nginx-data-nginx-2 pvc-27d944f7-1e62-4352-9023-3874e6867ef9

root@master30 ~# echo nginx-data-nginx-0 > /shares/statefulset-nginx-data-nginx-0-pvc-8eb06bf1-a5de-4e94-bf83-514941f73c9f/index.html
root@master30 ~# echo nginx-data-nginx-1 > /shares/statefulset-nginx-data-nginx-1-pvc-e853eccd-8060-4326-8c45-0b88c34a3d97/index.html
root@master30 ~# echo nginx-data-nginx-2 > /shares/statefulset-nginx-data-nginx-2-pvc-27d944f7-1e62-4352-9023-3874e6867ef9/index.html
步骤 3:创建 Headless Service

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

root@master30:~# 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:~# kubectl apply -f nginx-service.yaml

# 没有 CLUSTER-IP 
root@master30:~# kubectl get svc
NAME    TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
nginx   ClusterIP   None         <none>        80/TCP    16m
步骤 4:验证部署
# 通过pod名称验证 pod 内容
root@master30 ~# 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:~# kubectl delete pod nginx-2
root@master30:~# 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 ~# kubectl exec test -it -- bash
If you don't see a command prompt, try pressing enter.
root@test:/# curl nginx-2.nginx
nginx-data-nginx-2

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

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

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

root@master30 ~# 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 ~# kubectl apply -f nginx-service-www.yaml
root@master30 ~# kubectl get svc www
NAME    TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
www     ClusterIP   10.100.163.95   <none>        80/TCP    20s

# 通过svc访问后端pod
root@master30 ~# curl 10.100.163.95
nginx-data-nginx-0
root@master30 ~# curl 10.100.163.95
nginx-data-nginx-1
root@master30 ~# curl 10.100.163.95
nginx-data-nginx-2
步骤 5:清理环境
root@master30:~# kubectl delete sts nginx
root@master30:~# kubectl delete svc nginx www 
root@master30:~# kubectl delete pvc nginx-data-nginx-{0..2}
root@master30 ~# rm -fr /shares/statefulset-nginx-data-nginx-{0..2}-*
root@master30:~# kubectl delete pod test

示例 2:部署 Etcd 集群

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

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

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

root@master30:~# 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:~# kubectl apply -f etcd-service.yaml

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

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

root@master30:~# 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:~# kubectl apply -f etcd-pv.yaml
root@master30:~# 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>                          5s
etcd-pv-1   10Gi       RWO            Retain           Available           etcd-storage   <unset>                          5s
etcd-pv-2   10Gi       RWO            Retain           Available           etcd-storage   <unset>                          5s

建议:使用动态卷制备。

步骤 4:创建 StatefulSet

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

root@master30:~# 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:~# kubectl apply -f etcd-statefulset.yaml

# 查看 pods
root@master30:~# kubectl get pods
NAME     READY   STATUS    RESTARTS   AGE
etcd-0   1/1     Running   0          77s
etcd-1   1/1     Running   0          74s
etcd-2   1/1     Running   0          71s


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

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

验证集群状态

# 验证集群状态:建立集群
root@master30:~# 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:~# kubectl delete pod etcd-2
NAME     READY   STATUS    RESTARTS   AGE
etcd-0   1/1     Running   0          3m26s
etcd-1   1/1     Running   0          3m23s
etcd-2   1/1     Running   0          11s

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

验证数据

root@master30:~# kubectl exec etcd-0 -- etcdctl put /users/user1/name laowang
OK
root@master30:~# kubectl exec etcd-2 -- etcdctl get /users/user1/name
/users/user1/name
laowang
步骤 6:清理环境
root@master30:~# kubectl delete sts etcd
root@master30:~# kubectl delete svc etcd 
root@master30:~# kubectl delete pvc etcd-data-etcd-{0..2}
root@master30:~# kubectl delete pv etcd-pv-{0..2}
root@master30:~# rm -fr /shares/etcd

示例 3:部署 Redis 集群

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

步骤 1:准备 NFS 存储
# 准备应用目录
root@master30:~# mkdir -m 777 -p /shares/redis/data-{0..2}
步骤 2:创建 Headless Service

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

root@master30:~# cat > redis-service.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
  name: redis
  namespace: statefulset
  labels:
    app: redis
spec:
  ports:
  - port: 6379
    name: redis
  - port: 26379
    name: sentinel
  clusterIP: None
  selector:
    app: redis
EOF

root@master30:~# kubectl apply -f redis-service.yaml
root@master30:~# 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:~# 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

root@master30:~# kubectl apply -f redis-pv.yaml
root@master30:~# 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:~# 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:
EOF

root@master30:~# kubectl apply -f redis-statefulset.yaml

root@master30:~# kubectl get pods
NAME      READY   STATUS    RESTARTS   AGE
redis-0   1/1     Running   0          4m49s
redis-1   1/1     Running   0          4m46s
redis-2   1/1     Running   0          4m44s

root@master30:~# kubectl get sts
NAME    READY   AGE
redis   3/3     4m52s
步骤 5:验证部署

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

root@master30:~# 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.113.181,port=6379,state=online,offset=106182,lag=1
slave1:ip=10.224.19.24,port=6379,state=online,offset=106011,lag=1
master_failover_state:no-failover
master_replid:7048070355085a34084baef93981a849ae20db1c
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:106353
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:106353

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

root@master30:~# 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) "21cc9a4d3a506fa9b04734b62c22f051c7fb9806"
 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) "43"
19) "last-ping-reply"
20) "43"
21) "down-after-milliseconds"
22) "5000"
23) "info-refresh"
24) "9677"
25) "role-reported"
26) "master"
27) "role-reported-time"
28) "461721"
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"

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

## 在 redis-0(主节点)设置测试数据
root@master30:~# kubectl exec -it redis-0 -- redis-cli -a redis123 set test-key "redis-cluster-test"

## 在 redis-1(从节点)查看测试数据
root@master30:~# 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:~# 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.19.24
master_port:6379
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_read_repl_offset:210705
slave_repl_offset:210705
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:72c5eb5bf94433adb77857bea5d3c631841d5cfb
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:210705
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:209970
repl_backlog_histlen:736
步骤 6:清理环境
root@master30:~# kubectl delete svc redis
root@master30:~# kubectl delete sts redis
root@master30:~# kubectl delete pvc redis-data-redis-{0..2} 
root@master30:~# kubectl delete pv redis-pv-{0..2} 
root@master30:~# rm -fr /shares/redis/data-*/*

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 可保留,数据不丢失)。
    nline,offset=106011,lag=1
    master_failover_state:no-failover
    master_replid:7048070355085a34084baef93981a849ae20db1c
    master_replid2:0000000000000000000000000000000000000000
    master_repl_offset:106353
    second_repl_offset:-1
    repl_backlog_active:1
    repl_backlog_size:1048576
    repl_backlog_first_byte_offset:1
    repl_backlog_histlen:106353


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

```bash
root@master30:~# 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) "21cc9a4d3a506fa9b04734b62c22f051c7fb9806"
 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) "43"
19) "last-ping-reply"
20) "43"
21) "down-after-milliseconds"
22) "5000"
23) "info-refresh"
24) "9677"
25) "role-reported"
26) "master"
27) "role-reported-time"
28) "461721"
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"

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

## 在 redis-0(主节点)设置测试数据
root@master30:~# kubectl exec -it redis-0 -- redis-cli -a redis123 set test-key "redis-cluster-test"

## 在 redis-1(从节点)查看测试数据
root@master30:~# 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:~# 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.19.24
master_port:6379
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_read_repl_offset:210705
slave_repl_offset:210705
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:72c5eb5bf94433adb77857bea5d3c631841d5cfb
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:210705
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:209970
repl_backlog_histlen:736
步骤 6:清理环境
root@master30:~# kubectl delete svc redis
root@master30:~# kubectl delete sts redis
root@master30:~# kubectl delete pvc redis-data-redis-{0..2} 
root@master30:~# kubectl delete pv redis-pv-{0..2} 
root@master30:~# rm -fr /shares/redis/data-*/*

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编程工具,助力开发者即刻编程。

更多推荐