K8s NetworkPolicy与网络插件flannel、calico详细版

Pod是Kubernetes调度的最小单元。一个Pod可以包含一个或多个容器,因此它可以被看作是内部容器的逻辑宿主机。Pod的设计理念是为了支持多个容器在一个Pod中共享网络和文件系统。那么为什么Pod内的容器能够共享网络,IPC和PID命名空间?

原因:Kubernetes在每个Pod启动时,会自动创建一个镜像为gcr.io/google_containers/pause:version的容器,所有处于该Pod中的容器在启动时都会添加诸如--net=container:pause --ipc=contianer:pause --pid=container:pause的启动参数,因此Pod内所有容器共用pause容器的network,IPC和PID命名空间。所有容器共享pause容器的IP地址,也被称为Pod IP。因此处于同一个Pod内的容器,可以通过localhost进行相互访问。

在讲K8s Pod间通信前,我们先复习一下Docker容器间的网络通信。默认情况下,Docker使用一种名为bridge的网络模型。如下图所示:

K8s NetworkPolicy与网络插件flannel、calico详细版_第1张图片

 

Docker引擎在启动时,会在宿主机上创建一个名为docker0的虚拟网桥,这个虚拟网桥负责给所有容器分配不重复的ip地址以及容器间的网络通信。首先,Docker在创建一个容器时,会执行以下操作:

  1. 创建一对veth pair,一端置于容器中,一端置于docker0虚拟网桥中,从而实现网桥和容器的通信。
  2. 将容器中的veth命名为eth0,docker0网桥中的veth指定一个唯一的名字,例如veth0ac884e
  3. 从docker0网桥可用的地址段中选取一个分配给容器,并配置默认路由到docker0网桥的veth0ac884e
  4. 这样,容器就能够通过虚拟网卡eth0和其他容器进行通信了。当该容器结束后,eth0会随该容器的网络命名空间一起被清除,相对应的veth0ac884e也会被docker0网桥自动卸载掉。

通过这个docker0网桥,同一宿主机上的容器可以互相通信。然而由于宿主机的IP地址与容器veth pair的 IP地址均不在同一个网段,故仅仅依靠veth pair和namespace的技术,还不足以使宿主机以外的网络主动发现容器的存在。为了使外界可以访问容器中的进程,docker采用了端口绑定的方式,也就是通过iptables的NAT,将宿主机上的端口端口流量转发到容器内的端口上。

K8s 中的网络

K8s 默认不提供网络功能,所有Pod的网络功能,都依赖于宿主机上的Docker。因此,Pod IP即是依靠docker0网桥分配给pause容器的虚拟IP。

同一个Node上Pod的通信

同一个Node内,不同的Pod都有一个docker0网桥分配的IP,可以直接通过这个IP进行通信。Pod IP和docker0在同一个网段。因此,当同节点上的Pod-A发包给Pod-B时,包传送路线如下:

pod-a的eth0—>pod-a的vethxxx—>bridge0—>pod-b的vethxxx—>pod-b的eth0

不同Node间Pod的通信

不同的Node之间,Node的IP相当于外网IP,可以直接访问,而Node内的docker0和Pod的IP则是内网IP,无法直接跨Node访问。因此,不同Node上的Pod间需要通信,需要满足以下两个条件:

  1. 对整个集群中的Pod-IP分配进行规划,不能有冲突
  2. 将Node-IP与该Node上的Pod-IP关联起来,通过Node-IP再转发到Pod-IP

因此,为了实现以上两个需求,K8s提供了CNI(Container Network Interface)供第三方实现从而进行网络管理。由此出现了一系列开源的Kubernetes中的网络插件与方案,包括:flannel,calico,cilium等等。这些网络插件集中解决了以下需求:

  • 保证每个Pod拥有一个集群内唯一的IP地址
  • 保证不同节点的IP地址划分不会重复
  • 保证跨节点的Pod可以互相通信
  • 保证不同节点的Pod可以与跨节点的主机互相通信

CNI的介绍请参考这篇文章:https://jimmysong.io/kubernetes-handbook/concepts/cni.html



 

