Kubernetes中的多租户

多租户

多租户集群由多个用户和/或工作负载共享,这些用户和/或工作负载被称为“租户”。多租户集群的运营方必须将租户彼此隔离,以最大限度地减少被盗用的租户或恶意租户可能对集群和其他租户造成的损害。此外,必须在租户之间公平地分配集群资源。

在规划多租户架构时,应该考虑 Kubernetes 中的资源隔离层:集群、命名空间、节点、Pod 和容器。还应该考虑在租户之间共享不同类型资源的安全隐患。例如,将来自不同租户的 Pod 调度到同一节点上可以减少集群中所需的机器数量。另一方面,可能需要阻止某些工作负载共置。例如可能不允许来自组织外部的不受信任的代码与处理敏感信息的容器在同一节点上运行。

虽然 Kubernetes 不能保证租户之间完全安全地隔离,但它为特定使用场景提供了足够的相关功能。可以将每个租户及其 Kubernetes 资源分隔到各自的命名空间中。然后,可以使用一些限制策略来强制执行租户隔离。现在策略通常按命名空间划分,可用于限制 API 访问、资源使用以及允许容器执行的操作。

多租户集群的租户共享以下资源:

  • 扩展程序、控制器、插件和自定义资源定义 (CRD)。
  • 集群控制平面。这意味着集群操作、安全性和审核是集中管理的。

与运营多个单租户集群相比,运营多租户集群有几个优点:

  • 减少管理开销
  • 减少资源碎片
  • 新租户无需等待集群创建

多租户使用用例

企业多租户

在企业环境中,集群的租户是组织内的不同团队。通常,每个租户对应一个命名空间,同一个命名空间内的网络流量不受限制,但不同命名空间之间的网络流量必须明确列入白名单。可以使用 Kubernetes 网络策略来实现这些隔离。

集群的用户根据其权限分为三种不同的角色:

  • 集群管理员:此角色适用于在整个集群中管理所有租户的管理员。集群管理员可以创建、读取、更新和删除任何策略对象。他们可以创建命名空间并将其分配给命名空间管理员。
  • 命名空间管理员:此角色适用于特定单一租户的管理员,命名空间管理员可以管理其命名空间中的用户。
  • 开发者:此角色的成员可以创建、读取、更新和删除命名空间内的非策略对象,如 Pod、Job 和 Ingress。开发者只在他们有权访问的命名空间中拥有这些权限。

image

SaaS提供商多租户

SaaS 提供商集群的租户是应用的各个客户专用实例,以及 SaaS 的控制平面。要充分利用按命名空间划分的政策,应将各个应用实例安排到其各自的命名空间中,SaaS 控制平面的组件也应如此。最终用户无法直接与 Kubernetes 控制平面交互,而是使用 SaaS 的界面,由后者与 Kubernetes 控制平面进行交互。

例如,博客平台可以在多租户集群上运行,在这种情况下,租户是每个客户的博客实例和平台自己的控制平面。平台的控制平面和每个托管博客都将在不同的命名空间中运行。客户将通过平台的界面来创建和删除博客、更新博客软件版本,但无法了解集群的运作方式。

多租户策略

命名空间

Kubernetes 中的多租户都是根据 Namespace 来进行划分的,Namespace 是一组逻辑的集群,可以大概类似于租户的概念,可以做到一定程度的资源隔离、Quota。

如下面两条命令:

$ kubectl run nginx -image=nginx
$ kubectl run nginx -image=nginx -namespace=dev

这两条命令虽然都是 run 起来一个 nginx,但是作用域却不一样。第一条命令则是在 Default 这个命名空间中,第二条命令是在一个叫 dev 的 namespace 里运行nginx。

访问权限控制

对于多租户来说,访问权限控制是非常重要的,我们可以使用 Kubernetes 内置的 RBAC 来进行权限控制,可以为集群中的特定资源和操作授予细化的权限。

网络策略

通过集群网络策略,我们可以控制集群的 Pod 之间的通信,策略可以指定 Pod 可以与哪些命名空间、标签和 IP 地址范围进行通信。

资源配额

资源配额用于管理命名空间中对象使用的资源量,我们可以按 CPU 和内存用量或对象数量来设置配额。通过资源配额,可以确保租户不会使用超过其分配份额的集群资源。

