如何实现系列三:如何自己实现一个CNI插件

其实现在cni插件挺多的,最出名的就是calico了,然后还有几个很有特色的https://github.com/cilium/cilium https://github.com/cni-genie/CNI-Genie

正常情况下我们直接用这些开源的实现就好,不过通常情况下,我观察了下外面的分享的,好多公司都有类似需求,需要保持IP不变,那么就需要我们对相应的实现进行改造了。所以这里干脆讲下如果自己直接实现一个CNI,该如何实现。

我们以阿里云的CNIterway举例,主要看cni.go这个文件中的代码。

首先CNI我们需要知道它被调用的环节
CNI
可以看到CNI被调用的时候netns已经创建好了,我们需要做的事情就是配置这个netns,需要实现https://github.com/containernetworking/cni/blob/master/SPEC.md 这个spec描述的内容,
可以简单的实现引入github.com/containernetworking/cni ,然后我们其实只需要实现2个接口

1
func cmdAdd(args *skel.CmdArgs) error {


1
func cmdDel(args *skel.CmdArgs) error {

先不管Del,先看阿里这个Add

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
func cmdAdd(args *skel.CmdArgs) error {
versionDecoder := &cniversion.ConfigDecoder{}
confVersion, err := versionDecoder.Decode(args.StdinData)
if err != nil {
return err
}

cniNetns, err := ns.GetNS(args.Netns) //获得当前的NS
if err != nil {
return err
}
//...
allocResult, err := terwayBackendClient.AllocIP( //阿里自己的东西去拿ip,我们实现的话也是类似这样逻辑
timeoutContext,
&rpc.AllocIPRequest{
Netns: args.Netns,
K8SPodName: string(k8sConfig.K8S_POD_NAME),
K8SPodNamespace: string(k8sConfig.K8S_POD_NAMESPACE),
K8SPodInfraContainerId: string(k8sConfig.K8S_POD_INFRA_CONTAINER_ID),
IfName: args.IfName,
})
hostVethName := link.VethNameForPod(string(k8sConfig.K8S_POD_NAME), string(k8sConfig.K8S_POD_NAMESPACE), defaultVethPrefix)
var (
allocatedIPAddr net.IPNet
allocatedGatewayAddr net.IP
)

switch allocResult.IPType {
case rpc.IPType_TypeENIMultiIP:


err = networkDriver.Setup(hostVethName, args.IfName, subnet, gw, nil, int(deviceId), ingress, egress, cniNetns)
//阿里自己的网络驱动吧,其实也类似,如果我们有类似的东西的话需要去设置下
if err != nil {
return fmt.Errorf("setup network failed: %v", err)
}
allocatedIPAddr = *subnet
allocatedGatewayAddr = gw

case rpc.IPType_TypeVPCIP:
if allocResult.GetVpcIp() == nil || allocResult.GetVpcIp().GetPodConfig() == nil ||
allocResult.GetVpcIp().NodeCidr == "" {
return fmt.Errorf("vpc ip result is empty: %v", allocResult)
}
...
var r types.Result
//阿里这是调用ipam插件去设置网络
r, err = ipam.ExecAdd(delegateIpam, []byte(fmt.Sprintf(delegateConf, subnet.String())))
if err != nil {
return fmt.Errorf("error allocate ip from delegate ipam %v: %v", delegateIpam, err)
}
podIPAddr := ipamResult.IPs[0].Address
gateway := ipamResult.IPs[0].Gateway

ingress := allocResult.GetVpcIp().GetPodConfig().GetIngress()
egress := allocResult.GetVpcIp().GetPodConfig().GetEgress()

err = networkDriver.Setup(hostVethName, args.IfName, &podIPAddr, gateway, nil, 0, ingress, egress, cniNetns)
if err != nil {
return fmt.Errorf("setup network failed: %v", err)
}
allocatedIPAddr = podIPAddr
allocatedGatewayAddr = gateway
case rpc.IPType_TypeVPCENI:
if allocResult.GetVpcEni() == nil || allocResult.GetVpcEni().GetServiceCidr() == "" ||
allocResult.GetVpcEni().GetEniConfig() == nil {
return fmt.Errorf("vpcEni ip result is empty: %v", allocResult)
}
var srvSubnet *net.IPNet
_, srvSubnet, err = net.ParseCIDR(allocResult.GetVpcEni().GetServiceCidr())
if err != nil {
return fmt.Errorf("vpc eni return srv subnet is not vaild: %v", allocResult.GetVpcEni().GetServiceCidr())
}
...
ingress := allocResult.GetVpcEni().GetPodConfig().GetIngress()
egress := allocResult.GetVpcEni().GetPodConfig().GetEgress()
//这个看起来又是另外一个阿里的网络类型
err = nicDriver.Setup(hostVethName, args.IfName, eniAddrSubnet, gw, nil, int(deviceNumber), 0, 0, cniNetns)
if err != nil {
return fmt.Errorf("setup network for vpc eni failed: %v", err)
}
allocatedIPAddr = *eniAddrSubnet
allocatedGatewayAddr = gw
default:
return fmt.Errorf("not support this network type")
}
//这个是关键,其实我们正常简单实现就是在cmdAdd中加自己的逻辑,并且完成下面这个结构的拼装,其他就是和框架的事情了。
result := &current.Result{
IPs: []*current.IPConfig{{
Version: "4",
Address: allocatedIPAddr,
Gateway: allocatedGatewayAddr,
}},
}

return types.PrintResult(result, confVersion)
}

从上面看阿里的代码可以看到,其实大部分都是他们自身的逻辑,我们真正需要实现的其实就只有去获得IP的功能,然后我们可以在这个地方加入的自己逻辑,保持一个分组下容器的IP不变。
那么如果我们需要去实现一个标准库已经支持的网络(bridge ipvlan macvlan等)的CNI的时候就异常简单了
比如我们实现一个macvlan的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func cmdAdd(args *skel.CmdArgs) error {
types.LoadArgs()//加载参数,取你需要的东西
/*type CmdArgs struct {
ContainerID string
Netns string
IfName string
Args string //这里应该是k8s传过来,只看到有K8S_POD_NAMESPACE K8S_POD_NAME K8S_POD_INFRA_CONTAINER_ID
Path string
StdinData []byte
}*/

//写你自己的逻辑去拿下IP
//填写下面的结构
result := &current.Result{
IPs: []*current.IPConfig{{
Version: "4",
Address: allocatedIPAddr,
Gateway: allocatedGatewayAddr,
}},
}

return types.PrintResult(result, CONF_VERSION)
}

除此之外需要加个配置文件,符合下面的结构

1
2
3
4
5
6
7
8
9
10
11
12
// NetConf describes a network.
type NetConf struct {
CNIVersion string `json:"cniVersion,omitempty"`

Name string `json:"name,omitempty"`
Type string `json:"type,omitempty"`
Capabilities map[string]bool `json:"capabilities,omitempty"`
IPAM struct {
Type string `json:"type,omitempty"`
} `json:"ipam,omitempty"`
DNS DNS `json:"dns"`
}

比如

1
2
3
4
5
6
7
8
{
"name": "macvlan",//随意
"type": "macvlan",//需要有这么一个二进制程序,编译官方的就可以了
"master": "eth1",
"ipam": {
"type": "ipam",
}
}

建议阅读:
http://dockone.io/article/2578
https://kubernetes.feisky.xyz/cha-jian-kuo-zhan/network/index
https://jimmysong.io/kubernetes-handbook/concepts/cni.html
https://www.lijiaocn.com/项目/2017/05/03/cni.html
https://yucs.github.io/2017/12/06/2017-12-6-CNI/
https://page.pikeszfish.me/2018/01/26/write-cni-plugin-with-shell/
https://blog.51cto.com/tryingstuff/2165805

这块内容还是有点问题,有空细看下CNI标准本身的实现,以及kubelet相关的实现,再写篇文章。