Kubernetes的网络通信问题:
  1. 容器间通信: 即同一个Pod内多个容器间通信,通常使用loopback来实现。
  2. Pod间通信: K8s要求,Pod和Pod之间通信必须使用Pod-IP 直接访问另一个Pod-IP
  3. Pod与Service通信: 即PodIP去访问ClusterIP,当然,clusterIP实际上是IPVS 或 iptables规则的虚拟IP,是没有TCP/IP协议栈支持的。但不影响Pod访问它.
  4. Service与集群外部Client的通信,即K8s中Pod提供的服务必须能被互联网上的用户所访问到。

需要注意的是,k8s集群初始化时的service网段,pod网段,网络插件的网段,以及真实服务器的网段,都不能相同,如果相同就会出各种各样奇怪的问题,而且这些问题在集群做好之后是不方便改的,改会导致更多的问题,所以,就在搭建前将其规划好。

CNI(容器网络接口):
  这是K8s中提供的一种通用网络标准规范,因为k8s本身不提供网络解决方案。
  目前比较知名的网络解决方案有:
    flannel
    calico
    canel
    kube-router
    .......
等等,目前比较常用的时flannel和calico,flannel的功能比较简单,不具备复杂网络的配置能力,calico是比较出色的网络管理插件,单具备复杂网络配置能力的同时,往往意味着本身的配置比较复杂,所以相对而言,比较小而简单的集群使用flannel,考虑到日后扩容,未来网络可能需要加入更多设备,配置更多策略,则使用calico更好
所有的网络解决方案,它们的共通性:
  1. 虚拟网桥
  2. 多路复用:MacVLAN
  3. 硬件交换:SR-IOV(单根-I/O虚拟网络):它是一种物理网卡的硬件虚拟化技术,它通过输出VF(虚拟功能)来将网卡虚拟为多个虚拟子接口,每个VF绑定给一个VM后,该VM就可以直接操纵该物理网卡。

kubelet来调CNI插件时,会到 /etc/cni/net.d/目录下去找插件的配置文件,并读取它,来加载该插件,并让该网络插件来为Pod提供网络服务。

flannel网络插件要怎么部署?
 1. flannel部署到那个节点上?
  因为kubelet是用来管理Pod的,而Pod运行需要网络,因此凡是部署kubelet的节点,都需要部署flannel来提供网络,因为kubelet正是通过调用flannel来实现为Pod配置网络的(如:添加网络,配置网络,激活网络等)。

在默认的Docker配置中,每个节点上的Docker服务会分别负责所在节点容器的IP分配。这样导致的一个问题是,不同节点上容器可能获得相同的内外IP地址。

Flannel的设计目的就是为集群中的所有节点重新规划IP地址的使用规则,从而使得不同节点上的容器能够获得“同属一个内网”且”不重复的”IP地址,并让属于不同节点上的容器能够直接通过内网IP通信。

Flannel实质上是一种“覆盖网络(overlay network)”,也就是将TCP数据包装在另一种网络包里面进行路由转发和通信,目前已经支持UDP、VxLAN、AWS VPC和GCE路由等数据转发方式,默认的节点间数据通信方式是UDP转发。下图展示了数据包在flannel中的流转:

K8s NetworkPolicy与网络插件flannel、calico详细版_第2张图片

K8s NetworkPolicy与网络插件flannel、calico详细版_第3张图片

 

 

  • 数据从源容器中发出后,经由所在主机的docker0虚拟网卡转发到flannel0虚拟网卡,这是个P2P的虚拟网卡,flanneld服务监听在网卡的另外一端。
  • Flannel在Etcd服务维护了一张节点间的路由表。
  • 源主机的flanneld服务将原本的数据内容UDP封装后根据自己的路由表投递给目的节点的flanneld服务,数据到达以后被解包,然后直接进入目的节点的flannel0虚拟网卡,然后被转发到目的主机的docker0虚拟网卡,最后就像本机容器通信一下的有docker0路由到达目标容器。

 2. flannel自身要如何部署?
  1》它支持直接运行为宿主机上的一个守护进程。
  2》它也支持运行为一个Pod
  对于运行为一个Pod这种方式:就必须将flannel配置为共享当前宿主机的网络名称空间的Pod,若flannel作为控制器控制的Pod来运行的话,它的控制器必须是DaemonSet,在每一个节点上都控制它仅能运行一个Pod副本,而且该副本必须直接共享宿主机的网络名称空间,因为只有这样,此Pod才能设置宿主机的网络名称空间,因为flannel要在当前宿主机的网络名称空间中创建CNI虚拟接口,还要将其他Pod的另一半veth桥接到虚拟网桥上,若不共享宿主机的网络名称空间,这是没法做到的。

