Skip to content

Instantly share code, notes, and snippets.

@jschwinger233
Last active May 31, 2020 14:35
Show Gist options
  • Save jschwinger233/b1b5b3ff474074f446698a02cfe32f5e to your computer and use it in GitHub Desktop.
Save jschwinger233/b1b5b3ff474074f446698a02cfe32f5e to your computer and use it in GitHub Desktop.

自己动手实现 Calico CNI

为了方便表述简化了很多细节, 比如 CNI 命令行接口的详细参数和 Kubernetes 网络模型的准确翻译, 等, 主要是为了让大家能把握脉络.

一. CNI

CNI, 本质上就是一个命令行工具, 要实现这样的命令行接口:

zc-cni [add|del] $CONTAINER_ID $NETNS_PATH

输入容器 ID 和它对应的 network namespace path, 然后 CNI 完成 Kubernetes 网络模型:

  1. 多节点上的容器可以和任意容器互通
  2. 节点本身可以和自己节点上的容器互通

在众多 CNI 方案里由于 Calico 的清新简约和不拘一格, 很快就深入人心打成一片, 顺理成章成为了虾皮基础设施之一.

二. Calico

Calico 用人话来说, 完成了这么几种任务:

  1. 容器集群的网络配置(Kubernetes, OpenShift)
  2. 虚拟机集群的网络配置(OpenStack)
  3. non-cluster hosts, 物理机之间的网络配置

当然说到配置, 至少是包含跨节点通讯和 ACL 权限控制两方面, Calico 对跨节点问题使用 BGP 路由方案实现了纯三层的方案, 对权限控制使用 iptables.

相信大家对 CNI 和 Calico 都已经有了足够的了解了, 让我们开始写一个 Calico CNI 吧.

三. Calico 基本环境和配置

那么先搭建环境, 我们快刀斩乱码, 列一下基本过程和比较关键的配置.

跑一个 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

先实现 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

五. 设置 veth 和路由

这部分可以直接用 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 WEP

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 能完成剩下的工作.

七. 完成 Calico CNI

最终我们的 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 上, 完成.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment