Kubernetes 要求所有的网络插件实现必须满足如下要求:
先看下Linux网络名词:
Docker容器网络示意图如下:
问题:Pod是K8S最小调度单元,一个Pod由一个容器或多个容器组成,当多个容器时,怎么都用这一个Pod IP?
实现:k8s会在每个Pod里先启动一个infra container小容器,然后让其他的容器连接进来这个网络命名空间,然后其他容器看到的网络试图就完全一样了。即网络设备、IP地址、Mac地址等。这就是解决网络共享的一种解法。在Pod的IP地址就是infra container的IP地址。
在 Kubernetes 中,每一个 Pod 都有一个真实的 IP 地址,并且每一个 Pod 都可以使用此 IP 地址与 其他 Pod 通信。
Pod之间通信会有两种情况:
先看下第一种情况:两个Pod在同一个Node上
同节点Pod之间通信道理与Docker网络一样的,如下图:
再看下第二种情况:两个Pod在不同Node上
K8S网络模型要求Pod IP在整个网络中都可访问,这种需求是由第三方网络组件实现。
CNI(Container Network Interface,容器网络接口):是一个容器网络规范,Kubernetes网络采用的就是这个CNI规范,CNI实现依赖两种插件,一种CNI Plugin是负责容器连接到主机,另一种是IPAM负责配置容器网络命名空间的网络。
CNI插件默认路径:
# ls /opt/cni/bin/
地址:https://github.com/containernetworking/cni
当你在宿主机上部署Flanneld后,flanneld 启动后会在每台宿主机上生成它对应的CNI 配置文件(它其实是一个 ConfigMap),从而告诉Kubernetes,这个集群要使用 Flannel 作为容器网络方案。
CNI配置文件路径:
/etc/cni/net.d/10-flannel.conflist
当 kubelet 组件需要创建 Pod 的时候,先调用dockershim它先创建一个 Infra 容器。然后调用 CNI 插件为 Infra 容器配置网络。
这两个路径在kubelet启动参数中定义:
--network-plugin=cni \
--cni-conf-dir=/etc/cni/net.d \
--cni-bin-dir=/opt/cni/bin
Flannel是CoreOS维护的一个网络组件,Flannel为每个Pod提供全局唯一的IP,Flannel使用ETCD来存储Pod子网与Node IP之间的关系。flanneld守护进程在每台主机上运行,并负责维护ETCD信息和路由数据包。
https://github.com/coreos/flannel
kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
Flannel支持多种数据转发方式:
# kubeadm部署指定Pod网段
kubeadm init --pod-network-cidr=10.244.0.0/16
# 二进制部署指定
cat /opt/kubernetes/cfg/kube-controller-manager.conf
--allocate-node-cidrs=true \
--cluster-cidr=10.244.0.0/16 \
#配置文件 kube-flannel.yml
net-conf.json: |
{
"Network": "10.244.0.0/16",
"Backend": {
"Type": "vxlan"
}
}
为了能够在二层网络上打通“隧道”,VXLAN 会在宿主机上设置一个特殊的网络设备作为“隧道”的两端。这个设备就叫作 VTEP,即:VXLAN Tunnel End Point(虚拟隧道端点)。
如果Pod 1访问Pod 2,源地址10.244.2.250,目的地址10.244.1.33 ,数据包传输流程如下:
1、容器路由:容器根据路由表从eth0发出
# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
busybox-6cd57fd969-4n6fn 1/1 Running 0 61s 10.244.2.50 k8s-master1 <none> <none>
web-5675686b8-rc9bf 1/1 Running 0 7m12s 10.244.1.33 k8s-node01 <none> <none>
web-5675686b8-rrc6f 1/1 Running 0 7m12s 10.244.2.49 k8s-master1 <none> <none>
# kubectl exec -it busybox-6cd57fd969-4n6fn sh
/ # ip route
default via 10.244.2.1 dev eth0
10.244.0.0/16 via 10.244.2.1 dev eth0
10.244.2.0/24 dev eth0 scope link src 10.244.2.51
2、主机路由:数据包进入到宿主机虚拟网卡cni0,根据路由表转发到flannel.1虚拟网卡,也就是,来到了隧道的入口。
# ip route
default via 192.168.0.1 dev ens32 proto static metric 100
10.244.0.0/24 via 10.244.0.0 dev flannel.1 onlink
10.244.1.0/24 via 10.244.1.0 dev flannel.1 onlink
3、VXLAN封装:而这些VTEP设备(二层)之间组成二层网络必须要知道目的MAC地址。这个MAC地址从哪获取到呢?其实在flanneld进程启动后,就会自动添加其他节点ARP记录,可以通过ip命令查看,如下所示:
# ip neigh show dev flannel.1
10.244.0.0 lladdr 06:6a:9d:15:ac:e0 PERMANENT
10.244.1.0 lladdr 36:68:64:fb:4f:9a PERMANENT
4、二次封包:知道了目的MAC地址,封装二层数据帧(容器源IP和目的IP)后,对于宿主机网络来说这个帧并没有什么实际意义。接下来,Linux内核还要把这个数据帧进一步封装成为宿主机网络的一个普通数据帧,好让它载着内部数据帧,通过宿主机的eth0网卡进行传输。
5、封装到UDP包发出去:现在能直接发UDP包嘛?到目前为止,我们只知道另一端的flannel.1设备的MAC地址,却不知道对应的宿主机地址是什么。
flanneld进程也维护着一个叫做FDB的转发数据库,可以通过bridge fdb命令查看:
# bridge fdb show dev flannel.1
06:6a:9d:15:ac:e0 dst 192.168.0.134 self permanent
36:68:64:fb:4f:9a dst 192.168.0.133 self permanent
可以看到,上面用的对方flannel.1的MAC地址对应宿主机IP,也就是UDP要发往的目的地。使用这个目的IP进行封装。
6、数据包到达目的宿主机:Node1的eth0网卡发出去,发现是VXLAN数据包,把它交给flannel.1设备。flannel.1设备则会进一步拆包,取出原始二层数据帧包,发送ARP请求,经由cni0网桥转发给container。
host-gw模式相比vxlan简单了许多, 直接添加路由,将目的主机当做网关,直接路由原始封包。
# kube-flannel.yml
net-conf.json: |
{
"Network": "10.244.0.0/16",
"Backend": {
"Type": "host-gw"
}
}
当你设置flannel使用host-gw模式,flanneld会在宿主机上创建节点的路由表:
# ip route
default via 192.168.0.1 dev ens32 proto static metric 100
10.244.0.0/24 via 192.168.0.134 dev ens32
10.244.1.0/24 via 192.168.0.133 dev ens32
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
192.168.0.0/24 dev ens32 proto kernel scope link src 192.168.0.132 metric 100
目的 IP 地址属于 10.244.1.0/24 网段的 IP 包,应该经过本机的 eth0 设备发出去(即:dev eth0);并且,它下一跳地址是 192.168.0.132。一旦配置了下一跳地址,那么接下来,当 IP 包从网络层进入链路层封装成帧的时候,eth0 设备就会使用下一跳地址对应的 MAC 地址,作为该数据帧的目的 MAC 地址。
而 Node 2 的内核网络栈从二层数据帧里拿到 IP 包后,会“看到”这个 IP 包的目的 IP 地址是container-2 的 IP 地址。这时候,根据 Node 2 上的路由表,该目的地址会匹配到第二条路由规则,从而进入 cni0 网桥,进而进入到 container-2 当中。
# kubectl edit cm kube-flannel-cfg -n kube-system
net-conf.json: |
{
"Network": "10.244.0.0/16",
"Backend": {
"Type": "vxlan"
"Directrouting": true
}
}
# kubectl get cm kube-flannel-cfg -o json -n kube-system
"net-conf.json": "{\n \"Network\": \"10.244.0.0/16\",\n \"Backend\": {\n \"Type\": \"vxlan\"\n \"Directrouting\": true\n }\n}\n"
小结: 1、vxlan 不受网络环境限制,只要三层可达就行 2、vxlan 需要二层解封包,降低工作效率 3、hostgw 基于路由表转发,效率更高 4、hostgw 只适用于二层网络(本身网络架构受限,节点数量也受限)
Calico是一个纯三层的数据中心网络方案,Calico支持广泛的平台,包括Kubernetes、OpenStack等。
Calico 在每一个计算节点利用 Linux Kernel 实现了一个高效的虚拟路由器( vRouter) 来负责数据转发,而每个 vRouter 通过 BGP 协议负责把自己上运行的 workload 的路由信息向整个 Calico 网络内传播。
此外,Calico 项目还实现了 Kubernetes 网络策略,提供ACL功能。
实际上,Calico项目提供的网络解决方案,与Flannel的host-gw模式几乎一样。也就是说,Calico也是基于路由表实现容器数据包转发,但不同于Flannel使用flanneld进程来维护路由信息的做法,而Calico项目使用BGP协议来自动维护整个集群的路由信息。
BGP英文全称是Border Gateway Protocol,即边界网关协议,它是一种自治系统间的动态路由发现协议,与其他 BGP 系统交换网络可达信息。
为了能让你更清楚理解BGP,举个例子:
在这个图中,有两个自治系统(autonomous system,简称为AS):AS 1 和 AS 2。
在互联网中,一个自治系统(AS)是一个有权自主地决定在本系统中应采用何种路由协议的小型单位。这个网络单位可以是一个简单的网络也可以是一个由一个或多个普通的网络管理员来控制的网络群体,它是一个单独的可管理的网络单元(例如一所大学,一个企业或者一个公司个体)。一个自治系统有时也被称为是一个路由选择域(routing domain)。一个自治系统将会分配一个全局的唯一的16位号码,有时我们把这个号码叫做自治系统号(ASN)。
在正常情况下,自治系统之间不会有任何来往。如果两个自治系统里的主机,要通过 IP 地址直接进行通信,我们就必须使用路由器把这两个自治系统连接起来。BGP协议就是让他们互联的一种方式。
在了解了 BGP 之后,Calico 项目的架构就非常容易理解了,Calico主要由三个部分组成:
curl https://docs.projectcalico.org/v3.9/manifests/calico-etcd.yaml -o calico.yaml
下载完后还需要修改里面配置项:
具体步骤如下:
删除flannel网络
# kubectl delete -f kube-flannel.yaml
# ip link delete flannel.1
# ip link delete cni0
# ip route del 10.244.2.0/24 via 192.168.0.132 dev ens32
# ip route del 10.244.1.0/24 via 192.168.0.133 dev ens32
应用清单:
# kubectl apply -f calico.yaml
# kubectl get pods -n kube-system
下载工具:https://github.com/projectcalico/calicoctl/releases
# wget -O /usr/local/bin/calicoctl https://github.com/projectcalico/calicoctl/releases/download/v3.9.1/calicoctl
# chmod +x /usr/local/bin/calicoctl
# mkdir /etc/calico
# vim /etc/calico/calicoctl.cfg
apiVersion: projectcalico.org/v3
kind: CalicoAPIConfig
metadata:
spec:
datastoreType: "etcdv3"
etcdEndpoints: "https://192.168.0.132:2379,https://192.168.0.133:2379,https://192.168.0.134:2379"
etcdKeyFile: "/opt/etcd/ssl/server-key.pem"
etcdCertFile: "/opt/etcd/ssl/server.pem"
etcdCACertFile: "/opt/etcd/ssl/ca.pem"
使用calicoctl查看服务状态:
# calicoctl get node
NAME
k8s-master
k8s-node01
k8s-node02
# calicoctl node status
IPv4 BGP status
+---------------+-------------------+-------+----------+-------------+
| PEER ADDRESS | PEER TYPE | STATE | SINCE | INFO |
+---------------+-------------------+-------+----------+-------------+
| 192.168.0.133 | node-to-node mesh | up | 07:24:45 | Established |
| 192.168.0.134 | node-to-node mesh | up | 07:25:00 | Established |
+---------------+-------------------+-------+----------+-------------+
查看 IPAM的IP地址池:
# calicoctl get ippool -o wide
NAME CIDR NAT IPIPMODE VXLANMODE DISABLED SELECTOR
default-ipv4-ippool 10.244.0.0/16 true Never Never false all()
Pod 1 访问 Pod 2大致流程如下:
路由表:
# node1
default via 192.168.0.1 dev ens32 proto static metric 100
10.244.0.0/24 via 192.168.0.134 dev ens32
10.244.58.192/26 via 192.168.0.134 dev ens32 proto bird
10.244.85.192 dev calic32fa4483b3 scope link
blackhole 10.244.85.192/26 proto bird
10.244.235.192/26 via 192.168.0.132 dev ens32 proto bird
# node2
default via 192.168.0.1 dev ens32 proto static metric 100
10.244.58.192 dev calib29fd680177 scope link
blackhole 10.244.58.192/26 proto bird
10.244.85.192/26 via 192.168.0.133 dev ens32 proto bird
10.244.235.192/26 via 192.168.0.132 dev ens32 proto bird
其中,这里最核心的“下一跳”路由规则,就是由 Calico 的 Felix 进程负责维护的。这些路由规则信息,则是通过 BGP Client 也就是 BIRD 组件,使用 BGP 协议传输而来的。
不难发现,Calico 项目实际上将集群里的所有节点,都当作是边界路由器来处理,它们一起组成了一个全连通的网络,互相之间通过 BGP 协议交换路由规则。这些节点,我们称为 BGP Peer。
https://docs.projectcalico.org/master/networking/bgp
Calico 维护的网络在默认是(Node-to-Node Mesh)全互联模式,Calico集群中的节点之间都会相互建立连接,用于路由交换。但是随着集群规模的扩大,mesh模式将形成一个巨大服务网格,连接数成倍增加。
# netstat -antp |grep bird
tcp 0 0 0.0.0.0:179 0.0.0.0:* LISTEN 23124/bird
tcp 0 0 192.168.0.134:44366 192.168.0.132:179 ESTABLISHED 23124/bird
tcp 0 0 192.168.0.134:1215 192.168.0.133:179 ESTABLISHED 23124/bird
这时就需要使用 Route Reflector(路由器反射)模式解决这个问题。
确定一个或多个Calico节点充当路由反射器,让其他节点从这个RR节点获取路由信息。
具体步骤如下:
1、关闭 node-to-node BGP网格
添加 default BGP配置,调整 nodeToNodeMeshEnabled和asNumber:
cat << EOF | calicoctl create -f -
apiVersion: projectcalico.org/v3
kind: BGPConfiguration
metadata:
name: default
spec:
logSeverityScreen: Info
nodeToNodeMeshEnabled: false
asNumber: 63400
EOF
ASN号可以通过获取 # calicoctl get nodes --output=wide
2、配置指定节点充当路由反射器
为方便让BGPPeer轻松选择节点,通过标签选择器匹配。
给路由器反射器节点打标签:
kubectl label node k8s-node01 route-reflector=true
然后配置路由器反射器节点routeReflectorClusterID:
# calicoctl get node k8s-node01 -o yaml > rr-node.yaml
# vim rr-node.yaml
apiVersion: projectcalico.org/v3
kind: Node
apiVersion: projectcalico.org/v3
kind: Node
metadata:
annotations:
projectcalico.org/kube-labels: '{"beta.kubernetes.io/arch":"amd64","beta.kubernetes.io/os":"linux","kubernetes.io/arch":"amd64","kubernetes.io/hostname":"k8s-node01","kubernetes.io/os":"linux"}'
creationTimestamp: 2020-06-04T07:24:40Z
labels:
beta.kubernetes.io/arch: amd64
beta.kubernetes.io/os: linux
kubernetes.io/arch: amd64
kubernetes.io/hostname: k8s-node01
kubernetes.io/os: linux
name: k8s-node01
resourceVersion: "50638"
uid: 16452357-d399-4247-bc2b-ab7e7eb52dbc
spec:
bgp:
ipv4Address: 192.168.0.133/24
routeReflectorClusterID: 244.0.0.1
orchRefs:
- nodeName: k8s-node01
orchestrator: k8s
现在,很容易使用标签选择器将路由反射器节点与其他非路由反射器节点配置为对等:
# vi bgppeer.yaml
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
name: peer-with-route-reflectors
spec:
nodeSelector: all()
peerSelector: route-reflector == 'true'
查看节点的BGP连接状态:
# calicoctl node status
Calico process is running.
IPv4 BGP status
+---------------+---------------+-------+----------+-------------+
| PEER ADDRESS | PEER TYPE | STATE | SINCE | INFO |
+---------------+---------------+-------+----------+-------------+
| 192.168.0.133 | node specific | up | 09:54:05 | Established |
+---------------+---------------+-------+----------+-------------+
在前面提到过,Flannel host-gw 模式最主要的限制,就是要求集群宿主机之间是二层连通的。而这个限制对于 Calico 来说,也同样存在。
修改为IPIP模式:
# calicoctl get ipPool -o yaml > ipip.yaml
# vim ipip.yaml
apiVersion: projectcalico.org/v3
items:
- apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
creationTimestamp: 2020-06-04T07:24:39Z
name: default-ipv4-ippool
resourceVersion: "50630"
uid: e6515b21-44eb-4de9-8dc2-42f291da4273
spec:
blockSize: 26
cidr: 10.244.0.0/16
ipipMode: Always
natOutgoing: true
nodeSelector: all()
vxlanMode: Never
kind: IPPoolList
metadata:
resourceVersion: "76218"
# calicoctl apply -f ipip.yaml
Successfully applied 1 'IPPool' resource(s)
# calicoctl get ippool -o wide
NAME CIDR NAT IPIPMODE VXLANMODE DISABLED SELECTOR
default-ipv4-ippool 10.244.0.0/16 true Always Never false all()
Pod 1 访问 Pod 2大致流程如下:
路由表:
# node01
10.244.58.192/26 via 192.168.0.134 dev tunl0 proto bird onlink
10.244.85.192 dev calic32fa4483b3 scope link
# node02
10.244.58.192 dev calib29fd680177 scope link
10.244.85.192/26 via 192.168.0.133 dev tunl0 proto bird onlink
不难看到,当 Calico 使用 IPIP 模式的时候,集群的网络性能会因为额外的封包和解包工作而下降。所以建议你将所有宿主机节点放在一个子网里,避免使用 IPIP。
先考虑几个问题:
CNI插件插件解决了不同Node节点Pod互通问题,从而形成一个扁平化网络,默认情况下,Kubernetes 网络允许所有 Pod 到 Pod 的流量,在一些场景中,我们不希望Pod之间默认相互访问,例如:
所以,我们需要使用network policy对Pod网络进行隔离。支持对Pod级别和Namespace级别网络访问控制。
Pod网络入口方向隔离
Pod网络出口方向隔离
一个NetworkPolicy例子:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: test-network-policy
namespace: default
spec:
podSelector:
matchLabels:
role: db
policyTypes:
- Ingress
- Egress
ingress:
- from:
- ipBlock:
cidr: 172.17.0.0/16
except:
- 172.17.1.0/24
- namespaceSelector:
matchLabels:
project: myproject
- podSelector:
matchLabels:
role: frontend
ports:
- protocol: TCP
port: 6379
egress:
- to:
- ipBlock:
cidr: 10.0.0.0/24
ports:
- protocol: TCP
port: 5978
配置解析:
Pod访问限制
准备测试环境,一个web pod,两个client pod
# kubectl create deployment web --image=nginx
# kubectl run client1 --generator=run-pod/v1 --image=busybox --command -- sleep 36000
# kubectl run client2 --generator=run-pod/v1 --image=busybox --command -- sleep 36000
# kubectl get pods --show-labels
NAME READY STATUS RESTARTS AGE LABELS
client1 1/1 Running 0 49s run=client1
client2 1/1 Running 0 49s run=client2
web-5886dfbb96-8mkg9 1/1 Running 0 136m app=web,pod-template-hash=5886dfbb96
web-5886dfbb96-hps6t 1/1 Running 0 136m app=web,pod-template-hash=5886dfbb96
web-5886dfbb96-lckfs 1/1 Running 0 136m app=web,pod-template-hash=5886dfbb96
需求:将default命名空间携带run=web标签的Pod隔离,只允许default命名空间携带run=client1标签的Pod访问80端口
# vim pod-acl.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: test-network-policy
namespace: default
spec:
podSelector:
matchLabels:
app: web
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
project: default
- podSelector:
matchLabels:
run: client1
ports:
- protocol: TCP
port: 80
# kubectl apply -f pod-acl.yaml
测试
# kubectl exec -it client1 sh
/ # wget 10.244.85.192
Connecting to 10.244.85.192 (10.244.85.192:80)
saving to 'index.html'
index.html 100% |**************************************************************************************************************| 612 0:00:00 ETA
'index.html' saved
# kubectl exec -it client2 sh
/ # wget 10.244.85.192
Connecting to 10.244.85.192 (10.244.85.192:80)
wget: can't connect to remote host (10.244.85.192): Connection timed out
隔离策略配置:
Pod对象:default命名空间携带run=web标签的Pod
允许访问端口:80
允许访问对象:default命名空间携带run=client1标签的Pod
拒绝访问对象:除允许访问对象外的所有对象
命名空间隔离
需求:default命名空间下所有pod可以互相访问,但不能访问其他命名空间Pod,其他命名空间也不能访问default命名空间Pod。
# kubectl create ns test
namespace/test created
# kubectl create deployment web --image=nginx -n test
deployment.apps/web created
# kubectl run client -n test --generator=run-pod/v1 --image=busybox --command -- sleep 36000
# vim ns-acl.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-from-other-namespaces
namespace: default
spec:
podSelector: {}
policyTypes:
- Ingress
ingress:
- from:
- podSelector: {}
# kubectl apply -f ns-acl.yaml
# kubectl get pod -n test -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
client 1/1 Running 0 119s 10.244.85.195 k8s-node01 <none> <none>
web-d86c95cc9-gr88v 1/1 Running 0 10m 10.244.235.198 k8s-master1 <none> <none>
[root@k8s-master ~]# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
web-5886dfbb96-8mkg9 1/1 Running 0 158m 10.244.235.193 k8s-master1 <none> <none>
web-5886dfbb96-hps6t 1/1 Running 0 158m 10.244.85.192 k8s-node01 <none> <none>
web-5886dfbb96-lckfs 1/1 Running 0 158m 10.244.58.192 k8s-node02 <none> <none>
# kubectl exec -it client -n test sh
/ # wget 10.244.58.192
Connecting to 10.244.58.192 (10.244.58.192:80)
wget: can't connect to remote host (10.244.58.192): Connection timed out
/ # wget 10.244.235.198
Connecting to 10.244.235.198 (10.244.235.198:80)
saving to 'index.html'
index.html 100% |**************************************************************************************************************| 612 0:00:00 ETA
'index.html' saved
podSelector: {}:default命名空间下所有Pod
from.podSelector: {} : 如果未配置具体的规则,默认不允许