资源配额是通过 ResourceQuota 资源对象来定义的,可以对每个 namespace 的资源消耗总量提供限制。它可以按类型限制 namespace 下可以创建的对象的数量,也可以限制可被该项目以资源形式消耗的计算资源的总量。

资源配额的工作方式如下:

​ 1、管理员为每个 namespace 创建一个或多个资源配额对象

​ 2、用户在 namespace 下创建资源 (pods、 services 等),同时配额系统会跟踪使用情况,来确保其不超过资源配额中定义的硬性资源限额

​ 3、如果资源的创建或更新违反了配额约束,则请求会失败,并返回 HTTP 状态码 403 FORBIDDEN,以及说明违反配额约束的信息

​ 4、如果 namespace 下的计算资源(如 cpu 和 memory)的配额被启用,则用户必须为这些资源设定请求值(request) 和约束值(limit),否则配额系统将拒绝 Pod 的创建。

Kubernetes 中主要有3个层级的资源配额控制:

​ 1、容器:可以对 CPU 和 Memory 进行限制

​ 2、POD:可以对一个 Pod 内所有容器的的资源进行限制

​ 3、Namespace:为一个命名空间下的资源进行限制

其中容器层次主要利用容器本身的支持,比如 Docker 对 CPU、内存等的支持;

Pod 方面可以限制系统内创建 Pod 的资源范围,比如最大或者最小的 CPU、memory 需求;

Namespace 层次就是对用户级别的资源限额了,包括 CPU、内存,还可以限定 Pod、RC、Service 的数量。

要使用资源配额的话需要确保 apiserver--enable-admission-plugins= 参数中包含 ResourceQuota,当 namespace 中存在一个 ResourceQuota 对象时,该 namespace 即开始实施资源配额的管理工作了,另外需要注意的是一个 namespace 中最多只应存在一个 ResourceQuota 对象。

资源配额控制器支持的配额控制资源主要包括:计算资源配额、存储资源配额、对象数量资源配额以及配额作用域。

计算资源配额

用户可以对给定 namespace 下的计算资源总量进行限制,支持的资源类型如下所示:

资源名称 描述
CPU 所有非终止状态的Pod中,其CPU需求总量不能超过该值
limits.cpu 所有非终止状态的Pod中,其CPU限额总量不能超过该值
limits.memory 所有非终止状态的Pod中,其内存限额总量不能超过该值
memory 所有非终止状态的Pod中,其内存需求总量不能超过该值
request.cpu 所有非终止状态的Pod中,其CPU需求总量不能超过该值
request.memory 所有非终止状态的Pod中,其内存需求总量不能超过该值

比如我们现在来为一个命名空间创建内存和 CPU 配额,首先创建一个测试用的命名空间:

$ kubectl create namespace dev

然后定义一个如下所示的资源配额资源对象:(quota-mem-cpu.yaml)

apiVersion: v1
kind: ResourceQuota
metadata:
  name: mem-cpu-demo
  namespace: dev
spec:
  hard:
    requests.cpu: "1"
    requests.memory: 1Gi
    limits.cpu: "2"
    limits.memory: 2Gi

创建完成后,我们查看ResourceQuota这个对象:

$ kubectl describe quota mem-cpu-demo -n dev
Name:            mem-cpu-demo
Namespace:       dev
Resource         Used  Hard
--------         ----  ----
limits.cpu       0     2
limits.memory    0     2Gi
requests.cpu     0     1
requests.memory  0     1Gi

现在我们来创建一个如下所示的 Pod:

apiVersion: v1
kind: Pod
metadata:
  name: quota-mem-cpu-demo
  namespace: dev
spec:
  containers:
  - name: nginx
    image: nginx
    resources:
      limits:
        memory: "800Mi"
        cpu: "800m" 
      requests:
        memory: "600Mi"
        cpu: "400m"

创建上面的Pod,查看运行状态:

$ kubectl get pod -n dev
NAME                 READY   STATUS    RESTARTS   AGE
quota-mem-cpu-demo   1/1     Running   0          78s

可以看到Pod已经运行起来了,这时候我们再次查看我们定义的资源配额对象:

$ kubectl describe quota mem-cpu-demo -n dev
Name:            mem-cpu-demo
Namespace:       dev
Resource         Used   Hard
--------         ----   ----
limits.cpu       800m   2
limits.memory    800Mi  2Gi
requests.cpu     400m   1
requests.memory  600Mi  1Gi

