gitbook/深入剖析Kubernetes/docs/68316.md
2022-09-03 22:05:03 +08:00

18 KiB
Raw Permalink Blame History

36 | 为什么说Kubernetes只有soft multi-tenancy

你好我是张磊。今天我和你分享的主题是为什么说Kubernetes只有soft multi-tenancy

在前面的文章中我为你详细讲解了Kubernetes生态里主流容器网络方案的工作原理。

不难发现Kubernetes的网络模型以及前面这些网络方案的实现都只关注容器之间网络的“连通”却并不关心容器之间网络的“隔离”。这跟传统的IaaS层的网络方案区别非常明显。

你肯定会问了Kubernetes的网络方案对“隔离”到底是如何考虑的呢难道Kubernetes就不管网络“多租户”的需求吗

接下来,在今天这篇文章中,我就来回答你的这些问题。

在Kubernetes里网络隔离能力的定义是依靠一种专门的API对象来描述的NetworkPolicy。

一个完整的NetworkPolicy对象的示例如下所示

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: test-network-policy
  namespace: default
spec:
  podSelector:
    matchLabels:
      role: db
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - ipBlock:
        cidr: 172.17.0.0/16
        except:
        - 172.17.1.0/24
    - namespaceSelector:
        matchLabels:
          project: myproject
    - podSelector:
        matchLabels:
          role: frontend
    ports:
    - protocol: TCP
      port: 6379
  egress:
  - to:
    - ipBlock:
        cidr: 10.0.0.0/24
    ports:
    - protocol: TCP
      port: 5978

我在和你分享前面的内容时已经说过这里你可以再回顾下第34篇文章Kubernetes 网络模型与 CNI 网络插件中的相关内容),Kubernetes里的Pod默认都是“允许所有”Accept AllPod可以接收来自任何发送方的请求或者向任何接收方发送请求。而如果你要对这个情况作出限制就必须通过NetworkPolicy对象来指定。

而在上面这个例子里你首先会看到podSelector字段。它的作用就是定义这个NetworkPolicy的限制范围比如当前Namespace里携带了role=db标签的Pod。

而如果你把podSelector字段留空

spec:
 podSelector: {}

那么这个NetworkPolicy就会作用于当前Namespace下的所有Pod。

而一旦Pod被NetworkPolicy选中那么这个Pod就会进入“拒绝所有”Deny All的状态这个Pod既不允许被外界访问也不允许对外界发起访问。

而NetworkPolicy定义的规则其实就是“白名单”。

例如在我们上面这个例子里我在policyTypes字段定义了这个NetworkPolicy的类型是ingress和egress它既会影响流入ingress请求也会影响流出egress请求。

然后在ingress字段里我定义了from和ports允许流入的“白名单”和端口。其中这个允许流入的“白名单”里我指定了三种并列的情况分别是ipBlock、namespaceSelector和podSelector。

而在egress字段里我则定义了to和ports允许流出的“白名单”和端口。这里允许流出的“白名单”的定义方法与ingress类似。只不过这一次ipblock字段指定的是目的地址的网段。

综上所述这个NetworkPolicy对象指定的隔离规则如下所示

  1. 该隔离规则只对default Namespace下的携带了role=db标签的Pod有效。限制的请求类型包括ingress流入和egress流出
  2. Kubernetes会拒绝任何访问被隔离Pod的请求除非这个请求来自于以下“白名单”里的对象并且访问的是被隔离Pod的6379端口。这些“白名单”对象包括
    a. default Namespace里的携带了role=fronted标签的Pod
    b. 携带了project=myproject 标签的 Namespace 里的任何 Pod
    c. 任何源地址属于172.17.0.0/16网段且不属于172.17.1.0/24网段的请求。
  3. Kubernetes会拒绝被隔离Pod对外发起任何请求除非请求的目的地址属于10.0.0.0/24网段并且访问的是该网段地址的5978端口。

需要注意的是定义一个NetworkPolicy对象的过程容易犯错的是“白名单”部分from和to字段

举个例子:

  ...
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          user: alice
    - podSelector:
        matchLabels:
          role: client
  ...

