注: 本文采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。
CNI在真正的容器系统里是如何工作的呢?本文将利用CNI的bridge插件,分别以Docker和rkt为例,来演示它的工作原理。
Docker与CNI
在Kubernetes网络篇——认识CNI一文里,我们利用标准的CNI插件bridge,为手工创建的network namespace建立起了相应的网络环境。本文,我们来看一下如何把CNI用于真正的容器系统,首先来看一下Docker。
要让Docker使用CNI并没有想象中那么简单和直观。在Kubernetes网络篇——从docker0开始和模拟Docker网络里我们已经看到了,利用bridge network可以实现容器和外界的网络通信。但bridge network是位于Layer-2即数据链路层的设备,只提供基于Layer-2的数据包转发,没有办法实现基于Layer-3即网络层的路由功能,以及其他一些像overlay network这样的高阶功能。这些需求要单靠Docker自己是无法全部满足的。更好的办法是开放标准接口,让第三方来实现它们自己的网络服务,这就是Docker网络的设计思路,这和CNI的出发点很类似。Docker也有很多支持各种网络功能的插件,但它们遵循的标准并不是CNI,而是另一套被称为CNM(Container Network Model)的标准。
不过,Docker并不是唯一的容器技术,还有一些其他的竞争者,比如像rkt,就是支持CNI的。一般认为,CNI的接口更加简单,所以它被认可和采纳的范围更广一些。另外,以Kubernetes为代表的一些容器集群与编排技术,也不希望绑定到某个特定的容器技术上,所以就会倾向于接受程度更广的CNI。关于这一点,Kubernetes的网站上有一篇文章,专门提到了它之所以没有选择CNM的原因。
让Docker使用CNI
虽然Docker有自己的网络插件机制,但我们还是可以让它和CNI工作在一起的。为了做到这一点,可以在启动容器时指定--net=none
,告诉Docker不要带任何网络设备,下面我们就来演示一下。
这次我们选择以nginx为例,把它当作一个基本的Web服务器。默认的nginx镜像不包含ip
命令,为了方便后面的实验,我们以nginx作为base镜像,在它的基础上安装了iproute2。把下面的Dockerfile保存到当前目录:
FROM nginx
RUN apt-get update && \
apt-get install --no-install-recommends --no-install-suggests -y \
iproute2 && \
rm -rf /var/lib/apt/lists/*
然后执行下面的命令构建Docker镜像:
$ docker build -t morningspace/lab-web .
并启动相应的容器,不带任何网络:
$ docker run --name lab-web --net=none -d morningspace/lab-web
2296af8b515dcda2683deb4e95935427ffbce9a5e13332e33c52230fadc1aec0
等容器启动以后,查看它的网络设置:
$ docker exec lab-web ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1
link/ipip 0.0.0.0 brd 0.0.0.0
3: ip6tnl0@NONE: <NOARP> mtu 1452 qdisc noop state DOWN group default qlen 1
link/tunnel6 :: brd ::
我们会发现,这个时候容器里只有一个loopback和两个默认创建的网络接口,并没有带IP地址的以太网接口,所以容器也就没有和宿主机的bridge连接起来。
配置CNI插件
接下来,我们要利用CNI把这个Docker容器连接到宿主机的bridge上。首先为CNI插件创建配置文件:
$ cat > lab-br1.conf <<"EOF"
{
"cniVersion": "0.4.0",
"name": "lab-br1",
"type": "bridge",
"bridge": "lab-br1",
"isGateway": true,
"ipMasq": true,
"ipam": {
"type": "host-local",
"subnet": "10.15.20.0/24",
"routes": [
{ "dst": "0.0.0.0/0" }
]
}
}
EOF
关于文件里的每一项配置,这里就不再一一解释了,大家可以在Kubernetes网络篇——认识CNI一文里找到详细的说明。
运行CNI插件
接下来,我们要运行CNI插件为容器创建网络了。之前曾经提到过,在运行CNI插件时,我们需要通过环境变量CNI_CONTAINERID和CNI_NETNS告诉插件容器的Id和对应network namespace的路径。
为了拿到这两个数据,我们可以执行如下命令,对容器的元数据进行查询:
$ docker inspect lab-web | grep -E 'SandboxKey|Id'
"Id": "2296af8b515dcda2683deb4e95935427ffbce9a5e13332e33c52230fadc1aec0",
"SandboxKey": "/var/run/docker/netns/801b81aea4e1",
这里,属性Id的取值,对应的就是容器的Id,SandboxKey的取值就是network namespace的路径。有了这两个值,我们就可以运行CNI插件了。此处,我们确保当前位于/root/test-cni/plugins目录下:
$ CNI_COMMAND=ADD CNI_CONTAINERID=2296af8b515dcda2683deb4e95935427ffbce9a5e13332e33c52230fadc1aec0 CNI_NETNS=/var/run/docker/netns/801b81aea4e1 CNI_IFNAME=eth0 CNI_PATH=`pwd` ./bridge <../conf/lab-br1.conf
{
"cniVersion": "0.4.0",
"interfaces": [
{
"name": "lab-br1",
"mac": "52:ba:e2:62:01:1e"
},
{
"name": "vethd5747d99",
"mac": "2e:42:d8:e3:c9:11"
},
{
"name": "eth0",
"mac": "26:3f:a5:1f:c8:27",
"sandbox": "/var/run/docker/netns/801b81aea4e1"
}
],
"ips": [
{
"version": "4",
"interface": 2,
"address": "10.15.20.5/24",
"gateway": "10.15.20.1"
}
],
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"dns": {}
}
验证网络
接下来,让我们来验证一下CNI为我们配置的网络环境。首先查看一下宿主机的网络接口:
$ ip addr show
... ...
9: lab-br1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 52:ba:e2:62:01:1e brd ff:ff:ff:ff:ff:ff
inet 10.15.20.1/24 brd 10.15.20.255 scope global lab-br1
valid_lft forever preferred_lft forever
21: vethd5747d99@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master lab-br1 state UP group default
link/ether 2e:42:d8:e3:c9:11 brd ff:ff:ff:ff:ff:ff link-netnsid 2
... ...
可以看到,输出结果里和原来相比多了lab-br1和veth pair在宿主机一端的接口vethd5747d99@if5。并且,CNI为lab-br1自动分配的IP地址10.15.20.1
,正好位于我们指定的网段10.15.20.0/24
范围内。
再来看一下容器里的网络接口:
$ docker exec lab-web ip addr show
... ...
5: eth0@if21: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 26:3f:a5:1f:c8:27 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.15.20.5/24 brd 10.15.20.255 scope global eth0
valid_lft forever preferred_lft forever
这个时候,和原来相比除了默认的几个网络接口以外,还多了一个eth0,并且CNI为它分配的IP地址10.15.20.5
,也在我们指定的网段内。
接下来再看一下容器的路由规则:
$ docker exec lab-web ip route
default via 10.15.20.1 dev eth0
10.15.20.0/24 dev eth0 proto kernel scope link src 10.15.20.5
可以看到,所有来自10.15.20.5
,也就是当前容器的数据包,都将直接发往10.15.20.0/24
网段。对于其他情况,则选择默认路由规则,数据包将被发往位于宿主机上的网桥。
最后,我们来验证一下从宿主机到容器的网络连通性:
$ curl http://10.15.20.5
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
... ...
</body>
</html>
通过上面的实验我们看到了,利用CNI插件为Docker容器设置网络环境和为network namespace设置网络环境没有什么差别。我们只要告诉插件,目标网络的配置信息,以及容器的Id和对应network namespace的路径,剩下的事情就可以交给插件自己来完成了。
关于rkt
rkt是2014年底由CoreOS推出的。作为Docker容器技术的竞争者之一,它的主要特点包括:
- 以pod而非容器为基本执行单元,也就是所谓的“pod-native”。这里pod的概念和Kubernetes里的pod是类似的,即:在一个共享的上下文里执行的一个或多个容器化应用,这也是它和Docker的主要区别之一;
- rkt和Docker相比的另一个显著不同在于,它不像Docker那样有一个集中的Daemon来负责所有容器的生命周期管理,这样可以更好的和Linux的一些所谓的init系统相兼容,比如像:
systemd
,upstart
等; - 与Docker不同,rkt是直接原生支持CNI的,因此它不用像Docker那样,需要在启动容器时,通过特殊参数禁掉默认的网络设置逻辑以后,再手工执行CNI插件;
- rkt还兼容Docker的镜像格式,它可以直接从Docker的注册表里抓取镜像并运行容器;
如果说本文前半部分用Docker来演示CNI的使用看起来有点“不太正规”,那么后半部分作为原生支持CNI的rkt,通过它来演示CNI应该会更具有代表性,也更符合实际情况。下面我们就来看一下,rkt是怎么和CNI结合在一起工作的。
rkt结合CNI
关于实验环境
由于rkt是和Docker一样的容器运行环境,因此针对rkt的实验,就不太适合在morningspace/lab-dind
这样的Docker容器里进行了。对于这一部分实验,我是在本地MacOS上的一个虚机环境里进行的,我所使用的Linux发行版本是debian/jessie64。它是跑在VirtualBox上,并通过Vagrant来管理的。关于如何在MacOS上利用VirtualBox和Vagrant来跑Debian,因为不是本文关注的焦点,所以这里就不展开了。
笔者还真的尝试过在基于CentOS的Docker容器里跑rkt,而且貌似差不多要成功了。不过后来觉得太tricky了,加上时间关系,最后还是选择了放弃。过程中遇到的“坑”主要是:在容器里运行overlay文件系统的冲突,以及privileged权限的缺失问题。
安装rkt
在Debian上安装rkt很简单,rkt的发布版本包含了.deb的安装包,我们只要在它的GitHub库里选择希望安装的版本,然后把它下载到本地进行安装就可以了,比如:
$ wget https://github.com/rkt/rkt/releases/download/v1.30.0/rkt_1.30.0-1_amd64.deb
$ wget https://github.com/rkt/rkt/releases/download/v1.30.0/rkt_1.30.0-1_amd64.deb.asc
$ gpg --keyserver keys.gnupg.net --recv-key 18AD5014C99EF7E3BA5F6CE950BDD3E0FC8A365E
$ gpg --verify rkt_1.30.0-1_amd64.deb.asc
$ sudo dpkg -i rkt_1.30.0-1_amd64.deb
当安装完毕以后,可以执行下面的命令来验证安装是否成功:
$ rkt version
rkt Version: 1.30.0
appc Version: 0.8.11
Go Version: go1.8.3
Go OS/Arch: linux/amd64
Features: -TPM +SDJOURNAL
配置CNI插件
rkt是借助CNI来配置容器的网络接口的。因此,我们也需要为CNI插件提供相应的配置文件。由于rkt原生支持CNI,所以我们不需要像Docker那样手工执行CNI插件,而是把配置文件放到特定目录下,默认位于/etc/rkt/net.d
,rkt会在容器启动的时候自动扫描这个目录下的配置文件。
为方便后面的操作,我们先把当前用户切换到root,并创建/etc/rkt/net.d
目录:
$ su
Password:
$ mkdir /etc/rkt/net.d
然后在该目录下创建配置文件:
$ cd /etc/rkt/net.d
$ cat > lab-rkt-br.conf <<"EOF"
{
"cniVersion": "0.4.0",
"name": "lab-rkt-br",
"type": "bridge",
"bridge": "lab-rkt-br",
"isGateway": true,
"ipMasq": true,
"ipam": {
"type": "host-local",
"subnet": "10.15.20.0/16",
"routes": [
{ "dst": "0.0.0.0/0" }
]
}
}
EOF
启动容器
执行下面的命令启动一个容器:
$ rkt run --insecure-options=image --interactive --net=lab-rkt-br docker://busybox
Downloading sha256:8e674ad76dc [===============================] 763 KB / 763 KB
run: disabling overlay support: "overlay entry not present in /proc/filesystems"
/ #
其中,--net
参数告诉rkt,容器在启动时要加入的网络;--interactive
参数告诉rkt,容器在启动以后进入交互模式;另外,我们选择了位于Docker Hub上的Docker镜像busybox。为此,我们需要在镜像名前面加上docker://
。同时,因为Docker镜像不像rkt那样支持签名验证(signature verification),所以还要加上--insecure-options=image
参数。
验证网络
因为执行完rkt run
以后,就进入到busybox容器里面了,所以我们可以在容器里执行ip
命令,查看网络接口的变化情况:
/ # ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
3: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue
link/ether b2:e1:43:77:64:85 brd ff:ff:ff:ff:ff:ff
inet 10.15.20.2/16 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::b0e1:43ff:fe77:6485/64 scope link
valid_lft forever preferred_lft forever
可以看到,这里的eth0对应的就是veth pair在容器内的那一端,它的IP地址是10.15.20.2
,位于我们之前定义的网段10.15.20.0/16
范围内。
紧接着,我们再通过按键Ctrl + ]]]
,即:按住Ctrl
的同时连续按三次]
,从容器的交互模式里退出并回到宿主机。然后进入/var/lib/cni/networks
目录,也就是我们的host-local
插件保存网络配置信息的地方:
$ cd /var/lib/cni/networks/
$ ls
lab-rkt-br
$ cd lab-rkt-br/
$ ls
10.15.20.2
$ cat 10.15.20.2
0310ac0a-08f8-4d84-8b65-1669bb9c5f19
可以看到,networks
目录下的子目录名就是我们之前定义的网络名lab-rkt-br
。这个目录下包含的文件10.15.20.2
,其名称就是我们为容器分配的IP地址,文件内容则是容器的Id。
我们可以通过执行rkt list --full
查看当前运行的所有容器,并返回完整的容器Id:
$ rkt list --full
UUID APP IMAGE NAME IMAGE ID STATE CREATED STARTED NETWORKS
0310ac0a-08f8-4d84-8b65-1669bb9c5f19 busybox registry-1.docker.io/library/busybox:latest sha512-bea5a3990a66 running 2019-06-17 12:49:40.58 +0000 GMT 2019-06-17 12:49:40.784 +0000 GMT lab-rkt-br:ip4=10.15.20.2
在UUID一栏所显示的值,正是我们在10.15.20.2
文件里看到的值。
留下评论
您的电子邮箱地址并不会被展示。请填写标记为必须的字段。 *