我们可以看到已经明确告诉我们已经使用了多少计算资源了,比如内存的请求值只剩 400Mi(1Gi-600Mi)资源了,我们现在来创建一个大于 400Mi 请求内存的资源测试下:

apiVersion: v1
kind: Pod
metadata:
  name: quota-mem-cpu-demo-2
  namespace: quota-mem-cpu-example
spec:
  containers:
  - name: quota-mem-cpu-demo-2-ctr
    image: redis
    resources:
      limits:
        memory: "1Gi"
        cpu: "800m"      
      requests:
        memory: "700Mi"
        cpu: "400m"

我们创建这个Pod,结果发现不成功:

$ kubectl apply -f quota_nginx2.yaml 
Error from server (Forbidden): error when creating "quota_nginx2.yaml": pods "quota-mem-cpu-demo-2" is forbidden: exceeded quota: mem-cpu-demo, requested: requests.memory=700Mi, used: requests.memory=600Mi, limited: requests.memory=1Gi

可以看到已经被拒绝了,因为 requests.memory 已经超过了我们的资源配额的限制了。

从上面的案例看我们可以使用 ResourceQuota 来限制命名空间中运行的所有容器的 CPU 和 内存的资源配额总数,如果限制单个容器而不是所有容器的总数,就需要使用 LimitRange 资源对象了。另外如果在一个命名空间下面计算资源(如 CPU 和内存)的配额被启用了,则用户必须为这些资源设置请求值(request)和约束值(limit),否则配额系统将拒绝 Pod 的创建,除非我们配置了 LimitRange 资源对象。

要使用 LimitRange 同样需要在 --enable-admission-plugins= 参数中开启 LimitRanger。比如现在我们来配置一个命名空间中容器的最小和最大的内存限制,我们创建一个命名空间来进行配置:

$ kubectl create namespace mem-example

然后创建一个LimitRange的配置资源对象:

apiVersion: v1
kind: LimitRange
metadata:
  name: mem-min-max-demo-lr
  namespace: constraints-mem-example
spec:
  limits:
  - max:
      memory: 1Gi
    min:
      memory: 500Mi
    type: Container

创建成后,我们查看详细信息:

$ kubectl get limitrange mem-min-max-demo-lr --namespace=mem-example -o yaml
......
spec:
  limits:
  - default:
      memory: 1Gi
    defaultRequest:
      memory: 1Gi
    max:
      memory: 1Gi
    min:
      memory: 500Mi
    type: Container

上面输出显示了最小和最大的内存约束,但是要注意即使我们没有指定默认值,也会自动创建的。现在,只要在 mem-example 命名空间中创建容器,Kubernetes 就会执行下面的步骤:

  • 如果 Container 未指定自己的内存请求和限制,将为它指定默认的内存请求和限制
  • 验证 Container 的内存请求是否大于或等于 500 MiB
  • 验证 Container 的内存限制是否小于或等于1 GiB

下面我们这里来创建一个 Pod,其中容器声明了 600 MiB 的内存请求和 800 MiB 的内存限制,这些满足了 LimitRange 的最小和最大内存约束:

apiVersion: v1
kind: Pod
metadata:
  name: mem-demo
  namespace: mem-example
spec:
  containers:
  - name: nginx
    image: nginx
    resources:
      limits:
        memory: "800Mi"
      requests:
        memory: "600Mi"

然后直接创建查看状态:

$ kubectl get pod  -n mem-example
NAME       READY   STATUS    RESTARTS   AGE
mem-demo   1/1     Running   0          38s

可以看到是可以正常运行的。

然后我们再创建一个超过最大内存限制的 Pod 测试下:

apiVersion: v1
kind: Pod
metadata:
  name: mem-demo-2
  namespace: mem-example
spec:
  containers:
  - name: nginx
    image: nginx
    resources:
      limits:
        memory: "1.5Gi"
      requests:
        memory: "800Mi"

现在我们创建,发现不成功:

Error from server (Forbidden): error when creating "memdemo2.yaml": pods "mem-demo-2" is forbidden: maximum memory usage per Container is 1Gi, but limit is 1536Mi

输出结果显示 Pod 没有创建成功,因为容器声明的内存限制太大了。我们也可以去尝试下创建一个小于最小内存限制的 Pod 或没有声明内存请求和限制的 Pod。

存储配额

用户可以对给定 namespace 下的存储资源总量进行限制,此外,还可以根据相关的存储类(Storage Class)来限制存储资源的消耗。