像上面这样定义的namespaceSelector和podSelector是“或”OR的关系。所以说这个from字段定义了两种情况无论是Namespace满足条件还是Pod满足条件这个NetworkPolicy都会生效。

而下面这个例子,虽然看起来类似,但是它定义的规则却完全不同:

...
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          user: alice
      podSelector:
        matchLabels:
          role: client
  ...

注意看这样定义的namespaceSelector和podSelector其实是“与”AND的关系。所以说这个from字段只定义了一种情况只有Namespace和Pod同时满足条件这个NetworkPolicy才会生效。

这两种定义方式的区别,请你一定要分清楚。

此外如果要使上面定义的NetworkPolicy在Kubernetes集群里真正产生作用你的CNI网络插件就必须是支持Kubernetes的NetworkPolicy的。

在具体实现上凡是支持NetworkPolicy的CNI网络插件都维护着一个NetworkPolicy Controller通过控制循环的方式对NetworkPolicy对象的增删改查做出响应然后在宿主机上完成iptables规则的配置工作。

在Kubernetes生态里目前已经实现了NetworkPolicy的网络插件包括Calico、Weave和kube-router等多个项目但是并不包括Flannel项目。

所以说如果想要在使用Flannel的同时还使用NetworkPolicy的话你就需要再额外安装一个网络插件比如Calico项目来负责执行NetworkPolicy。

安装Flannel + Calico的流程非常简单你直接参考这个文档一键安装即可。

那么这些网络插件又是如何根据NetworkPolicy对Pod进行隔离的呢

接下来我就以三层网络插件为例比如Calico和kube-router来为你分析一下这部分的原理。

为了方便讲解这一次我编写了一个比较简单的NetworkPolicy对象如下所示

apiVersion: extensions/v1beta1
kind: NetworkPolicy
metadata:
  name: test-network-policy
  namespace: default
spec:
  podSelector:
    matchLabels:
      role: db
  ingress:
   - from:
     - namespaceSelector:
         matchLabels:
           project: myproject
     - podSelector:
         matchLabels:
           role: frontend
     ports:
       - protocol: tcp
         port: 6379

可以看到我们指定的ingress“白名单”是任何Namespace里携带project=myproject标签的Namespace里的Pod以及default Namespace里携带了role=frontend标签的Pod。允许被访问的端口是6379。

而被隔离的对象是所有携带了role=db标签的Pod。

那么这个时候Kubernetes的网络插件就会使用这个NetworkPolicy的定义在宿主机上生成iptables规则。这个过程我可以通过如下所示的一段Go语言风格的伪代码来为你描述

for dstIP := range 所有被networkpolicy.spec.podSelector选中的Pod的IP地址
  for srcIP := range 所有被ingress.from.podSelector选中的Pod的IP地址
    for port, protocol := range ingress.ports {
      iptables -A KUBE-NWPLCY-CHAIN -s $srcIP -d $dstIP -p $protocol -m $protocol --dport $port -j ACCEPT 
    }
  }
} 

可以看到这是一条最基本的、通过匹配条件决定下一步动作的iptables规则。

这条规则的名字是KUBE-NWPLCY-CHAIN含义是当IP包的源地址是srcIP、目的地址是dstIP、协议是protocol、目的端口是port的时候就允许它通过ACCEPT

而正如这段伪代码所示匹配这条规则所需的这四个参数都是从NetworkPolicy对象里读取出来的。

可以看到Kubernetes网络插件对Pod进行隔离其实是靠在宿主机上生成NetworkPolicy对应的iptable规则来实现的。

此外在设置好上述“隔离”规则之后网络插件还需要想办法将所有对被隔离Pod的访问请求都转发到上述KUBE-NWPLCY-CHAIN规则上去进行匹配。并且如果匹配不通过这个请求应该被“拒绝”。

在CNI网络插件中上述需求可以通过设置两组iptables规则来实现。

第一组规则负责“拦截”对被隔离Pod的访问请求。生成这一组规则的伪代码,如下所示:

for pod := range 该Node上的所有Pod {
    if pod是networkpolicy.spec.podSelector选中的 {
        iptables -A FORWARD -d $podIP -m physdev --physdev-is-bridged -j KUBE-POD-SPECIFIC-FW-CHAIN
        iptables -A FORWARD -d $podIP -j KUBE-POD-SPECIFIC-FW-CHAIN
        ...
    }
}

可以看到这里的的iptables规则使用到了内置链FORWARD。它是什么意思呢

说到这里我就得为你稍微普及一下iptables的知识了。

实际上iptables只是一个操作Linux内核Netfilter子系统的“界面”。顾名思义Netfilter子系统的作用就是Linux内核里挡在“网卡”和“用户态进程”之间的一道“防火墙”。它们的关系可以用如下的示意图来表示


可以看到这幅示意图中IP包“一进一出”的两条路径上有几个关键的“检查点”它们正是Netfilter设置“防火墙”的地方。在iptables中这些“检查点”被称为Chain。这是因为这些“检查点”对应的iptables规则是按照定义顺序依次进行匹配的。这些“检查点”的具体工作原理可以用如下所示的示意图进行描述

可以看到当一个IP包通过网卡进入主机之后它就进入了Netfilter定义的流入路径Input Path里。

在这个路径中IP包要经过路由表路由来决定下一步的去向。而在这次路由之前Netfilter设置了一个名叫PREROUTING的“检查点”。在Linux内核的实现里所谓“检查点”实际上就是内核网络协议栈代码里的Hook比如在执行路由判断的代码之前内核会先调用PREROUTING的Hook

而在经过路由之后IP包的去向就分为了两种

  • 第一种,继续在本机处理;
  • 第二种,被转发到其他目的地。

我们先说一下IP包的第一种去向。这时候IP包将继续向上层协议栈流动。在它进入传输层之前Netfilter会设置一个名叫INPUT的“检查点”。到这里IP包流入路径Input Path结束。

接下来这个IP包通过传输层进入用户空间交给用户进程处理。而处理完成后用户进程会通过本机发出返回的IP包。这时候这个IP包就进入了流出路径Output Path

此时IP包首先还是会经过主机的路由表进行路由。路由结束后Netfilter就会设置一个名叫OUTPUT的“检查点”。然后在OUTPUT之后再设置一个名叫POSTROUTING“检查点”。

你可能会觉得奇怪为什么在流出路径结束后Netfilter会连着设置两个“检查点”呢

这就要说到在流入路径里,路由判断后的第二种去向了。

在这种情况下这个IP包不会进入传输层而是会继续在网络层流动从而进入到转发路径Forward Path。在转发路径中Netfilter会设置一个名叫FORWARD的“检查点”。

而在FORWARD“检查点”完成后IP包就会来到流出路径。而转发的IP包由于目的地已经确定它就不会再经过路由也自然不会经过OUTPUT而是会直接来到POSTROUTING“检查点”。

所以说POSTROUTING的作用其实就是上述两条路径最终汇聚在一起的“最终检查点”。

需要注意的是在有网桥参与的情况下上述Netfilter设置“检查点”的流程实际上也会出现在链路层二层并且会跟我在上面讲述的网络层三层的流程有交互。

这些链路层的“检查点”对应的操作界面叫作ebtables。所以准确地说数据包在Linux Netfilter子系统里完整的流动过程其实应该如下所示这是一幅来自Netfilter官方的原理图,建议你点击图片以查看大图):

可以看到我前面为你讲述的正是上图中绿色部分也就是网络层的iptables链的工作流程。

另外你应该还能看到每一个白色的“检查点”上还有一个绿色的“标签”比如raw、nat、filter等等。

在iptables里这些标签叫作表。比如同样是OUTPUT这个“检查点”filter Output和nat Output在iptables里的语法和参数就完全不一样实现的功能也完全不同。

所以说iptables表的作用就是在某个具体的“检查点”比如Output按顺序执行几个不同的检查动作比如先执行nat再执行filter

