0x00 前言

说到容器就不能不提 CNI,CNI 作为容器网络的统一标准,让各个容器管理平台(k8s,mesos等)都可以通过相同的接口调用各式各样的网络插件(flannel,calico,weave 等)来为容器配置网络。容器管理系统与网络插件之间的关系图如下所示。
1.png
我们可以发现其实 CNI 定义的就是一组容器运行时(containerd,rkt 等)与网络插件之间的规范定义,所以我们如果要想深入到容器网络插件的开发,了解 CNI 是必须的。

正好最近看了一篇入门 CNI(Container Network Interface) 的博客,觉得写的还挺不错的,但是没有找到中文文档,所以尝试着翻译下分享给需要的人,水平有限,不足之处多多指教。

原文: http://www.dasblinkenlichten.com/understanding-cni-container-networking-interface/

0x01 正文

如果您一直有关注容器网络的讨论的话,您可能听说过CNI。CNI全称叫Container Network Interface(容器网络接口),其目标是为容器创建基于插件的通用网络解决方案。CNI的说明在规范(立即去阅读,不会耗费你太多时间的)中定义了,以下是我在初读CNI规范时了解到的一些要点...

  • CNI 规范将为一个容器定义一个Linux网络命名空间。我们应该熟悉这种定义,因为像Docker这样的容器运行时会为每个Docker容器都创建一个新的网络命名空间。
  • CNI的网络定义存储为JSON格式。
  • 网络定义通过STDIN输入流传输到插件,这意味着宿主机上不会存储网络配置文件。
  • 其他的配置参数通过环境变量传递给插件
  • CNI插件为可执行文件。
  • CNI插件负责连通容器网络,也就是说,它要完成所有的工作才能使容器连入网络。在Docker中,这些工作包括以某种方式将容器网络命名空间连接回宿主机。
  • CNI插件负责调用IPAM插件,IPAM负责IP地址分配和设置容器所需的路由。

如果您平时习惯和 Docker 打交道,CNI 看上去似乎并不怎么适用。CNI 插件是负责容器网络的,这很显而易见,但一开始它是如何实现的我并不清楚。所以我的下一个问题是,我可以将CNI与Docker一起使用吗?答案是肯定的,但这不是个完整的解决方案。Docker有自己的 CNM 标准,CNM允许插件直接与Docker交互,并可以将CNM插件注册到Docker并直接使用。也就是说,您可以使用Docker运行容器,并将其网络直接分配给CNM注册的插件。这很好,但是因为Docker具有CNM,所以它们不直接与CNI集成(据我所知)。但是,这并不意味着您不能将CNI与Docker一起使用,再回到上面第六点看看,它说了CNI插件负责连接容器,因此有可能只是用Docker的容器的运行时,而不调用 Docker 的网络端的工作(在以后的文章中将对此进行更多介绍)。

在这一点上,我认为有必要了解下 CNI 具体干了些什么,以便更好地了解其是如何与容器结合的。让我们看一个使用插件的简单示例。

首先我们下载预构建的 CNI 二进制文件...

user@ubuntu-1:~$ mkdir cni
user@ubuntu-1:~$ cd cni
user@ubuntu-1:~/cni$ curl -O -L https://github.com/containernetworking/cni/releases/download/v0.4.0/cni-amd64-v0.4.0.tgz
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   597    0   597    0     0   1379      0 --:--:-- --:--:-- --:--:--  1381
100 15.3M  100 15.3M    0     0  4606k      0  0:00:03  0:00:03 --:--:-- 5597k
user@ubuntu-1:~/cni$
user@ubuntu-1:~/cni$ tar -xzvf cni-amd64-v0.4.0.tgz
./
./macvlan
./dhcp
./loopback
./ptp
./ipvlan
./bridge
./tuning
./noop
./host-local
./cnitool
./flannel
user@ubuntu-1:~/cni$
user@ubuntu-1:~/cni$ ls
bridge  cni-amd64-v0.4.0.tgz  cnitool  dhcp  flannel  host-local  ipvlan  loopback  macvlan  noop  ptp  tuning

如上所示

  • 我们首先创建了一个目录cni
  • 然后使用 curl 命令下载了 CNI 的归档文件,当使用 curl 下载文件时,我们需要传递 -O 参数来告诉 curl 保存为文件,使用 -L 参数来允许 curl 跟随重定向,因为我们下载的URL实际上会将我们30x重定向到其他URL。
  • 下载完成后,我们使用 tar 命令解压缩归档文件。

在完成这些操作后,我们可以看到有一些新文件解压出来了。现在,让我们聚焦在网桥插件bridge文件,Bridge 是 CNI 官方插件之一。您可能已经猜到,他的工作是将容器依附到网桥接口上。现在我们插件有了,接下来怎么使用它们呢?上面规范有一点提到,网络配置是通过STDIN流传输到插件中的,因此我们需要用STDIN将网络的配置信息输入到插件中,但这还不是插件所需的全部信息。插件还需要更多信息,比如您希望执行的操作,希望使用的命名空间以及其他各种信息。此信息通过环境变量传递到插件。没听懂?别担心,让我们来看一个例子。