资源名称 描述
requests.storage 所有的PVC中,存储资源的需求不能超过该值
persistentvolumeclaims namespace中所允许的PVC总量
.storageclass.storage.k8s.io/requests.storage 所有该storage-class-name相关的PVC中,存储资源的需求不能超过该值
.storageclass.storage.k8s.io/persistentvolumeclaims namespace中所允许的该sorage-calss-name相关的PVC总量

对象数量配置

资源名称 描述
configmaps namespace下允许存在的configmap的数量
persistentvolumeclaims namespace下允许存在的PVC数量
pods namespace下允许存在的非终止状态的Pod数量,如果Pod的status.phase为Failed或Succeeded,那么其处于终止状态。
replicationcontrollers namespace下允许存在的replication controllers的数量
resourcequotas namespace下允许存在的resource quotas的数量
services namespace下允许存在的service的数量
services.loadbalancers namespace下允许存在的load balancer类型的service的数量
servicesnodeports namespace下允许存在的node port类型的service的数量
secrets namespace下允许存在的secret的数量

Qos服务质量

当 Kubernetes 创建 Pod 时,它会将以下 QoS 中的一类分配给 Pod:

​ 1、Guaranteed

​ 2、Burstable

​ 3、BestEffort

Guaranteed

要为 Pod 提供 Guaranteed 的 QoS 类必须满足以下需求:

​ 1、Pod 中的每个 Container 都必须有内存限制和内存请求。

​ 2、对于 Pod 中的每个 Container,内存限制必须等于内存请求。

​ 3、Pod 中的每个 Container 都必须有一个 CPU 限制和一个 CPU 请求。

​ 4、对于 Pod 中的每个 Container,CPU 限制必须等于 CPU 请求。

这些限制同样适用于 init 容器和 app 容器。

下面我们来创建Pod,Container 有一个内存限制和一个内存请求,两者都等于 200 MiB。Container 有一个 CPU 限制和一个 CPU 请求,都等于 700 milliCPU:

apiVersion: v1
kind: Namespace
metadata:
  name: qos-example

---
apiVersion: v1
kind: Pod
metadata:
  name: qos-demo
  namespace: qos-example
spec:
  containers:
  - name: qos-demo-ctr
    image: nginx
    resources:
      limits:
        memory: "200Mi"
        cpu: "700m"
      requests:
        memory: "200Mi"
        cpu: "700m"

创建Pod后,查看Pod的详细信息:

$ kubectl get pod qos-demo -n qos-example -oyaml
spec:
  containers:
  - image: nginx
    imagePullPolicy: Always
    name: qos-demo-ctr
    resources:
      limits:
        cpu: 700m
        memory: 200Mi
      requests:
        cpu: 700m
        memory: 200Mi
    ...
status:
  qosClass: Guaranteed

输出显示 Kubernetes 为 Pod 提供了 Guaranteed 的 QoS 类。输出还验证 Pod 容器是否有与其内存限制相匹配的内存请求,并且它有一个与其 CPU 限制相匹配的 CPU 请求。

注意:

如果 Container 指定了自己的内存限制,但没有指定内存请求,Kubernetes 会自动分配与限制匹配的内存请求。同样,如果一个Container指定了自己的CPU限制,但没有指定CPU请求,Kubernetes会自动分配一个匹配该限制的CPU请求。

Burstable

在以下情况下,Pod 会被赋予 Burstable 的 QoS 类:

​ 1、Pod 不满足 QoS 等级 Guaranteed 的标准。

​ 2、Pod 中至少有一个 Container 有内存或 CPU 请求。

这是具有一个容器的 Pod 的配置文件。Container 的内存限制为 200 MiB,内存请求为 100 MiB。

apiVersion: v1
kind: Pod
metadata:
  name: qos-demo-2
  namespace: qos-example
spec:
  containers:
  - name: qos-demo-2-ctr
    image: nginx
    resources:
      limits:
        memory: "200Mi"
      requests:
        memory: "100Mi"

同样,创建Pod后,查看Pod的详细信息:

$  kubectl get pod -n qos-example qos-demo-2 -oyaml
spec:
  containers:
  - image: nginx
    imagePullPolicy: Always
    name: qos-demo-2-ctr
    resources:
      limits:
        memory: 200Mi
      requests:
        memory: 100Mi
	...
status:
  qosClass: Burstable

输出显示 Kubernetes 为 Pod 提供了 Burstable 的 QoS 类。

创建一个包含两个Container的Pod

这是具有两个容器的 Pod 的配置文件。一个容器指定 200 MiB 的内存请求。另一个 Container 没有指定任何请求或限制。

apiVersion: v1
kind: Pod
metadata:
  name: qos-demo-4
  namespace: qos-example
spec:
  containers:

  - name: qos-demo-4-ctr-1
    image: nginx
    resources:
      requests:
        memory: "200Mi"

  - name: qos-demo-4-ctr-2
    image: redis

此 Pod 符合 QoS 类 Burstable 的标准。也就是说,它不满足 QoS 类 Guaranteed 的标准,并且它的一个 Container 有内存请求。

创建后查看Pod的详情:

$ kubectl get pod qos-demo-4 -n qos-example -oyaml
spec:
  containers:
    ...
    name: qos-demo-4-ctr-1
    resources:
      requests:
        memory: 200Mi
    ...
    name: qos-demo-4-ctr-2
    resources: {}
    ...
status:
  qosClass: Burstable
BestEffort

要为 Pod 提供 BestEffort 的 QoS 类,Pod 中的容器不得有任何内存或 CPU 限制或请求。

这是具有一个容器的 Pod 的配置文件。容器没有内存或 CPU 限制或请求:

apiVersion: v1
kind: Pod
metadata:
  name: qos-demo-3
  namespace: qos-example
spec:
  containers:
  - name: qos-demo-3-ctr
    image: nginx

同样,创建Pod后进行查看详情:

$ kubectl get pod qos-demo-3 -n qos-example -oyaml
spec:
  containers:
  - image: nginx
    imagePullPolicy: Always
    name: qos-demo-3-ctr
    resources: {}
	...
status:
  qosClass: BestEffort

输出显示 Kubernetes 为 Pod 提供了 BestEffort 的 QoS 类。

Pod安全策略

Pod 安全策略是控制 pod 规范的安全敏感方面的集群级资源。PodSecurityPolicy对象定义了一组条件,Pod 必须满足这些条件才能被系统接受,以及相关字段的默认值。它们允许管理员控制以下内容:

控制方面 字段名称
运行特权容器 privileged
主机命名空间的使用 hostPID, hostIPC
主机网络和端口的使用 hostNetwork, hostPorts
卷类型的使用 volumes
主机文件系统的使用 allowedHostPaths
允许特定的 FlexVolume 驱动程序 allowedFlexVolumes
分配拥有 pod 卷的 FSGroup fsGroup
要求使用只读根文件系统 readOnlyRootFilesystem
容器的用户和组 ID runAsUser, runAsGroup,supplementalGroups
限制升级到 root 权限 allowPrivilegeEscalation, defaultAllowPrivilegeEscalation
Linux 功能 defaultAddCapabilities, requiredDropCapabilities,allowedCapabilities
容器的 SELinux 上下文 seLinux
容器的 Allowed Proc Mount 类型 allowedProcMountTypes
容器使用的 AppArmor 配置文件 注释
容器使用的 sysctl 配置文件 forbiddenSysctls,allowedUnsafeSysctls
容器使用的 seccomp 配置文件 注释

注意:PodSecurityPolicy 自 Kubernetes v1.21 起已弃用,并将在 v1.25 中删除。

启用 Pod 安全策略

Pod 安全策略实现为一种可选的 准入控制器启用了准入控制器 即可强制实施 Pod 安全策略,不过如果没有授权认可策略之前即启用 准入控制器 将导致集群中无法创建任何 Pod

由于 Pod 安全策略 API(policy/v1beta1/podsecuritypolicy)是独立于准入控制器 来启用的,对于现有集群而言,建议在启用准入控制器之前先添加策略并对其授权。

授权策略

PodSecurityPolicy 资源被创建时,并不执行任何操作。为了使用该资源,需要对 发出请求的用户或者目标 Pod 的 服务账号 授权,通过允许其对策略执行 use 动词允许其使用该策略。

大多数 Kubernetes Pod 不是由用户直接创建的。相反,这些 Pod 是由 Deployment、 ReplicaSet或者经由控制器管理器模版化的控制器创建。 赋予控制器访问策略的权限意味着对应控制器所创建的 所有 Pod 都可访问策略。 因此,对策略进行授权的优先方案是为 Pod 的服务账号授予访问权限 。