在理解了iptables的工作原理之后我们再回到NetworkPolicy上来。这时候前面由网络插件设置的、负责“拦截”进入Pod的请求的三条iptables规则就很容易读懂了

iptables -A FORWARD -d $podIP -m physdev --physdev-is-bridged -j KUBE-POD-SPECIFIC-FW-CHAIN
iptables -A FORWARD -d $podIP -j KUBE-POD-SPECIFIC-FW-CHAIN
...

其中,第一条FORWARD链“拦截”的是一种特殊情况它对应的是同一台宿主机上容器之间经过CNI网桥进行通信的流入数据包。其中--physdev-is-bridged的意思就是这个FORWARD链匹配的是通过本机上的网桥设备发往目的地址是podIP的IP包。

当然如果是像Calico这样的非网桥模式的CNI插件就不存在这个情况了。

kube-router其实是一个简化版的Calico它也使用BGP来维护路由信息但是使用CNI bridge插件负责跟Kubernetes进行交互。

第二条FORWARD链“拦截”的则是最普遍的情况容器跨主通信。这时候流入容器的数据包都是经过路由转发FORWARD检查点来的。

不难看到,这些规则最后都跳转(即:-j到了名叫KUBE-POD-SPECIFIC-FW-CHAIN的规则上。它正是网络插件为NetworkPolicy设置的第二组规则。

而这个KUBE-POD-SPECIFIC-FW-CHAIN的作用就是做出“允许”或者“拒绝”的判断。这部分功能的实现可以简单描述为下面这样的iptables规则

iptables -A KUBE-POD-SPECIFIC-FW-CHAIN -j KUBE-NWPLCY-CHAIN
iptables -A KUBE-POD-SPECIFIC-FW-CHAIN -j REJECT --reject-with icmp-port-unreachable

可以看到首先在第一条规则里我们会把IP包转交给前面定义的KUBE-NWPLCY-CHAIN规则去进行匹配。按照我们之前的讲述如果匹配成功那么IP包就会被“允许通过”。

而如果匹配失败IP包就会来到第二条规则上。可以看到它是一条REJECT规则。通过这条规则不满足NetworkPolicy定义的请求就会被拒绝掉从而实现了对该容器的“隔离”。

以上就是CNI网络插件实现NetworkPolicy的基本方法了。当然对于不同的插件来说上述实现过程可能有不同的手段但根本原理是不变的。

总结

在本篇文章中我主要和你分享了Kubernetes对Pod进行“隔离”的手段NetworkPolicy。

可以看到NetworkPolicy实际上只是宿主机上的一系列iptables规则。这跟传统IaaS里面的安全组Security Group其实是非常类似的。

而基于上述讲述,你就会发现这样一个事实:

Kubernetes的网络模型以及大多数容器网络实现其实既不会保证容器之间二层网络的互通也不会实现容器之间的二层网络隔离。这跟IaaS项目管理虚拟机的方式是完全不同的。

所以说Kubernetes从底层的设计和实现上更倾向于假设你已经有了一套完整的物理基础设施。然后Kubernetes负责在此基础上提供一种“弱多租户”soft multi-tenancy的能力。

并且基于上述思路Kubernetes将来也不大可能把Namespace变成一个具有实质意义的隔离机制或者把它映射成为“子网”或者“租户”。毕竟你可以看到NetworkPolicy对象的描述能力要比基于Namespace的划分丰富得多。

这也是为什么到目前为止Kubernetes项目在云计算生态里的定位其实是基础设施与PaaS之间的中间层。这是非常符合“容器”这个本质上就是进程的抽象粒度的。

当然随着Kubernetes社区以及CNCF生态的不断发展Kubernetes项目也已经开始逐步下探“吃”掉了基础设施领域的很多“蛋糕”。这也正是容器生态继续发展的一个必然方向。

思考题

请你编写这样一个NetworkPolicy它使得指定的Namespace比如my-namespace里的所有Pod都不能接收任何Ingress请求。然后请你说说这样的NetworkPolicy有什么实际的作用

感谢你的收听,欢迎你给我留言,也欢迎分享给更多的朋友一起阅读。