Networkpolicy的含义与现状
networkpolicy是k8s在很早就提出的一个抽象概念。它用一个对象来描述一类pod的网络出入站规则。关于networkpolicy的语义可以参考我之前的文章
networkpolicy的作用对象是pod,作用效果包括出站、入站,作用效果拓扑包括IP段、namespace、pod、端口、协议。
与以往IaaS服务场景下,针对虚拟机、网卡对象的安全组规则不同,networkpolicy是k8s原语。因此,在k8s场景下,进行网络安全规则的规划时,用networkpolicy能做到更加的灵活和自动化。举个例子:
有一套工作负载A是做类似数据库代理一类的工作,它只允许代理服务B访问,不允许其他业务访问。
- 在k8s场景下,如果不使用networkpolicy,我们需要规划好A类pod的部署节点,配置相应的ACL规则,将B类pod的IP予以放行,一旦A/B类pod做了扩缩容,可能要在重新配置一份甚至多份ACL规则。
- 在k8s场景下,我们会给A和B类分别配置label,创建好networkpolicy后限制A只放行B类pod,每当A或B扩缩容时,无需做任何额外操作。
业界的networkpolicy实现
当前社区对于k8s的networkpolicy的实现,不外乎三种方案:
方案 | 依赖 | 案例 | 支持的CNI |
---|---|---|---|
基于iptables+ipset实现规则 | 容器流量需要经过宿主机的协议栈 | calico felix,kube-router | calico、flannel |
基于ovs流表实现规则 | 使用openvswitch | openshift-sdn | openshift-sdn |
基于ebpf hook实现规则 | 需要较高版本内核 | cilium | cilium、flannel |
从上面的表格可以看出:
基于ovs流表实现的方案,典型的就是openshift-sdn,ovs table的设计,其中有一个专门的table(tableid=21)就是用来实现networkpolicy的规则。 该方案是直接内建于openshift-sdn项目,基本无法移植。 而openshift-sdn虽然代码开源,但设计上、代码逻辑上与openshift平台耦合还是比较紧密的。比如说:
- 公开的openshift-sdn部署方案需要依赖openshift-network-operator
- openshift-sdn代码中硬编码了要访问的容器运行时为crio,不支持dockershim
- cilium是最先使用ebpf技术实现网络数据面的CNI,它力图实现大而全的容器网络,封装、加密、全面、高性能等特点应有尽有,它对于networkpolicy的支持也已经十分完善。但ebpf hook的实现方式,依赖较高的内核版本,且在数据面排障时比较吃力。ebpf技术对于网络性能的提升很大,未来势必会越来越流行,所以值得关注。
- 基于iptables+ipset技术实现的方案,其实在几年前就比较成熟了calico-felix、romana、kube-router等开源的网络方案都是基于此实现了支持networkpolicy。其中,felix是calico网络方案中的一个组件,官方支持在calico中enable networkpolicy,且能够与flannel配合使用。阿里云的terway便是直接套用felix实现了对networkpolicy的支持(最近还套用了cilium)。这套方案要求容器流量要进过宿主机协议栈,否则包就不会进入内核的netfilter模块,iptables规则就无法生效。
业界各种k8s集群产品对于networkpolicy的支持:
产品 | 支持 | 实现 |
---|---|---|
AKS(Azure) | Azure CNI使用自研networkpolicy方案 自定义路由CNI使用集成calico方案 | 自研方案也是基于iptables+ipset实现的 |
EKS(AWS) | 直接使用calico | calico天然支持 |
TKE(腾讯云) | 直接使用kube-router | 对kube-router做兼容性改造 |
ACK(阿里云) | veth虚拟网卡使用calico ipvlan虚拟网卡使用cilium | 对calico的felix(v3.5.8)或cilium(v1.8.1)做兼容性改造 |
目标
基于上述现状,我们希望基于现有的开源实现方案,进行兼容性调研或改造,适配我们自研的网络方案。
因为这些网络方案都满足felix的要求,同时felix有较为活跃的社区和较多的适配案例,因此我们决定基于felix,实现一套即插即用的networkpolicy addon。本文接下来将会着重介绍该方案的实现。
calico/felix的设计实现
架构
calico在部署架构上做了多次演进,我们以最新版本v3.17.1为准。calico的完整架构包括了若干组件:
- calico/kube-controllers: calico控制器,用于监听一些k8s资源的变更,从而进行相应的calico资源的变更。例如根据networkpolicy对象的变更,变更相应的calicopolicy对象
- pod2daemon: 一个initcontainer,用于构建一个Unix Domain Socket,来让Felix程序与
Dikastes
(calico中支持istio的一种sidecar,详见calico的istio集成)进行加密通信. - cni plugin / ipam plugin: 标准的CNI插件,用于配置/解除网络;分配/回收网络配置
calico-node calico-node其实是一个数据面工具总成,包括了:
- felix: 管理节点上的容器网卡、路由、ACL规则;并上报节点状态
- bird/bird6: 用来建立bgp连接, 并根据felix配置的路由,在不同节点间分发
- confd: 根据当前集群数据生成本地brid的配置
- calicoctl: calico的CLI工具。
- datastore plugin: 即calico的数据库,可以是独立的etcd,也可以以crd方式记录于所在集群的k8s中
- typha: 类似于数据库代理,可以尽量少避免有大量的连接建立到apiserver。适用于超过100个node的集群。
官网给出了calico整体的组件架构图:
原理
在网络连通性(Networking)方面:calico的数据面是非常简单的三层路由转发。路由的学习和分发由bgp协议完成。如果k8s的下层是VPC之类的三层网络环境,则需要进行overlay,calico支持ipip封装实现overlay。
在网络安全性方面:calico考虑到其Networking是依赖宿主机协议栈进行路由转发实现的,因此可以基于iptables+ipset进行流量标记、地址集规划、流量处理(放行或DROP),并且基于这些操作可以实现:
- networkpolicy的抽象概念
- calico自定义的networkpolicy,为了在openstack场景下应用而设计
- calico自定义的profile,已废弃。
这里所有的iptables规则都作用在:
- pod在宿主机namespace中的veth网卡(calico中将之称为workload)
- 宿主机nodeIP所在网卡(calico中将之称为host-endpoint,实际上这部分规则不属于k8s的networkpolicy范畴)。
主要包括如下几类规则(注意这些规则都针对宿主机上的网卡,如node网卡,pod的host-veth):
iptables的INPUT链规则中,会先跳入
cali-INPUT
链,在cali-INPUT
链中,会判断和处理两种方向的流量:- pod访问node(
cali-wl-to-host
)实际上这个链中只走了cali-from-wl-dispatch
链,如果是应用在openstack中,该链还会允许访问metaserver;如果使用ipv6,该链中还会允许发出icmpv6的一系列包 - 来自node的流量(
cali-from-host-endpoint
)
- pod访问node(
iptables的OUTPUT链中,会首先跳入
cali-OUTPUT
链,在cali-OUTPUT
链中,主要会处理:- 不作任何处理。
- 特殊情况下处理node访问node的流量(
cali-to-host-endpoint
)
iptables的FORWARD链中,会首先跳入
cali-FORWARD
链,在cali-FORWARD
链中会处理如下几种流量:- 来自node转发的流量
cali-from-hep-forward
- 从pod中发出的流量
cali-from-wl-dispatch
- 到达pod的流量
cali-to-wl-dispatch
- 到达node的转发流量
cali-to-hep-forward
- 纯粹的IP段到IP段的转发流量
cali-cidr-block
- 来自node转发的流量
k8s的networkpolicy只需要关注上述流量中与pod相关的流量,因此只需要关心:
cali-from-wl-dispatch
cali-to-wl-dispatch
这两个链的规则,对应到pod的egress和ingress networkpolicy。
1. 除了nat表,在raw和mangle表中还有对calico关注的网卡上的收发包进行初始标记的规则,和最终的判断规则。
2. 在https://github.com/projectcalico/felix/blob/master/rules/static.go中可以看到完整的静态iptables表项的设计
接着,iptables规则中还会在cali-from-wl-dispatch
和cali-to-wl-dispatch
两个链中根据收包/发包的网卡判断这是哪个pod,走到该pod的egress或ingress链中。每个pod的链中则又设置了对应networkpolicy实例规则的链,以此递归调用。
这样,pod的流量经过INPUT/OUTPUT/FORWARD等链后,递归地走了多个链,每个链都会Drop或者Return,如果把链表走一遍下来一直Return,会Return到INPUT/OUTPUT/FORWARD, 然后执行ACCEPT,也就是说这个流量满足了某个networkpolicy的规则限制。如果过程中被Drop了,就表示受某些规则限制,这个链路不通。
我们通过一个简单的例子来描述iptables这块的链路顺序。
felix实现networkpolicy的案例1
假设有如下一个networkpolicy:
spec:
egress:
- {}
ingress:
- from:
- podSelector:
matchLabels:
hyapp: client1
- from:
- ipBlock:
cidr: 10.16.2.0/24
except:
- 10.16.2.122/32
ports:
- port: 3456
protocol: TCP
podSelector:
matchLabels:
hyapp: server
- 他作用于有
hyapp=server
的label的pod - 这类pod出方向不限制
这类pod的入站规则中只允许如下几种流量:
- 来自于有
hyapp=client1
的label的pod - 10.16.2.0/24网段中除了10.16.2.122/32以外的IP可以访问该类pod的3456 TCP端口。
- 来自于有
我们使用iptables -L
或iptables-save
命令来分析机器上的iptables规则。
因为是入站规则,所以我们可以观察iptables表中的cali-to-wl-dispatch
链。另外,该networkpolicy的作用pod只有一个,它的host侧网卡是veth-13dd25c5cb
。我们可以看到如下的几条规则:
Chain cali-to-wl-dispatch (1 references)
target prot opt source destination
cali-to-wl-dispatch-0 all -- anywhere anywhere [goto] /* cali:Ok_j0t6AwtLyoFYU */
cali-tw-veth-13dd25c5cb all -- anywhere anywhere [goto] /* cali:909gC5dwdBI3E96S */
DROP all -- anywhere anywhere /* cali:4M4uUxEEGrRKj1PR */ /* Unknown interface */
注意,这里有一个cali-to-wl-dispatch-0
的链,是用来做前缀映射的, 该链的规则下包含所有cali-tw-veth-0
这个前缀的链:
Chain cali-to-wl-dispatch-0 (1 references)
target prot opt source destination
cali-tw-veth-086099497f all -- anywhere anywhere [goto] /* cali:Vt4xxuTYlCRFq62M */
cali-tw-veth-0ddbc02656 all -- anywhere anywhere [goto] /* cali:7FDgBEq4y7PN7kMf */
DROP all -- anywhere anywhere /* cali:up42FFMQCctN8FcW */ /* Unknown interface */
这是felix设计上用于减少iptables规则遍历次数的一个优化手段。
我们通过iptables-save |grep cali-to-wl-dispatch
命令,可以发现如下的规则:
cali-to-wl-dispatch -o veth-13dd25c5cb -m comment --comment "cali:909gC5dwdBI3E96S" -g cali-tw-veth-13dd25c5cb
意思就是:在cali-to-wl-dispatch
链中,根据pod在host侧网卡的名字,会执行cali-tw-veth-13dd25c5cb
链, 我们再看这条链:
Chain cali-tw-veth-13dd25c5cb (1 references)
target prot opt source destination
1 ACCEPT all -- anywhere anywhere /* cali:RvljGbJwZ8z9q-Ee */ ctstate RELATED,ESTABLISHED
2 DROP all -- anywhere anywhere /* cali:krH_zVU1BetG5Q5_ */ ctstate INVALID
3 MARK all -- anywhere anywhere /* cali:Zr20J0-I__oX_Y2w */ MARK and 0xfffeffff
4 MARK all -- anywhere anywhere /* cali:lxQlOdcUUS4hyf-h */ /* Start of policies */ MARK and 0xfffdffff
5 cali-pi-_QW8Cu1Tr3dYs2pTUY0- all -- anywhere anywhere /* cali:d2UTZGk8zG6ol0ME */ mark match 0x0/0x20000
6 RETURN all -- anywhere anywhere /* cali:zyuuqgEt28kbSlc_ */ /* Return if policy accepted */ mark match 0x10000/0x10000
7 DROP all -- anywhere anywhere /* cali:DTh9dO0o6NsmIQSx */ /* Drop if no policies passed packet */ mark match 0x0/0x20000
8 cali-pri-kns.default all -- anywhere anywhere /* cali:krKqEtFijSLu5oTz */
9 RETURN all -- anywhere anywhere /* cali:dgRtRf38hD2ZVmC7 */ /* Return if profile accepted */ mark match 0x10000/0x10000
10 cali-pri-ksa.default.default all -- anywhere anywhere /* cali:NxmrZYbhCNLKgL6O */
11 RETURN all -- anywhere anywhere /* cali:zDbjbrN6JPMZx9S1 */ /* Return if profile accepted */ mark match 0x10000/0x10000
12 DROP all -- anywhere anywhere /* cali:d-mHGbHkL0VRl6I6 */ /* Drop if no profiles matched */
- 第1、2条:如果ct表中能检索到该连接的状态,我们直接根据状态来确定这个流量的处理方式,这样可以省略很大一部分工作。
- 第3条:先对包进行标记(将第17位置0),在本链的规则执行完毕后,会判断标记是否match(判断第17位是否有被置1),不匹配(没有被置1)就DROP;
- 第4条:如果该网卡对应的pod有相关的networkpolicy,要再打一次mark,与之前的mark做与计算后目前mark应该是0xfffcffff(17、18位为0);
- 第5条:如果包mark match 0x0/0x20000(第18位为0), 执行
cali-pi-_QW8Cu1Tr3dYs2pTUY0-
链进入networkpolicy的判断。 - 第6、7条:如果networkpolicy检查通过,会对包进行mark修改, 所以检查是否mark match 0x10000/0x10000, 匹配说明通过,直接RETURN,不再检查其他的规则;如果mark没有修改,与原先一致,视为没有任何一个networkpolicy允许该包通过,直接DROP
- 第8、9、10、11条:当没有任何相关的networkpolicy时(即第4~7条不存在)才会被执行,执行calico的profile策略,分成namespace维度和serviceaccount维度,如果在这两个策略里没有对包的mark做任何修改,就表示通过。这两个策略是calico的概念,且为了不与networkpolicy混淆,已经被弃用了。因此此处都是空的。
- 第12条:如果包没有进入 上述两个profile链,DROP。
接着看networkpolicy的链cali-pi-_QW8Cu1Tr3dYs2pTUY0-
,只要在这个链里执行Return前有将包打上mark使其match 0x10000/0x10000,就表示匹配了某个networkpolicy规则,包允许放行:
Chain cali-pi-_QW8Cu1Tr3dYs2pTUY0- (1 references)
target prot opt source destination
MARK all -- anywhere anywhere /* cali:fdm8p72wShIcZesY */ match-set cali40s:9WLohU2k-3hMTr5j-HlIcA0 src MARK or 0x10000
RETURN all -- anywhere anywhere /* cali:63L9N_r1RGeYN8er */ mark match 0x10000/0x10000
MARK all -- anywhere anywhere /* cali:xLfB_tIU4esDK000 */ MARK xset 0x40000/0xc0000
MARK all -- 10.16.2.122 anywhere /* cali:lUSV425ikXY6zWDE */ MARK and 0xfffbffff
MARK tcp -- 10.16.2.0/24 anywhere /* cali:8-qnPNq_KdC2jrNT */ multiport dports 3456 mark match 0x40000/0x40000 MARK or 0x10000
RETURN all -- anywhere anywhere /* cali:dr-rzJrx0I6Vqfkl */ mark match 0x10000/0x10000
- 第1、2条:如果src ip match ipset:
cali40s:9WLohU2k-3hMTr5j-HlIcA0
,将包 mark or 0x10000, 并检查是否match,match就RETUR。 我们可以在机器上执行ipset list cali40s:9WLohU2k-3hMTr5j-HlIcA0
, 可以看到这个ipset里包含的就是networkpolicy中指明的、带有hyapp=client1
这个label的两个pod的ip。 - 第3、4、5、6条则是针对networkpolicy中的第二部分规则,先对包设置正向标记,然后将要隔离的src IP/IP段进行判断并做反向标记,接着判断src段是否在准入范围,如果在,并且目的端口匹配,并且标记为正向,就再对包进行MARK or 0x10000 , 这样,最终判断match了就会Return。
- 实际上我们可以看到,这里就算不match,这个链执行完了也还是会RETURN的,所以这个链执行的结果是通过mark返回给上一级的,这就是为什么调用该链的上一级,会在调用完毕后要判断mark并确认是否ACCEPT。
至此,一个完整的networkpolicy的实现链路就完成了。
felix实现networkpolicy的案例2
思考一个问题:
假设我们对pod的出入站规则都做了限制,pod是否就完全被隔离了呢?如果pod被完全隔离,那么pod中设计的健康检查规则是否就不生效了?这样pod是否就永远无法ready?
felix对networkpolicy的实现主要集中在iptables的FORWARD链中,而k8s对pod的健康检查是由node访问pod。基于不同的网络方案,有几种不同的链路:
- flannel。node上基于路由直接找到flannel网桥(默认是cbr0),网桥内部二层转发到veth。最后包是从pod的host-veth发出,在容器内的veth收到包。
- calico。node上基于scope link路由直接找到pod的host-veth,最后包是从pod的host-veth发出,在容器内的veth收到包。
可见,包会走host-veth的OUTPUT链,整个过程中没有经过host-veth的FORWARD链。上文我们提到,在cali-OUTPUT中没有任何特殊规则,因此包可以顺利送达容器。
容器回包时,host-veth是要收包的,基于回包的目的IP的不同,可能走PREROUTING->INPUT, 或者PREROUTING->FORWARD。 两种路径不管哪种都会经过cali-from-wl-dispatch
链,从而进入对pod的egress规则的判断。
egress规则中我们禁止pod出站的所有规则,那回包应该会被DROP吧?
答案是不会,因为请求包在CT表中构成了记录,回包与请求包是同一个连接的,因此这种关联包会被felix设计的iptables规则放行(在例1中我们有提到入站的networkpolicy链里也有类似的规则)。
Chain cali-fw-veth-877c5ccffd (1 references)
target prot opt source destination
ACCEPT all -- anywhere anywhere /* cali:OEbQGqO7Ne_PHJxs */ ctstate RELATED,ESTABLISHED
DROP all -- anywhere anywhere /* cali:2VWpV2kgclFmj3-s */ ctstate INVALID
那是否意味着实际上这个networkpolicy规则对于pod和node之间的网络不生效呢?
不尽然。尽管node可以访问pod,但pod内无法直接访问node上的IP,这是因为pod内主动发出的包请求,在host-veth侧直接进入cali-from-wl-dispatch
链,此时ctstate是 NEW,而非RELATED或ESTABLISHED。因此要接着走下面的networkpolicy规则。
总结
综上所述。felix对networkpolicy的实现,处理了:
- node上经过转发后进入pod的流量(即ingress)
- pod内主动向外发出的流量(即egress)
基于上述的案例,我们可以分析出felix设计的networkpolicy iptables规则的大致流程,参考下图:
- Tip1:图中的
cali-fw-veth***
和cali-tw-veth***
链下,都会首先判断连接的状态,如果ctstate是RELATED或ESTABLISHED,是允许放行的。(关于ctstate的说明可以参考这篇文章)
通用的felix插件设计
如果你看了上文calico/felix的设计实现,你就会发现原理其实非常简单,这个设计完全可以应用到任何一个“基于三层路由转发”的网络方案中。但实际应用过程中我们还是遇到了一些问题。
问题1:networkpolicy-only
我们知道,较新版本的felix都是集成到calico-node组件中运行。calico-node默认情况下会完成容器网络和networkpolicy两块工作,如何部署一个只负责实现networkpolicy规则的calico-node呢?
可以参考calico官方提供的canal方案是如何适配flannel的。从canal的部署模板中我们可以基本确认,只要部署好kube-controllers
,pod2daemon
,calico-node
并且通过环境变量控制calico-node
的CALICO_NETWORKING
环境变量为"false"
(禁止配置容器网络)即可。
另外,AWS的calico部署模板 ,则是只部署calico-node
和typha
,有兴趣的同学也可以实践一下。
问题2:网卡名映射
我们尝试在网易轻舟k8s集群(使用网易云VPC作为容器网络)中尝试以这样的方式部署一套calico套件,部署后,我们会发现calico-node的日志里定期报错,提示:找不到cali****
的网卡,无法配置iptables规则。 有用过calico的同学应该看得明白,cali
是calico方案在宿主机侧生成的网卡名前缀。而我们基于网易云VPC设计的容器网络方案,会以veth-
为容器hostveth前缀。如果calico是基于前缀来找到容器网卡的,那么是否有参数可以指定前缀呢?
官方的felix配置文档中提到:可以使用InterfacePrefix
参数或FELIX_INTERFACEPREFIX
环境变量,决定felix要检索的host侧网卡前缀。一开始看到这个说明令人欣喜万分。但是当我们实际配置了之后,会发现,calico-node还是会报错,提示: 找不到veth-*****
的网卡,这个网卡名超过了linux内核的常数限制(15个字符)。
我们按照日志里打印的网卡名去找,确实找不到这个网卡,看来必须要搞清楚calico是如何给host侧的网卡进行命名的。
calico为pod的veth命名的规则实现在libcalico-go
项目中,存在如下的一个接口
type WorkloadEndpointConverter interface {
VethNameForWorkload(namespace, podName string) string
PodToWorkloadEndpoints(pod *kapiv1.Pod) ([]*model.KVPair, error)
}
这个接口用用来实现pod映射到workload的,同时还能根据pod的信息,推导pod的hostveth网卡名是啥,该接口只有一种实现:defaultWorkloadEndpointConverter
, 其中VethNameForWorkload
的实现如下:
// VethNameForWorkload returns a deterministic veth name
// for the given Kubernetes workload (WEP) name and namespace.
func (wc defaultWorkloadEndpointConverter) VethNameForWorkload(namespace, podname string) string {
// A SHA1 is always 20 bytes long, and so is sufficient for generating the
// veth name and mac addr.
h := sha1.New()
h.Write([]byte(fmt.Sprintf("%s.%s", namespace, podname)))
prefix := os.Getenv("FELIX_INTERFACEPREFIX")
if prefix == "" {
// Prefix is not set. Default to "cali"
prefix = "cali"
} else {
// Prefix is set - use the first value in the list.
splits := strings.Split(prefix, ",")
prefix = splits[0]
}
log.WithField("prefix", prefix).Debugf("Using prefix to create a WorkloadEndpoint veth name")
return fmt.Sprintf("%s%s", prefix, hex.EncodeToString(h.Sum(nil))[:11])
}
可以看到,calico根据pod的namespace和name进行hash,然后根据FELIX_INTERFACEPREFIX
环境变量的值决定网卡名前缀,将前缀与hash的前11个字符拼凑起来。 libcalico-go
是所有calico组件的lib库,也就是说,不论是calico-cni去创建veth,还是felix去根据pod查找对应的网卡, 都是基于这个逻辑去匹配的。
显然这个代码漏洞很大!calico没有对前缀做长度检查,这里要填充hash的前11位,完全是因为默认的前缀是四个字符的cali
!
问题非常明确了,要想在自己的网络方案下无痛享受felix,就得自己实现一个WorkloadEndpointConverter
接口,并编译出定制化的calico-node镜像。
案例1: canal如何接入
从canal的部署模板中我们就可以看得出来,canal方案中使用的CNI plugin实际上也是calico,只不过calico只负责创建veth对,配置IP和路由等工作,veth的命名交给calico来做,自然就按照calico的官方配置来命名了,实际使用过程中就可以看到,canal方案下容器在宿主机上的veth名称也是cali
前缀。
案例2:阿里云terway如何接入
阿里云的ACK使用其自研的terway来作为容器网络方案。terway中支持两种容器网卡虚拟化方案:
- veth
- ipvlan L2
veth方案下会使用felix来实现networkpolicy,而ipvlan下则使用cilium。我们此处主要关注veth方案。
veth方案下felix是如何使用的呢?terway在部署时,直接基于社区v3.5.8版本的felix代码进行编译(编译前还往代码中加入了一个terway自定义的patch),将编译出来的felix二进制文件丢到terway的docker镜像中, daemonset里启动三个terway镜像容器,分别用于安装cni插件;运行agent;运行felix。
terway是如何实现兼容felix的呢?上文提到的网卡名的问题,它如何解决呢?
通过阅读terway的源码 ,我们发现terway做得比较暴力——直接复用了calico代码中的网卡命名方式,对host侧的veth进行命名,网卡前缀为硬编码的cali
。
自给自足——基于calico源码改造设计通用的networkpolicy
在我们的自研网络方案中,host侧网卡命名是以某个前缀加上pod的sandbox容器id来命名的(原因见下文)。因此我们即便把前缀改成cali
或者其他长度4以内的字符串,felix也无法基于calico的那套逻辑找到网卡。
因此我们改写了该接口。实现了一个sandboxWorkloadEndpointConverter
, 将VethNameForWorkload
做了另一种实现:
- 根据felix参数感知自定义的网卡名前缀,这里为了避免prefix太长,导致网卡名冲突,对prefix长度进行限制,建议不超过5个字符,至少给后缀保留10个字符(我们曾经在线上环境出现过同一个节点的两个podsandbox容器id前9位完全相同的情况)
- 根据pod信息获取到他对应的sandbox容器ID,取其
15-len(prefix)
位作为后缀。 - 通过前缀与sandboxID后缀构成workload的网卡名。
未来我们会尝试对这部分代码做更通用化的改造,支持多种前缀,并支持自动选用网卡命名方法。
为什么我们要以podsandbox容器id来命名网卡?
因为实际使用过程中我们发现,kubelet对于sandbox容器的处理并不一定是有序的,可能出现如下场景:
- 为poda创建出sandbox1, 调用CNI ADD失败,但veth已经创建;
- 为poda创建出sandbox2, 调用 CNI ADD成功,直接使用了上一次创建的veth;
- 删除此前已经失败的sandbox1,调用 CNI DEL。 将第2步创建的veth删除,导致poda的网络异常。
因此,如果kubelet调用CNI是以sandbox为粒度,那么我们创建的资源就理应也以sandbox为粒度。
编译与构建
目前我们将通用的felix基于calico/node的v3.17分支构建, 并将它引用的libcalico-go
fork至:163yun/libcalico-go,并建立分支tag:v1.7.2-nks.1
。
这样,我们可以直接拉取社区代码,进行编译:
cd $GOPATH/src
mkdir -p github.com/projectcalico
cd github.com/projectcalico
git clone https://github.com/projectcalico/node
cd node
# 修改依赖包,改为引用我们修改过后的libcalico-go
go mod edit -replace=github.com/projectcalico/libcalico-go=github.com/163yun/libcalico-go@v1.7.2-nks.1
# 编译出calico/node的docker image
make calico/node
docker tag calico/node:latest-amd64 $EXPECTED_IMAGE_PATH:$EXPECTED_IMAGE_TAG
编译构建过程中可能出现一些网络原因导致编译阻塞:
- 编译过程中会在容器里进行
go build
,为了方便执行go module,所以建议在Makefile、以及执行make时下载的临时版本Makefile.common.v***
文件中的部分位置注入环境变量:GOPROXY=https://goproxy.cn
- 编译完毕后构建docker image时,会在基础镜像中下载安装多个依赖工具,可能出现yum源无法解析等问题,建议在
Makefile
文件中调用docker build
的语句里追加参数--network=host
测试方法
在一套现成的k8s集群中部署felix套件(参考上图)。
我们使用sonobuoy工具进行测试工作,该工具也可以从来进行k8s集群的conformance认证。下载该工具的二进制文件,然后执行:
sonobuoy run --e2e-focus="\[Feature:NetworkPolicy\]" --e2e-skip="" --image-pull-policy IfNotPresent
即可在当前集群里进行networkpolicy相关的e2e测试(并且测试过程创建的pod的imagePullPolicy都是IfNotPresent
)。
执行命令后可以查看集群中的pods,会看到sonobuoy的pod以及它创建出来的一些e2e相关的pod,如果有pod阻塞于ImagePullBackoff,可以尝试在pod所在节点上拉取备用镜像并修改成所需镜像:
docker pull hub.c.163.com/combk8s/conformance:v1.19.3
docker tag hub.c.163.com/combk8s/conformance:v1.19.3 k8s.gcr.io/conformance:v1.19.3
docker pull hub.c.163.com/combk8s/e2e-test-images/agnhost:2.20
docker tag hub.c.163.com/combk8s/e2e-test-images/agnhost:2.20 k8s.gcr.io/e2e-test-images/agnhost:2.20
整个networkpolicy的e2e测试用例有29个,整体测试完毕耗时约1~1.5h。
测试完毕后,理论上所有e2e前缀的pod都会被删除,此时执行sonobuoy retrieve
命令, 会在当前目录生成一个tar.gz
文件,解压该文件,并读取plugins/e2e/results/global/e2e.log
, 就可以看到整个e2e测试的执行结果。 也可以通过plugins/e2e/sonobuoy_results.yaml
文件查看,但这个文件内容包括了未执行的用例,可读性可能不太好。
简要的e2e测试结果如下:
root@pubt2-nks-for-dev6:/home/hzhuangyang1/plugins/e2e/results/global# tail -n 10 e2e.log
JUnit report was created: /tmp/results/junit_01.xml
{"msg":"Test Suite completed","total":29,"completed":29,"skipped":5204,"failed":0}
Ran 29 of 5233 Specs in 4847.335 seconds
SUCCESS! -- 29 Passed | 0 Failed | 0 Pending | 5204 Skipped
PASS
Ginkgo ran 1 suite in 1h20m48.75547671s
Test Suite Passed
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。