通过RBAC授权

首先,某 RoleClusterRole 需要获得使用 use 访问目标策略的权限。 访问授权的规则看起来像这样:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: <Role 名称>
rules:
- apiGroups: ['policy']
  resources: ['podsecuritypolicies']
  verbs:     ['use']
  resourceNames:
  - <要授权的策略列表>

接下来将该 Role(或 ClusterRole)绑定到授权的用户:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: <binding name>
roleRef:
  kind: ClusterRole
  name: <role name>
  apiGroup: rbac.authorization.k8s.io
subjects:
# 授权命名空间下的所有服务账号(推荐):
- kind: Group
  apiGroup: rbac.authorization.k8s.io
  name: system:serviceaccounts:<authorized namespace>
# 授权特定的服务账号(不建议这样操作):
- kind: ServiceAccount
  name: <authorized service account name>
  namespace: <authorized pod namespace>
# 授权特定的用户(不建议这样操作):
- kind: User
  apiGroup: rbac.authorization.k8s.io
  name: <authorized user name>

如果使用的是 RoleBinding(而不是 ClusterRoleBinding),授权仅限于 与该 RoleBinding 处于同一名字空间中的 Pods。 可以考虑将这种授权模式和系统组结合,对命名空间中的所有 Pod 授予访问权限。

# 授权某名字空间中所有服务账号
- kind: Group
  apiGroup: rbac.authorization.k8s.io
  name: system:serviceaccounts
# 或者与之等价,授权给某名字空间中所有被认证过的用户
- kind: Group
  apiGroup: rbac.authorization.k8s.io
  name: system:authenticated

策略顺序

除了限制 Pod 创建与更新,Pod 安全策略也可用来为其所控制的很多字段 设置默认值。当存在多个策略对象时,Pod 安全策略控制器依据以下条件选择 策略:

  1. 优先考虑允许 Pod 保持原样,不会更改 Pod 字段默认值或其他配置的 PodSecurityPolicy。 这类非更改性质的 PodSecurityPolicy 对象之间的顺序无关紧要。
  2. 如果必须要为 Pod 设置默认值或者其他配置,(按名称顺序)选择第一个允许 Pod 操作的 PodSecurityPolicy 对象。

说明: 在更新操作期间(这时不允许更改 Pod 规约),仅使用非更改性质的 PodSecurityPolicy 来对 Pod 执行验证操作。

具体内容可以参考官方文档

Pod反亲和性

注意:恶意租户可以规避 Pod 反亲和性规则。以下示例应仅用于具有受信任租户的集群,或租户无法直接访问 Kubernetes 控制平面的集群。 我们可以利用 Pod 反亲和性来防止不同租户的 Pod 被调度到同一节点上。例如,下面的 Pod 规范描述了一个标签为 team: billing 的 Pod,以及阻止该 Pod 与没有该标签的 Pod 调度到一起的反亲和性规则。

apiVersion: v1
kind: Pod
metadata:
  name: bar
  labels:
    team: "billing"
spec:
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:  # 硬策略
      - topologyKey: "kubernetes.io/hostname"
        labelSelector:
          matchExpressions:
          - key: "team"
            operator: NotIn
            values: ["billing"]

上面这个资源清单的意思就是 bar 这个 Pod 不能调度到不具有 team: billing 这样的标签的 Pod 所在的节点,不过这种做法的缺点是恶意用户可以通过将 team: billing 标签添加到任意 Pod 来规避规则,仅使用 Pod 反亲和性机制不足以在具有不受信任的租户的集群上安全地强制执行政策。

污点和容忍

注意:恶意租户可以规避由节点污点和容忍机制强制执行的政策。以下示例应仅用于具有受信任租户的集群,或租户无法直接访问 Kubernetes 控制平面的集群。

节点污点是控制工作负载调度的另一种方法,可以使用节点污点来将专用节点留给某些租户使用。例如,可以将配备 GPU 的节点专门留给那些工作负载需要 GPU 的特定租户。要将某个节点池专门留给某个租户,请将具有 effect: "NoSchedule" 的污点应用于该节点池,然后,只有具备相应容忍设置的 Pod 可以被调度到该节点池中的节点。

这种做法的缺点是恶意用户可以通过为其 Pod 添加相应容忍设置来访问专用节点池,所以仅使用节点污点和容忍机制不足以在具有不受信任的租户的集群上安全地强制执行政策。

