天天看點

Service Mesh · Istio · 以實踐入門

前言

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

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

概念

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

Service Mesh · Istio · 以實踐入門

圖檔來源:

https://landscape.cncf.io/

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

服務網格

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

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

首先看看 Sidecar 的設計:

Service Mesh · Istio · 以實踐入門

圖檔來源于網絡

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

曆史

Service Mesh · Istio · 以實踐入門

原始的應用程式--圖檔來源于網絡

Service Mesh · Istio · 以實踐入門

獨立的網絡層--圖檔來源于網絡

Service Mesh · Istio · 以實踐入門

出現網絡層(4層協定)控制的需求--圖檔來源于網絡

Service Mesh · Istio · 以實踐入門

控制邏輯下移到網絡層--圖檔來源于網絡

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

于是,人們提出,将不同的功能分離到不同的程式(程序)中,減低子產品的耦合度,靈活開發疊代,這就是微服務概念的興起。

Service Mesh · Istio · 以實踐入門

出現新的應用層(7層協定)需求(服務發現、熔斷、逾時重試等)--圖檔來源于網絡

Service Mesh · Istio · 以實踐入門

封裝成三方庫(服務發現:Dubbo/HSF)--圖檔來源于網絡

困難:

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

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

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

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

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

Service Mesh · Istio · 以實踐入門

封裝到隔離的程序中代理--圖檔來源于網絡

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

Sidecar 解決什麼問題?

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

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

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

Service Mesh · Istio · 以實踐入門

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

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

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

小結

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

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

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

Service Mesh · Istio · 以實踐入門

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

Service Mesh · Istio · 以實踐入門

是以說,回到服務網格的概念上來,雖然概念是不同的,但是邏輯上我們可以了解成:所有使用中間件的服務組成了一個大的服務網格,這樣可以幫助我們了解。服務網格基于 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 協定擴充了其控制平面。

Service Mesh · Istio · 以實踐入門
Service Mesh · Istio · 以實踐入門

Istio基于Envoy實作Service Mesh資料平面--圖檔來源于網絡

Service Mesh · Istio · 以實踐入門

Envoy角色--圖檔來源于網絡

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

也就是下圖中的 Proxy :

Service Mesh · Istio · 以實踐入門

圖檔來源于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 與其附屬的應用程式具有相同的權限。

Service Mesh · Istio · 以實踐入門
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/
Service Mesh · Istio · 以實踐入門

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

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

Service Mesh · Istio · 以實踐入門

是以我們打算賣什麼?

實踐

鋪墊這麼多概念,我們可以實操起來了。具體怎麼做?從安裝 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 ,來調整它們的流量權重,這是真實生産環境版本更新的場景。

再來看增加的部分:

Service Mesh · Istio · 以實踐入門

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

Service Mesh · Istio · 以實踐入門

這部分可以看到,原有的服務容器沒有任何改動,隻是增加了一個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。

Service Mesh · Istio · 以實踐入門

圖檔來源于 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

頁面:

Service Mesh · Istio · 以實踐入門

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

Service Mesh · Istio · 以實踐入門

流量控制 - 網絡可見性

基于前面安裝好的 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 注冊,是以微服務之間東西流量的信任通路,需要靠安全機制來解決。

安全機制

概述

Service Mesh · Istio · 以實踐入門

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

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

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

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

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

Service Mesh · 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做二次開發擴充,等等問題都是在生産實踐當中需要關注的點,以後有機會再分享整理。

參考文檔

作者資訊:

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