使用來源 IP

在 Kubernetes 叢集中執行的應用程式透過服務抽象化來尋找彼此並與外部世界通訊。本文件說明傳送到不同類型服務的封包來源 IP 會發生什麼情況,以及您如何根據需求切換此行為。

準備開始

術語

本文件使用下列術語

NAT
網路位址轉譯
來源 NAT
取代封包上的來源 IP;在本頁中,這通常表示取代為節點的 IP 位址。
目的地 NAT
取代封包上的目的地 IP;在本頁中,這通常表示取代為 Pod 的 IP 位址
VIP
虛擬 IP 位址,例如指派給 Kubernetes 中每個服務的位址
kube-proxy
在每個節點上協調服務 VIP 管理的網路守護程序

先決條件

您需要有一個 Kubernetes 叢集,而且必須設定 kubectl 命令列工具以與您的叢集通訊。建議在至少有兩個節點且未充當控制平面主機的叢集上執行本教學課程。如果您還沒有叢集,可以使用 minikube 建立一個,或使用這些 Kubernetes 實驗場之一

範例使用小型 nginx 網路伺服器,該伺服器會透過 HTTP 標頭回應它收到的請求來源 IP。您可以如下建立它

kubectl create deployment source-ip-app --image=registry.k8s.io/echoserver:1.10

輸出為

deployment.apps/source-ip-app created

目標

  • 透過各種服務類型公開簡單的應用程式
  • 瞭解每種服務類型如何處理來源 IP NAT
  • 瞭解保留來源 IP 所涉及的權衡

Type=ClusterIP 服務的來源 IP

如果您在 iptables 模式(預設)下執行 kube-proxy,則從叢集內部傳送到 ClusterIP 的封包永遠不會進行來源 NAT。您可以透過擷取執行 kube-proxy 的節點上的 http://localhost:10249/proxyMode 來查詢 kube-proxy 模式。

kubectl get nodes

輸出類似於此

NAME                           STATUS     ROLES    AGE     VERSION
kubernetes-node-6jst   Ready      <none>   2h      v1.13.0
kubernetes-node-cx31   Ready      <none>   2h      v1.13.0
kubernetes-node-jj1t   Ready      <none>   2h      v1.13.0

在其中一個節點上取得 Proxy 模式 (kube-proxy 監聽連接埠 10249)

# Run this in a shell on the node you want to query.
curl http://localhost:10249/proxyMode

輸出為

iptables

您可以透過在來源 IP 應用程式上建立服務來測試來源 IP 保留

kubectl expose deployment source-ip-app --name=clusterip --port=80 --target-port=8080

輸出為

service/clusterip exposed
kubectl get svc clusterip

輸出類似於

NAME         TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)   AGE
clusterip    ClusterIP   10.0.170.92   <none>        80/TCP    51s

並從同一個叢集中的 Pod 點擊 ClusterIP

kubectl run busybox -it --image=busybox:1.28 --restart=Never --rm

輸出類似於此

Waiting for pod default/busybox to be running, status is Pending, pod ready: false
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
    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
3: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1460 qdisc noqueue
    link/ether 0a:58:0a:f4:03:08 brd ff:ff:ff:ff:ff:ff
    inet 10.244.3.8/24 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::188a:84ff:feb0:26a5/64 scope link
       valid_lft forever preferred_lft forever

…然後使用 wget 查詢本機網路伺服器

# Replace "10.0.170.92" with the IPv4 address of the Service named "clusterip"
wget -qO - 10.0.170.92
CLIENT VALUES:
client_address=10.244.3.8
command=GET
...

無論用戶端 Pod 和伺服器 Pod 是否在同一個節點或不同的節點中,client_address 永遠是用戶端 Pod 的 IP 位址。

Type=NodePort 服務的來源 IP

預設情況下,傳送到 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 }')

如果您在雲端提供者上執行,您可能需要為上面報告的 nodes:nodeport 開啟防火牆規則。現在您可以嘗試透過上面配置的節點連接埠從叢集外部連線到服務。

for node in $NODES; do curl -s $node:$NODEPORT | grep -i client_address; done

輸出類似於

client_address=10.180.1.1
client_address=10.240.0.5
client_address=10.240.0.3

請注意,這些不是正確的用戶端 IP,它們是叢集內部 IP。這就是發生的情況

  • 用戶端將封包傳送到 node2:nodePort
  • node2 將封包中的來源 IP 位址 (SNAT) 取代為自己的 IP 位址
  • node2 將封包上的目的地 IP 取代為 Pod IP
  • 封包路由到節點 1,然後路由到端點
  • Pod 的回覆路由回節點 2
  • Pod 的回覆傳回給用戶端

視覺化

source IP nodeport figure 01

圖。使用 SNAT 的來源 IP Type=NodePort

為了避免這種情況,Kubernetes 具有保留用戶端來源 IP的功能。如果您將 service.spec.externalTrafficPolicy 設定為值 Local,kube-proxy 僅將 Proxy 請求 Proxy 到本機端點,而不會將流量轉發到其他節點。此方法保留了原始來源 IP 位址。如果沒有本機端點,則傳送到節點的封包會被丟棄,因此您可以依靠任何封包處理規則中的正確來源 IP,您可能會將封包應用於使其到達端點的封包。