如果一个节点标记为污点(Taints),除非 Pod 也被标识为可以容忍污点节点,否则该 Taints 节点不会被调度 pod。

比如用户希望把 Master 节点保留给 Kubernetes 系统组件使用,或者把一组具有特殊资源预留给某些 Pod,则污点就很有用了,Pod 不会再被调度到 taint 标记过的节点。我们使用 kubeadm 搭建的集群默认就给 master 节点添加了一个污点标记,所以我们创建的普通Pod都没有被调度到Master上去:

$ kubectl describe node master
Name:               master
Roles:              master
Labels:             beta.kubernetes.io/arch=amd64
                    beta.kubernetes.io/os=linux
                    kubernetes.io/arch=amd64
                    kubernetes.io/hostname=master
                    kubernetes.io/os=linux
                    node-role.kubernetes.io/master=
......
Taints:             node-role.kubernetes.io/master:NoSchedule
Unschedulable:      false
.....

我们可以使用上面的命令查看 master 节点的信息,其中有一条关于 Taints 的信息:node-role.kubernetes.io/master:NoSchedule,就表示给 master 节点打了一个污点的标记,其中影响的参数是NoSchedule,表示 Pod 不会被调度到标记为 taints 的节点,除了 NoSchedule 外,还有另外两个选项:

  • PreferNoSchedule:NoSchedule 的软策略版本,表示尽量不调度到污点节点上去
  • NoExecute:该选项意味着一旦 Taint 生效,如该节点内正在运行的 Pod 没有对应 Tolerate 设置,会直接被逐出

污点 taint 标记节点的命令如下:

$ kubectl taint nodes node1 test=node1:NoSchedule
node/node1 tainted

上面的命名将 node1 节点标记为了污点,影响策略是 NoSchedule,只会影响新的 Pod 调度,如果仍然希望某个 Pod 调度到 taint 节点上,则必须在 Spec 中做出 Toleration 定义,才能调度到该节点,比如现在我们想要将一个 Pod 调度到 master 节点:(taint-demo.yaml)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: taint
  labels:
    app: taint
spec:
  selector:
    matchLabels:
      app: taint
  replicas: 2
  template:
    metadata:
      labels:
        app: taint
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - name: http
          containerPort: 80
      tolerations:
      - key: "node-role.kubernetes.io/master"
        operator: "Exists"
        effect: "NoSchedule"

由于 master 节点被标记为了污点节点,所以我们这里要想 Pod 能够调度到 master 节点去,就需要增加容忍的声明:

tolerations:
- key: "node-role.kubernetes.io/master"
  operator: "Exists"
  effect: "NoSchedule"

然后创建上面的资源,查看结果:

kubectl get pod -o wide
NAME                     READY   STATUS              RESTARTS   AGE   IP           NODE     NOMINATED NODE   READINESS GATES
busybox                  1/1     Running             0          2d    10.244.2.3   node2    <none>           <none>
taint-5779c44f78-lq9gg   0/1     ContainerCreating   0          27s   <none>       master   <none>           <none>
taint-5779c44f78-m46lm   0/1     ContainerCreating   0          27s   <none>       master   <none>           <none>
test-np                  1/1     Running             0          2d    10.244.1.6   node1    <none>           <none>

我们可以看到两个 Pod 副本被调度到了 master 节点,这就是容忍的使用方法。

对于 tolerations 属性的写法,其中的 key、value、effect 与节点的 Taint 设置需保持一致, 还有以下几点说明:

  1. 如果 operator 的值是 Exists,则 value 属性可省略
  2. 如果 operator 的值是 Equal,则表示其 key 与 value 之间的关系是 equal(等于)
  3. 如果不指定 operator 属性,则默认值为 Equal

另外,还有两个特殊值:

  1. 空的 key 如果再配合 Exists 就能匹配所有的 key 与 value,也就是能容忍所有节点的所有 Taints
  2. 空的 effect 匹配所有的 effect

最后,如果我们要取消节点的污点标记,可以使用下面的命令:

$ kubectl taint nodes node1 test-
node/node1 untainted

参考资料

https://www.qikqiak.com/k8strain/tenant/#pod_1

https://kubernetes.io/zh/docs/home/

posted @ 2022-01-22 19:05  李大鹅  阅读(1158)  评论(0编辑  收藏  举报