k8s & docker network

Jas0n0ss發表於2024-10-13

Kubernetes的網路模型和網路策略

1、Kubernetes網路模型和CNI外掛

在Kubernetes中設計了一種網路模型,要求無論容器執行在叢集中的哪個節點,所有容器都能透過一個扁平的網路平面進行通訊,即在同一IP網路中。需要注意的是:在K8S叢集中,IP地址分配是以Pod物件為單位,而非容器,同一Pod內的所有容器共享同一網路名稱空間。

1.1、Docker網路模型

瞭解Docker的友友們都應該清楚,Docker容器的原生網路模型主要有3種:Bridge(橋接)、Host(主機)、none。

  • Bridge:藉助虛擬網橋裝置為容器建立網路連線。
  • Host:設定容器直接共享當前節點主機的網路名稱空間。
  • none:多個容器共享同一個網路名稱空間。
# 使用以下命令檢視docker原生的三種網路
[root@localhost ~]# docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
0efec019c899        bridge              bridge              local
40add8bb5f07        host                host                local
ad94f0b1cca6        none                null                local

# none網路,在該網路下的容器僅有lo網路卡,屬於封閉式網路,通常用於對安全性要求較高並且不需要聯網的應用
[root@localhost ~]# docker run -it --network=none busybox
/ # ifconfig
lo        Link encap:Local Loopback  
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

# host網路,共享宿主機的網路名稱空間,容器網路配置和host一致,但是存在埠衝突的問題
[root@localhost ~]# docker run -it --network=host busybox
/ # ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast qlen 1000
    link/ether 00:0c:29:69:a7:23 brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.4/24 brd 192.168.1.255 scope global dynamic eth0
       valid_lft 84129sec preferred_lft 84129sec
    inet6 fe80::20c:29ff:fe69:a723/64 scope link 
       valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue 
    link/ether 02:42:29:09:8f:dd brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:29ff:fe09:8fdd/64 scope link 
       valid_lft forever preferred_lft forever
/ # hostname
localhost

# bridge網路,Docker安裝完成時會建立一個名為docker0的linux bridge,不指定網路時,建立的網路預設為橋接網路,都會橋接到docker0上。
[root@localhost ~]# brctl show
bridge name	bridge id		STP enabled	interfaces
docker0		8000.024229098fdd	no		

[root@localhost ~]# docker run -d nginx	#執行一個nginx容器
c760a1b6c9891c02c992972d10a99639d4816c4160d633f1c5076292855bbf2b

[root@localhost ~]# brctl show		
bridge name	bridge id		STP enabled	interfaces
docker0		8000.024229098fdd	no		veth3f1b114

一個新的網路介面veth3f1b114橋接到了docker0上,veth3f1b114就是新建立的容器的虛擬網路卡。進入容器檢視其網路配置:
[root@localhost ~]# docker exec -it c760a1b6c98 bash
root@c760a1b6c989:/# apt-get update
root@c760a1b6c989:/# apt-get iproute
root@c760a1b6c989:/# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
38: eth0@if39: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

從上可以看到容器內有一個網路卡eth0@if39,實際上eth0@if39veth3f1b114是一對veth pairveth pair是一種成對出現的特殊網路裝置,可以想象它們由一根虛擬的網線進行連線的一對網路卡,eth0@if39在容器中,veth3f1b114掛在網橋docker0上,最終的效果就是eth0@if39也掛在了docker0上

橋接式網路是目前較為流行和預設的解決方案。但是這種方案的弊端是無法跨主機通訊的,僅能在宿主機本地進行,而解決該問題的方法就是NAT。所有接入到該橋接裝置上的容器都會被NAT隱藏,它們發往Docker主機外部的所有流量都會經過源地址轉換後發出,並且預設是無法直接接受節點之外的其他主機發來的請求。當需要接入Docker主機外部流量,就需要進行目標地址轉換甚至埠轉換將其暴露在外部網路當中。如下圖:

img

容器內的屬於私有地址,需要在左側的主機上的eth0上進行源地址轉換,而右側的地址需要被訪問,就需要將eth0的地址進行NAT轉換。SNAT---->DNAT

這樣的通訊方式會比較麻煩,從而需要藉助第三方的網路外掛實現這樣的跨主機通訊的網路策略。

1.2、Kubernetes網路模型

我們知道的是,在K8S上的網路通訊包含以下幾類:

  • 容器間的通訊:同一個Pod內的多個容器間的通訊,它們之間透過lo網路卡進行通訊。

  • Pod之間的通訊:透過Pod IP地址進行通訊。

  • Pod和Service之間的通訊:Pod IP地址和Service IP進行通訊,兩者並不屬於同一網路,實現方式是透過IPVS或iptables規則轉發。

  • Service和叢集外部客戶端的通訊,實現方式:Ingress、NodePort、Loadbalance

    K8S網路的實現不是叢集內部自己實現,而是依賴於第三方網路外掛----CNI(Container Network Interface)

    flannel、calico、canel等是目前比較流行的第三方網路外掛。

    這三種的網路外掛需要實現Pod網路方案的方式通常有以下幾種:

虛擬網橋、多路複用(MacVLAN)、硬體交換(SR-IOV)

無論是上面的哪種方式在容器當中實現,都需要大量的操作步驟,而K8S支援CNI外掛進行編排網路,以實現Pod和叢集網路管理功能的自動化。每次Pod被初始化或刪除,kubelet都會呼叫預設的CNI外掛去建立一個虛擬裝置介面附加到相關的底層網路,為Pod去配置IP地址、路由資訊並對映到Pod物件的網路名稱空間。

