# 理解 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 的回覆會傳回到用戶端  為了避免這種情況,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 的端點  --- ## 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 端點的節點,透過故意使健康檢查失敗的方式,將自己從符合負載平衡流量資格的節點清單中移除。  您可以透過設定註解來測試: ```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 ```
×
Sign in
Email
Password
Forgot password
or
Sign in via Google
Sign in via Facebook
Sign in via X(Twitter)
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
Continue with a different method
New to HackMD?
Sign up
By signing in, you agree to our
terms of service
.