3. flannel的工作方式有3种:
  1) VxLAN:
   而VxLAN有两种工作方式:
    a. VxLAN: 这是原生的VxLAN,即直接封装VxLAN首部,UDP首部,IP,MAC首部这种的。
    b. DirectRouting: 这种是混合自适应的方式, 即它会自动判断,若当前是相同二层网络
       (即:不垮路由器,二层广播可直达),则直接使用Host-GW方式工作,若发现目标是需要跨网段
       (即:跨路由器)则自动转变为使用VxLAN的方式。
  2) host-GW: 这种方式是宿主机内Pod通过虚拟网桥互联,然后将宿主机的物理网卡作为网关,当需要访问其它Node上的Pod时,只需要将报文发给宿主机的物理网卡,由宿主机通过查询本地路由表,来做路由转发,实现跨主机的Pod通信,这种模式带来的问题时,当k8s集群非常大时,会导致宿主机上的路由表变得非常巨大,而且这种方式,要求所有Node必须在同一个二层网络中,否则将无法转发路由,这也很容易理解,因为如果Node之间是跨路由的,那中间的路由器就必须知道Pod网络的存在,它才能实现路由转发,但实际上,宿主机是无法将Pod网络通告给中间的路由器,因此它也就无法转发理由。
  3) UDP: 这种方式性能最差的方式,这源于早期flannel刚出现时,Linux内核还不支持VxLAN,即没有VxLAN核心模块,因此flannel采用了这种方式,来实现隧道封装,其效率可想而知,因此也给很多人一种印象,flannel的性能很差,其实说的是这种工作模式,若flannel工作在host-GW模式下,其效率是非常高的,因为几乎没有网络开销。

4. flannel的网络配置参数:
  1) Network: flannel使用的CIDR格式的网络地址,主要用于为Pod配置网络功能。
   如: 10.10.0.0/16 --->
    master: 10.10.0.0/24
    node01: 10.10.1.0/24
    .....
    node255: 10.10.255.0/24

  2) SubnetLen: 把Network切分为子网供各节点使用时,使用多长的掩码来切分子网,默认是24位.
  3) SubnetMin: 若需要预留一部分IP时,可设置最小从那里开始分配IP,如:10.10.0.10/24 ,这样就预留出了10个IP
  4) SubnetMax: 这是控制最多分配多个IP,如: 10.10.0.100/24 这样在给Pod分配IP时,最大分配到10.10.0.100了。
  5) Backend: 指定后端使用的协议类型,就是上面提到的:vxlan( 原始vxlan,directrouter),host-gw, udp

flannel的配置:
  .....
  net-conf.json: |
    {
     "Network": "10.10.0.0/16",
     "Backend": {
     "Type": "vxlan",    #当然,若你很确定自己的集群以后也不可能跨网段,你完全可以直接设置为 host-gw.
     "Directrouting": true  #默认是false,修改为true就是可以让VxLAN自适应是使用VxLAN还是使用host-gw了。
     }
    }

#在配置flannel时,一定要注意,不要在半道上,去修改,也就是说要在你部署k8s集群后,就直接规划好,而不要在k8s集群已经运行起来了,你再去修改,虽然可能也不会出问题,但一旦出问题,你就!!

K8s NetworkPolicy与网络插件flannel、calico详细版_第4张图片

 #在配置好,flannel后,一定要测试,创建新Pod,看看新Pod是否能从flannel哪里获得IP地址,是否能通信。

Calico:

Flannel是一种典型的Overlay网络,它将已有的物理网络(Underlay网络)作为基础,在其上建立叠加的逻辑网络,实现网络资源的虚拟化。Overlay网络有一定额外的封包和解包等网络开销,对网络通信的性能有一定损耗。

