仅仅是初步学习一下 CNI 的运作流程
Ref
CNI 作用 推荐这个视频 Youtube: Kubernetes Networking: How to Write a CNI Plugin From Scratch - Eran Yanay, Twistlock ,作者用 bash 写了一个 cni https://github.com/eranyanay/cni-from-scratch 。
假设 Pod 中只有一个容器,我对于 Kubernetes 启动一个 Pod 的流程的理解如下:
首先,会先启动一个 pause 容器,Pod 的各种命名空间都是由它来创建的。
调用 CNI 插件为这个网络命名空间进行相关配置,例如 ip、路由,并确保之后各个网络命名空间之间可以互相通信。
启动容器,将其将入到 pause 容器的网络命名空间中。
所以我觉得,CNI 的本质作用是对一个网络命名空间做一些配置,并在后续管理容器流量,确保集群中各个网络命名空间之间可以互相通信。
同一宿主机上不同网络命名空间通信最简单是采用 bridge 模式,也就是使用 veth pair 将所有的网络命名空间都与宿主机网络命名空间中的一个网桥连接,这个网络起到了一个交换机的作用。关于如何模拟这种行为的见我之前写过的一篇博文 veth + iptables 模拟 Docker 网络 Bridge 模式 。
CNI 的官方 Github 仓库给出了 CNI 的规范 https://github.com/containernetworking/cni/blob/master/SPEC.md ,主要内容大致分为两部分:
CNI 网络插件与调用者交互的方式和输入输出格式
CNI 网络插件需要实现的功能
在仓库 https://github.com/containernetworking/plugins 中给出了一些样例插件,例如 bridge https://github.com/containernetworking/plugins/blob/master/plugins/main/bridge 。网上还有一些供人学习的 CNI 样例插件,比如说这个用 Bash 实现的 https://github.com/eranyanay/cni-from-scratch 。
使用 CNI 这里做一个实验,实验不需要搭建 Kubernetes 集群,流程如下:
首先,创建两个网络命名空间 ns-2,ns-3
使用 CNI 网络插件 bridge 对 ns-2,ns-3 进行配置
使用 containerd 启动一个 nginx 容器并加入至 ns-2 中
测试在宿主机以及 ns-3 中是否可以访问 ns-2 中的 nginx
1 2 3 4 5 6 7 8 ┌─────────────────────────────────────────────┐ │ ns-2 ns-3 │ │ ┌───────────────┐ ┌───────────────┐ │ │ │ ┌─────────┐ │ │ │ │ │ │ │ Nginx │ │ │ │ │ │ │ └─────────┘ │ │ │ │ │ └───────────────┘ └───────────────┘ │ └─────────────────────────────────────────────┘
环境:ubuntu 16.04,root用户
创建命名空间 1 2 ip netns add ns-2 ip netns add ns-3
检查是否创建成功
1 2 3 ip netns list ls /var/run/netns/
使用 CNI 进行配置 编译 https://github.com/containernetworking/cni 中的插件过程在链接仓库的 README.md 中有详细说明,这里不再赘述。这里 CNI 插件最终存放在 /opt/cni/bin 目录下
这里使用 https://github.com/containerd/go-cni 对 CNI 插件进行操作,代码如下
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 package mainimport ( "context" "flag" "fmt" "log" "os" "os/signal" "strconv" gocni "github.com/containerd/go-cni" ) var idF = flag.Int("id" , 2 , "网络命名空间的编号" )func init () { flag.Parse() } func main () { id := *idF netns := fmt.Sprintf("/var/run/netns/ns-%d" , id) l, err := gocni.New( gocni.WithMinNetworkCount(2 ), gocni.WithPluginDir([]string {"/opt/cni/bin" }), gocni.WithInterfacePrefix("eth" )) if err != nil { log.Fatalf("failed to initialize cni library: %v" , err) } if err := l.Load(gocni.WithLoNetwork, gocni.WithConfListFile("/etc/cni/net.d/10-c.conf" )); err != nil { log.Fatalf("failed to load cni configuration: %v" , err) } ctx := context.Background() defer func () { if err := l.Remove(ctx, strconv.Itoa(id), netns); err != nil { log.Fatalf("failed to teardown network: %v" , err) } }() result, err := l.Setup(ctx, strconv.Itoa(id), netns) if err != nil { log.Fatalf("failed to setup network for namespace: %v" , err) } for key, iff := range result.Interfaces { if len (iff.IPConfigs) > 0 { IP := iff.IPConfigs[0 ].IP.String() fmt.Printf("IP of the interface %s:%s\n" , key, IP) } } ch := make (chan os.Signal, 1 ) signal.Notify(ch, os.Interrupt) <-ch }
配置文件 /etc/cni/net.d/10-c.conf 如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 { "cniVersion" : "0.4.0" , "name" : "hdls-net" , "plugins" : [ { "type" : "bridge" , "bridge" : "cni0" , "isGateway" : true , "ipMasq" : true , "ipam" : { "type" : "host-local" , "subnet" : "10.22.0.0/16" , "routes" : [ { "dst" : "0.0.0.0/0" } ] } }, { "type" : "firewall" , "backend" : "iptables" } ] }
打开两个终端分别执行如下命令对网络命名空间进行配置
1 2 ./program -id 2 ./program -id 3
会看到相关的 ip 输出,我这里输出的分别是是10.22.0.6 和 10.22.0.7
上述配置文件中添加了 “filewall” 的原因是 https://serverfault.com/questions/162366/iptables-bridge-and-forward-chain ,可以执行如下命令插件新增的 iptable filter 规则:
1 iptables -t filter -L -n
部分输出如下
1 2 3 4 5 6 7 8 9 10 11 Chain FORWARD (policy ACCEPT) target prot opt source destination CNI-FORWARD all -- 0.0.0.0/0 0.0.0.0/0 /* CNI firewall plugin rules */ Chain CNI-FORWARD (1 references) target prot opt source destination CNI-ADMIN all -- 0.0.0.0/0 0.0.0.0/0 /* CNI firewall plugin rules */ ACCEPT all -- 0.0.0.0/0 10.22.0.6 ctstate RELATED,ESTABLISHED ACCEPT all -- 10.22.0.6 0.0.0.0/0 ACCEPT all -- 0.0.0.0/0 10.22.0.7 ctstate RELATED,ESTABLISHED ACCEPT all -- 10.22.0.7 0.0.0.0/0
创建容器 containerd 是一个容器运行时,它提供了相关的 SDK 供第三方程序与其交互。containerd 的二进制程序可以直接从 github 上下载它编译好的文件 ,或者直接自己编译,具体过程不再赘述。
启动容器的代码如下,注意,运行此代码时 containerd 需要启动,且这里已经写死了容器使用的网络命名空间是 “/var/run/netns/ns-2” 以及使用的容器镜像是 “docker.io/library/nginx:alpine”
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 package mainimport ( "context" "fmt" "os" "os/signal" "syscall" "github.com/containerd/containerd" "github.com/containerd/containerd/cio" "github.com/containerd/containerd/log" "github.com/containerd/containerd/namespaces" "github.com/containerd/containerd/oci" "github.com/opencontainers/runtime-spec/specs-go" ) func main () { if err := createAndRunContainer("/var/run/netns/ns-2" , "app1" , "docker.io/library/nginx:alpine" ); err != nil { log.G(context.TODO()).Errorf("%+v\n" , err) } } func createAndRunContainer (netNSPath string , containerID string , image string ) (err error) { var ( client *containerd.Client appImage containerd.Image app1 containerd.Container task containerd.Task exitStatusC <-chan containerd.ExitStatus ) if client, err = containerd.New("/run/containerd/containerd.sock" ); err != nil { return err } defer client.Close() ctx := namespaces.WithNamespace(context.Background(), "example" ) if appImage, err = client.Pull(ctx, image, containerd.WithPullUnpack); err != nil { return err } app1, err = client.NewContainer(ctx, containerID, containerd.WithImage(appImage), containerd.WithNewSnapshot(fmt.Sprintf("%s-snapshot" , containerID), appImage), containerd.WithNewSpec( oci.WithImageConfig(appImage), oci.WithLinuxNamespace(specs.LinuxNamespace{ Type: specs.NetworkNamespace, Path: netNSPath, }), ), ) if err != nil { return err } defer app1.Delete(ctx, containerd.WithSnapshotCleanup) if task, err = app1.NewTask(ctx, cio.NewCreator(cio.WithStdio)); err != nil { return err } defer task.Delete(ctx) if exitStatusC, err = task.Wait(ctx); err != nil { return err } go func () { ch := make (chan os.Signal, 1 ) signal.Notify(ch, os.Interrupt, os.Kill) <-ch _ = task.Kill(ctx, syscall.SIGTERM) }() if err = task.Start(ctx); err != nil { return err } _, _, err = (<-exitStatusC).Result() return }
网络连通性测试 从 ns-3 中访问 nginx
1 ip netns exec ns-3 curl 10.22.0.6
从宿主机中访问 nginx
CNI 都干了啥 这里跳过了处理输入和输出的步骤,仅仅是大致模拟一遍 bridge 模式的网络配置流程。需要使用到 ip、brctl 工具。仅模拟配置 ns-2 的过程(没有配置 DNS)
首先,如果 cni0 不存在的话先要创建个 cni0 网桥,并将网桥配置为网关
1 2 3 4 brctl addbr cni0 ip link set cni0 up ip addr add 10.22.0.1/16 dev cni0 route add -net 10.22.0.0/16 dev cni0
随后,创建 veth pair,一边放到 ns-2 网络命名空间中,另一端连在 cni0 网桥上
1 2 3 4 ip link add eth0 type veth peer name veth_ns-2 ip link set eth0 netns ns-2 ifconfig veth_ns-2 up brctl addif cni0 veth_ns-2
然后进入到 ns-2 中配置 ip 和路由,这里忽略了调用 ipam 生成 ip 的过程
1 2 ip netns exec ns-2 ifconfig eth0 10.22.0.6/16 up ip netns exec ns-2 ip route add default via 10.22.0.1 dev eth0
随后对 iptables nat 的 masquerade 做配置,确保它能与外部网络通信
1 iptables -t nat -A POSTROUTING -s 10.22.0.6/16 -j MASQUERADE
由于还添加了 firewalls 插件,它会允许同一节点上不同网络命名空间之前的通信
1 2 iptables -A FORWARD -s 10.22.0.6/16 -j ACCEPT iptables -A FORWARD -d 10.22.0.6/16 -j ACCEPT
对于 ns-3 同理,配置如下,不再详细介绍了
1 2 3 4 5 6 7 8 9 ip link add eth0 type veth peer name veth_ns-3 ip link set eth0 netns ns-3 ifconfig veth_ns-3 up brctl addif cni0 veth_ns-3 ip netns exec ns-3 ifconfig eth0 10.22.0.7/16 up ip netns exec ns-3 ip route add default via 10.22.0.1 dev eth0 iptables -t nat -A POSTROUTING -s 10.22.0.7/16 -j MASQUERADE iptables -A FORWARD -s 10.22.0.7/16 -j ACCEPT iptables -A FORWARD -d 10.22.0.7/16 -j ACCEPT
跨节点还需要做额外的路由配置,例如
1 ip route add <other node ns cidr> via <host ip> dev <host if >