Service Mesh · Istio · 以實踐入門

  • 2020 年 2 月 25 日
  • 筆記

Photo @ Jez Timms

文 | 三辰

前言


本文是筆者在學習官方文檔、相關部落格文章和實踐過程中,整理了一些知識概念和自己的思考,主要在探索 lstio 的實際應用場景, Sidecar 原理, Service Mesh 為什麼出現、要解決什麼問題等,幫助我們思考微服務技術架構的升級和落地的可行性。

本文不是 Istio 的全部,但是希望入門僅此一篇就夠。

概念


圍繞雲原生(CN)的概念,給人一種知識大爆炸的感覺,但假如你深入了解每一個概念的細節,你會發現它和你很近,甚至就是你手裡每天做的事情。

圖片來源:https://landscape.cncf.io/

關鍵詞:Service Mesh、Istio、Sidecar、Envoy 等。

服務網格

服務網格( Service Mesh )是一個新瓶裝舊酒的概念,它的發展隨著微服務興起,必然是早於 Kubernates 出現了。但 Kubernates 和 Istio 的出現,促使它成為了一種更火更標準化的概念。

Sidecar 是服務網格技術中常用的(其中)一種設計架構,在 Kubernates 中,不同的容器允許被運行在同一個 Pod 中(即多個進程運行在同一個 cgroup 下),這在很大程度上給 Sidecar 模式提供了良好的土壤。

首先看看 Sidecar 的設計:

圖片來源於網路

為什麼是新瓶舊酒?任何技術的發展都不是憑空地跳躍式發展的。

歷史

原始的應用程式–圖片來源於網路

獨立的網路層–圖片來源於網路

出現網路層(4層協議)控制的需求–圖片來源於網路

控制邏輯下移到網路層–圖片來源於網路

早期,應用程式隨著功能迭代發展,尤其是一個大型項目,程式堆積了越來越多的功能,功能之間緊密耦合在一起,變得越來越難以維護(因為模組耦合度較高,沒有人敢動古老的模組程式碼),迭代周期變長(工程複雜度成幾何增長)。

於是,人們提出,將不同的功能分離到不同的程式(進程)中,減低模組的耦合度,敏捷開發迭代,這就是微服務概念的興起。

出現新的應用層(7層協議)需求(服務發現、熔斷、超時重試等)–圖片來源於網路

封裝成三方庫(服務發現:Dubbo/HSF)–圖片來源於網路

困難:

服務被拆分成眾多的微服務,最困難的問題就是——調用它自己:

1、原本在進程中互相調用那麼簡單的事情,都要變成一次在 7 層網路上的遠程調用。

2、原本公共工具類做的事情,現在需要寫成二方庫 SDK 等,在每一個進程中使用,版本迭代成為了災難。

3、原本是內部透明調用的不需要做任何防護,分離後卻要額外增加安全防護和隔離的工作。

4、不再是程式碼即文檔,需要維護大量的 API 定義和版本管理。

封裝到隔離的進程中代理–圖片來源於網路

到這裡,獨立進程的方式基本成型,即為Sidecar模式。

Sidecar 解決什麼問題?

這裡有個服務網格里的概念:微服務之間的調用,一般在架構圖中是橫向的,被稱為東西流量。服務暴露到外部被公網可見的外部調用,被稱為南北流量。

Sidecar 的設計就是為了解決微服務互相調用(東西流量)的問題。

先來一張我們比較熟悉的圖:

圖片來源於網路

Consumer 與 Provider 就是微服務互相調用的一種解決方案。

毫無疑問,我們熟知的一整套中間件解決方案,解決的正是東西流量的問題,圖為Dubbo 架構。

只不過, Dubbo 中間件一整套組件是基於 SPI 機制以一種較為隔離的方式侵入到運行時的程式碼中。並且,這隻能限定 Java 這樣被官方支援的語言來開發服務應用。

小結

歸納一下與東西流量有關的問題:

流量管理(服務發現、負載均衡、路由、限流、熔斷、容錯等)、可觀測性(監控、日誌聚合、計量、跟蹤)、安全(認證、授權),再甚至更高級的動態配置、故障注入、鏡像流量等

相比來說, Sidecar 的模式更為巧妙並更進一步。通過容器機制,在進程上是隔離的,基於 L7 代理進行通訊,允許微服務是由任何語言進行開發的。

圖片來源於網路

以下是微服務集群基於Sidecar互相通訊的簡化場景:

圖片來源於網路

所以說,回到服務網格的概念上來,雖然概念是不同的,但是邏輯上我們可以理解成:所有使用中間件的服務組成了一個大的服務網格,這樣可以幫助我們理解。服務網格基於 Kubernates 這樣的容器技術,將東西流量的問題解決得更加透明無感。

一句話總結,服務網格( Service Mesh )是解決微服務之間的網路問題和可觀測性問題的(事實)標準,並且正在走向標準化。

Service Mesh 是 Kubernetes 支撐微服務能力拚圖的最後一塊

Istio 和 Envoy

Istio,第一個字母是(ai)。

Istio 實現的服務網格分為數據平面和控制平面。核心能力包括4大塊:

1、流量控制(Traffic Management)。

