Write a simple CNI plugin from scratch.
在编写自己的 CNI 插件前,我们需要先了解一下 CNI Spec。
规范规定,CNI v0.4.0 必须实现的接口有 ADD
,DEL
,CHECK
,VERSION
四个。不过,对于学习目的来说,我们主要关注 ADD
接口即可。
ADD
:将容器添加到网络中。- 参数:
- 容器 ID。需要是唯一的非空字符串,由运行时生成。
- Network Namespace 路径。如
/proc/[pid]/ns/net
。 - 网络配置。
- 额外参数。
- 容器内的设备名。
- 返回值:
- 设备列表。
- IP 列表。
- DNS 信息。
- 参数:
在调用 CNI 插件时,运行时会设置如下环境变量:
CNI_COMMAND
:需要执行的命令,如ADD
。CNI_CONTAINERID
: 容器 ID。CNI_NETNS
: Network Namespace 路径。CNI_IFNAME
:设备名。CNI_ARGS
: 额外参数。CNI_PATH
: CNI 插件的 PATH 列表。
我们主要关注前四个参数。
使用双 KVM 节点的 Minikube 作为实验环境。
minikube start -n 2 --driver kvm2 --kubernetes-version v1.20.0 --memory 2000
在 minikube 启动后,默认是使用 Kindnet 作为 CNI 插件,可在 kube-system
中看到。
检查节点的 Pod CIDR:
kubectl describe nodes | grep CIDR
以 master 节点为 10.244.0.0/24
,worker 节点为 10.244.1.0/24
为例。
先来几个工具函数。
MINIKUBE_HOME=$HOME/.minikube
profile() {
MINIKUBE_PROFILE=${1}
IP=$(jq -r ".Driver.IPAddress" $MINIKUBE_HOME/machines/$MINIKUBE_PROFILE/config.json)
SSH_OPT="-o PasswordAuthentication=no -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o LogLevel=quiet -i $MINIKUBE_HOME/machines/$MINIKUBE_PROFILE/id_rsa"
SSH="ssh $SSH_OPT docker@$IP"
SCP="scp $SSH_OPT"
}
函数 profile
可以设置 SSH
和 SCP
两个环境变量,方便后续脚本编写。
首先,我们需要为每个节点创建一个与容器网络连接的网桥 cni0
,占据 .1
的 IP。
profile "minikube"
$SSH sudo brctl addbr cni0
$SSH sudo ip link set cni0 up
$SSH sudo ip addr add 10.244.0.1/24 dev cni0
profile "minikube-m02"
$SSH sudo brctl addbr cni0
$SSH sudo ip link set cni0 up
$SSH sudo ip addr add 10.244.1.1/24 dev cni0
由于两个 VM 在一个子网下,此时在任意一个 VM 里应该都能 ping 通另一个 VM 的 cni0
。
然后就可以开始编写 CNI 插件了。
#!/bin/bash -e
exec 3>&1
exec &>> /var/log/bash-cni-plugin.log
echo "CNI command: $CNI_COMMAND"
stdin=`cat /dev/stdin`
echo "stdin: $stdin"
到这一步为止,将日志打到 /var/log/bash-cni-plugin.log
中。
考虑 IP 分配的问题。我们简单地假设 netmask 为 24,将已分配的 IP 记录到一个文件中,每次分配时读取一次这个文件,将其加一。
IP_STORE=/tmp/reserved_ips
allocate_ip() {
if [[ ! -f $IP_STORE ]]; then
echo "1" > $IP_STORE # 1 被 cni0 占据
fi
local LAST_USED=$(cat $IP_STORE)
local NEXT=$(($LAST_USED + 1))
echo $NEXT > $IP_STORE
echo $NEXT
return
}
get_ip_from_subnet() {
local SUBNET=${1}
local D=${2}
local IP_PREFIX=$(echo "$SUBNET" | cut -d. -f1-3)
echo "${IP_PREFIX}.$D"
}
由于在这个简单实现中,不考虑 DEL
命令,因此使用简单的递增实现。
在此,先跳过 DEL
和 CHECK
命令,并实现 VERSION
。
case $CNI_COMMAND in
ADD)
# TODO
;;
DEL)
exit 0
;;
CHECK)
exit 0
;;
VERSION)
echo '{
"cniVersion": "0.4.0",
"supportedVersions": [ "0.3.0", "0.3.1", "0.4.0" ]
}' >&3
;;
*)
echo "Unknown command: $CNI_COMMAND"
exit 1
;;
esac
考虑 ADD
实现。
首先提取子网和掩码,并分配容器 IP:
subnet=$(echo "$stdin" | jq -r ".subnet")
subnet_mask_size=$(echo $subnet | awk -F "/" '{print $2}')
gateway_ip=$(get_ip_from_subnet $subnet 1)
container_ip=$(get_ip_from_subnet $subnet $(allocate_ip))
随机生成宿主机(VM)的设备名:
rand=$(tr -dc 'A-F0-9' < /dev/urandom | head -c4)
host_if_name="bashveth$rand"
绑定 network namespace:
mkdir -p /var/run/netns/
ln -sfT $CNI_NETNS /var/run/netns/$CNI_CONTAINERID
为容器网络创建网络设备:
ip -n $CNI_CONTAINERID link add $CNI_IFNAME type veth peer name $host_if_name netns 1
ip -n $CNI_CONTAINERID link set $CNI_IFNAME up
ip -n $CNI_CONTAINERID addr add $container_ip/$subnet_mask_size dev $CNI_IFNAME
ip link set $host_if_name up
ip link set $host_if_name master cni0
创建设备时,需要注意使用的是 ip -n $CNI_CONTAINERID link add
,而非 ip link add $CNI_IFNAME netns $CNI_CONTAINERID
。
从 man page 中可以得知,
netns NETNSNAME | PID move the device to the network namespace associated with name NETNSNAME or process PID.
参数 netns
是将设备“移动”到目标 netns,这意味着它首先是在当前 netns 中创建设备,这可能存在潜在的设备冲突。
为容器网络创建默认路由:
ip -n $CNI_CONTAINERID route add default via $gateway_ip dev $CNI_IFNAME
获取 Mac 地址:
mac=$(ip -n $CNI_CONTAINERID link show $CNI_IFNAME | awk '/ether/ {print $2}')
至此,容器网络即设置完毕。
输出结果:
echo "{
\"cniVersion\": \"0.4.0\",
\"interfaces\": [
{
\"name\": \"$CNI_IFNAME\",
\"mac\": \"$mac\",
\"sandbox\": \"$CNI_NETNS\"
}
],
\"ips\": [
{
\"version\": \"4\",
\"address\": \"$container_ip/$subnet_mask_size\",
\"gateway\": \"$gateway_ip\",
\"interface\": 0
}
]
}" >&3
将该脚本命名为 bcni.sh
,将其转移到 Minikube VM 中。
setup_executable() {
$SCP ./bcni.sh docker@$IP:/home/docker/bcni
$SSH sudo mv /home/docker/bcni /opt/cni/bin/bcni
$SSH sudo chmod +x /opt/cni/bin/bcni
$SSH sudo chown root:root /opt/cni/bin/bcni
}
profile "minikube"
setup_executable
profile "minikube-m02"
setup_executable
此外,还需要配置 cni conf,以 master 为例:
{
"cniVersion": "0.4.0",
"name": "bcni",
"type": "bcni",
"network": "10.244.0.0/16",
"subnet": "10.244.0.0/24"
}
需要注意 type
字段需与可执行文件名相同。
将该配置文件保存到 /etc/cni/net.d/
路径下,并使用一个优先级最高的文件名即可。
至此,CNI 插件已经能够正确配置容器网络,不同 VM 节点上的 Pod 已经可以正常通信。
使用如下配置可进行简单的测试。
YAML
apiVersion: v1
kind: Pod
metadata:
name: nginx-master
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: Never
ports:
- containerPort: 80
nodeSelector:
kubernetes.io/hostname: minikube
---
apiVersion: v1
kind: Pod
metadata:
name: bash-master
spec:
containers:
- name: ubuntu
image: smatyukevich/ubuntu-net-utils
imagePullPolicy: Never
command:
- "bin/bash"
- "-c"
- "sleep 10000"
nodeSelector:
kubernetes.io/hostname: minikube
---
apiVersion: v1
kind: Pod
metadata:
name: nginx-worker
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: Never
ports:
- containerPort: 80
nodeSelector:
kubernetes.io/hostname: minikube-m02
---
apiVersion: v1
kind: Pod
metadata:
name: bash-worker
spec:
containers:
- name: ubuntu
image: smatyukevich/ubuntu-net-utils
imagePullPolicy: Never
command:
- "bin/bash"
- "-c"
- "sleep 10000"
nodeSelector:
kubernetes.io/hostname: minikube-m02
需要注意,因为在启动 Minikube 时,没有指定 --cni=false
,因此一些其他的配置,例如 NAT 与 VM 的路由规则,都是已经设置好的。