为了方便表述简化了很多细节, 比如 CNI 命令行接口的详细参数和 Kubernetes 网络模型的准确翻译, 等, 主要是为了让大家能把握脉络.
CNI, 本质上就是一个命令行工具, 要实现这样的命令行接口:
zc-cni [add|del] $CONTAINER_ID $NETNS_PATH
输入容器 ID 和它对应的 network namespace path, 然后 CNI 完成 Kubernetes 网络模型:
- 多节点上的容器可以和任意容器互通
- 节点本身可以和自己节点上的容器互通
在众多 CNI 方案里由于 Calico 的清新简约和不拘一格, 很快就深入人心打成一片, 顺理成章成为了虾皮基础设施之一.
Calico 用人话来说, 完成了这么几种任务:
- 容器集群的网络配置(Kubernetes, OpenShift)
- 虚拟机集群的网络配置(OpenStack)
non-cluster hosts
, 物理机之间的网络配置
当然说到配置
, 至少是包含跨节点通讯和 ACL 权限控制两方面, Calico 对跨节点问题使用 BGP 路由方案实现了纯三层的方案, 对权限控制使用 iptables.
相信大家对 CNI 和 Calico 都已经有了足够的了解了, 让我们开始写一个 Calico CNI 吧.
那么先搭建环境, 我们快刀斩乱码, 列一下基本过程和比较关键的配置.
跑一个 calico-node
容器, 会用 $HOSTNAME
注册 calico node, 要注意必须是小写名字.
calicoctl node run --node-image="calico/node:release-v3.4" --disable-docker-networking
然后搞一个 IPPool, whose CIDR is 10.1.0.0/16
.
cat <<! | calicoctl create -f -
- apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
name: zcpool
spec:
cidr: 10.1.0.0/16
!
当然 profile 也是很重要的!
cat <<! | calicoctl create -f -
apiVersion: projectcalico.org/v3
kind: Profile
metadata:
name: zcpool
spec:
egress:
- action: Allow
destination: {}
source: {}
ingress:
- action: Allow
destination: {}
source: {}
!
然后配一下 Calico CNI 的配置文件, ipam 部分要指定刚才创建的 ippool:
cat <<! > /etc/cni/net.d/10-calico.conf
{
"name": "zc",
"cniVersion": "0.3.1",
"type": "calico",
"etcd_endpoints": "http://127.0.0.1:2379",
"ipam": {
"type": "calico-ipam",
"ipv4_pools": ["zcpool"]
},
}
!
好了, 那么开始吧.
先实现 IPAM 的部分, 由于这部分没有简单可用的命令行工具, 我们只能自己写 Go:
完整代码在 https://github.com/jschwinger23/zc-ipam/blob/master/main.go, 我们这里只看最核心的部分:
poolsClient := client.IPPools()
ipPool, err := poolsClient.Get(context.Background(), poolName, options.GetOptions{})
_, ipNet, err := caliconet.ParseCIDR(ipPool.Spec.CIDR)
poolV4 = []caliconet.IPNet{caliconet.IPNet{IPNet: ipNet.IPNet}}
IPs, _, err := client.IPAM().AutoAssign(
context.Background(),
calicoipam.AutoAssignArgs{
Num4: 1,
Hostname: hostname,
IPv4Pools: poolV4,
},
)
总之调用 libcalico-go/lib/ipam
提供的 IPAM().AutoAssign()
就完事了.
最终我们通过命令行能够调用 zc-ipam
:
# ETCD_ENDPOINTS=http://127.0.0.1:2379 ./zc-ipam addr request zcpool
10.1.0.2
这部分可以直接用 bash script 调用 iproute2(8)
来实现
#! /bin/bash
# /usr/local/bin/set-veth.sh
NS_PATH=$1
IP=$2
ns=$(head /dev/urandom | tr -dc a-z0-9 | head -c 8)
veth=v-$ns
ln -s $NS_PATH /run/netns/$ns
ip l add $veth type veth peer name $veth-peer
ip l s $veth-peer up
ip l s $veth netns $ns name eth0
ip netns exec $ns ip l s eth0 up
ip netns exec $ns ip a a $IP/32 dev eth0
ip netns exec $ns ip r a default dev eth0
echo $ns
使用方式是
# set-veth.sh $NETNS_PATH $IPV4
o4z9s9lk
这样就打通了 netns 和 host 之间的三层, 为之后的跨节点通讯做好了准备.
我们没有设置 veth peer 与 host 之间的路由, 因为这部分工作是 calico-felix 完成的, 我们接下来来做这部分的活.
calico-felix 的工作模式是 watch WorkloadEndpoint (WEP), 对每一个新创建的 WEP 都进行内核路由表和 iptables 编程; WEP 本质上对应一个拥有 IPAM 分出去的 IP 的虚拟环境(容器/虚拟机).
#! /bin/bash
# /usr/local/bin/set-wep.sh
CONTAINER_ID=$1
NS=$2
IP=$3
POOL=$4
VETH=v-$NS
MAC=$(ip l sh v-$NS-peer | grep -Po '(?<=link/ether )\w{2}(:\w{2}){5}')
cat <<! | calicoctl create -f -
apiVersion: projectcalico.org/v3
kind: WorkloadEndpoint
metadata:
labels:
projectcalico.org/namespace: default
projectcalico.org/orchestrator: zc
name: localhost-zc-$NS-$NS
namespace: default
spec:
containerID: $CONT_ID
workload: $NS
endpoint: $NS
interfaceName: $VETH-peer
ipNetworks:
- $IP/32
mac: $MAC
node: $HOSTNAME
orchestrator: zc
profiles:
- $POOL
!
用法
set-wep.sh $CONT_ID $NS $IP $IPPOOL
然后 calico-felix 能完成剩下的工作.
最终我们的 zc-cni
很简单:
#! /bin/bash
# /usr/local/bin/zc-cni.sh
container_id=$1
netns_path=$2
ippool=$(cat /etc/cni/net.d/10-calico.conf | jq -r '.ipam.ipv4_pool[0]')
export ETCD_ENDPOINTS=$(cat /etc/cni/net.d/10-calico.conf | jq -r '.etcd_endpoints')
ipv4=$(zc-ipam addr request $ippool)
ns=$(set-veth.sh $netns_path $ipv4)
set-wep.sh $container_id $ns $ipv4 $ippool
那么来使用一下:
#! /bin/bash
# docker-run-cni.bash
cont_id=$(docker run -d --net=none busybox:latest /bin/sleep 10000000)
pid=$(docker inspect -f '{{ .State.Pid }}' $cont_id)
netns_path=/proc/$pid/ns/net
zc-cni.sh add $cont_id $netns_path
docker run --net=container:$cont_id ip a
这里创建了两个容器, 第一个 busybox 容器其实熟悉 Kubernetes 的小朋友应该能意识到这就是 pause 容器, Kubernetes Pod 模型的第一个容器; pause 容器提供了 netns, 然后 calico CNI 把 netns 加入到 calico 网络, 最后创建第二个容器 attach 都 pause 容器的 netns 上, 完成.