1.CNI简介
CNI是“容器网络接口”的简称,其目标是规范容器运行时引擎和网络实现之间的接口。它是一种最简化的方式,用于将容器连接到一个网络。
CNI项目提供了三样东西:
- CNI规范:这是一个标准化的文档,详细描述了容器和网络之间的通信应该如何进行。
- Golang库:这是一个库,它提供了CNI规范的一个实现,允许开发人员使用Golang来实现CNI规范。
- 插件:这是一系列的参考实现,用于满足各种用例的需求,它们按照CNI规范创建,以满足不同的网络需求。
讨论CNI时,首先要知道一些信息:
- 一个实现CNI标准的插件是二进制文件,而不是守护程序。它在运行时应始终至少具有CAP_NET_ADMIN能力。
- 网络定义或网络配置存储为json文件。这些json文件通过stdin传送给插件。
- 任何只有在要创建容器时才知道的信息(运行时变量)都应通过环境变量传递给插件。尽管如此,在最新的CNI中,还可以通过stdin上的json发送某些运行时配置,特别是对于一些扩展和可选功能。更多信息在这里。
- 二进制文件不应该有除上述两点之外的任何其他输入配置。 CNI插件负责连接容器,并有望隐藏网络复杂性。
- 对于CNI插件来说,容器与Linux网络命名空间是一样的。
在一个OCI/CNI兼容版本中,容器运行时[引擎]是一个守护进程,它位于容器调度器和实际创建容器的二进制文件之间。这个守护进程不一定需要以root用户身份运行。它会监听调度器的请求。它不会接触内核,因为它通过容器标准使用外部二进制文件来实际创建或删除一个容器。
比如在Kubernetes的情况下,容器运行时可以是cri-o(或cri-containerd),它通过CRI接口监听来自kubelet的请求,kubelet是位于每个节点中的调度器代理。Kubelet指示容器运行时启动一个容器,运行时通过以标准方式调用runc(实现OCI-runtime规范的二进制文件)和flannel(实现CNI规范的二进制文件)来执行。
为了创建一个容器,容器运行时需要做以下几件事情:
- 创建rootfs文件系统:这一步意味着将为容器创建一个基本的文件系统,它通常包含了运行容器所需的所有文件和目录。
- 创建容器:这是一个由一系列在命名空间中隔离运行的进程组成的实体,它们受到cgroups的限制,确保资源的分配和限制。
- 将容器连接到网络:通过使用特定的网络技术和配置,来确保容器可以与其他容器和/或外部网络通信。
- 启动用户进程(entrypoint):最后一步是启动容器中的主进程,它通常是用户指定的一个应用或服务。
在网络部分,重要的是容器运行时会要求OCI-Runtime二进制文件将容器进程置于一个新的网络命名空间(net ns X)中。接下来,容器运行时将使用新的命名空间作为运行时环境变量来调用CNI插件。CNI插件应该具有进行网络配置所需的所有信息。
3.容器运行时怎样运行CNI
我们将通过一个例子来说明运行时如何使用CNI通过bridge插件将一个容器连接到一个桥接器。我们将使用简单的bash命令“模拟”运行时的操作。
准备阶段
在运行时可以启动一个容器之前,它需要一些服务器的准备工作。使用哪个工具(例如bosh,ansible,手动脚本)都无关紧要。它只需确保所需的二进制文件就绪。在我们简单的案例中,我们需要OCI-runtime二进制文件(runc)和CNI插件二进制文件(bridge,host-local)。
我们可以从仓库下载预构建的二进制文件,或者我们可以从源代码构建二进制文件。
# as user
go get github.com/opencontainers/runc
go get github.com/containernetworking/plugins
cd $GOPATH/src/github.com/containernetworking/plugins
./build.sh
sudo mkdir -p /opt/cni/{bin,netconfs}
sudo cp bin/* /opt/cni/bin/
which /opt/cni/bin/{bridge,host-local} runc
在准备阶段,我们创建了一个桥接器,容器将被连接到这个桥接器上。
# as root
ip link add name br0 type bridge
ip addr add 10.10.10.1/24 dev br0
ip link set dev br0 up
这一步可能并非必需,因为bridge插件可以创建bridge,但原则上,设置网络介质不是CNI插件的任务。
最后,我们应该将网络配置添加到文件系统的某个位置。
# as root
export NETCONFPATH=/opt/cni/netconfs
cat > $NETCONFPATH/10-mynet.conf <<EOF
{
"cniVersion": "0.2.0",
"name": "mynet",
"type": "bridge",
"bridge": "br0",
"isGateway": true,
"ipMasq": true,
"ipam": {
"type": "host-local",
"subnet": "10.10.10.0/24",
"routes": [
{ "dst": "0.0.0.0/0" }
],
"dataDir": "/run/ipam-state"
},
"dns": {
"nameservers": [ "8.8.8.8" ]
}
}
EOF
运行时阶段
容器编排器最终会指示容器运行时启动一个容器。
运行时将执行以下简化步骤:
# Step 1: creates the rootfs directory
mkdir bundle && cd bundle/
mkdir -p rootfs && docker export $(docker create busybox) | tar -C rootfs -xvf -
# Step 2:
# a. creates the OCI runtime config
runc spec --rootless
# b. modifies the OCI runtime config accordingly
# asks for a new network namespace, other changes e.g. tty
# cat config.json |grep -A20 namespace
# "namespaces": [
# {
# "type": "pid"
# },
# {
# "type": "ipc"
# },
# {
# "type": "uts"
# },
# {
# "type": "mount"
# },
# {
# "type": "user"
# },
# {
# "type": "network" <--adds network namespace
# }
# ],
# "maskedPaths": [
#
# c. creates the container
runc create cake
# $ runc list
# ID PID STATUS BUNDLE CREATED OWNER
# cake 13076 created /home/vagrant/bundle 2018-01-22T13:09:40.326479248Z vagrant
runc exec -t -c "CAP_NET_RAW" cake sh # To enter the container
容器运行时需要获取新创建的网络命名空间。在runc的情况下,这可以通过state.json文件来完成。
# as root
ns=$(cat /var/run/user/1000/runc/cake/state.json | jq '.namespace_paths.NEWNET' -r)
mkdir -p /var/run/netns
ln -sf $ns /var/run/netns/cake
ip netns
# cat /var/run/user/1000/runc/cake/state.json | jq '.namespace_paths'
# {
# "NEWIPC": "/proc/13076/ns/ipc",
# "NEWNET": "/proc/13076/ns/net",
# "NEWNS": "/proc/13076/ns/mnt",
# "NEWPID": "/proc/13076/ns/pid",
# "NEWUSER": "/proc/13076/ns/user",
# "NEWUTS": "/proc/13076/ns/uts"
# }
# $ ps -aux |grep 13076
# vagrant 13076 0.0 0.3 58016 6848 ? Ssl 13:09 0:00 runc init
# # the `runc init` is the process that starts when `runc create` command is
# # executed and which keeps the namespaces alive
#
接着,它将设置bash环境变量(CNI_*),其中包含如网络命名空间之类的运行时信息。
export NETCONFPATH=/opt/cni/netconfs
export CNI_PATH=/opt/cni/bin/
export CNI_CONTAINERID=cake
export CNI_NETNS=/var/run/netns/cake
export CNI_IFNAME=eth0
export CNI_COMMAND=ADD
最后,它将调用CNI二进制文件,通过stdin提供配置和上述变量。运行时将以json格式获取返回的结果。
cat $NETCONFPATH/10-mynet.conf | $CNI_PATH/bridge
# $cat $NETCONFPATH/10-mynet.conf | $CNI_PATH/bridge
# {
# "cniVersion": "0.2.0",
# "ip4": {
# "ip": "10.10.10.2/24",
# "gateway": "10.10.10.1",
# "routes": [
# {
# "dst": "0.0.0.0/0",
# "gw": "10.10.10.1"
# }
# ]
# },
# "dns": {
# "nameservers": [
# "8.8.8.8"
# ]
# }
# }
#
我们可以通过在容器网络空间内运行ip命令来观察到接口已被正确设置。
# $ ip netns exec cake ip a s eth0
# 3: eth0@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
# link/ether d6:d6:48:92:b3:25 brd ff:ff:ff:ff:ff:ff link-netnsid 0
# inet 10.10.10.2/24 scope global eth0
# valid_lft forever preferred_lft forever
# inet6 fe80::d4d6:48ff:fe92:b325/64 scope link
# valid_lft forever preferred_lft forever
# $ ip netns exec cake ip route
# default via 10.10.10.1 dev eth0
# 10.10.10.0/24 dev eth0 proto kernel scope link src 10.10.10.2
#
从结果输出来看,有一点令人困惑的是DNS结果条目。CNI插件实际上并没有应用DNS条目(也就是说,没有写入/etc/resolv.conf文件)。插件只是返回该值,而容器运行时则需要应用DNS服务器。
删除的工作方式很简单,只需更改动作CNI_COMMAND。
export CNI_COMMAND=DEL
cat $NETCONFPATH/10-mynet.conf | $CNI_PATH/bridge
# no output expected when success