控制 Egress 流量

米開朗基楊發表於2019-03-31

原文連結:控制 Egress 流量

本文主要內容來自 Istio 官方文件,並對其進行了大量擴充套件和補充。

預設情況下,Istio 服務網格內的 Pod,由於其 iptables 將所有外發流量都透明的轉發給了 Sidecar,所以這些叢集內的服務無法訪問叢集之外的 URL,而只能處理叢集內部的目標。

本文的任務描述瞭如何將外部服務暴露給 Istio 叢集中的客戶端。你將會學到如何通過定義 ServiceEntry 來呼叫外部服務;或者簡單的對 Istio 進行配置,要求其直接放行對特定 IP 範圍的訪問。

1. 開始之前

$ kubectl apply -f samples/sleep/sleep.yaml
複製程式碼

否則在部署 sleep 應用之前,就需要手工注入 Sidecar:

$ kubectl apply -f <(istioctl kube-inject -f samples/sleep/sleep.yaml)
複製程式碼

實際上任何可以 execcurl 的 Pod 都可以用來完成這一任務。

2. Istio 中配置外部服務

通過配置 Istio ServiceEntry,可以從 Istio 叢集中訪問外部任意的可用服務。這裡我們會使用 httpbin.org 以及 www.baidu.com 進行試驗。

配置外部服務

1. 建立一個 ServiceEntry 物件,放行對一個外部 HTTP 服務的訪問:

$ cat <<EOF | istioctl create -f -
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: httpbin-ext
spec:
  hosts:
  - httpbin.org
  ports:
  - number: 80
    name: http
    protocol: HTTP
EOF
複製程式碼

2. 另外建立一個 ServiceEntry 物件和一個 VirtualService,放行對一個外部 HTTPS 服務的訪問:

$ cat <<EOF | istioctl create -f -
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: baidu
spec:
  hosts:
  - www.baidu.com
  ports:
  - number: 443
    name: https
    protocol: HTTPS
  resolution: DNS
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: baidu
spec:
  hosts:
  - www.baidu.com
  tls:
  - match:
    - port: 443
      sniHosts:
      - www.baidu.com
    route:
    - destination:
        host: www.baidu.com
        port:
          number: 443
      weight: 100
EOF
複製程式碼

發起對外部服務的訪問

使用 kubectl exec 命令進入測試 Pod。假設使用的是 sleep 服務,執行如下命令:

$ export SOURCE_POD=$(kubectl get pod -l app=sleep -o go-template='{{range .items}}{{.metadata.name}}{{end}}')
$ kubectl exec -it $SOURCE_POD -c sleep bash
複製程式碼

發起一個對外部 HTTP 服務的請求:

$ curl http://httpbin.org/headers
複製程式碼

發起一個對外部 HTTPS 服務的請求:

$ curl https://www.baidu.com
複製程式碼

HTTP ServiceEntry 配置深度解析

按照之前的慣例,還是先來解讀一下 HTTP 協議的 ServiceEntry 對映到 Envoy 配置層面具體是哪些內容,這樣才能對 ServiceEntry 有更加深刻的認識。

建立一個 HTTP 協議的 ServiceEntry(不指定 GateWay) 本質上是在服務網格內的所有應用的所有 Pod上建立相應的路由規則和與之對應的 Cluster。指定 GateWay 的 ServiceEntry 遵循的是另一套法則,後面我們再說。

可以通過 istioctl 來驗證一下(以 httpbin-ext 為例):

# 檢視 sleep 的 Pod Name:
$ kubectl get pod -l app=sleep

NAME                     READY     STATUS    RESTARTS   AGE
sleep-5bc866558c-89shb   2/2       Running   0          49m
複製程式碼

檢視路由