2、安全(Security)。

3、動態規則(Policy)。

4、可觀測能力(Observability)。

Envoy 面向數據平面,也就是服務之間調用的代理。

Envoy 是 Istio Service Mesh 中默認的 Sidecar 方案。

Istio 在 Enovy 的基礎上按照 Envoy 的 xDS 協議擴展了其控制平面。

Istio基於Envoy實現Service Mesh數據平面–圖片來源於網路

Envoy角色–圖片來源於網路

Envoy 是一個由 C++ 實現的高性能代理,與其等價的,還有 Nginx、Traefik ,這就不難理解了。

也就是下圖中的 Proxy :

圖片來源於Istio官網

Istio 在控制平面上主要解決流量管理、安全、可觀測性三個方面的問題,也就是前面提到的東西流量相關的問題。類似一個有配置中心的微服務集群架構。具體細節不在這裡贅述。

Sidecar注入

前面在介紹服務網格時,只是簡單地提到Sidecar設計在其中的作用和特性,這裡詳細展開介紹其中的原理。

首先是一些預備概念:

1、Sidecar 模式:容器應用模式之一,Service Mesh 架構的一種實現方式

2、Init 容器:Pod 中的一種專用的容器,在應用程式容器啟動之前運行,用來包含一些應用鏡像中不存在的實用工具或安裝腳本。

3、iptables:流量劫持是通過 iptables 轉發實現的。

Sidecar 模式解決微服務之間的網路通訊(遠程調用),通常通訊層的實現方式,有以下選擇:

1、在微服務應用程式中導入 SDK 類庫。

2、節點代理(使用縱向的API網關或者是本地 Agent ),代理介面的調用路由規則,轉發到特定的機器。

3、用 Sidecar 容器的形式運行,和應用容器一同運行,透明地劫持所有應用容器的出入流量。

SDK 庫的方式是很自然的,並且調用方式是進程內的,沒有安全隔離的包袱。但是隨著程式語言的發展,很多新的語言為特定的場景而生,而SDK庫的方式限制了使用方必須用支援列表中的語言。

節點代理的方式,是使用一個特定的服務專門代理微服務中的請求,是一個中間人的角色。但這個代理人的安全性要求非常高,因為它需要處理來自不同微服務的請求,並鑒別它們各自的身份。

Sidecar 模型是介於 SDK 庫和節點代理中間的一種形式,相當於是給每個微服務都配上一個自己獨有的代理。這樣,每個微服務自己的 Sidecar 就代表了自己特定的身份,有利於調用的安全審計。因此,從外部看, Sidecar 與其附屬的應用程式具有相同的許可權。

圖片來源:https://toutiao.io/posts/uva4uy/preview

以 Istio 為例:

在 Istio 中, Sidecar 模式啟動時會首先執行一個init 容器 istio-init ,容器只做一件事情,通過 iptables 命令配置 Pod 的網路路由規則,讓 Envoy 代理可以攔截所有的進出 Pod 的流量。

之後,微服務應用通過 Pod 中共享的網路命名空間內的 loopback ( localhost )與 Sidecar 通訊。而外部流量也會通過 Sidecar 處理後,傳入到微服務。

因為它們共享一個 Pod ,對其他 Pod 和節點代理都是不可見的,可以理解為兩個容器共享存儲、網路等資源,可以廣義的將這個注入了 Sidecar 容器的 Pod 理解為一台主機,兩個容器共享主機資源。

下圖是具體 iptables 與 Sidecar 之間互作用原理,來源:

https://jimmysong.io/posts/envoy-sidecar-injection-in-istio-service-mesh-deep-dive/

具體原理上的細節,我們可以通過實踐,慢慢挖掘。

小結

最後給概念章節有個階段性的總結:

圖片來源於網路

所以我們打算賣什麼?

實踐


鋪墊這麼多概念,我們可以實操起來了。具體怎麼做?從安裝 Istio 開始。

準備工作

首先,預備一個Kubernates集群,這裡不贅述。

如果是本地測試,Docker-Desktop也可以啟動一個單機的k8s集群

裝 Istio 的命令行工具 istioctl :

下載 istio-release(包括 istioctl 、示例文件和安裝配置)。


curl -sL "https://github.com/istio/istio/releases/download/1.4.2/istio-1.4.2-osx.tar.gz" | tar xz

安裝 helm (可選):

從 1.4.0 版本開始,不再使用 helm 來安裝 Istio


# helm工具  $ brew install kubernetes-helm

安裝Istio

進入到安裝文件的目錄,開始將 Istio 安裝到 k8s 上。

首先確認 kubectl 連接的正確的 k8s 集群。

選擇以下其中一種方式:

方式1、使用 istioctl 安裝


cd istio-1.4.2  # 安裝istioctl  cp bin/istioctl /usr/local/bin/ # 也可以加一下PATH  # (可選)先查看配置文件  istioctl manifest generate --set profile=demo > istio.demo.yaml  # 安裝istio  istioctl manifest apply --set profile=demo  ## 以下是舊版本istio的helm安裝方式 ##  # 創建istio專屬的namespace  kubectl create namespace istio-system  # 通過helm初始化istio  helm template install/kubernetes/helm/istio-init --name istio-init --namespace istio-system | kubectl apply -f -  # 通過helm安裝istio的所有組件  helm template install/kubernetes/helm/istio --name istio --namespace istio-system | kubectl apply -f -