首先,我们定义一个网桥的网络配置文件…

cat > mybridge.conf <<"EOF"
{
    "cniVersion": "0.2.0",
    "name": "mybridge",
    "type": "bridge",
    "bridge": "cni_bridge0",
    "isGateway": true,
    "ipMasq": true,
    "ipam": {
        "type": "host-local",
        "subnet": "10.15.20.0/24",
        "routes": [
            { "dst": "0.0.0.0/0" },
            { "dst": "1.1.1.1/32", "gw":"10.15.20.1"}
        ]
    }
}
EOF

如上所示,我们为桥接网络创建了JSON格式的定义。上面列出了一些CNI的通用定义,以及一些bridge插件独有的定义,各个定义的作用如下

CNI通用参数
  • cniVersion:使用的CNI规范的版本
  • 名称:桥接网络名称
  • type:插件名称,在本例中,名称为 bridge 可执行文件的名称
  • args:可选的附加参数
  • ipMasq:是否为该网络配置出站地址转换(SNAT)
  • ipam

    • type:IPAM插件可执行文件的名称
    • subnet:要分配出的子网(实际上是IPAM插件的一部分)
    • routes

      • dst:您希望地址可达的子网
      • gw:到达目标的下一跳IP,如果未指定,则使用默认网关
  • DNS:

    • nameservers: 该网络的 DNS
    • domain:用于DNS请求的搜索域
    • search:搜索域列表
    • options:要传递的选项
Bridge 插件特有参数
  • isgateway:如果为true,则为网桥分配一个IP地址,以便连到网桥的容器可以将其用作网关。
  • isdefaultgateway:如果为true,则将分配的IP地址设置为默认路由。
  • forceAddress:如果先前的值已更改,则告诉插件分配一个新的IP。
  • mtu:定义网桥的MTU。
  • hairpinMode:为网桥上的接口设置发夹模式

上面粗体字的部分参数是我们在此示例中用到的。其他参数您也可以试下,了解他们的作用,但大部分看参数名就知道啥意思了。您可能注意到了,参数其中有一部分是 IPAM 插件的定义,但是关于 IPAM 我们不会在这篇文章中介绍(以后会!),我们只需要知道它是使用了多个 CNI 插件就行了。

好了,现在我们有了网络定义,我们要把它运行起来。然而,我们目前仅定义了网桥,而 CNI 的重点是配置容器网络,因此我们也需要告诉 CNI 插件要使用网络定义来配置容器,因为要通过传递环境变量给插件来操作,所以我们的命令可能看起来像这样...

sudo CNI_COMMAND=ADD CNI_CONTAINERID=1234567890 CNI_NETNS=/var/run/netns/1234567890 CNI_IFNAME=eth12 CNI_PATH=`pwd` ./bridge < mybridge.conf

让我们来看一下上面这条命令,我想大多数人可能都知道可以在shell或系统级别设置环境变量,但除此之外,您还可以把环境变量直接传递给命令,这样它们将仅由您正在调用的可执行文件使用,并且仅在执行过程中有效。因此,在这种情况下,以下变量将被传递给网桥可执行程序...

  • CNI_COMMAND = ADD: 告诉CNI要添加连接
  • CNI_CONTAINER = 1234567890:告诉CNI要使用的网络命名空间为“1234567890”
  • CNI_NETNS = /var/run/netns/1234567890:网络命名空间路径
  • CNI_IFNAME = eth12:我们希望命名空间里使用的网络接口名
  • CNI_PATH = `pwd` :我们需要告诉CNI插件的可执行文件在哪里。在这我们的实验中,由于我们已经在cni目录中,因此使用`pwd`命令(当前工作目录)。

在定义了要传递给可执行文件的环境变量后,接下来选择我们要使用的插件(在本案例中为bridge),然后我们使用 < 通过STDIN将网络定义传给插件。在运行命令之前,我们还需要创建插件将要配置的网络命名空间。通常容器运行时会自动创建命名空间,但由于我们是自己手动实验,所以首先我们得自己先创建一个网络命名空间...

sudo ip netns add 1234567890

创建完成后,让我们运行插件...

user@ubuntu-1:~/cni$ sudo CNI_COMMAND=ADD CNI_CONTAINERID=1234567890 CNI_NETNS=/var/run/netns/1234567890 CNI_IFNAME=eth12 CNI_PATH=`pwd` ./bridge < mybridge.conf
2017/02/17 09:46:01 Error retriving last reserved ip: Failed to retrieve last reserved ip: open /var/lib/cni/networks/mybridge/last_reserved_ip: no such file or directory
{
    "ip4": {
        "ip": "10.15.20.2/24",
        "gateway": "10.15.20.1",
        "routes": [
            {
                "dst": "0.0.0.0/0"
            },
            {
                "dst": "1.1.1.1/32",
                "gw": "10.15.20.1"
            }
        ]
    },
    "dns": {}
}user@ubuntu-1:~/cni$

