# 理解 K8s 如何處理不同類型 service 的封包來源 IP 在 Kubernetes 叢集中執行的應用程式會透過服務抽象(Service abstraction)來尋找彼此並與外界溝通。 本文件將說明傳送至不同類型服務的封包的來源 IP 會如何處理,以及您如何根據需求切換此行為。 在 Kubernetes 叢集中運行的應用程式,透過 Service(服務)的抽象概念來找到彼此並進行溝通,也可以和外部世界連接。這份文件將解釋,當封包發送到不同類型的 Service 時,來源 IP(Source IP)會發生什麼變化,以及如何根據需求調整這種行為。 ## 術語 [**NAT**](https://en.wikipedia.org/wiki/Network_address_translation) 網路位址轉換 [**Source NAT**](https://en.wikipedia.org/wiki/Network_address_translation#SNAT) 取代封包上的來源 IP;在本頁中,通常是指取代為 node 的 IP 位址。 [**Destination NAT**](https://en.wikipedia.org/wiki/Network_address_translation#DNAT) 取代封包上的目的地 IP;在本頁中,這通常是指取代為 Pod 的 IP 位址 [**VIP**](https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies) 虛擬 IP 位址,例如指定給 Kubernetes 中每個 Service 的虛擬 IP 位址 [**kube-proxy**](https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies) 一個網路背景程序,負責在每個 node 上協調管理 Service VIP(虛擬 IP)。 ## 目標 - 透過各種不同類型的 Service 將簡單應用程式對外公開 - 瞭解每種 Service 類型如何處理來源 IP 的 NAT(網路地址轉換) - 瞭解在保留來源 IP 時涉及的權衡取捨 ## 建立測試的應用程式 ```! $ kubectl create deployment source-ip-app --image=registry.k8s.io/echoserver:1.10 ``` 執行結果 :  ``` deployment.apps/source-ip-app created ``` ## Source IP for Services with Type=ClusterIP 如果您在叢集中運行 kube-proxy 並使用預設的 **iptables mode**,從叢集內部發送到 **ClusterIP** 的封包,不會進行來源 NAT(Source NAT)。您可以透過查詢 kube-proxy 所在節點上的網址 `http://localhost:10249/proxyMode`,來確認 kube-proxy 的運行模式。 ``` $ curl http://localhost:10249/proxyMode ``` 執行結果 : ``` ipvs ``` > 如果使用 [Cilium 取代 kube-proxy](https://docs.cilium.io/en/stable/network/kubernetes/kubeproxy-free/#kubernetes-without-kube-proxy),則此命令將不適用。 您可以透過為來源 IP 測試應用程式建立一個 Service,來測試來源 IP 的保留功能 : ```! $ kubectl expose deployment source-ip-app --name=clusterip --port=80 --target-port=8080 ``` 執行結果 : ``` service/clusterip exposed ``` 查看 Service 資訊 ``` $ kubectl get svc clusterip ``` 執行結果 : ``` NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE clusterip ClusterIP 172.100.53.4 <none> 80/TCP 28s ``` 在同一個 K8s 叢集中,從某個 Pod 發送請求到 ClusterIP 的 Service ```! $ kubectl run busybox -it --image=busybox:1.28 --restart=Never --rm ``` 輸出與此類似: ``` If you don't see a command prompt, try pressing enter. / # ``` 然後,您可以在 Pod 內執行指令: ``` # Run this inside the terminal from "kubectl run" 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: tunl0@NONE: <NOARP> mtu 1480 qdisc noop qlen 1000 link/ipip 0.0.0.0 brd 0.0.0.0 3: eth0@if14: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1480 qdisc noqueue qlen 1000 link/ether 2e:2b:01:66:08:1b brd ff:ff:ff:ff:ff:ff inet 172.99.236.23/32 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::2c2b:1ff:fe66:81b/64 scope link valid_lft forever preferred_lft forever ``` ...然後使用 `wget` 查詢本機 webserver ```! # Replace "172.100.53.4" with the IPv4 address of the Service named "clusterip" wget -qO - 172.100.53.4 ``` 執行結果 : <pre> ... Request Information: <font color=red>client_address=172.99.236.23</font> method=GET ... </pre> **無論客戶端 Pod 和 server 端 Pod 是否位於同一個節點,`client_address`(客戶端位址)始終會顯示為客戶端 Pod 的 IP 地址。這表示,當您從叢集中的一個 Pod 向 ClusterIP 發送請求時,來源 IP 並未被修改**。 在 node 使用 `wget` 查詢本機 webserver ``` curl 172.100.53.4 ``` 執行結果 : ``` ... Request Information: client_address=172.20.1.11 method=GET ... ``` --- ## Source IP for Services with Type=NodePort 傳送至 `Type=NodePort` 服務的封包預設為來源 NAT。 您可以建立一個 NodePort 服務來測試: ```! $ kubectl expose deployment source-ip-app --name=nodeport --port=80 --target-port=8080 --type=NodePort ``` 執行結果 : ``` service/nodeport exposed ``` 現在您可以嘗試從**群集外部**透過上述分配的節點連接埠連線到服務。 ```! $ NODEPORT=$(kubectl get -o jsonpath="{.spec.ports[0].nodePort}" services nodeport) $ NODES=$(kubectl get nodes -o jsonpath='{ $.items[*].status.addresses[?(@.type=="InternalIP")].address }') $ for node in $NODES; do curl -s $node:$NODEPORT | grep -i client_address; done ``` 輸出類似於: ``` client_address=172.20.22.11 ``` 請注意,這些不是正確的用戶端 IP,而是群集內部 IP。 這就是所發生的事: * 用戶端傳送封包到 `node2:nodePort` * 節點 2 以自己的 IP 位址取代封包中的來源 IP 位址 (SNAT) * 節點 2 以 pod IP 取代封包上的目的地 IP * 封包會傳送到節點 1,然後傳送到端點 * pod 的回覆會傳回到節點 2 * pod 的回覆會傳回到用戶端 ![image](https://hackmd.io/_uploads/BJ86_PaQyg.png) 為了避免這種情況,Kubernetes 有一個保留 Client 端 source IP 的功能。 如果您將 `service.spec.externalTrafficPolicy` 設定為 `Local` 值,kube-proxy 只會 proxies proxy requests to local endpoints,而不會將流量轉送至其他節點。 此方法可保留原始來源 IP 位址。 如果沒有 local endpoints,傳送到節點的封包會被丟棄,因此您可以在任何封包處理規則中依賴正確的原始 IP 位址,這些規則可能會應用於傳送到端點的封包。 設定 `service.spec.externalTrafficPolicy` 欄位如下: ```bash! $ kubectl patch svc nodeport -p '{"spec":{"externalTrafficPolicy":"Local"}}' ``` 輸出類似於: ``` service/nodeport patched ``` 現在,重新執行測試: ```bash! $ for node in $NODES; do curl --connect-timeout 1 -s $node:$NODEPORT | grep -i client_address; done ``` 輸出類似於: ``` client_address=172.20.22.11 ``` 請注意,您只從端點 Pod 執行的節點得到一個回覆,並且是正確的 Client IP。 這就是發生的事情: * 客戶端傳送封包到 `node2:nodePort`,而 `node2:nodePort` 沒有任何 endpoints * 封包被丟棄 * 用戶端傳送封包到 `node1:nodePort`,而 `node1:nodePort` 確實有 endpoints * node1 路由封包到具有正確來源 IP 的端點 ![image](https://hackmd.io/_uploads/HJjUxOTXJx.png) --- ## Source IP for Services with Type=LoadBalancer 傳送至 Type=LoadBalancer 的服務的封包,預設會進行來源 NAT,因為所有處於 Ready 狀態的可排程 Kubernetes 節點,都符合負載平衡流量的資格。 因此,如果封包到達沒有端點的節點,系統會將其代理到有端點的節點,並以節點的 IP 取代封包上的來源 IP (如上一節所述)。 您可以透過負載平衡器暴露來源 IP 應用程式來測試: ```bash! $ kubectl expose deployment source-ip-app --name=loadbalancer --port=80 --target-port=8080 --type=LoadBalancer ``` 輸出為 : ``` service/loadbalancer exposed ``` 列印服務的 IP 位址: ``` $ kubectl get svc loadbalancer ``` 輸出與此類似 : ``` NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE loadbalancer LoadBalancer 10.43.205.52 172.20.22.199 80:30729/TCP 8m24s ``` 接下來,傳送一個要求到此服務的 external-ip: ``` curl 172.20.22.199 ``` 輸出與此類似: ``` client_address=172.20.22.11 ``` 但是,如果您在 Google Kubernetes Engine/GCE 上執行,將相同的 `service.spec.externalTrafficPolicy` 欄位設定為 `Local`,就會強制沒有 Service 端點的節點,透過故意使健康檢查失敗的方式,將自己從符合負載平衡流量資格的節點清單中移除。 ![image](https://hackmd.io/_uploads/B1LW3_67Je.png) 您可以透過設定註解來測試: ```bash! $ kubectl patch svc loadbalancer -p '{"spec":{"externalTrafficPolicy":"Local"}}' ``` 您應該會立即看到 Kubernetes 分配的 `service.spec.healthCheckNodePort` 欄位: ```bash! $ kubectl get svc loadbalancer -o yaml | grep -i healthCheckNodePort ``` 輸出與此類似: ``` healthCheckNodePort: 30865 ``` `service.spec.healthCheckNodePort` 欄位會指向每個節點上為 `/healthz` 提供健康檢查服務的連接埠。 您可以測試這一點: ```bash! $ kubectl get pod -o wide -l app=source-ip-app ``` 輸出與此類似: ``` NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES source-ip-app-75c58c94c6-m57pb 1/1 Running 0 99m 10.42.0.132 antony-rancher <none> <none> ``` 使用 `curl` 在不同節點上取得 `/healthz` endpoint : ``` # Run this locally on a node you choose $ curl localhost:30865/healthz ``` 輸出與此類似: ``` { "service": { "namespace": "default", "name": "loadbalancer" }, "localEndpoints": 1, "serviceProxyHealthy": true } ``` 在不同的節點上,您可能會得到不同的結果: ``` # Run this locally on a node you choose $ curl localhost:30865/healthz ``` 輸出與此類似: ``` No Service Endpoints Found ``` ## Cleaning up ```bash! # Delete the Services: $ kubectl delete svc -l app=source-ip-app # Delete the Deployment, ReplicaSet and Pod: $ kubectl delete deployment source-ip-app ```