方式2、使用 helm 安裝


## 以下是舊版本istio的helm安裝方式 ##  # 創建istio專屬的namespace  kubectl create namespace istio-system  # 通過helm初始化istio  helm template install/kubernetes/helm/istio-init --name istio-init --namespace istio-system | kubectl apply -f -  # 通過helm安裝istio的所有組件  helm template install/kubernetes/helm/istio --name istio --namespace istio-system | kubectl apply -f -

等待所有的 Istio 組件的容器啟動,直到:


$ kubectl get crds | grep 'istio.io' | wc -l  23

如果是阿里雲ACS集群,安裝完Istio後,會有對應的一個SLB被創建出來,轉發到Istio提供的虛擬伺服器組

示例:Hello World

示例程式碼在源碼的 samples 目錄中

cd samples/hello-world    

注入

Istio Sidecar 的注入有兩種方式:自動、手動。

這裡先通過 istioctl 命令直接手工inject:


istioctl kube-inject -f helloworld.yaml -o helloworld-istio.yaml
實際上就是通過腳本修改了原文件,增加了:

1、sidecar init容器。

2、istio proxy sidecar容器。

分析

我們可以簡單對比一下注入的配置,原文件:


apiVersion: v1  kind: Service  metadata:    name: helloworld    labels:      app: helloworld  spec:    ports:    - port: 5000      name: http    selector:      app: helloworld  ---  apiVersion: apps/v1  kind: Deployment  metadata:    creationTimestamp: null    labels:      version: v1    name: helloworld-v1  spec:    replicas: 1    selector:      matchLabels:        app: helloworld        version: v1    strategy: {}    template:      metadata:        labels:          app: helloworld          version: v1      spec:        containers:        - image: docker.io/istio/examples-helloworld-v1          imagePullPolicy: IfNotPresent          name: helloworld          ports:          - containerPort: 5000          resources:            requests:              cpu: 100m  ---  apiVersion: apps/v1  kind: Deployment  metadata:    creationTimestamp: null    labels:      version: v2    name: helloworld-v2  spec:    replicas: 1    selector:      matchLabels:        app: helloworld        version: v2    strategy: {}    template:      metadata:        labels:          app: helloworld          version: v2      spec:        containers:        - image: docker.io/istio/examples-helloworld-v2          imagePullPolicy: IfNotPresent          name: helloworld          ports:          - containerPort: 5000          resources:            requests:              cpu: 100m

可以看到,需要部署兩個版本 helloworld-v1/v2 的容器,掛載在同一個服務下。

這是一個典型的藍綠部署方式,後續我們可以通過配置 Istio ,來調整它們的流量權重,這是真實生產環境版本升級的場景。

再來看增加的部分:

這裡增加了一部分 Istio 的配置,是 K8s 中的標準做法 annotations 。