如下設定 service.spec.externalTrafficPolicy 欄位

kubectl patch svc nodeport -p '{"spec":{"externalTrafficPolicy":"Local"}}'

輸出為

service/nodeport patched

現在,重新執行測試

for node in $NODES; do curl --connect-timeout 1 -s $node:$NODEPORT | grep -i client_address; done

輸出類似於

client_address=198.51.100.79

請注意,您只從端點 Pod 執行所在的節點收到一個回覆,其中包含正確的用戶端 IP。

這就是發生的情況

  • 用戶端將封包傳送到 node2:nodePort,該節點沒有任何端點
  • 封包被丟棄
  • 用戶端將封包傳送到 node1:nodePort,該節點確實有端點
  • node1 使用正確的來源 IP 將封包路由到端點

視覺化

source IP nodeport figure 02

圖。來源 IP Type=NodePort 保留用戶端來源 IP 位址

Type=LoadBalancer 服務的來源 IP

預設情況下,傳送到 Type=LoadBalancer 服務的封包會進行來源 NAT,因為處於 Ready 狀態的所有可排程 Kubernetes 節點都有資格參與負載平衡流量。因此,如果封包到達沒有端點的節點,系統會將其 Proxy 到具有端點的節點,並將封包上的來源 IP 取代為節點的 IP(如上一節所述)。

您可以透過透過負載平衡器公開來源 IP 應用程式來測試這一點

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.0.65.118   203.0.113.140     80/TCP    5m

接下來,將請求傳送到此服務的外部 IP

curl 203.0.113.140

輸出類似於此

CLIENT VALUES:
client_address=10.240.0.5
...

但是,如果您在 Google Kubernetes Engine/GCE 上執行,將相同的 service.spec.externalTrafficPolicy 欄位設定為 Local 會強制沒有服務端點的節點透過故意讓健康檢查失敗,將自己從符合負載平衡流量的節點清單中移除。

視覺化

Source IP with externalTrafficPolicy

您可以透過設定註解來測試這一點

kubectl patch svc loadbalancer -p '{"spec":{"externalTrafficPolicy":"Local"}}'

您應該立即看到 Kubernetes 分配的 service.spec.healthCheckNodePort 欄位。

kubectl get svc loadbalancer -o yaml | grep -i healthCheckNodePort

輸出類似於此

  healthCheckNodePort: 32122

service.spec.healthCheckNodePort 欄位指向每個節點上服務於 /healthz 健康檢查的埠口。你可以測試這個。

kubectl get pod -o wide -l app=source-ip-app

輸出類似於此

NAME                            READY     STATUS    RESTARTS   AGE       IP             NODE
source-ip-app-826191075-qehz4   1/1       Running   0          20h       10.180.1.136   kubernetes-node-6jst

使用 curl 來獲取各個節點上的 /healthz 端點。

# Run this locally on a node you choose
curl localhost:32122/healthz
1 Service Endpoints found

在不同的節點上,你可能會得到不同的結果。

# Run this locally on a node you choose
curl localhost:32122/healthz
No Service Endpoints Found

運行在控制平面上的控制器負責分配雲端負載平衡器。 同一個控制器也分配指向每個節點上此埠口/路徑的 HTTP 健康檢查。 等待約 10 秒鐘,讓沒有端點的 2 個節點健康檢查失敗,然後使用 curl 查詢負載平衡器的 IPv4 位址。

curl 203.0.113.140

輸出類似於此

CLIENT VALUES:
client_address=198.51.100.79
...

跨平台支援

只有部分雲端供應商透過 Type=LoadBalancer 的服務提供來源 IP 保留的支援。 您使用的雲端供應商可能會以幾種不同的方式滿足對負載平衡器的請求。

  1. 透過一個代理伺服器終止用戶端連線,並開啟一個到您的節點/端點的新連線。在這種情況下,來源 IP 將始終是雲端 LB 的 IP,而不是用戶端的 IP。

  2. 透過一個封包轉發器,這樣從用戶端發送到負載平衡器 VIP 的請求最終會到達節點,並且來源 IP 是用戶端的 IP,而不是中間代理伺服器的 IP。

第一類中的負載平衡器必須在負載平衡器和後端之間使用約定的協議來傳達真實的用戶端 IP,例如 HTTP ForwardedX-FORWARDED-FOR 標頭,或 proxy protocol。 第二類中的負載平衡器可以利用上述功能,方法是在服務上建立指向儲存在 service.spec.healthCheckNodePort 欄位中的埠口的 HTTP 健康檢查。

清理

刪除服務 (Services)

kubectl delete svc -l app=source-ip-app

刪除 Deployment、ReplicaSet 和 Pod

kubectl delete deployment source-ip-app

接下來是什麼?

最後修改時間:2024 年 9 月 8 日下午 5:21 PST:Update source-ip.md (9c58a926d5)