在配置Pod網路時,kubelet會在預設的/etc/cni/net.d/目錄中去查詢CNI JSON配置檔案,然後透過type屬性到/opt/cni/bin中查詢相關的外掛二進位制檔案,如下面的"portmap"。然後CNI外掛呼叫IPAM外掛(IP地址管理外掛)來配置每個介面的IP地址:

[root@k8s-master ~]# cat /etc/cni/net.d/10-flannel.conflist 
{
  "name": "cbr0",
  "plugins": [
    {
      "type": "flannel",
      "delegate": {
        "hairpinMode": true,
        "isDefaultGateway": true
      }
    },
    {
      "type": "portmap",
      "capabilities": {
        "portMappings": true
      }
    }
  ]
}

# kubelet呼叫第三方外掛,進行網路地址的分配

CNI主要是定義容器網路模型規範,連結容器管理系統和網路外掛,兩者主要透過上面的JSON格式檔案進行通訊,實現容器的網路功能。CNI的主要核心是:在建立容器時,先建立好網路名稱空間(netns),然後呼叫CNI外掛為這個netns配置網路,最後在啟動容器內的程序。

常見的CNI網路外掛包含以下幾種:

  • Flannel:為Kubernetes提供疊加網路的網路外掛,基於TUN/TAP隧道技術,使用UDP封裝IP報文進行建立疊 加網路,藉助etcd維護網路的分配情況,缺點:無法支援網路策略訪問控制。
  • Calico:基於BGP的三層網路外掛,也支援網路策略進而實現網路的訪問控制;它在每臺主機上都執行一個虛擬路由,利用Linux核心轉發網路資料包,並藉助iptables實現防火牆功能。實際上Calico最後的實現就是將每臺主機都變成了一臺路由器,將各個網路進行連線起來,實現跨主機通訊的功能。
  • Canal:由Flannel和Calico聯合釋出的一個統一網路外掛,提供CNI網路外掛,並支援網路策略實現。
  • 其他的還包括Weave Net、Contiv、OpenContrail、Romana、NSX-T、kube-router等等。而Flannel和Calico是目前最流行的選擇方案。

1.3、Flannel網路外掛

在各節點上的Docker主機在docker0上預設使用同一個子網,不同節點的容器都有可能會獲取到相同的地址,那麼在跨節點通訊時就會出現地址衝突的問題。並且在多個節點上的docker0使用不同的子網,也會因為沒有準確的路由資訊導致無法準確送達報文。

而為了解決這一問題,Flannel的解決辦法是,預留一個使用網路,如10.244.0.0/16,然後自動為每個節點的Docker容器引擎分配一個子網,如10.244.1.0/24和10.244.2.0/24,並將分配資訊儲存在etcd持久儲存。

第二個問題的解決,Flannel是採用不同型別的後端網路模型進行處理。其後端的型別有以下幾種:

  • VxLAN:使用核心中的VxLAN模組進行封裝報文。也是flannel推薦的方式,其報文格式如下:

img

  • host-gw:即Host GateWay,透過在節點上建立目標容器地址的路由直接完成報文轉發,要求各節點必須在同一個2層網路,對報文轉發效能要求較高的場景使用。
  • UDP:使用普通的UDP報文封裝完成隧道轉發。

1.4、VxLAN後端和direct routing

VxLAN(Virtual extensible Local Area Network)虛擬可擴充套件區域網,採用MAC in UDP封裝方式,具體的實現方式為:

  • 1、將虛擬網路的資料幀新增到VxLAN首部,封裝在物理網路的UDP報文中
  • 2、以傳統網路的通訊方式傳送該UDP報文
  • 3、到達目的主機後,去掉物理網路報文的頭部資訊以及VxLAN首部,並交付給目的終端

跨節點的Pod之間的通訊就是以上的一個過程,整個過程中通訊雙方對物理網路是沒有感知的。如下網路圖:

img

VxLAN的部署可以直接在官方上找到其YAML檔案,如下:

[root@k8s-master:~# kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/bc79dd1505b0c8681ece4de4c0d86c5cd2643275/Documentation/kube-flannel.yml
clusterrole.rbac.authorization.k8s.io/flannel created
clusterrolebinding.rbac.authorization.k8s.io/flannel created
serviceaccount/flannel created
configmap/kube-flannel-cfg created
daemonset.extensions/kube-flannel-ds-amd64 created
daemonset.extensions/kube-flannel-ds-arm64 created
daemonset.extensions/kube-flannel-ds-arm created
daemonset.extensions/kube-flannel-ds-ppc64le created
daemonset.extensions/kube-flannel-ds-s390x created

#輸出如下結果表示flannel可以正常執行了
[root@k8s-master ~]# kubectl get daemonset -n kube-system
NAME              DESIRED   CURRENT   READY     UP-TO-DATE   AVAILABLE   NODE SELECTOR                   AGE
kube-flannel-ds   3         3         3         3            3           beta.kubernetes.io/arch=amd64   202d
kube-proxy        3         3         3         3            3           beta.kubernetes.io/arch=amd64   202d

執行正常後,flanneld會在宿主機的/etc/cni/net.d目錄下生成自已的配置檔案,kubelet將會呼叫它。

網路外掛執行成功後,Node狀態才Ready。

[root@k8s-master ~]# kubectl get node
NAME         STATUS    ROLES     AGE       VERSION
k8s-master   Ready     master    202d      v1.11.2
k8s-node01   Ready     <none>    202d      v1.11.2
k8s-node02   Ready     <none>    201d      v1.11.2

flannel執行後,在各Node宿主機多了一個網路介面:

#master節點的flannel.1網路介面,其網段為:10.244.0.0
[root@k8s-master ~]# ifconfig flannel.1
flannel.1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1450
        inet 10.244.0.0  netmask 255.255.255.255  broadcast 0.0.0.0
        inet6 fe80::31:5dff:fe01:4bc0  prefixlen 64  scopeid 0x20<link>
        ether 02:31:5d:01:4b:c0  txqueuelen 0  (Ethernet)
        RX packets 1659239  bytes 151803796 (144.7 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 2115439  bytes 6859187242 (6.3 GiB)
        TX errors 0  dropped 10 overruns 0  carrier 0  collisions 0

#node1節點的flannel.1網路介面,其網段為:10.244.1.0
[root@k8s-node01 ~]# ifconfig flannel.1
flannel.1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1450
        inet 10.244.1.0  netmask 255.255.255.255  broadcast 0.0.0.0
        inet6 fe80::2806:4ff:fe71:2253  prefixlen 64  scopeid 0x20<link>
        ether 2a:06:04:71:22:53  txqueuelen 0  (Ethernet)
        RX packets 136904  bytes 16191144 (15.4 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 180775  bytes 512637365 (488.8 MiB)
        TX errors 0  dropped 8 overruns 0  carrier 0  collisions 0

#node2節點的flannel.1網路介面,其網段為:10.244.2.0
[root@k8s-node02 ~]# ifconfig flannel.1
flannel.1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1450
        inet 10.244.2.0  netmask 255.255.255.255  broadcast 0.0.0.0
        inet6 fe80::58b7:7aff:fe8d:2d  prefixlen 64  scopeid 0x20<link>
        ether 5a:b7:7a:8d:00:2d  txqueuelen 0  (Ethernet)
        RX packets 9847824  bytes 12463823121 (11.6 GiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 2108796  bytes 185073173 (176.4 MiB)
        TX errors 0  dropped 13 overruns 0  carrier 0  collisions 0

從上面的結果可以知道 :

  1. flannel預設就是VXLAN模式,即Overlay Network。
  2. flanneld建立了一個flannel.1介面,它是專門用來封裝隧道協議的,預設分給叢集的Pod網段為10.244.0.0/16。
  3. flannel給k8s-master節點配置的Pod網路為10.244.0.0段,給k8s-node01節點配置的Pod網路為10.244.1.0段,給k8s-node01節點配置的Pod網路為10.244.2.0段,如果有更多的節點,以此類推。

舉個實際例子

#啟動一個nginx容器,副本為3
[root@k8s-master ~]# kubectl run nginx --image=nginx:1.14 --port=80 --replicas=3
deployment.apps/nginx created

#檢視Pod
[root@k8s-master ~]# kubectl get pods -o wide |grep nginx
nginx-5bd76bcc4f-8s64s                1/1       Running    0          2m        10.244.2.85    k8s-node02
nginx-5bd76bcc4f-mr6k5                1/1       Running    0          2m        10.244.1.146   k8s-node01
nginx-5bd76bcc4f-pb257                1/1       Running    0          2m        10.244.0.17    k8s-master

可以看到,3個Pod都分別執行在各個節點之上,其中master上的Pod的ip為:10.244.0.17,在master節點上檢視網路介面可以發現在各個節點上多了一個虛擬介面cni0,其ip地址為10.244.0.1。它是由flanneld建立的一個虛擬網橋叫cni0,在Pod本地通訊使用。 這裡需要注意的是,cni0虛擬網橋,僅作用於本地通訊!!!!

[root@k8s-master ~]# ifconfig cni0
cni0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1450
        inet 10.244.0.1  netmask 255.255.255.0  broadcast 0.0.0.0
        inet6 fe80::848a:beff:fe44:4959  prefixlen 64  scopeid 0x20<link>
        ether 0a:58:0a:f4:00:01  txqueuelen 1000  (Ethernet)
        RX packets 2772994  bytes 300522237 (286.6 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 3180562  bytes 928687288 (885.6 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

flanneld為每個Pod建立一對veth虛擬裝置,一端放在容器介面上,一端放在cni0橋上。 使用brctl檢視該網橋:

#可以看到有一veth的網路介面橋接在cni0網橋上
[root@k8s-master ~]# brctl show cni0
bridge name	bridge id		STP enabled	interfaces
cni0		8000.0a580af40001	no		veth020fafae


#宿主機ping測試訪問Pod ip
[root@k8s-master ~]# ping 10.244.0.17
PING 10.244.0.17 (10.244.0.17) 56(84) bytes of data.
64 bytes from 10.244.0.17: icmp_seq=1 ttl=64 time=0.291 ms
64 bytes from 10.244.0.17: icmp_seq=2 ttl=64 time=0.081 ms
^C
--- 10.244.0.17 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3000ms
rtt min/avg/max/mdev = 0.055/0.129/0.291/0.094 ms

在現有的Flannel VxLAN網路中,兩臺主機上的Pod間通訊,也是正常的,如master節點上的Pod訪問node01上的Pod:

[root@k8s-master ~]# kubectl exec -it nginx-5bd76bcc4f-pb257 -- /bin/bash
root@nginx-5bd76bcc4f-pb257:/# ping 10.244.1.146
PING 10.244.1.146 (10.244.1.146) 56(84) bytes of data.
64 bytes from 10.244.1.146: icmp_seq=1 ttl=62 time=1.44 ms
64 bytes from 10.244.1.146: icmp_seq=2 ttl=62 time=0.713 ms
64 bytes from 10.244.1.146: icmp_seq=3 ttl=62 time=0.713 ms
64 bytes from 10.244.1.146: icmp_seq=4 ttl=62 time=0.558 ms
^C
--- 10.244.1.146 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3004ms
rtt min/avg/max/mdev = 0.558/0.858/1.448/0.346 ms

可以看到容器跨主機是可以正常通訊的,那麼容器的跨主機通訊是如何實現的呢?????master上檢視路由表資訊:

[root@k8s-master ~]# ip route
......
10.244.1.0/24 via 10.244.1.0 dev flannel.1 onlink 
10.244.2.0/24 via 10.244.2.0 dev flannel.1 onlink 
......

傳送到10.244.1.0/2410.244.20/24網段的資料包文發給本機的flannel.1介面,即進入二層隧道,然後對資料包文進行封裝(封裝VxLAN首部-->UDP首部-->IP首部-->乙太網首部),到達目標Node節點後,由目標Node上的flannel.1進行解封裝。使用tcpdump進行 抓一下包,如下:

#在宿主機和容器內都進行ping另外一臺主機上的Pod ip並進行抓包
[root@k8s-master ~]# ping -c 10 10.244.1.146
[root@k8s-master ~]# kubectl exec -it nginx-5bd76bcc4f-pb257 -- /bin/bash
root@nginx-5bd76bcc4f-pb257:/# ping 10.244.1.146 

[root@k8s-master ~]# tcpdump -i flannel.1 -nn host 10.244.1.146
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on flannel.1, link-type EN10MB (Ethernet), capture size 262144 bytes

#宿主機ping後抓包情況如下:
22:22:35.737977 IP 10.244.0.0 > 10.244.1.146: ICMP echo request, id 29493, seq 1, length 64
22:22:35.738902 IP 10.244.1.146 > 10.244.0.0: ICMP echo reply, id 29493, seq 1, length 64
22:22:36.739042 IP 10.244.0.0 > 10.244.1.146: ICMP echo request, id 29493, seq 2, length 64
22:22:36.739789 IP 10.244.1.146 > 10.244.0.0: ICMP echo reply, id 29493, seq 2, length 64

#容器ping後抓包情況如下:
22:33:49.295137 IP 10.244.0.17 > 10.244.1.146: ICMP echo request, id 837, seq 1, length 64
22:33:49.295933 IP 10.244.1.146 > 10.244.0.17: ICMP echo reply, id 837, seq 1, length 64
22:33:50.296736 IP 10.244.0.17 > 10.244.1.146: ICMP echo request, id 837, seq 2, length 64
22:33:50.297222 IP 10.244.1.146 > 10.244.0.17: ICMP echo reply, id 837, seq 2, length 64
22:33:51.297556 IP 10.244.0.17 > 10.244.1.146: ICMP echo request, id 837, seq 3, length 64
22:33:51.298054 IP 10.244.1.146 > 10.244.0.17: ICMP echo reply, id 837, seq 3, length 64

# 可以看到報文都是經過flannel.1網路介面進入2層隧道進而轉發

VXLAN是Linux核心本身支援的一種網路虛擬化技術,是核心的一個模組,在核心態實現封裝解封裝,構建出覆蓋網路,其實就是一個由各宿主機上的Flannel.1裝置組成的虛擬二層網路。

由於VXLAN由於額外的封包解包,導致其效能較差,所以Flannel就有了host-gw模式,即把宿主機當作閘道器,除了本地路由之外沒有額外開銷,效能和calico差不多,由於沒有疊加來實現報文轉發,這樣會導致路由表龐大。因為一個節點對應一個網路,也就對應一條路由條目。

host-gw雖然VXLAN網路效能要強很多。,但是種方式有個缺陷:要求各物理節點必須在同一個二層網路中。物理節點必須在同一網段中。這樣會使得一個網段中的主機量會非常多,萬一發一個廣播報文就會產生干擾。在私有云場景下,宿主機不在同一網段是很常見的狀態,所以就不能使用host-gw了。

VXLAN還有另外一種功能,VXLAN也支援類似host-gw的玩法,如果兩個節點在同一網段時使用host-gw通訊,如果不在同一網段中,即 當前pod所在節點與目標pod所在節點中間有路由器,就使用VXLAN這種方式,使用疊加網路。 結合了Host-gw和VXLAN,這就是VXLAN的Direct routing模式

Flannel VxLAN的Direct routing模式配置

修改kube-flannel.yml檔案,將flannel的configmap物件改為:

[root@k8s-master ~]# vim kube-flannel.yml 
......
 net-conf.json: |
    {
      "Network": "10.244.0.0/16",	#預設網段
      "Backend": {
        "Type": "vxlan",
        "Directrouting": true	#增加
      }
    }
......

[root@k8s-master ~]# kubectl apply -f kube-flannel.yml 
clusterrole.rbac.authorization.k8s.io/flannel configured
clusterrolebinding.rbac.authorization.k8s.io/flannel configured
serviceaccount/flannel unchanged
configmap/kube-flannel-cfg configured
daemonset.extensions/kube-flannel-ds-amd64 created
daemonset.extensions/kube-flannel-ds-arm64 created
daemonset.extensions/kube-flannel-ds-arm created
daemonset.extensions/kube-flannel-ds-ppc64le created
daemonset.extensions/kube-flannel-ds-s390x created

#檢視路由資訊
[root@k8s-master ~]# ip route
......
10.244.1.0/24 via 192.168.56.12 dev eth0 
10.244.2.0/24 via 192.168.56.13 dev eth0 
......

從上面的結果可以看到,發往10.244.1.0/2410.244.1.0/24的包都是直接經過eth0網路介面直接發出去的,這就是Directrouting。如果兩個節點是跨網段的,則flannel自動降級為VxLAN模式。

此時,在各 個 叢集節點上執行“iptables -nL”命令 可以 看到, iptables filter 表 的 FORWARD 鏈 上 由其 生成 了 如下 兩條 轉發 規則, 它 顯 式 放行 了10. 244. 0. 0/ 16 網路 進出 的 所有 報文, 用於 確保 由 物理 介面 接收 或 傳送 的 目標 地址 或 源 地址 為10. 244. 0. 0/ 16網路 的 所有 報文 均能 夠 正常 通行。 這些 是 Direct Routing 模式 得以 實現 的 必要條件:

target 		prot 	opt 	source 				destination 
ACCEPT 		all 	-- 	10. 244. 0. 0/ 16 		0. 0. 0. 0/ 0 
ACCEPT 		all 	-- 	0. 0. 0. 0/ 0 		    10. 244. 0. 0/ 16

再在此之前建立的Pod和宿主機上進行ping測試,可以看到在flannel.1介面上已經抓不到包了,在eth0上可以用抓到ICMP的包,如下:

[root@k8s-master ~]# tcpdump -i flannel.1 -nn host 10.244.1.146
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on flannel.1, link-type EN10MB (Ethernet), capture size 262144 bytes
^C
0 packets captured
0 packets received by filter
0 packets dropped by kernel

[root@k8s-master ~]# tcpdump -i eth0 -nn host 10.244.1.146
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
22:48:52.376393 IP 10.244.0.17 > 10.244.1.146: ICMP echo request, id 839, seq 1, length 64
22:48:52.376877 IP 10.244.1.146 > 10.244.0.17: ICMP echo reply, id 839, seq 1, length 64
22:48:53.377005 IP 10.244.0.17 > 10.244.1.146: ICMP echo request, id 839, seq 2, length 64
22:48:53.377621 IP 10.244.1.146 > 10.244.0.17: ICMP echo reply, id 839, seq 2, length 64

22:50:28.647490 IP 192.168.56.11 > 10.244.1.146: ICMP echo request, id 46141, seq 1, length 64
22:50:28.648320 IP 10.244.1.146 > 192.168.56.11: ICMP echo reply, id 46141, seq 1, length 64
22:50:29.648958 IP 192.168.56.11 > 10.244.1.146: ICMP echo request, id 46141, seq 2, length 64
22:50:29.649380 IP 10.244.1.146 > 192.168.56.11: ICMP echo reply, id 46141, seq 2, length 64

1.5、Host-gw後端

Flannel除了上面2種資料傳輸的方式以外,還有一種是host-gw的方式,host-gw後端是透過新增必要的路由資訊使用節點的二層網路直接傳送Pod的通訊報文。它的工作方式類似於Directrouting的功能,但是其並不具備VxLan的隧道轉發能力。

編輯kube-flannel的配置清單,將ConfigMap資源kube-flannel-cfg的data欄位中網路配置進行修改,如下:

[root@k8s-master ~]# vim kube-flannel.yml 
......
  net-conf.json: |
    {
      "Network": "10.244.0.0/16",
      "Backend": {
        "Type": "host-gw"
      }
    }
......

[root@k8s-master ~]# kubectl apply -f kube-flannel.yml 

配置完成後,各節點會生成類似directrouting一樣的 路由和iptables規則,用於實現二層轉發Pod網路的通訊報文,省去了隧道轉發模式的額外開銷。但是存在的問題點是,對於不在同一個二層網路的報文轉發,host-gw是無法實現的。延續上面的例子,進行抓包檢視:

master上的Pod:10.244.0.17向node01節點上的Pod:10.244.1.146傳送ICMP報文

#檢視路由表資訊,可以看到其報文的傳送方向都是和Directrouting是一樣的
[root@k8s-master ~]# ip route
......
10.244.1.0/24 via 192.168.56.12 dev eth0 
10.244.2.0/24 via 192.168.56.13 dev eth0 
.....

#進行ping包測試
[root@k8s-master ~]# ping -c 2 10.244.1.146

#在eth0上進行抓包
[root@k8s-master ~]# tcpdump -i eth0 -nn host 10.244.1.146
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
23:11:05.556972 IP 192.168.56.11 > 10.244.1.146: ICMP echo request, id 59528, seq 1, length 64
23:11:05.557794 IP 10.244.1.146 > 192.168.56.11: ICMP echo reply, id 59528, seq 1, length 64
23:11:06.558231 IP 192.168.56.11 > 10.244.1.146: ICMP echo request, id 59528, seq 2, length 64
23:11:06.558610 IP 10.244.1.146 > 192.168.56.11: ICMP echo reply, id 59528, seq 2, length 64

該模式下,報文轉發的相關流程如下:

  • 1、Pod(10.244.0.17)向Pod(10.244.1.146)傳送報文,檢視到報文中的目的地址為:10.244.1.146,並非本地網段,會直接傳送到閘道器(192.168.56.11);

  • 2、閘道器發現該目標地址為10.244.1.146,要到達10.244.1.0/24網段,需要送達到node2 的物理網路卡,node2接收以後發現該報文的目標地址屬於本機上的另一個虛擬網路卡,然後轉發到相對應的Pod(10.244.1.146)

    工作模式流程圖如下:

img

以上就是Flannel網路模型的三種工作模式,但是flannel自身並不具備為Pod網路實現網路策略和網路通訊隔離的功能,為此只能藉助於Calico聯合統一的專案Calnal專案進行構建網路策略的功能。

2、網路策略

網路策略(Network Policy )是 Kubernetes 的一種資源。Network Policy 透過 Label 選擇 Pod,並指定其他 Pod 或外界如何與這些 Pod 通訊。

Pod的網路流量包含流入(Ingress)和流出(Egress)兩種方向。預設情況下,所有 Pod 是非隔離的,即任何來源的網路流量都能夠訪問 Pod,沒有任何限制。當為 Pod 定義了 Network Policy,只有 Policy 允許的流量才能訪問 Pod。

Kubernetes的網路策略功能也是由第三方的網路外掛實現的,因此,只有支援網路策略功能的網路外掛才能進行配置網路策略,比如Calico、Canal、kube-router等等。

PS:Kubernetes自1.8版本才支援Egress網路策略,在該版本之前僅支援Ingress網路策略。

2.1、部署Canal提供網路策略功能

calico官網

Calico可以獨立地為Kubernetes提供網路解決方案和網路策略,也可以和flannel相結合,由flannel提供網路解決方案,Calico僅用於提供網路策略,此時將Calico稱為Canal。結合flannel工作時,Calico提供的預設配置清單式以flannel預設使用的10.244.0.0/16為Pod網路,因此在叢集中kube-controller-manager啟動時就需要透過--cluster-cidr選項進行設定使用該網路地址,並且---allocate-node-cidrs的值應設定為true。

#設定RBAC
[root@k8s-master ~]# kubectl apply -f https://docs.projectcalico.org/v3.2/getting-started/kubernetes/installation/hosted/canal/rbac.yaml

#部署Canal提供網路策略
[root@k8s-master ~]# kubectl apply -f https://docs.projectcalico.org/v3.2/getting-started/kubernetes/installation/hosted/canal/canal.yaml

[root@k8s-master ~]# kubectl get ds canal -n kube-system
NAME      DESIRED   CURRENT   READY     UP-TO-DATE   AVAILABLE   NODE SELECTOR                 AGE
canal     3         3         0         3            0           beta.kubernetes.io/os=linux   2m

部署canal需要的映象,建議先拉取映象,避免耗死資源:
quay.io/calico/node:v3.2.6
quay.io/calico/cni:v3.2.6
quay.io/coreos/flannel:v0.9.1

[root@k8s-master ~]# kubectl get pods -n kube-system -o wide |grep canal
canal-2hqwt                            3/3       Running        0          1h        192.168.56.11   k8s-master
canal-c5pxr                            3/3       Running        0          1h        192.168.56.13   k8s-node02
canal-kr662                            3/3       Running        6          1h        192.168.56.12   k8s-node01

Canal作為DaemonSet部署到每個節點,屬於kube-system這個名稱空間。需要注意的是,Canal只是直接使用了Calicoflannel專案,程式碼本身沒有修改,Canal只是一種部署的模式,用於安裝和配置專案。

2.2、配置網路策略

在Kubernetes系統中,報文的流入和流出的核心元件是Pod資源,它們也是網路策略功能的主要應用物件。NetworkPolicy物件透過podSelector選擇 一組Pod資源作為控制物件。NetworkPolicy是定義在一組Pod資源之上用於管理入站流量,或出站流量的一組規則,有可以是出入站規則一起生效,規則的生效模式通常由spec.policyTypes進行 定義。如下圖:

img

預設情況下,Pod物件的流量控制是為空的,報文可以自由出入。在附加網路策略之後,Pod物件會因為NetworkPolicy而被隔離,一旦名稱空間中有任何NetworkPolicy物件匹配了某特定的Pod物件,則該Pod將拒絕NetworkPolicy規則中不允許的所有連線請求,但是那些未被匹配到的Pod物件依舊可以接受所有流量。

就特定的Pod集合來說,入站和出站流量預設是放行狀態,除非有規則可以進行匹配。還有一點需要注意的是,在spec.policyTypes中指定了生效的規則型別,但是在networkpolicy.spec欄位中巢狀定義了沒有任何規則的Ingress或Egress時,則表示拒絕入站或出站的一切流量。定義網路策略的基本格式如下:

apiVersion: networking.k8s.io/v1		#定義API版本
kind: NetworkPolicy					   #定義資源型別
metadata:
  name: allow-myapp-ingress			    #定義NetwokPolicy的名字
  namespace: default
spec:								  #NetworkPolicy規則定義
  podSelector: 						   #匹配擁有標籤app:myapp的Pod資源
    matchLabels:
      app: myapp
  policyTypes ["Ingress"]			    #NetworkPolicy型別,可以是Ingress,Egress,或者兩者共存
  ingress:							  #定義入站規則
  - from:
    - ipBlock:						  #定義可以訪問的網段
        cidr: 10.244.0.0/16
        except:						  #排除的網段
        - 10.244.3.0/24
    - podSelector:					  #選定當前default名稱空間,標籤為app:myapp可以入站
        matchLabels:
          app: myapp
    ports:							 #開放的協議和埠定義
    - protocol: TCP
      port: 80
  
該網路策略就是將default名稱空間中擁有標籤"app=myapp"的Pod資源開放80/TCP埠給10.244.0.0/16網段,並排除10.244.3.0/24網段的訪問,並且也開放給標籤為app=myapp的所有Pod資源進行訪問。  

為了看出Network Policy的效果,先部署一個httpd的應用。配置清單檔案如下:

[root@k8s-master ~]# mkdir network-policy-demo
[root@k8s-master ~]# cd network-policy-demo/
[root@k8s-master network-policy-demo]# vim httpd.yaml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: httpd
spec:
  replicas: 3
  template:
    metadata:
      labels:
        run: httpd
    spec:
      containers:
      - name: httpd
        image: httpd:latest
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80

---
apiVersion: v1
kind: Service
metadata:
  name: httpd-svc
spec:
  type: NodePort
  selector:
    run: httpd
  ports:
  - protocol: TCP
    nodePort: 30000
    port: 8080
    targetPort: 80

建立三個副本,透過NodePort型別的Service對外方服務,部署應用:

[root@k8s-master network-policy-demo]# kubectl apply -f httpd.yaml 
deployment.apps/httpd unchanged
service/httpd-svc created
[root@k8s-master network-policy-demo]# kubectl get pods -o wide |grep httpd
httpd-75f655479d-882hz                1/1       Running    0          4m        10.244.0.2     k8s-master
httpd-75f655479d-h7lrr                1/1       Running    0          4m        10.244.2.2     k8s-node02
httpd-75f655479d-kzr5g                1/1       Running    0          4m        10.244.1.2     k8s-node01

[root@k8s-master network-policy-demo]# kubectl get svc httpd-svc
NAME        TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
httpd-svc   NodePort   10.99.222.179   <none>        8080:30000/TCP   4m

當前沒有定義任何Network Policy,驗證應用的訪問:

#啟動一個busybox Pod,可以訪問Service,也可以ping副本的Pod

[root@k8s-master ~]# kubectl run busybox --rm -it --image=busybox /bin/sh
If you don't see a command prompt, try pressing enter.
/ # wget httpd-svc:8080
Connecting to httpd-svc:8080 (10.99.222.179:8080)
index.html           100% |*********************************************************************************************|    45  0:00:00 ETA
/ # ping -c 2 10.244.1.2
PING 10.244.1.2 (10.244.1.2): 56 data bytes
64 bytes from 10.244.1.2: seq=0 ttl=63 time=0.507 ms
64 bytes from 10.244.1.2: seq=1 ttl=63 time=0.228 ms

--- 10.244.1.2 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.228/0.367/0.507 ms


#叢集節點也可以訪問Sevice和ping通副本Pod
[root@k8s-node01 ~]# curl 10.99.222.179:8080
<html><body><h1>It works!</h1></body></html>
[root@k8s-node01 ~]# ping -c 2 10.244.2.2
PING 10.244.2.2 (10.244.2.2) 56(84) bytes of data.
64 bytes from 10.244.2.2: icmp_seq=1 ttl=63 time=0.931 ms
64 bytes from 10.244.2.2: icmp_seq=2 ttl=63 time=0.812 ms

--- 10.244.2.2 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 0.812/0.871/0.931/0.066 ms

#叢集外部訪問192.168.56.11:30000也是通的
[root@localhost ~]# curl 192.168.56.11:30000
<html><body><h1>It works!</h1></body></html>

那麼下面再去設定不同的Network Policy來管控Pod的訪問。

2.3、管控入站流量

NetworkPolicy資源屬於名稱空間級別,它的作用範圍為其所屬的名稱空間。

1、設定預設的Ingress策略

使用者可以建立一個NetworkPolicy來為名稱空間設定一個預設的隔離策略,該策略選擇所有的Pod物件,然後允許或拒絕任何到達這些Pod的入站流量,如下:

[root@k8s-master network-policy-demo]# vim policy-demo.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-all-ingress
spec:
  podSelector: {}
  policyTypes: ["Ingress"]	
  #指明瞭Ingress生效規則,但不定義任何Ingress欄位,因此不能匹配任何源端點,從而拒絕所有的入站流量

[root@k8s-master network-policy-demo]# kubectl apply -f policy-demo.yaml 
networkpolicy.networking.k8s.io/deny-all-ingress created
[root@k8s-master network-policy-demo]# kubectl get networkpolicy
NAME               POD-SELECTOR   AGE
deny-all-ingress   <none>         11s

#此時再去訪問測試,是無法ping通,無法訪問的
[root@k8s-master ~]# kubectl run busybox --rm -it --image=busybox /bin/sh
If you don't see a command prompt, try pressing enter.
/ # wget httpd-svc:8080
Connecting to httpd-svc:8080 (10.99.222.179:8080)
wget: can't connect to remote host (10.99.222.179): Connection timed out

如果要將預設策略設定為允許所有入站流量,只需要定義Ingress欄位,並將這個欄位設定為空,以匹配所有源端點,但本身不設定網路策略,就已經是預設允許所有入站流量訪問的,下面給出一個定義的格式:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-all-ingress
spec:
  podSelector: {}
  policyTypes: ["Ingress"]
  ingress:
  - {}

實踐中,通常將預設的網路策略設定為拒絕所有入站流量,然後再放行允許的源端點的入站流量。

2、放行特定的入站流量

[root@k8s-master network-policy-demo]# vim policy-demo.yaml 
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: access-httpd
spec:
  podSelector: 
    matchLabels:
      run: httpd
  policyTypes: ["Ingress"]
  ingress:
  - from:
    - ipBlock:
        cidr: 10.244.0.0/16
        except:
        - 10.244.2.0/24
        - 10.244.1.0/24
    - podSelector:
        matchLabels:
          access: "true"
    ports:
    - protocol: TCP
      port: 80

[root@k8s-master network-policy-demo]# kubectl apply -f policy-demo.yaml 
networkpolicy.networking.k8s.io/access-httpd created
[root@k8s-master network-policy-demo]# kubectl get networkpolicy
NAME           POD-SELECTOR   AGE
access-httpd   run=httpd      6s

驗證NetworkPolicy的有效性:

#建立帶有標籤的busybox pod訪問,是可以正常訪問的,但是因為僅開放了TCP協議,所以PING是無法ping通的
[root@k8s-master ~]# kubectl run busybox --rm -it --labels="access=true" --image=busybox /bin/sh
If you don't see a command prompt, try pressing enter.
/ # wget httpd-svc:8080
Connecting to httpd-svc:8080 (10.99.222.179:8080)
index.html           100% |*********************************************************************************************|    45  0:00:00 ETA
/ # ping -c 3 10.244.0.2
PING 10.244.0.2 (10.244.0.2): 56 data bytes

--- 10.244.0.2 ping statistics ---
3 packets transmitted, 0 packets received, 100% packet loss

2.4、管控出站流量

通常,出站的流量預設策略應該是允許透過的,但是當有精細化需求,僅放行那些有對外請求需要的Pod物件的出站流量,也可以先為名稱空間設定“禁止所有”的預設策略,再細化制定准許的策略。networkpolicy.spec中巢狀的Egress欄位用來定義出站流量規則。

1、設定預設Egress策略

和Igress一樣,只需要透過policyTypes欄位指明生效的Egress型別規則,然後不去定義Egress欄位,就不會去匹配到任何目標端點,從而拒絕所有的出站流量。

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-all-egress
spec:
  podSelector: {}
  policyTypes: ["Egress"]

實踐中,需要進行嚴格隔離的環境通常將預設的策略設定為拒絕所有出站流量,再去細化配置允許到達的目標端點的出站流量。

2、放行特定的出站流量

下面舉個例子定義一個Egress規則,對標籤run=httpd的Pod物件,到達標籤為access=true的Pod物件的80埠的流量進行放行。

[root@k8s-master network-policy-demo]# vim egress-policy.yaml 
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: httpd-egress
spec:
  podSelector: 
    matchLabels:
      run: httpd
  policyTypes: ["Egress"]
  egress:
  - to:
    - podSelector:
        matchLabels:
          access: "true"
    ports:
    - protocol: TCP
      port: 80


#NetworkPolicy檢測,一個帶有access=true標籤,一個不帶
[root@k8s-master ~]# kubectl run busybox --rm -it --labels="access=true" --image=busybox /bin/sh
If you don't see a command prompt, try pressing enter.
/ # wget httpd-svc:8080
Connecting to httpd-svc:8080 (10.99.222.179:8080)
index.html           100% |*********************************************************************************************|    45  0:00:00 ETA
/ # exit
Session ended, resume using 'kubectl attach busybox-686cb649b6-6j4qx -c busybox -i -t' command when the pod is running
deployment.apps "busybox" deleted

[root@k8s-master ~]# kubectl run busybox2 --rm -it --image=busybox /bin/sh
If you don't see a command prompt, try pressing enter.
/ # wget httpd-svc:8080
Connecting to httpd-svc:8080 (10.99.222.179:8080)
wget: can't connect to remote host (10.99.222.179): Connection timed out

從上面的檢測結果可以看到,帶有標籤access=true的Pod才能訪問到httpd-svc,說明上面配置的Network Policy已經生效。

2.5、隔離名稱空間

實踐中,通常需要彼此隔離所有的名稱空間,但是又需要允許它們可以和kube-system名稱空間中的Pod資源進行流量交換,以實現監控和名稱解析等各種管理功能。下面的配置清單示例在default名稱空間定義相關規則,在出站和入站都預設均為拒絕的情況下,它用於放行名稱空間內部的各Pod物件之間的通訊,以及和kube-system名稱空間內各Pod間的通訊。

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: namespace-deny-all
  namespace: default
spec:
  policyTypes: ["Ingress","Egress"]
  podSelector: {}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: namespace-allow
  namespace: default
spec:
  policyTypes: ["Ingress","Egress"]
  podSelector: {}
  ingress:
  - from:
    - namespaceSelector:
        matchExpressions:
        - key: name
          operator: In
          values: ["default","kube-system"]
  egress:
  - to:
    - namespaceSelector:
        matchExpressions:
        - key: name
          operator: In
          values: ["default","kube-system"]

需要注意的是,有一些額外的系統附件可能會單獨部署到獨有的名稱空間中,比如將prometheus監控系統部署到prom名稱空間等,這類具有管理功能的附件所在的名稱空間和每一個特定的名稱空間的出入流量也是需要被放行的。

相關文章