当Kubernetes遇见Macvlan—实现CNI路由插件

    当Kubernetes遇见Macvlan——实现CNI路由插件.png

    实验背景

    上次做了一个《基于Macvlan的Kubernetes网络方案》,Macvlan插件执行以后,Pod和Host网络还没有互通。于是我们创建了一个Bridge模式的Macvlan子设备,手写从Bridge到Pod的路由规则,使得Pod和Host网络互通。现在我们实现一个CNI插件,通过链式执行自动完成上面的事情。

    创建项目

    CNI插件使用golang开发,我们随便创建一个文件夹,在其中go mod init <module>一下就可以了。下面是我的项目结构。

    .
    ├── go.mod
    ├── go.sum 
    ├── main.go
    ├── Makefile
    └── net.d
        └── 00-default.conflist
    

    go.mod来看,我们的项目只依赖两个外部模块,一个是CNI插件框架,一个用来操作Linux网络设备。

    ...
    require (
        // CNI插件框架
    	github.com/containernetworking/cni v1.0.1
        // 操作Linux网络设备
    	github.com/vishvananda/netlink v1.1.0
    )
    ...
    

    00-default.conflist是我们的CNI插件配置,最后会放到/etc/cni/net.d下面。

    {
        "cniVersion": "0.4.0",
        "name": "default",
        "plugins": [
            {
                "type": "macvlan",
                "master": "eno1",
                "ipam": {
                    "type": "host-local",
                    "ranges": [
                        [
                            {
                                "subnet": "192.168.0.0/16",
                                "rangeStart": "192.168.1.2",
                                "rangeEnd": "192.168.1.254",
                                "gateway": "192.168.0.1"
                            }
                        ]
                    ],
                    "routes": [
                        {"dst": "0.0.0.0/0"}
                    ]
                }
            },
            {
                "type": "route",
                "master": "eno1",
                "bridge": "eno1.host"
            }
        ]
    }
    

    代码实现

    这里是我们用到的的所有go module。

    import (
    	"encoding/json"
    	"net"
    
    	"github.com/containernetworking/cni/pkg/skel"
    	"github.com/containernetworking/cni/pkg/types"
    	"github.com/containernetworking/cni/pkg/types/040"
    	"github.com/containernetworking/cni/pkg/version"
    	"github.com/vishvananda/netlink"
    )
    

    首先我们需要声明一个结构体,用来解析我们自定义插件的配置,以及接收macvlan插件的执行结果。

    type PluginConfig struct {
        // 组合代替继承
    	types.NetConf
    
    	Master      string `json:"master"`
    	Bridge      string `json:"bridge"`
    	MasterIndex int    `json:"-"`
    	BridgeIndex int    `json:"-"`
    }
    

    每次调用CNI插件的ADD和DEL方法时,会通过stdin传入插件配置和上一步的执行结果,我们实现一个parseConfig方法解析传入的内容。

    func parseConfig(stdin []byte) (*PluginConfig, error) {
    	var config PluginConfig
    
        // 解析当前插件的配置
        if err := json.Unmarshal(stdin, &config); err != nil {
    		return nil, err
    	}
    
        // 解析上一步执行结果
    	if err := version.ParsePrevResult(&config.NetConf); err != nil {
    		return nil, err
    	}
    
    	return &config, nil
    }
    

    然后实现一个initBridge方法,在Host网络空间创建Bridge模式的Macvlan子设备。

    func initBridge(config *PluginConfig) error {
        // 找到macvlan主设备的索引
    	masterLink, err := netlink.LinkByName(config.Master)
    	if err != nil {
    		return err
    	}
    	config.MasterIndex = masterLink.Attrs().Index
    
        // 检查当前是否存在bridge
    	bridgeLink, err := netlink.LinkByName(config.Bridge)
        if err == nil {
    		config.BridgeIndex = bridgeLink.Attrs().Index
    		return nil
    	}
        if _, ok := err.(netlink.LinkNotFoundError); !ok {
            return err
        }
    	
        // 创建bridge模式的macvlan子设备
    	if err := netlink.LinkAdd(&netlink.Macvlan{
    		LinkAttrs: netlink.LinkAttrs{
    			Name:        config.Bridge,
    			ParentIndex: config.MasterIndex,
    		},
    		Mode: netlink.MACVLAN_MODE_BRIDGE,
    	}); err != nil {
    		return err
    	}
    
        // 找到创建的bridge并启动它
    	bridgeLink, err = netlink.LinkByName(config.Bridge)
    	if err != nil {
    		return err
    	}
    	if err := netlink.LinkSetUp(bridgeLink); err != nil {
    		return err
    	}
    	config.BridgeIndex = bridgeLink.Attrs().Index
    	
        return nil
    }
    

    实现CNI插件的ADD方法,根据Pod IP在Host上创建路由。

    func cmdAdd(args *skel.CmdArgs) error {
        // 解析插件配置和上一步的结果
    	config, err := parseConfig(args.StdinData)
    	if err != nil {
    		return err
    	}
    
        // 获取上一步(macvlan插件)的执行结果
    	prevResult, err := types040.GetResult(config.PrevResult)
    	if err != nil {
    		return err
    	}
    
        // 检查或初始化macvlan bridge
        if err := initBridge(config); err != nil {
    		return err
    	}
    
        // 获得Pod IP
    	podIP := prevResult.IPs[0].Address
    	podIP.Mask = net.CIDRMask(32, 32)
    
        // 创建路由
    	route := &netlink.Route{
    		Dst:       &podIP,
    		Scope:     netlink.SCOPE_LINK,
    		LinkIndex: config.BridgeIndex,
    	}
    	if err := netlink.RouteAdd(route); err != nil {
    		return err
    	}
    
        // 透传macvlan插件的执行结果
        // 这里必须主动打印一下,否则执行会报错
    	return types.PrintResult(prevResult, config.CNIVersion)
    }
    

    实现CNI插件的DEL方法,Pod被回收时删除路由。

    func cmdDel(args *skel.CmdArgs) error {
        // 解析插件配置和上一步的结果
    	config, err := parseConfig(args.StdinData)
    	if err != nil {
    		return err
    	}
    
        // 获取ADD操作的的执行结果
    	prevResult, err := types040.GetResult(config.PrevResult)
    	if err != nil {
    		return err
    	}
    
        // 获得Pod IP
    	podIP := prevResult.IPs[0].Address
    	podIP.Mask = net.CIDRMask(32, 32)
    
        // 删除路由
    	route := &netlink.Route{
    		Dst:       &podIP,
    		Scope:     netlink.SCOPE_LINK,
    		LinkIndex: config.BridgeIndex,
    	}
    	return netlink.RouteDel(route)
    }
    

    CNI插件的CHECK方法不用实现,声明一下就可以。

    func cmdCheck(args *skel.CmdArgs) error {
    	return nil
    }
    

    下面是CNI插件的执行入口,在CNI框架的支持下,一行代码足矣。

    func main() {
    	skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, "CNI route plugin 0.0.1")
    }
    

    功能测试

    没有将插件应用到Kubernetes时,我们用cnitool这个工具对插件进行测试,可以现场装一个。

    go install github.com/containernetworking/cni/cnitool
    

    构建route插件,放到/opt/cni/bin这个目录。

    go build -o /opt/cni/bin/route main.go
    

    使用实验配置测试ADD功能。

    $ NETCONFPATH=$PWD/net.d CNI_PATH=/opt/cni/bin cnitool add default /var/run/netns/testing
    

    CNI插件链被成功执行,产生了一个JSON输出。这是CNI插件链的执行结果,会被缓存到CNI工作目录。

    {
        "cniVersion": "0.4.0",
        "interfaces": [
            {
                "name": "eth0",
                "mac": "ea:a8:70:cd:18:62",
                "sandbox": "/var/run/netns/testing"
            }
        ],
        "ips": [
            {
                "version": "4",
                "interface": 0,
                "address": "192.168.1.2/16",
                "gateway": "192.168.0.1"
            }
        ],
        "routes": [
            {
                "dst": "0.0.0.0/0"
            }
        ],
        "dns": {}
    }
    

    我们在/var/lib/cni/networks/default下面可以看到IPAM分配的Pod IP。

    $ ls /var/lib/cni/networks/default
    192.168.1.2  last_reserved_ip.0  lock
    
    # 上一个被分配的IP
    $ cat /var/lib/cni/networks/default/last_reserved_ip.0 
    192.168.1.2
    

    /var/lib/cni/results下可以看到缓存的CNI执行结果,执行DEL操作时缓存内容会被传入。

    $ ls /var/lib/cni/results
    default-cnitool-77383ca0a0715733ca6f-eth0
    
    $ cat /var/lib/cni/results/default-cnitool-77383ca0a0715733ca6f-eth0 | jq
    {
      "kind": "cniCacheV1",
      "containerId": "cnitool-77383ca0a0715733ca6f",
      "config": ...(base64后的原始配置)
      "ifName": "eth0",
      "networkName": "default",
      "result": {
        ...(刚才那一大坨JSON执行结果)
      }
    }
    

    现在我们测试DEL功能。

    $ NETCONFPATH=$PWD/net.d CNI_PATH=/opt/cni/bin cnitool del default /var/run/netns/testing
    

    虽然什么输出都没有,但是执行是成功的。这时你再去/var/lib/cni下面查看ADD的执行结果,很多文件已经被删除了。

    集成测试

    上一步我们已经将route插件放到了/opt/cni/bin目录,这里只要覆盖一下Kubernetes的CNI配置。

    $ cp $PWD/net.d/00-default.conflist /etc/cni/net.d
    

    删除从前创建的Macvlan Bridge。

    ip link del eno1.host
    

    然后我们创建一个Nginx Deployment,观察Pod IP。

    $ kubectl create deployment nginx --image nginx:stable-alpine 
    deployment.apps/nginx created
    
    $ kubectl get pods -o wide
    NAME                     READY   STATUS    RESTARTS   AGE   IP             NODE   
    nginx-7bd849c599-vkppk   1/1     Running   0          28s   192.168.1.3   lyr620
    

    测试Pod到Host的网络连通性。

    $ ping -c 3 192.168.1.3
    PING 192.168.1.3 (192.168.1.3) 56(84) bytes of data.
    64 bytes from 192.168.1.3: icmp_seq=1 ttl=64 time=0.172 ms
    64 bytes from 192.168.1.3: icmp_seq=2 ttl=64 time=0.057 ms
    64 bytes from 192.168.1.3: icmp_seq=3 ttl=64 time=0.089 ms
    
    --- 192.168.1.3 ping statistics ---
    3 packets transmitted, 3 received, 0% packet loss, time 2036ms
    rtt min/avg/max/mdev = 0.057/0.106/0.172/0.048 ms
    
    
    $ kubectl exec nginx-7bd849c599-vkppk -it -- ping -c 3 192.168.0.5
    PING 192.168.0.5 (192.168.0.5): 56 data bytes
    64 bytes from 192.168.0.5: seq=0 ttl=64 time=0.149 ms
    64 bytes from 192.168.0.5: seq=1 ttl=64 time=0.118 ms
    64 bytes from 192.168.0.5: seq=2 ttl=64 time=0.132 ms
    
    --- 192.168.0.5 ping statistics ---
    3 packets transmitted, 3 packets received, 0% packet loss
    round-trip min/avg/max = 0.118/0.133/0.149 m
    

    查看Host上创建的Macvlan子设备。

    $ ip link list
    ...
    47: eno1.host@eno1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default 
        link/ether be:c3:ad:8b:1f:53 brd ff:ff:ff:ff:ff:ff
    

    查看Host上为Pod创建的路由。

    $ ip route list
    ...
    192.168.1.3 dev eno1.host scope link
    

    参考资料

    联系我们

    加入社区

    微信扫码
    加入官方交流群

    立即体验

    在线开通,按量计费,真正的云服务!

    立即开始

    选择观测云版本

    代码托管平台