這部分可以看到,原有的服務容器沒有任何改動,只是增加了一個sidecar容器,包括啟動參數和環境變數(因為配置排序的問題, args 排在了最前面,整體的定義:


 - args:          - proxy          - sidecar          - ...          env:          - name: POD_NAME            valueFrom:              fieldRef:                fieldPath: metadata.name          - ...          image: docker.io/istio/proxyv2:1.3.2          imagePullPolicy: IfNotPresent          name: istio-proxy          ports:          - containerPort: 15090            name: http-envoy-prom            protocol: TCP          readinessProbe:            failureThreshold: 30            httpGet:              path: /healthz/ready              port: 15020            initialDelaySeconds: 1            periodSeconds: 2          resources:            limits:              cpu: "2"              memory: 1Gi            requests:              cpu: 100m              memory: 128Mi          securityContext:            readOnlyRootFilesystem: true            runAsUser: 1337          volumeMounts:          - mountPath: /etc/istio/proxy            name: istio-envoy          - mountPath: /etc/certs/            name: istio-certs            readOnly: true
鏡像名 docker.io/istio/proxyv2:1.3.2  。

另外一部分,就是 initContainer :


 initContainers:        - args:          - -p          - "15001"          - -z          - "15006"          - -u          - "1337"          - -m          - REDIRECT          - -i          - '*'          - -x          - ""          - -b          - '*'          - -d          - "15020"          image: docker.io/istio/proxy_init:1.3.2          imagePullPolicy: IfNotPresent          name: istio-init          resources:            limits:              cpu: 100m              memory: 50Mi            requests:              cpu: 10m              memory: 10Mi          securityContext:            capabilities:              add:              - NET_ADMIN            runAsNonRoot: false            runAsUser: 0        volumes:        - emptyDir:            medium: Memory          name: istio-envoy        - name: istio-certs          secret:            optional: true            secretName: istio.default

部署


$ kubectl apply -f helloworld-istio.yaml  service/helloworld created  deployment.apps/helloworld-v1 created  deployment.apps/helloworld-v2 created  $ kubectl get deployments.apps -o wide  NAME            READY   UP-TO-DATE   AVAILABLE   AGE   CONTAINERS               IMAGES                                                                 SELECTOR  helloworld-v1   1/1     1            1           20m   helloworld,istio-proxy   docker.io/istio/examples-helloworld-v1,docker.io/istio/proxyv2:1.3.2   app=helloworld,version=v1  helloworld-v2   1/1     1            1           20m   helloworld,istio-proxy   docker.io/istio/examples-helloworld-v2,docker.io/istio/proxyv2:1.3.2   app=helloworld,version=v2      並啟用一個簡單的gateway來監聽,便於我們訪問測試頁面$ kubectl apply -f helloworld-gateway.yaml  gateway.networking.istio.io/helloworld-gateway created  virtualservice.networking.istio.io/helloworld created      部署完成之後,我們就可以通過gateway訪問hello服務了:$ curl "localhost/hello"  Hello version: v2, instance: helloworld-v2-7768c66796-hlsl5  $ curl "localhost/hello"  Hello version: v2, instance: helloworld-v2-7768c66796-hlsl5  $ curl "localhost/hello"  Hello version: v1, instance: helloworld-v1-57bdc65497-js7cm

兩個版本的 Deployment 都可以隨機被訪問到

深入探索


接著剛才我們部署好的 hello-world ,我們隨著Istio的feature進行探索。

流量控制 – 切流

首先,我們嘗試控制一下流量,比如只走v2。參考Traffic Shifting:

https://istio.io/docs/tasks/traffic-management/traffic-shifting/

我們可以通過 VirtualService 配置控制版本流量,詳情參考:

https://istio.io/docs/reference/config/networking/v1alpha3/virtual-service/

先查看一下當前 Gateway 和 VirtualService 的配置:


$ kubectl get gw helloworld-gateway -o yaml  apiVersion: networking.istio.io/v1alpha3  kind: Gateway  metadata:    name: helloworld-gateway  spec:    selector:      istio: ingressgateway # use istio default controller    servers:    - port:        number: 80        name: http        protocol: HTTP      hosts:      - "*"  $ kubectl get vs helloworld -o yaml  apiVersion: networking.istio.io/v1alpha3  kind: VirtualService  metadata:    name: helloworld  spec:    hosts:    - "*"    gateways:    - helloworld-gateway    http:    - match:      - uri:          exact: /hello      route:      - destination:          host: helloworld  # short for helloworld.${namespace}.svc.cluster.local          port:            number: 5000

可以看到,VS 轉發 /hello 路徑的請求到 helloworld:5000 ,不過,這種 short 寫法不推薦。我們可以改成 helloworld.${namespace}.svc.cluster.local 。

我們將其中 VirtualService 的配置修改為:


apiVersion: networking.istio.io/v1alpha3  kind: VirtualService  metadata:    name: helloworld  spec:    hosts:    - "*"    gateways:    - helloworld-gateway    http:    - match:      - uri:          exact: /hello      route:      - destination:          host: helloworld.default.svc.cluster.local          subset: v1          weight: 0      - destination:          host: helloworld.default.svc.cluster.local          subset: v2          weight: 100

在 http.route 里增加一個 destination ,並將 v2 的 weight 權重配置到100 。

並增加一個 DestinationRule 對 subset 進行定義。


apiVersion: networking.istio.io/v1alpha3  kind: DestinationRule  metadata:    name: helloworld-destination  spec:    host: helloworld.default.svc.cluster.local    subsets:    - name: v1      labels:        version: v1    - name: v2      labels:        version: v2

然後應用更新:


$ kubectl apply -f helloworld-gateway.yaml  gateway.networking.istio.io/helloworld-gateway unchanged  virtualservice.networking.istio.io/helloworld configured  destinationrule.networking.istio.io/helloworld-destination created

測試一下效果:


$ while true;do sleep 0.05 ;curl localhost/hello;done  Hello version: v2, instance: helloworld-v2-76d6cbd4d-tgsq6  Hello version: v2, instance: helloworld-v2-76d6cbd4d-tgsq6  Hello version: v2, instance: helloworld-v2-76d6cbd4d-tgsq6  Hello version: v2, instance: helloworld-v2-76d6cbd4d-tgsq6  Hello version: v2, instance: helloworld-v2-76d6cbd4d-tgsq6  Hello version: v2, instance: helloworld-v2-76d6cbd4d-tgsq6  Hello version: v2, instance: helloworld-v2-76d6cbd4d-tgsq6  Hello version: v2, instance: helloworld-v2-76d6cbd4d-tgsq6  Hello version: v2, instance: helloworld-v2-76d6cbd4d-tgsq6  Hello version: v2, instance: helloworld-v2-76d6cbd4d-tgsq6  Hello version: v2, instance: helloworld-v2-76d6cbd4d-tgsq6  Hello version: v2, instance: helloworld-v2-76d6cbd4d-tgsq6  Hello version: v2, instance: helloworld-v2-76d6cbd4d-tgsq6  Hello version: v2, instance: helloworld-v2-76d6cbd4d-tgsq6  Hello version: v2, instance: helloworld-v2-76d6cbd4d-tgsq6  Hello version: v2, instance: helloworld-v2-76d6cbd4d-tgsq6  Hello version: v2, instance: helloworld-v2-76d6cbd4d-tgsq6  Hello version: v2, instance: helloworld-v2-76d6cbd4d-tgsq6  Hello version: v2, instance: helloworld-v2-76d6cbd4d-tgsq6  Hello version: v2, instance: helloworld-v2-76d6cbd4d-tgsq6  Hello version: v2, instance: helloworld-v2-76d6cbd4d-tgsq6

流量完美切走。不過,到目前為止我們只接觸了Gateway 、VirtualService和DestinationRule。我們來回顧一下:

Gateway

Gateway 用於處理服務網格的邊界,定義了出入負載的域名、埠、協議等規則。

VirtualService

VirtualService 可以控制路由(包括subset/version權重、匹配、重定向等)、故障注入、TLS 。

DestinationRule

DestinationRule 定義確定路由的細節規則,比如 subset 定義、負載均衡的策略,甚至可以針對特定的埠再重新定義規則。

示例:Bookinfo

前面的例子,通過控制流量權重達到版本切流的目的。

下面,我們再通過另外一個 Bookinfo 的例子繼續探索其它Istio的feature。

圖片來源於 Istio 官網

本例是一個多實例微服務架構,並且由不同語言開發。

開始


$ cd samples/bookinfo

注入

這次Pod定義比較多,我們打開auto sidecar-injection


$ kubectl label namespace default istio-injection=enabled

打開之後,每次創建的Pod都會自動注入上istio-proxy和相應的initContainer

部署


$ kubectl apply -f platform/kube/bookinfo.yaml  service/details created  serviceaccount/bookinfo-details created  deployment.apps/details-v1 created  service/ratings created  serviceaccount/bookinfo-ratings created  deployment.apps/ratings-v1 created  service/reviews created  serviceaccount/bookinfo-reviews created  deployment.apps/reviews-v1 created  deployment.apps/reviews-v2 created  deployment.apps/reviews-v3 created  service/productpage created  serviceaccount/bookinfo-productpage created  deployment.apps/productpage-v1 created

創建一個Gateway用於查看頁面:


$ kubectl apply -f networking/bookinfo-gateway.yaml  gateway.networking.istio.io/bookinfo-gateway created  virtualservice.networking.istio.io/bookinfo created

訪問 http://localhost/productpage 頁面:

不斷刷新可以看到右側Reviews有三個版本:

流量控制 – 網路可見性

基於前面安裝好的 Bookinfo 應用,起一個 Pod 探索一下網路可見性:


$ kubectl run --image centos:7 -it probe  # 請求productpage服務上的介面  [root@probe-5577ddd7b9-rbmh7 /]# curl -sL http://productpage:9080 | grep -o "<title>.*</title>"  <title>Simple Bookstore App</title>  $ kubectl exec -it $(kubectl get pod -l run=probe -o jsonpath='{..metadata.name}') -c probe -- curl www.baidu.com | grep -o "<title>.*</title>"  <title>百度一下,你就知道</title>

我們可以看到,默認情況下,所有的微服務(容器)之間都是公開可訪問的,並且微服務可以訪問外部網路。

接下來,介紹 Sidecar 配置對可見性進行控制。

Sidecar

由於每個容器都自動注入了Sidecar容器,託管了所有的出入請求。所以基於這個 Sidecar 容器,我們可以很容易對它進行配置。

Sidecar 配置就是 Istio 中專門用於配置 sidecar 之間的網路可見性。

首先,修改全局配置,使用 blocked-by-default 的方式。


$ kubectl get configmap istio -n istio-system -o yaml | sed 's/mode: ALLOW_ANY/mode: REGISTRY_ONLY/g' | kubectl replace -n istio-system -f -  configmap "istio" replaced  $ kubectl get configmap istio -n istio-system -o yaml | grep -n1 -m1 "mode: REGISTRY_ONLY"  67-    outboundTrafficPolicy:  68:      mode: REGISTRY_ONLY

outboundTrafficPolicy.mode=REGISTRY_ONLY 表示默認容器不允許訪問外部網路,只允許訪問已知的ServiceEntry。

然後,我們設置一個全局性的 Sidecar 配置:


$ kubectl apply -f - <<EOF  apiVersion: networking.istio.io/v1alpha3  kind: Sidecar  metadata:    name: default    namespace: istio-system  spec:    egress:    - hosts:      - "./*"      - "istio-system/*"  EOF  sidecar.networking.istio.io/default configured

  • 每個namespace只允許一個無 workloadSelector 的配置
  • rootNamespace中無 workloadSelector 的配置是全局的,影響所有namespace,默認的rootNamespace=istio-system

這個配置的含義是:

所有namespace里的容器出流量(egress)只能訪問自己的namespace或namespace=istio-system 中提供的 services 。

egress

我們先測試一下外網連通性, Sidecar 的出流量被稱作 egress 流量。

這裡需要等待一會生效,或者直接銷毀重新部署一個測試容器


$ kubectl exec -it $(kubectl get pod -l run=probe -o jsonpath='{..metadata.name}') -c probe -- curl -v www.baidu.com  * About to connect() to www.baidu.com port 80 (#0)  *   Trying 220.181.38.150...  * Connected to www.baidu.com (220.181.38.150) port 80 (#0)  > GET / HTTP/1.1  > User-Agent: curl/7.29.0  > Host: www.baidu.com  > Accept: */*  >  * Recv failure: Connection reset by peer  * Closing connection 0  curl: (56) Recv failure: Connection reset by peer  command terminated with exit code 56
效果是:外網已經訪問不通。

恢復:這時,我們將需要訪問的域名註冊到 ServiceEntry 中,並且增加一個 Sidecar 的 egress 規則,例如:


apiVersion: networking.istio.io/v1alpha3  kind: ServiceEntry  metadata:    name: baidu  spec:    hosts:    - www.baidu.com    ports:    - number: 80      name: http      protocol: HTTP    resolution: DNS    location: MESH_EXTERNAL  ---  apiVersion: networking.istio.io/v1alpha3  kind: Sidecar  metadata:    name: default  spec:    egress:    - hosts:      - "./www.baidu.com"      port:        number: 80        protocol: HTTP        name: http

重新請求,確認恢復了。


$ kubectl exec -it $(kubectl get pod -l run=probe -o jsonpath='{..metadata.name}') -c probe -- curl -v www.baidu.com  * About to connect() to www.baidu.com port 80 (#0)  *   Trying 220.181.38.150...  * Connected to www.baidu.com (220.181.38.150) port 80 (#0)  > GET / HTTP/1.1  > User-Agent: curl/7.29.0  > Host: www.baidu.com  > Accept: */*  >  < HTTP/1.1 200 OK  < accept-ranges: bytes  < cache-control: private, no-cache, no-store, proxy-revalidate, no-transform  < content-length: 2381  < content-type: text/html  < date: Tue, 15 Oct 2019 07:45:33 GMT  < etag: "588604c8-94d"  < last-modified: Mon, 23 Jan 2017 13:27:36 GMT  < pragma: no-cache  < server: envoy  < set-cookie: BDORZ=27315; max-age=86400; domain=.baidu.com; path=/  < x-envoy-upstream-service-time: 21

同樣地,容器之間的流量同理:


$ kubectl exec -it $(kubectl get pod -l run=probe -o jsonpath='{..metadata.name}') -c probe -- curl productpage:9080  curl: (56) Recv failure: Connection reset by peer  command terminated with exit code 56  配置上ServiceEntryapiVersion: networking.istio.io/v1alpha3  kind: Sidecar  metadata:    name: default  spec:    egress:    - hosts:      - "./www.baidu.com"      - "./productpage.default.svc.cluster.local"  # 這裡必須用長名稱  ---  apiVersion: networking.istio.io/v1alpha3  kind: ServiceEntry  metadata:    name: baidu  spec:    hosts:    - www.baidu.com    resolution: DNS    location: MESH_EXTERNAL  ---  apiVersion: networking.istio.io/v1alpha3  kind: ServiceEntry  metadata:    name: productpage  spec:    hosts:    - productpage    resolution: DNS    location: MESH_EXTERNAL


$ kubectl exec -it $(kubectl get pod -l run=probe -o jsonpath='{..metadata.name}') -c probe -- curl productpage:9080 | grep -o "<title>.*</title>"  <title>Simple Bookstore App</title>

需要留意的是,不帶workloadSelector的(不指定特定容器的)Sidecar配置只能有一個,所以規則都需要寫在一起。

ingress

下面我們探究容器入流量的配置:


apiVersion: networking.istio.io/v1alpha3  kind: Sidecar  metadata:    name: productpage-sidecar  spec:    workloadSelector:      labels:        app: productpage    ingress:    - port:        number: 9080        protocol: HTTP      defaultEndpoint: 127.0.0.1:10080    egress:    - hosts:      - "*/*"
這個配置的效果是讓 productpage 應用的容器收到 9080 埠的 HTTP 請求時,轉發到容器內的10080埠。

由於容器內沒有監聽 10080 ,所以會訪問失敗。


$ kubectl exec -it $(kubectl get pod -l run=probe -o jsonpath='{..metadata.name}') -c probe -- curl -s productpage:9080  upstream connect error or disconnect/reset before headers. reset reason: connection failure

小結

Sidecar 的示例說明就到這裡,這只是一個示例。

egress 配置覆蓋的域名訪問規則沒必要在 ingress 中重複,所以 ingress 配置主要用於配置代理流量的規則。例如,我們可以將所有的入口流量傳入 sidecar 中的監聽進程(做一些訂製開發的許可權檢查等),然後再傳給下游微服務。

egress 的配置更多的是關注在服務網格對外訪問的能力,服務內部如果限制了,應用自身訪問都會需要大量的 ServiceEntry 註冊,所以微服務之間東西流量的信任訪問,需要靠安全機制來解決。

安全機制

概述

圖片來源於Istio官網

Istio 提供包含了南北流量和東西流量兩部分的防禦機制:

1、Security by default:微服務應用不需要提供任何程式碼或引入三方庫來保證自身安全。

2、Defense in depth:能夠與已有的安全體系融合,深度訂製安全能力。

3、Zero-trust Network:提供安全的方案都是假定服務網格的內部和外部都是0信任(不安全)網路。

下圖是 Istio 中每個組件的角色:

圖片來源於Istio官網

1、Citadel,證書(CA)管理

2、Sidecar等Envoy Proxy,提供TLS保障

3、Pilot,策略(Policy)和身份資訊下發

4、Mixer,認證和審計

策略(Policy)

Istio 支援在運行時實時地配置策略(Policy),支援:

1、服務入流量速率控制。

2、服務訪問控制,黑白名單規則等。

3、Header重寫,重定向。

也可以自己訂製 Policy Adapter 來定義業務邏輯。

TLS

在介紹安全機制之前,有必要先科普一下 TLS 。

SSL ( Security Socket Layer ,安全 Socket 層),是一個解決 4 層 TCP 和 7 層HTTPS 中間的協議層,解決安全傳輸的問題。

TLS ( Transport Layer Security ,傳輸層安全協議),是基於 SSL v3 的後續升級版本,可以理解為相當於 SSL v3.1 。

主要提供:

1、認證(Transport Authentication),用戶、伺服器的身份,確保數據發送到正確的目的地。

2、加密數據,防止數據明文泄露。

3、數據一致性,傳輸過程不被串改。

Istio 中的安全傳輸機制都是建立在 TLS 之上的。

更多資訊可以查看官方概念,詳情參考:

https://istio.io/docs/concepts/security

認證(Authentication)與鑒權(Authorization)

這兩個詞很相近,甚至縮寫 auth 都是相同的,所以有時候很容混淆。

在 istioctl 中有兩個命令 authn 和 authz ,這樣就可以區分它們。

認證和鑒權分別做什麼,在後文兩節會具體介紹。這裡先說一下它們的關係。

認證 實際上是 鑒權 的必要條件

認證 實際上是 鑒權 的必要條件

認證 實際上是 鑒權 的必要條件

為什麼?

1、認證是識別身份(Identification)。

2、鑒權是檢查特定身份(Identity)的許可權。

這樣就很好理解了。二者時常相隨,我們常說的比如登錄,就是:

1、基於登錄機制的cookie來識別訪問來源的身份——認證。

2、然後判斷來源的身份是否具備登錄系統的許可權(或者是訪問某一個頁面的具體細節的許可權)——鑒權。

那麼在 Istio 體系中,Authentication 是基於 mTLS 機制來做的,那麼開啟mTLS之後,就可以設置一些 AuthorizationPolicy 來做訪問控制。細節可以看下文。

認證(Authentication)

Istio 中的認證包含兩種:

1、Transport Authentication ,傳輸層認證。基於 mTLS ( Mutual TLS ),檢查東西流量的合法性。

2、Origin Authentication ,客戶端認證。基於 JWT 等校驗南北流量的登錄身份。

示例:配置Policy

這次我們跟著 Task: Authentication Policy 例子走,這裡簡化一下過程不做全程搬運,只分析關鍵點。

準備環境:

這個例子中,創建了 3 個 namespace ,其中兩個 foo 和 bar 注入了Sidecar, legacy 不注入用於對比。


#!/bin/bash  kubectl create ns foo  kubectl apply -f <(istioctl kube-inject -f samples/httpbin/httpbin.yaml) -n foo  kubectl apply -f <(istioctl kube-inject -f samples/sleep/sleep.yaml) -n foo  kubectl create ns bar  kubectl apply -f <(istioctl kube-inject -f samples/httpbin/httpbin.yaml) -n bar  kubectl apply -f <(istioctl kube-inject -f samples/sleep/sleep.yaml) -n bar  kubectl create ns legacy  kubectl apply -f samples/httpbin/httpbin.yaml -n legacy  kubectl apply -f samples/sleep/sleep.yaml -n legacy

默認情況下,容器之間是互通的(mTLS運行在PRESSIVE_MODE)。

這裡通過一個 check.sh 腳本檢查連通性:


#!/bin/bash  for from in "foo" "bar" "legacy"; do for to in "foo" "bar" "legacy"; do kubectl exec $(kubectl get pod -l app=sleep -n ${from} -o jsonpath={.items..metadata.name}) -c sleep -n ${from} -- curl "http://httpbin.${to}:8000/ip" -s -o /dev/null -w "sleep.${from} to httpbin.${to}: %{http_code}n"; done; done$ ./check.sh  sleep.foo to httpbin.foo: 200  sleep.foo to httpbin.bar: 200  sleep.foo to httpbin.legacy: 200  sleep.bar to httpbin.foo: 200  sleep.bar to httpbin.bar: 200  sleep.bar to httpbin.legacy: 200  sleep.legacy to httpbin.foo: 200  sleep.legacy to httpbin.bar: 200  sleep.legacy to httpbin.legacy: 200

打開TLS:

通過全局的 MeshPolicy 配置打開mTLS:


$ kubectl apply -f - <<EOF  apiVersion: "authentication.istio.io/v1alpha1"  kind: "MeshPolicy"  metadata:    name: "default"  spec:    peers:    - mtls: {}  EOF

這時,原本互通的容器訪問不通了

執行:


$ ./check.sh  sleep.foo to httpbin.foo: 503  sleep.foo to httpbin.bar: 503  sleep.foo to httpbin.legacy: 200  sleep.bar to httpbin.foo: 503  sleep.bar to httpbin.bar: 503  sleep.bar to httpbin.legacy: 200  sleep.legacy to httpbin.foo: 000  command terminated with exit code 56  sleep.legacy to httpbin.bar: 000  command terminated with exit code 56  sleep.legacy to httpbin.legacy: 200

Sidecar 注入的 namespace 中,會返回 503. 而沒有注入的 ns 上,連接會直接被重置(connection reset)。

配置託管的 mTLS 能力

接著,通過 DestinationRule ,重新對注入Sidecar的微服務增加 mTLS 能力:


kubectl apply -f - <<EOF  apiVersion: "networking.istio.io/v1alpha3"  kind: "DestinationRule"  metadata:    name: "default"    namespace: "istio-system"  spec:    host: "*.local"    trafficPolicy:      tls:        mode: ISTIO_MUTUAL  EOF

1、*.local 配置的含義是,對所有 K8s 集群內任意 namespace 之間的東西流量有效

2、tls.mode=ISTIO_MUTUAL :查看文檔,表示完全由 Istio 託管 mTLS 的實現,其它選項失效。具體配置後面再涉及。

重新運行 check.sh :


$ ./check.sh  sleep.foo to httpbin.foo: 200  sleep.foo to httpbin.bar: 200  sleep.foo to httpbin.legacy: 503  sleep.bar to httpbin.foo: 200  sleep.bar to httpbin.bar: 200  sleep.bar to httpbin.legacy: 503  sleep.legacy to httpbin.foo: 000  command terminated with exit code 56  sleep.legacy to httpbin.bar: 000  command terminated with exit code 56  sleep.legacy to httpbin.legacy: 200

注意,如果走了前面的例子會有全局 default 的 Sidecar Egress 配置,限制了只能訪問同 namespace 的服務,那麼跨 namespace 的調用仍然會 503 :


sleep.foo to httpbin.bar: 503

可以自己試驗一下,回顧一下配置:


apiVersion: networking.istio.io/v1alpha3  kind: Sidecar  metadata:    name: default    namespace: istio-system  spec:    egress:    - hosts:      - ./*   # <--      - istio-system/*

分析

對比之前的結果,有兩點變化:

1、同樣注入Sidecar的微服務互相可以訪問了(200)。

2、沒有注入Sidecar(ns=legacy)的微服務不能被已注入Sidecar的微服務訪問(503)。

ns=legacy中的行為仍然不變

變化1:說明微服務之間的 TLS 已經由 Istio 託管,這個期間我們沒有修改任何服務的程式碼,很魔性。

變化2:說明服務網格對外部容器也要求具備 TLS 能力,因為 legacy 中的服務沒有注入 Sidecar ,因此訪問失敗。

鑒權(Authorization)

Istio 的鑒權機制的前提就是打開 mTLS 認證,讓每一個或特定的微服務的 sidecar 互相訪問都基於 mTLS 機制。

不是必要前提 有一部分鑒權規則是不依賴mTLS的,但是很少。

鑒權基於 istio CRD :AuthorizationPolicy

例如,默認拒絕所有微服務互相訪問:


apiVersion: security.istio.io/v1beta1  kind: AuthorizationPolicy  metadata:   name: deny-all   namespace: foo  spec:

需要留意的是,如果默認全部拒絕,那麼甚至 istio-system 中的 istio-ingressgateway 流量訪問 foo 這個namespace的服務也都會被拒絕。就無法從外部訪問所有 foo 的服務了。所以我們可以改為:


apiVersion: security.istio.io/v1beta1  kind: AuthorizationPolicy  metadata:    name: deny-all    namespace: foo  spec:    rules:    - from:      - source:          namespaces:          - "istio-system"

AuthorizationPolicy 的規則文檔里都已經很詳細了,這裡就不再贅述。

應用配置之後,在任意一個微服務中訪問另外一個微服務,就都會遇到 403 錯誤,消息為 RBAC access denied 。

其它


Istio 能力本文僅覆蓋了流量控制(Traffic Management)、安全機制(Security)中比較淺顯的部分,有關高級的 Policy 設置(限流、路由的干預等)、服務觀測(Telemetry)等能力沒有涉及。

此外,如何地高效運維管理(比如升級 istio 版本、管理不同集群的配置等),0 信任基礎下的安全訪問策略配置,基於istio做二次開發擴展,等等問題都是在生產實踐當中需要關注的點,以後有機會再分享整理。

參考文檔

  • Istio官方文檔 https://istio.io/docs/
  • Istio Handbook https://www.servicemesher.com/istio-handbook/concepts-and-principle/what-is-service-mesh.html
  • Pattern Service Mesh https://philcalcado.com/2017/08/03/pattern_service_mesh.html

作者資訊:

袁賡拓,花名三辰,阿里雲智慧-計算平台事業部技術專家,負責數加平台 &DataWorks 的微服務生態建設,目前主要關注微服務、Service Mesh 等相關技術方向。