$ istioctl pc routes sleep-5bc866558c-89shb --name 80 -o json
複製程式碼
[
    {
        "name": "80",
        "virtualHosts": [
            {
                "name": "httpbin.org:80",
                "domains": [
                    "httpbin.org",
                    "httpbin.org:80"
                ],
                "routes": [
                    {
                        "match": {
                            "prefix": "/"
                        },
                        "route": {
                            "cluster": "outbound|80||httpbin.org",
                            "timeout": "0.000s",
                            "maxGrpcTimeout": "0.000s"
                        },
                        "decorator": {
                            "operation": "httpbin.org:80/*"
                        },
...
複製程式碼

可以看到從 Pod sleep-5bc866558c-89shb 內部對域名 httpbin.org 發起的請求通過 HTTP 路由被定向到叢集 outbound|80||httpbin.orgoutbound 表示這是出站流量

檢視 Cluster

$ istioctl pc clusters sleep-5bc866558c-89shb --fqdn httpbin.org -o json
複製程式碼
[
    {
        "name": "outbound|80||httpbin.org",
        "type": "ORIGINAL_DST",
        "connectTimeout": "1.000s",
        "lbPolicy": "ORIGINAL_DST_LB",
        "circuitBreakers": {
            "thresholds": [
                {}
            ]
        }
    }
]
複製程式碼
  • type : 服務發現型別。ORIGINAL_DST 表示原始目的地型別,大概意思就是:連線進入之前已經被解析為一個特定的目標 IP 地址。這種連線通常是由代理使用 IP table REDIRECT 或者 eBPF 之類的機制轉發而來的。完成路由相關的轉換之後,代理伺服器會將連線轉發到該 IP 地址。httpbin.org 是外網域名,當然可以解析,所以連線進入之前可以被解析為一個特定的目標 IP 地址。Envoy 服務發現型別的詳細解析可以參考:Service discoveryServiceEntry.Resolution 欄位的解析可以參考:ServiceEntry.Resolution

    這裡我簡要說明一下,ServiceEntry 的 resolution 欄位可以取三個不同的值,分別對應 Envoy 中的三種服務發現策略:

    • NONE : 對應於 Envoy 中的 ORIGINAL_DST。如果不指定 resolution 欄位,預設使用這個策略。
    • STATIC : 對應於 Envoy 中的 STATIC。表示使用 endpoints 中指定的靜態 IP 地址作為服務後端。
    • DNS : 對應於 Envoy 中的 STRICT_DNS。表示處理請求時嘗試向 DNS 查詢 IP 地址。如果沒有指定 endpoints,並且沒有使用萬用字元,代理伺服器會使用 DNS 解析 hosts 欄位中的地址。如果指定了 endpoints,那麼指定的地址就會作為目標 IP 地址。
  • lbPolicy : 負載均衡策略。ORIGINAL_DST_LB 表示使用原始目的地的負載均衡策略。具體參考: Load balancing

如果你還部署了 bookinfo 示例應用,可以通過執行 istioctl pc routes <productpage_pod_name> --name 80 -o jsonistioctl pc clusters <productpage_pod_name> --fqdn httpbin.org -o json 來驗證一下,你會發現輸出的結果和上面一模一樣。如果還不放心,可以檢視 bookinfo 應用內的所有 Pod,你會得到相同的答案。至此你應該可以理解在服務網格內的所有應用的所有 Pod上建立相應的路由規則和與之對應的 Cluster這句話的含義了。

HTTPS ServiceEntry 配置深度解析

HTTPS 協議的 ServiceEntry 與 Envoy 配置檔案的對映關係與 HTTP 協議有所不同。

建立一個 HTTPS 協議的 ServiceEntry(不指定 GateWay) 本質上是在服務網格內的所有應用的所有 Pod上建立相應的監聽器和與之對應的 Cluster。指定 GateWay 的 ServiceEntry 我會另行發文詳述。

可以通過 istioctl 來驗證(以 baidu 為例)。為了更精確地分析該 ServiceEntry,可以先把 VirtualService 刪除:

$ istioctl delete virtualservice baidu
複製程式碼

檢視監聽器:

$ istioctl pc listeners sleep-5bc866558c-89shb --address 0.0.0.0 --port 443 -o json
複製程式碼
[
    {
        "name": "0.0.0.0_443",
        "address": {
            "socketAddress": {
                "address": "0.0.0.0",
                "portValue": 443
            }
        },
        "filterChains": [
            {
                "filters": [
                    ...
                    {
                        "name": "envoy.tcp_proxy",
                        "config": {
                            "cluster": "outbound|443||www.baidu.com",
                            "stat_prefix": "outbound|443||www.baidu.com"
                        }
                    }
...
複製程式碼
  • name : 監聽器過濾器的名稱。該欄位的值必須與 Envoy 所支援的過濾器匹配,不可隨意填寫,具體參考:listener.Filter。此處 envoy.tcp_proxy 表示使用 TCP 代理,而 TCP 代理是無法基於路由過濾的,所以這裡不會建立路由規則,而是直接將請求轉到 Cluster

檢視 Cluster:

$ istioctl pc clusters sleep-5bc866558c-89shb --fqdn www.baidu.com -o json
複製程式碼
[
    {
        "name": "outbound|443||www.baidu.com",
        "type": "STRICT_DNS",
        "connectTimeout": "1.000s",
        "hosts": [
            {
                "socketAddress": {
                    "address": "www.baidu.com",
                    "portValue": 443
                }
            }
        ],
        "circuitBreakers": {
            "thresholds": [
                {}
            ]
        },
        "dnsLookupFamily": "V4_ONLY"
    }
]
複製程式碼

從監聽器的配置來看,由於繫結的是 0.0.0.0,而且也沒有指定域名,看起來應該可以訪問叢集外任何 443 埠的服務。實際上這是行不通的,因為當請求通過監聽器轉到 Cluster 之後,由於 Cluster 採用的是嚴格的 DNS 服務發現策略,只要域名不是 www.baidu.com,都不會解析。你可以使用 kubectl exec 命令進入 sleep Pod 來測試一下:

$ kubectl exec -it $SOURCE_POD -c sleep bash
複製程式碼

發起對外部 HTTPS 服務的請求:

$ curl https://www.163.com
curl: (51) SSL: no alternative certificate subject name matches target host name 'www.163.com'

$ curl https://www.taobao.com
curl: (51) SSL: no alternative certificate subject name matches target host name 'www.taobao.com'

$ curl https://192.192.192.192
curl: (51) SSL: certificate subject name 'baidu.com' does not match target host name '192.192.192.192'
複製程式碼

而如果你將服務發現策略改為 NONE,就會發現除了可以訪問 www.baidu.com,還可以訪問 www.163.comwww.taobao.com 等其他 https 協議的網站,至於為什麼會這樣,前面介紹服務發現策略的時候我已經詳細解釋過了。

TLS VirtualService 配置深度解析

關於 VirtualService 的解析之前的文章已有相關說明,不過這裡的 VirtualService 與之前遇到的不同,涉及到了 TLSRoute

  • tls : 透傳 TLS 和 HTTPS 流量。TLS 路由通常應用在 https-tls- 字首的平臺服務埠,或者經 Gateway 透傳的 HTTPS、TLS 協議 埠,以及使用 HTTPS 或者 TLS 協議的 ServiceEntry 埠上。具體參考:TLSRoute
    • sniHosts : 必要欄位。要匹配的 SNI(伺服器名稱指示)。可以在 SNI 匹配值中使用萬用字元。比如 *.com 可以同時匹配 foo.example.comexample.com
  • route : 流量的轉發目標。目前 TLS 服務只允許一個轉發目標(所以權重必須設定為 100)。當 Envoy 支援 TCP 權重路由之後,這裡就可以使用多個目標了。

檢視對映到 Envoy 中的配置:

$ istioctl pc listeners sleep-5bc866558c-89shb --address 0.0.0.0 --port 443 -o json
複製程式碼
[
    {
        "name": "0.0.0.0_443",
        "address": {
            "socketAddress": {
                "address": "0.0.0.0",
                "portValue": 443
            }
        },
        "filterChains": [
            {
                "filterChainMatch": {
                    "serverNames": [
                        "www.baidu.com"
                    ]
                },
                "filters": [
                    ...
                    {
                        "name": "envoy.tcp_proxy",
                        "config": {
                            "cluster": "outbound|443||www.baidu.com",
                            "stat_prefix": "outbound|443||www.baidu.com"
                        }
                    }
...
複製程式碼

最後我們來思考一下:既然不建立 TLS VirtualService 也可以訪問 www.baidu.com,那麼建立 TLS VirtualService 和不建立 TLS VirtualService 有什麼區別呢?正確答案是:沒有關聯 VirtualServicehttps- 或者 tls- 埠流量會被視為透傳 TCP 流量,而不是透傳 TLS 和 HTTPS 流量。

為外部服務設定路由規則

通過 ServiceEntry 訪問外部服務的流量,和網格內流量類似,都可以進行 Istio 路由規則 的配置。下面我們使用 istioctl 為 httpbin.org 服務設定一個超時規則。

1. 在測試 Pod 內部,呼叫 httpbin.org 這一外部服務的 /delay 端點:

$ kubectl exec -it $SOURCE_POD -c sleep bash
$ time curl -o /dev/null -s -w "%{http_code}\n" http://httpbin.org/delay/5

200

real    0m5.024s
user    0m0.003s
sys     0m0.003s
複製程式碼

這個請求會在大概五秒鐘左右返回一個內容為 200 (OK) 的響應。

2. 退出測試 Pod,使用 istioctl 為 httpbin.org 外部服務的訪問設定一個 3 秒鐘的超時:

cat <<EOF | istioctl create -f -
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: httpbin-ext
spec:
  hosts:
    - httpbin.org
  http:
  - timeout: 3s
    route:
      - destination:
          host: httpbin.org
        weight: 100
EOF
複製程式碼

3. 等待幾秒鐘之後,再次發起 curl 請求:

$ kubectl exec -it $SOURCE_POD -c sleep bash
$ time curl -o /dev/null -s -w "%{http_code}\n" http://httpbin.org/delay/5

504

real    0m3.149s
user    0m0.004s
sys     0m0.004s
複製程式碼

這一次會在 3 秒鐘之後收到一個內容為 504 (Gateway Timeout) 的響應。雖然 httpbin.org 還在等待他的 5 秒鐘,Istio 卻在 3 秒鐘的時候切斷了請求。

3. 直接呼叫外部服務

如果想要跳過 Istio,直接訪問某個 IP 範圍內的外部服務,就需要對 Envoy sidecar 進行配置,阻止 Envoy 對外部請求的劫持。可以在 Helm 中設定 global.proxy.includeIPRanges 變數,然後使用 kubectl apply 命令來更新名為 istio-sidecar-injectorConfigmap。在 istio-sidecar-injector 更新之後,global.proxy.includeIPRanges 會在所有未來部署的 Pod 中生效。

使用 global.proxy.includeIPRanges 變數的最簡單方式就是把內部服務的 IP 地址範圍傳遞給它,這樣就在 Sidecar proxy 的重定向列表中排除掉了外部服務的地址了。

內部服務的 IP 範圍取決於叢集的部署情況。例如你的叢集中這一範圍是 10.0.0.1/24,這個配置中,就應該這樣更新 istio-sidecar-injector:

$ helm template install/kubernetes/helm/istio <安裝 Istio 時所使用的引數> --set global.proxy.includeIPRanges="10.0.0.1/24" -x templates/sidecar-injector-configmap.yaml | kubectl apply -f -
複製程式碼

注意這裡應該使用和之前部署 Istio 的時候同樣的 Helm 命令,尤其是 --namespace 引數。在安裝 Istio 原有命令的基礎之上,加入 --set global.proxy.includeIPRanges="10.0.0.1/24" -x templates/sidecar-injector-configmap.yaml 即可。

然後和前面一樣,重新部署 sleep 應用。更新了 ConfigMap istio-sidecar-injector 並且重新部署了 sleep 應用之後,Istio sidecar 就應該只劫持和管理叢集內部的請求了。任意的外部請求都會簡單的繞過 Sidecar,直接訪問目的地址。

$ export SOURCE_POD=$(kubectl get pod -l app=sleep -o go-template='{{range .items}}{{.metadata.name}}{{end}}')
$ kubectl exec -it $SOURCE_POD -c sleep curl http://httpbin.org/headers
複製程式碼

4. 總結

這個任務中,我們使用兩種方式從 Istio 服務網格內部來完成對外部服務的呼叫:

  1. 使用 ServiceEntry (推薦方式)
  2. 配置 Istio sidecar,從它的重定向 IP 表中排除外部服務的 IP 範圍

第一種方式(ServiceEntry)中,網格內部的服務不論是訪問內部還是外部的服務,都可以使用同樣的 Istio 服務網格的特性。我們通過為外部服務訪問設定超時規則的例子,來證實了這一優勢。

第二種方式越過了 Istio sidecar proxy,讓服務直接訪問到對應的外部地址。然而要進行這種配置,需要了解雲供應商特定的知識和配置。

5. 清理

1. 刪除規則:

$ istioctl delete serviceentry httpbin-ext baidu
$ istioctl delete virtualservice httpbin-ext baidu
複製程式碼

2. 停止 sleep 服務:

$ kubectl delete -f samples/sleep/sleep.yaml
複製程式碼

相關文章