Calico是纯三层的SDN 实现,它基于BPG 协议和Linux自身的路由转发机制,不依赖特殊硬件,容器通信也不依赖iptables NAT或Tunnel 等技术。能够方便的部署在物理服务器、虚拟机(如 OpenStack)或者容器环境下。同时calico自带的基于iptables的ACL管理组件非常灵活,能够满足比较复杂的安全隔离需求。

  • Calico创建和管理一个扁平的三层网络(不需要overlay),每个容器会分配一个可路由的IP。由于通信时不需要解包和封包,网络性能损耗小,易于排查,且易于水平扩展。
  • 小规模部署时可以通过BGP client直接互联,大规模下可通过指定的BGP Route Reflector来完成,这样保证所有的数据流量都是通过IP路由的方式完成互联的。
  • Calico基于iptables还提供了丰富而灵活的网络Policy,保证通过各个节点上的ACL来提供Workload的多租户隔离、安全组以及其他可达性限制等功能。

iptable是内核中的一个网络数据包处理模块,它具有网络数据包修改,网络地址转换(NAT)等功能。而Routes路由表存储着指向特定网络地址的路径,即下一跳的地址。ACL其实是一种报文过滤器,根据ACL中的匹配条件对进站和出站的报文进行过滤处理


 

K8s NetworkPolicy与网络插件flannel、calico详细版_第5张图片

  • Etcd:负责存储网络信息
  • BGP client:负责将Felix配置的路由信息分发到其他节点
  • Felix:Calico Agent,每个节点都需要运行,主要负责配置路由、配置ACLs、报告状态
  • BGP Route Reflector:大规模部署时需要用到,作为BGP client的中心连接点,可以避免每个节点互联

  Calico是一种非常复杂的网络组件,它需要自己的etcd数据库集群来存储自己通过BGP协议获取的路由等各种所需要持久保存的网络数据信息,因此在部署Calico时,早期是需要单独为Calico部署etcd集群的,因为在k8s中,访问etcd集群只有APIServer可以对etcd进行读写,其它所有组件都必须通过APIServer作为入口,将请求发给APIServer,由APIServer来从etcd获取必要信息来返回给请求者,但Caclico需要自己写,因此就有两种部署Calico网络插件的方式,一种是部署两套etcd,另一种就是Calico不直接写,而是通过APIServer做为代理,来存储自己需要存储的数据。通常第二种使用的较多,这样可降低系统复杂度。
  当然由于Calico本身很复杂,但由于很多k8s系统可能存在的问题是,早期由于各种原因使用了flannel来作为网络插件,但后期发现需要使用网络策略的需求,怎么办?
  目前比较成熟的解决方案是:flannel + Calico, 即使用flannel来提供简单的网络管理功能,而使用Calico提供的网络策略功能。

Calico 还基于 iptables 还提供了丰富而灵活的网络 policy, 保证通过各个节点上的 ACLs 来提供 workload 的多租户隔离、安全组以及其他可达性限制等功能。

K8s NetworkPolicy与网络插件flannel、calico详细版_第6张图片

 

K8s NetworkPolicy与网络插件flannel、calico详细版_第7张图片

 

核心问题是,nodeA怎样得知下一跳的地址?答案是node之间通过BGP协议交换路由信息。

每个node上运行一个软路由软件bird,并且被设置成BGP Speaker,与其它node通过BGP协议交换路由信息。

可以简单理解为,每一个node都会向其它node通知这样的信息:

我是X.X.X.X,某个IP或者网段在我这里,它们的下一跳地址是我。

通过这种方式每个node知晓了每个workload-endpoint的下一跳地址。

更多关于Calico的文章,请参考:
https://jimmysong.io/kubernetes-handbook/concepts/calico.html
https://blog.51cto.com/dengaosky/2069666
https://cloud.tencent.com/developer/article/1482739
https://qiankunli.github.io/2018/02/04/calico.html

K8s NetworkPoclicy

K8s NetworkPoclicy 用于实现Pod间的网络隔离。在使用Network Policy前,必须先安装支持K8s NetworkPoclicy的网络插件,包括:Calico,Romana,Weave Net,Trireme,OpenContrail等。

Pod的网络隔离

在未使用NetworkPolicy前,K8s中所有的Pod并不存在网络隔离,他们能够接收任何网络流量。一旦使用NetworkPolicy选中某个namespace下的某些Pod,那么这些Pod只能接收特定来源的流量(由Ingress属性定义),并且只能向特定出口发送网络请求(由Egress属性定义)。其他未被这个NetworkPolicy选中的Pod,依然不具备网络隔离。

