【译】理解CNI(容器网络接口)
CNI的入门和理解
0x00 前言
说到容器就不能不提 CNI,CNI 作为容器网络的统一标准,让各个容器管理平台(k8s,mesos等)都可以通过相同的接口调用各式各样的网络插件(flannel,calico,weave 等)来为容器配置网络。容器管理系统与网络插件之间的关系图如下所示。
我们可以发现其实 CNI 定义的就是一组容器运行时(containerd,rkt 等)与网络插件之间的规范定义,所以我们如果要想深入到容器网络插件的开发,了解 CNI 是必须的。
正好最近看了一篇入门 CNI(Container Network 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的工作原理,但没有演示一些实际情况的典型用例。