执行完命令后返回了两部分输出

  • 首先由于 IPAM 找不到本地存储的保留IP分配信息文件,因此返回错误。如果我们对其他网络命名空间再次运行此命令,则不会出现此错误了,因为该文件在我们首次运行插件时创建了。
  • 其次是返回一个JSON格式的IP配置,在本例中,网桥本身配置为10.15.20.1/24的IP,而网络命名空间接口将会分配到10.15.20.2/24,它还设置了默认网关和我们在网络配置JSON中定义的1.1.1.1/32路由。

让我们看看它做了什么...

user@ubuntu-1:~/cni$ ifconfig
cni_bridge0 Link encap:Ethernet  HWaddr 0a:58:0a:0f:14:01
          inet addr:10.15.20.1  Bcast:0.0.0.0  Mask:255.255.255.0
          inet6 addr: fe80::3cd5:6cff:fef9:9066/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:8 errors:0 dropped:0 overruns:0 frame:0
          TX packets:8 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:536 (536.0 B)  TX bytes:648 (648.0 B)

ens32     Link encap:Ethernet  HWaddr 00:0c:29:3e:49:51
          inet addr:10.20.30.71  Bcast:10.20.30.255  Mask:255.255.255.0
          inet6 addr: fe80::20c:29ff:fe3e:4951/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:17431176 errors:0 dropped:1240 overruns:0 frame:0
          TX packets:14162993 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:2566654572 (2.5 GB)  TX bytes:9257712049 (9.2 GB)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:45887226 errors:0 dropped:0 overruns:0 frame:0
          TX packets:45887226 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1
          RX bytes:21016155576 (21.0 GB)  TX bytes:21016155576 (21.0 GB)

veth1fbfe91d Link encap:Ethernet  HWaddr 26:68:37:93:26:4a
          inet6 addr: fe80::2468:37ff:fe93:264a/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:8 errors:0 dropped:0 overruns:0 frame:0
          TX packets:16 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:648 (648.0 B)  TX bytes:1296 (1.2 KB)

user@ubuntu-1:~/cni$

注意,我们现在有了一个名为”cni_bridge0“的网桥接口,该接口 IP 和我们预期一致,注意在底部,有veth设备的一端。回想一下,我们还启用了ipMasq,如果我们查看主机的iptables,将看到如下规则...

user@ubuntu-1:~/cni$ sudo iptables-save | grep mybridge
-A POSTROUTING -s 10.15.20.0/24 -m comment --comment "name: \"mybridge\" id: \"1234567890\"" -j CNI-26633426ea992aa1f0477097
-A CNI-26633426ea992aa1f0477097 -d 10.15.20.0/24 -m comment --comment "name: \"mybridge\" id: \"1234567890\" -j ACCEPT
-A CNI-26633426ea992aa1f0477097 ! -d 224.0.0.0/4 -m comment --comment "name: \"mybridge\" id: \"1234567890\"" -j MASQUERADE
user@ubuntu-1:~/cni$

再让我们看一下网络命名空间...

user@ubuntu-1:~/cni$ sudo ip netns exec 1234567890 ifconfig
eth12     Link encap:Ethernet  HWaddr 0a:58:0a:0f:14:02
          inet addr:10.15.20.2  Bcast:0.0.0.0  Mask:255.255.255.0
          inet6 addr: fe80::d861:8ff:fe46:33ac/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:16 errors:0 dropped:0 overruns:0 frame:0
          TX packets:8 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:1296 (1.2 KB)  TX bytes:648 (648.0 B)

user@ubuntu-1:~/cni$ sudo ip netns exec 1234567890 ip route
default via 10.15.20.1 dev eth12
1.1.1.1 via 10.15.20.1 dev eth12
10.15.20.0/24 dev eth12  proto kernel  scope link  src 10.15.20.2
user@ubuntu-1:~/cni$

我们的网络命名空间配置也如预期所示,命名空间有一个名为”eth12”的网络接口,其IP地址为10.15.20.2/24,我们之前定义的路由也在那里,至此大功告成!

这只是一个简单的示例,但我认为它阐述了CNI的实现和工作方式。下周,我们将研究一个如何在容器运行时中使用CNI的示例,进一步研究CNI插件。

在总结之前,我想简单说下我最初卡壳的一个地方——调用插件的方式。在我们的示例中,我们命令中./bridge直接调用了一个特定插件,因此,我一开始对为什么还需要使用“CNI_PATH”环境变量指定插件的位置感到困惑,显然我们已经了知道插件的路径。但其实真正原因是手动调用插件这不是通常使用CNI的方式,通常会有另一个应用程序或系统会读取CNI的网络定义并运行,在种情况下,“CNI_PATH"会在系统内定义好。由于网络配置文件定义了要使用的插件(在我们的案例中为bridge),因此所有系统都需要知道在哪里可以找到插件。为了找到插件,系统们会引用CNI_PATH变量。我们将在以后的文章中讨论这个问题,我们将讨论其他应用程序使用CNI(cough,Kubernetes)的原因.到目前为止,我们只知道上面示例的CNI的工作原理,但没有演示一些实际情况的典型用例。


N0mansky
167 声望14 粉丝

Done is better than perfect.