下面是一个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
  • podSelector: 用于定义这个NetworkPolicy适用于哪些Pod。如果其值为空,则表示适用于所有此namespace下的Pod
  • policyTypes: 包含两个可选值:用于控制进入流量的Ingress和控制出口流量的Egress
  • ingress: 用于定义能够进入Pod的流量的规则。例子中允许三种流量与podSelector选中的Pod的6379端口进行通信,三种流量分别为:ip地址位于172.17.0.0/16网段内的流量(但不包括172.17.1.0/24)允许与目标Pod通信,任何拥有role=frontend的label的Pod的流量允许进入,任何拥有label "project=myproject"的namespace下的Pod的流量允许进入。其他任何进入流量将被禁止。
  • egress: 用于定义允许的出口流量。上面的例子中表示podSelector选中的Pod能够向10.0.0.0/24的5978端口发送TCP请求。其他任何TCP出流量都将被禁止。

下面的例子表示默认禁止所有Pod间的Ingress流量:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
spec:
  podSelector: {}
  policyTypes:
  - Ingress

默认拒绝所有 Pod 之间 Egress 通信的策略为:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
spec:
  podSelector: {}
  policyTypes:
  - Egress

而默认允许所有 Pod 之间 Ingress 通信的策略为:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-all
spec:
  podSelector: {}
  ingress:
  - {}

默认允许所有 Pod 之间 Egress 通信的策略为:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-all
spec:
  podSelector: {}
  egress:
  - {}

calico 实例

以 calico 为例看一下 Network Policy 的具体用法。首先配置 kubelet 使用 CNI 网络插件:

kubelet --network-plugin=cni --cni-conf-dir=/etc/cni/net.d --cni-bin-dir=/opt/cni/bin ...

安装 calio 网络插件:

kubectl apply -f https://docs.projectcalico.org/v3.0/getting-started/kubernetes/installation/hosted/kubeadm/1.7/calico.yaml

首先部署一个 nginx 服务,此时,通过其他 Pod 是可以访问 nginx 服务的:

$ kubectl run nginx --image=nginx --replicas=2
deployment "nginx" created
$ kubectl expose deployment nginx --port=80
service "nginx" exposed

开启 default namespace 的 DefaultDeny Network Policy 后,其他 Pod(包括 namespace 外部)不能访问 nginx 了:

$ cat default-deny.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
spec:
  podSelector: {}
  policyTypes:
  - Ingress

$ kubectl create -f default-deny.yaml

$ kubectl run busybox --rm -ti --image=busybox /bin/sh
Waiting for pod default/busybox-472357175-y0m47 to be running, status is Pending, pod ready: false

Hit enter for command prompt

/ # wget --spider --timeout=1 nginx
Connecting to nginx (10.100.0.16:80)
wget: download timed out
/ #

最后再创建一个运行带有 access=true label的 Pod 访问的网络策略:

$ cat nginx-policy.yaml
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
  name: access-nginx
spec:
  podSelector:
    matchLabels:
      run: nginx
  ingress:
  - from:
    - podSelector:
        matchLabels:
          access: "true"

$ kubectl create -f nginx-policy.yaml
networkpolicy "access-nginx" created

# 不带 access=true 标签的 Pod 还是无法访问 nginx 服务
$ kubectl run busybox --rm -ti --image=busybox /bin/sh
Waiting for pod default/busybox-472357175-y0m47 to be running, status is Pending, pod ready: false

Hit enter for command prompt

/ # wget --spider --timeout=1 nginx
Connecting to nginx (10.100.0.16:80)
wget: download timed out
/ #


# 而带有 access=true 标签的 Pod 可以访问 nginx 服务
$ kubectl run busybox --rm -ti --labels="access=true" --image=busybox /bin/sh
Waiting for pod default/busybox-472357175-y0m47 to be running, status is Pending, pod ready: false

Hit enter for command prompt

/ # wget --spider --timeout=1 nginx
Connecting to nginx (10.100.0.16:80)
/ #

参考文章

  • https://www.huweihuang.com/kubernetes-notes/network/kubernetes-network.html
  • https://jimmysong.io/kubernetes-handbook/concepts/networking.html
  • https://www.huweihuang.com/kubernetes-notes/network/flannel/flannel-introduction.html
  • https://feisky.gitbooks.io/kubernetes/concepts/network-policy.html

你可能感兴趣的:(k8s,linux,运维)