1. 程式人生 > >1.深入Istio:Sidecar自動注入如何實現的?

1.深入Istio:Sidecar自動注入如何實現的?

![80614165_p0_master1200](https://img.luozhiyun.com/20201107230410.jpg) > 轉載請宣告出處哦~,本篇文章釋出於luozhiyun的部落格:https://www.luozhiyun.com > > 本文使用的Istio原始碼是 release 1.5。 > 這篇文章打算講一下sidecar,我在剛學習Istio的時候會有一些疑惑,sidecar是如何做到無感知的注入的,很多學習資料都沒有詳細去講這部分的內容,下面打算解析一下。 ## Sidecar 介紹 在Sidecar部署方式中會為每個應用的容器部署一個伴生容器。對於Istio,Sidecar接管進出應用程式容器的所有網路流量。 使用 Sidecar 模式部署服務網格時,無需在節點上執行代理,但是叢集中將執行多個相同的 Sidecar 副本。在 Kubernetes 的 Pod 中,在原有的應用容器旁邊執行一個 Sidecar 容器,可以理解為兩個容器共享儲存、網路等資源,可以廣義的將這個注入了 Sidecar 容器的 Pod 理解為一臺主機,兩個容器共享主機資源。 ### Sidecar 注入過程 注入 Sidecar的時候會在生成pod的時候附加上兩個容器:istio-init、istio-proxy。istio-init這個容器從名字上看也可以知道它屬於k8s中的Init Containers,主要用於設定iptables規則,讓出入流量都轉由 Sidecar 進行處理。istio-proxy是基於Envoy實現的一個網路代理容器,是真正的Sidecar,應用的流量會被重定向進入或流出Sidecar。 我們在使用Sidecar自動注入的時候只需要給對應的應用部署的名稱空間打個istio-injection=enabled標籤,這個名稱空間中新建的任何 Pod 都會被 Istio 注入 Sidecar。 應用部署後我們可以通過kubectl describe檢視pod內的容器: ```shell [root@localhost ~]# kubectl describe pod details-v1-6c9f8bcbcb-shltm Name: details-v1-6c9f8bcbcb-shltm Namespace: default ... Labels: app=details pod-template-hash=6c9f8bcbcb security.istio.io/tlsMode=istio service.istio.io/canonical-name=details service.istio.io/canonical-revision=v1 version=v1 Annotations: sidecar.istio.io/status: {"version":"3bc68d1f27d8b6b9bf1cb3e9904f5d5f8c2ecab1c93d933fbb3d0db76fae2633","initContainers":["istio-init"],"containers":["istio-proxy"]... Status: Running IP: 172.20.0.14 IPs: IP: 172.20.0.14 Controlled By: ReplicaSet/details-v1-6c9f8bcbcb Init Containers: istio-init: Container ID: docker://6d14ccc83bd119236bf8fda13f6799609c87891be9b2c5af7cbf7d8c913ce17e Image: docker.io/istio/proxyv2:1.5.10 Image ID: docker-pullable://istio/proxyv2@sha256:abbe8ad6d50474814f1aa9316dafc2401fbba89175638446f01afc36b5a37919 ... Ready: True Restart Count: 0 ... Containers: details: Container ID: docker://ed216429216ea1b8a1ba20960590edb7322557467c38cceff3c3e847bcff0a14 Image: docker.io/istio/examples-bookinfo-details-v1:1.15.1 Image ID: docker-pullable://istio/examples-bookinfo-details-v1@sha256:344b1c18703ab1e51aa6d698f459c95ea734f8317d779189f4638de7a00e61ae ... istio-proxy: Container ID: docker://a3862cc8f53198c8f86a911089e73e00f4cc4aa02eea05aaeb0bd267a8e98482 Image: docker.io/istio/proxyv2:1.5.10 Image ID: docker-pullable://istio/proxyv2@sha256:abbe8ad6d50474814f1aa9316dafc2401fbba89175638446f01afc36b5a37919 ... Ready: True ``` details-v1-6c9f8bcbcb-shltm這個應用是我們在上篇文章中建立的一個details服務,裡面有istio-init、istio-proxy、details這三個container。 ## Sidecar 注入原理 Sidecar 注入主要是依託k8s的准入控制器Admission Controller來實現的。 准入控制器會攔截 Kubernetes API Server 收到的請求,攔截髮生在認證和鑑權完成之後,物件進行持久化之前。可以定義兩種型別的 Admission webhook:Validating 和 Mutating。Validating 型別的 Webhook 可以根據自定義的准入策略決定是否拒絕請求;Mutating 型別的 Webhook 可以根據自定義配置來對請求進行編輯。 我們可以看看配置詳情: ```shell [root@localhost ~]# kubectl get mutatingwebhookconfiguration istio-sidecar-injector -o yaml apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration metadata: annotations: ... creationTimestamp: "2020-10-18T08:22:01Z" generation: 2 labels: app: sidecar-injector operator.istio.io/component: Pilot operator.istio.io/managed: Reconcile operator.istio.io/version: 1.5.10 release: istio ... webhooks: - admissionReviewVersions: - v1beta1 clientConfig: caBundle: ... service: name: istiod namespace: istio-system path: /inject port: 443 failurePolicy: Fail matchPolicy: Exact name: sidecar-injector.istio.io namespaceSelector: matchLabels: istio-injection: enabled rules: - apiGroups: - "" apiVersions: - v1 operations: - CREATE resources: - pods scope: '*' ... ``` 這裡有一個namespaceSelector,match的標籤是istio-injection: enabled的名稱空間,請求規則rules是CREATE,表示匹配所有pod的建立請求。當apiserver收到一個符合規則的請求時,apiserver會給 Webhook 服務傳送一個准入稽核的請求,在上面的配置中webhook指定的是一個叫istiod的service。 ```shell [root@localhost ~]# kubectl get svc --namespace=istio-system | grep istiod istiod ClusterIP 10.68.222.38 15012/TCP,443/TCP 32h ``` 通常Sidecar注入由以下步驟完成: 1. 解析Webhook REST請求,將AdmissionReview原始資料反序列化; 2. 解析pod,將AdmissionReview中的AdmissionRequest反序列化; 3. 利用Pod及網格配置渲染Sidecar配置模板; 4. 利用Pod及渲染後的模板建立Json Patch; 5. 構造AdmissionResponse; 6. 構造AdmissionReview,將其發給apiserver; 原始碼流程差不多是這個樣子: ![image-20201107174326312](https://img.luozhiyun.com/20201107230416.png) 下面我們來看看原始碼。 原始碼位置:pkg/kube/inject/webhook.go ```go func NewWebhook(p WebhookParameters) (*Webhook, error) { wh := &Webhook{ ... } ... if p.Mux != nil { p.Mux.HandleFunc("/inject", wh.serveInject) mux = p.Mux } else { wh.server = &http.Server{ Addr: fmt.Sprintf(":%v", p.Port), TLSConfig: &tls.Config{GetCertificate: wh.getCert}, } mux = http.NewServeMux() mux.HandleFunc("/inject", wh.serveInject) wh.server.Handler = mux } ... } ``` 在初始化Webhook例項的時候會註冊/inject對應的處理器,也就是當apiserver回撥/inject請求的時候會呼叫到serveInject方法中。 然後我們進入到serveInject方法中: 檔案位置:pkg/kube/inject/webhook.go ```go func (wh *Webhook) serveInject(w http.ResponseWriter, r *http.Request) { totalInjections.Increment() var body []byte if r.Body != nil { //讀取請求體 if data, err := ioutil.ReadAll(r.Body); err == nil { body = data } } ... var reviewResponse *v1beta1.AdmissionResponse ar := v1beta1.AdmissionReview{} //解碼請求體 if _, _, err := deserializer.Decode(body, nil, &ar); err != nil { handleError(fmt.Sprintf("Could not decode body: %v", err)) reviewResponse = toAdmissionResponse(err) } else { //解碼成功呼叫inject方法,並傳入AdmissionReview reviewResponse = wh.inject(&ar) } //構建AdmissionReview作為引數返回給呼叫方 response := v1beta1.AdmissionReview{} if reviewResponse != nil { response.Response = reviewResponse if ar.Request != nil { response.Response.UID = ar.Request.UID } } resp, err := json.Marshal(response) if err != nil { log.Errorf("Could not encode response: %v", err) http.Error(w, fmt.Sprintf("could not encode response: %v", err), http.StatusInternalServerError) } if _, err := w.Write(resp); err != nil { log.Errorf("Could not write response: %v", err) http.Error(w, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError) } } ``` 這個方法很簡單,主要就是讀取請求體並解碼,然後呼叫inject方法,構建AdmissionReview作為引數返回給呼叫方。 主要邏輯從這裡可以看出都在inject方法裡面,下面看看這個方法: ```go func (wh *Webhook) inject(ar *v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { req := ar.Request var pod corev1.Pod //json反序列化請求資料 if err := json.Unmarshal(req.Object.Raw, &pod); err != nil { handleError(fmt.Sprintf("Could not unmarshal raw object: %v %s", err, string(req.Object.Raw))) return toAdmissionResponse(err) } ... //封裝模板資料 spec, iStatus, err := InjectionData(wh.Config.Template, wh.valuesConfig, wh.sidecarTemplateVersion, typeMetadata, deployMeta, &pod.Spec, &pod.ObjectMeta, wh.meshConfig.DefaultConfig, wh.meshConfig) // nolint: lll if err != nil { handleError(fmt.Sprintf("Injection data: err=%v spec=%v\n", err, iStatus)) return toAdmissionResponse(err) } ... //將需要注入的有istio-init/istio-proxy container封裝成patch操作 //具體可以看這裡:https://kubernetes.io/zh/docs/reference/access-authn-authz/extensible-admission-controllers/#response patchBytes, err := createPatch(&pod, injectionStatus(&pod), annotations, spec, deployMeta.Name) if err != nil { handleError(fmt.Sprintf("AdmissionResponse: err=%v spec=%v\n", err, spec)) return toAdmissionResponse(err) } log.Debugf("AdmissionResponse: patch=%v\n", string(patchBytes)) //將需要patch的配置封裝成AdmissionResponse返回 reviewResponse := v1beta1.AdmissionResponse{ Allowed: true, Patch: patchBytes, PatchType: func() *v1beta1.PatchType { pt := v1beta1.PatchTypeJSONPatch return &pt }(), } totalSuccessfulInjections.Increment() return &reviewResponse } ``` inject方法邏輯主要分為以下幾個步驟: 1. json反序列化請求資料到pod中; 2. 呼叫InjectionData根據模板封裝資料,主要是構造istio-init、istio-proxy等容器配置; 3. 呼叫createPatch方法將模板資料轉化成json形式,到時候在建立容器的時候會patch到建立容器的配置中,具體可以看這裡:https://kubernetes.io/zh/docs/reference/access-authn-authz/extensible-admission-controllers/#response 4. 最後將資料封裝成AdmissionResponse返回; ## 總結 本篇文章重點講解Sidecar容器注入實現原理,通過使用k8s的准入控制器來做到在每個新建的pod裡面都無感知的建立sidecar做流量託管。 ## Reference https://github.com/istio/istio.io/blob/release-1.1/content/blog/2019/data-plane-setup/index.md https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/ https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/ https://jimmysong.io/blog/envoy-sidecar-injection-in-istio-service-mesh-de