1. 程式人生 > >Istio Sidecar注入原理

Istio Sidecar注入原理

## 概念 簡單來說,Sidecar 注入會**將額外容器**的配置**新增到 Pod 模板中**。這裡特指將Envoy容器注應用所在Pod中。 Istio 服務網格目前所需的容器有: `istio-init` 用於設定 iptables 規則,以便將入站/出站流量通過 Sidecar 代理。 初始化容器與應用程式容器在以下方面有所不同: - 它在啟動應用容器之前執行,並一直執行直至完成。 - 如果有多個初始化容器,則每個容器都應在啟動下一個容器之前成功完成。 因此,您可以看到,對於不需要成為實際應用容器一部分的設定或初始化作業來說,這種容器是多麼的完美。在這種情況下,`istio-init` 就是這樣做並設定了 `iptables` 規則。 `istio-proxy` 這個容器是真正的 Sidecar 代理(基於 Envoy)。 下面的內容描述了向 pod 中注入 Istio Sidecar 的兩種方法: 1. 使用 `istioctl`手動注入 2. 啟用 pod 所屬名稱空間的 Istio Sidecar 注入器自動注入。 手動注入直接修改配置,如 deployment,並將代理配置注入其中。 當 pod 所屬`namespace`啟用自動注入後,自動注入器會使用准入控制器在建立 Pod 時自動注入代理配置。 通過應用 `istio-sidecar-injector` ConfigMap 中定義的模版進行注入。 ### 自動注入 當你在一個`namespace`中設定了 `istio-injection=enabled` 標籤,且 injection webhook 被啟用後,任何新的 pod 都有將在建立時自動新增 Sidecar. 請注意,區別於手動注入,自動注入發生在 pod 層面。你將看不到 deployment 本身有任何更改 。 ```shell kubectl label namespace default istio-inhection=enabled kubectl get namespace -L istio-injection NAME STATUS AGE ISTIO-INJECTION default Active 1h enabled istio-system Active 1h kube-public Active 1h kube-system Active 1h ``` 注入發生在 pod 建立時。殺死正在執行的 pod 並驗證新建立的 pod 是否注入 sidecar。原來的 pod 具有 READY 為 1/1 的容器,注入 sidecar 後的 pod 則具有 READY 為 2/2 的容器 。 #### 自動注入原理 自動注入是利用了k8s Admission webhook 實現的。 Admission webhook 是一種用於接收准入請求並對其進行處理的 HTTP 回撥機制, 它可以更改傳送到 API 伺服器的物件以執行自定義的設定預設值操作。 具體細節可以查閱 [Admission webhook ](https://kubernetes.io/zh/docs/reference/access-authn-authz/extensible-admission-controllers/)文件。 istio 對應的istio-sidecar-injector webhook配置,預設會回撥istio-sidecar-injector service的`/inject `地址。 ``` apiVersion: admissionregistration.k8s.io/v1beta1 kind: MutatingWebhookConfiguration metadata: name: istio-sidecar-injector webhooks: - name: sidecar-injector.istio.io clientConfig: service: name: istio-sidecar-injector namespace: istio-system path: "/inject" caBundle: ${CA_BUNDLE} rules: - operations: [ "CREATE" ] apiGroups: [""] apiVersions: ["v1"] resources: ["pods"] namespaceSelector: matchLabels: istio-injection: enabled ``` 回撥API入口程式碼在 `pkg/kube/inject/webhook.go` 中 ```go // 建立一個用於自動注入sidecar的新例項 func NewWebhook(p WebhookParameters) (*Webhook, error) { // ...省略一萬字... wh := &Webhook{ Config: sidecarConfig, sidecarTemplateVersion: sidecarTemplateVersionHash(sidecarConfig.Template), meshConfig: p.Env.Mesh(), configFile: p.ConfigFile, valuesFile: p.ValuesFile, valuesConfig: valuesConfig, watcher: watcher, healthCheckInterval: p.HealthCheckInterval, healthCheckFile: p.HealthCheckFile, env: p.Env, revision: p.Revision, } //api server 回撥函式,監聽/inject回撥 p.Mux.HandleFunc("/inject", wh.serveInject) p.Mux.HandleFunc("/inject/", wh.serveInject) // ...省略一萬字... return wh, nil } ``` `serveInject`邏輯 ```go func (wh *Webhook) serveInject(w http.ResponseWriter, r *http.Request) { // ...省略一萬字... 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邏輯 reviewResponse = wh.inject(&ar, path) } // 響應inject sidecar後的內容給k8s api server response := v1beta1.AdmissionReview{} if reviewResponse != nil { response.Response = reviewResponse if ar.Request != nil { response.Response.UID = ar.Request.UID } } // ...省略一萬字... } // 注入邏輯實現 func (wh *Webhook) inject(ar *v1beta1.AdmissionReview, path string) *v1beta1.AdmissionResponse { // ...省略一萬字... // injectRequired判斷是否有設定自動注入 if !injectRequired(ignoredNamespaces, wh.Config, &pod.Spec, &pod.ObjectMeta) { log.Infof("Skipping %s/%s due to policy check", pod.ObjectMeta.Namespace, podName) totalSkippedInjections.Increment() return &v1beta1.AdmissionResponse{ Allowed: true, } } // ...省略一萬字... // 返回需要注入Pod的物件 spec, iStatus, err := InjectionData(wh.Config.Template, wh.valuesConfig, wh.sidecarTemplateVersion, typeMetadata, deployMeta, &pod.Spec, &pod.ObjectMeta, wh.meshConfig, path) // nolint: lll if err != nil { handleError(fmt.Sprintf("Injection data: err=%v spec=%vn", err, iStatus)) return toAdmissionResponse(err) } // 執行容器注入邏輯 patchBytes, err := createPatch(&pod, injectionStatus(&pod), wh.revision, annotations, spec, deployMeta.Name, wh.meshConfig) if err != nil { handleError(fmt.Sprintf("AdmissionResponse: err=%v spec=%vn", err, spec)) return toAdmissionResponse(err) } reviewResponse := v1beta1.AdmissionResponse{ Allowed: true, Patch: patchBytes, PatchType: func() *v1beta1.PatchType { pt := v1beta1.PatchTypeJSONPatch return &pt }(), } return &reviewResponse } ``` `injectRequired`函式 ```go func injectRequired(ignored []string, config *Config, podSpec *corev1.PodSpec, metadata *metav1.ObjectMeta) bool { // HostNetwork模式直接跳過注入 if podSpec.HostNetwork { return false } // k8s系統名稱空間(kube-system/kube-public)跳過注入 for _, namespace := range ignored { if metadata.Namespace == namespace { return false } } annos := metadata.GetAnnotations() if annos == nil { annos = map[string]string{} } var useDefault bool var inject bool // 優先判斷是否申明瞭`sidecar.istio.io/inject` 註解,會覆蓋命名配置 switch strings.ToLower(annos[annotation.SidecarInject.Name]) { case "y", "yes", "true", "on": inject = true case "": // 使用名稱空間配置 useDefault = true } // 指定Pod不需要注入Sidecar的標籤選擇器 if useDefault { for _, neverSelector := range config.NeverInjectSelector { selector, err := metav1.LabelSelectorAsSelector(&neverSelector) if err != nil { } else if !selector.Empty() && selector.Matches(labels.Set(metadata.Labels)) // 設定不需要注入 inject = false useDefault = false break } } } // 總是將 sidecar 注入匹配標籤選擇器的 pod 中,而忽略全域性策略 if useDefault { for _, alwaysSelector := range config.AlwaysInjectSelector { selector, err := metav1.LabelSelectorAsSelector(&alwaysSelector) if err != nil { log.Warnf("Invalid selector for AlwaysInjectSelector: %v (%v)", alwaysSelector, err) } else if !selector.Empty() && selector.Matches(labels.Set(metadata.Labels)){ // 設定需要注入 inject = true useDefault = false break } } } // 如果都沒有配置則使用預設注入策略 var required bool switch config.Policy { default: // InjectionPolicyOff log.Errorf("Illegal value for autoInject:%s, must be one of [%s,%s]. Auto injection disabled!", config.Policy, InjectionPolicyDisabled, InjectionPolicyEnabled) required = false case InjectionPolicyDisabled: if useDefault { required = false } else { required = inject } case InjectionPolicyEnabled: if useDefault { required = true } else { required = inject } } return required } ``` 從上面我們可以看出,是否注入Sidecar的優先順序為 > Pod Annotations → NeverInjectSelector → AlwaysInjectSelector → Default Policy `createPath`函式 ```go func createPatch(pod *corev1.Pod, prevStatus *SidecarInjectionStatus, revision string, annotations map[string]string, sic *SidecarInjectionSpec, workloadName string, mesh *meshconfig.MeshConfig) ([]byte, error) { var patch []rfc6902PatchOperation // ...省略一萬字... // 注入初始化啟動容器 patch = append(patch, addContainer(pod.Spec.InitContainers, sic.InitContainers, "/spec/initContainers")...) // 注入Sidecar容器 patch = append(patch, addContainer(pod.Spec.Containers, sic.Containers, "/spec/containers")...) // 注入掛載卷 patch = append(patch, addVolume(pod.Spec.Volumes, sic.Volumes, "/spec/volumes")...) patch = append(patch, addImagePullSecrets(pod.Spec.ImagePullSecrets, sic.ImagePullSecrets, "/spec/imagePullSecrets")...) // 注入新註解 patch = append(patch, updateAnnotation(pod.Annotations, annotations)...) // ...省略一萬字... return json.Marshal(patch) } ``` **總結:可以看到,整個注入過程實際就是原本的Pod配置反解析成Pod物件,把需要注入的Yaml內容(如:Sidecar)反序列成物件然後append到對應Pod (如:Container)上,然後再把修改後的Pod重新解析成yaml 內容返回給k8s的api server,然後k8s 拿著修改後內容再將這兩個容器排程到同一臺機器進行部署,至此就完成了對應Sidecar的注入。** #### 解除安裝 sidecar 自動注入器 ```shell kubectl delete mutatingwebhookconfiguration istio-sidecar-injector kubectl -n istio-system delete service istio-sidecar-injector kubectl -n istio-system delete deployment istio-sidecar-injector kubectl -n istio-system delete serviceaccount istio-sidecar-injector-service-account kubectl delete clusterrole istio-sidecar-injector-istio-system kubectl delete clusterrolebinding istio-sidecar-injector-admin-role-binding-istio-system ``` 上面的命令不會從 pod 中移除注入的 sidecar。需要進行滾動更新或者直接刪除對應的pod,並強制 deployment 重新建立新pod。 ### 手動注入 sidecar 手動注入 deployment ,需要使用 使用 `istioctl kube-inject` ```shell istioctl kube-inject -f samples/sleep/sleep.yaml | kubectl apply -f - ``` 預設情況下將使用叢集內的配置,或者使用該配置的本地副本來完成注入。 ```shell kubectl -n istio-system get configmap istio-sidecar-injector -o=jsonpath='{.data.config}' > inject-config.yaml kubectl -n istio-system get configmap istio-sidecar-injector -o=jsonpath='{.data.values}' > inject-values.yaml kubectl -n istio-system get configmap istio -o=jsonpath='{.data.mesh}' > mesh-config.yaml ``` 指定輸入檔案,執行 `kube-inject` 並部署 ```shell istioctl kube-inject --injectConfigFile inject-config.yaml --meshConfigFile mesh-config.yaml --valuesFile inject-values.yaml --filename samples/sleep/sleep.yaml | kubectl apply -f - ``` 驗證 sidecar 已經被注入到 READY 列下 `2/2` 的 sleep pod 中 ```shell kubectl get pod -l app=sleep NAME READY STATUS RESTARTS AGE sleep-64c6f57bc8-f5n4x 2/2 Running 0 24s ``` #### 手動注入原理 手動注入的程式碼入口在 `istioctl/cmd/kubeinject.go` 手工注入跟自動注入還是有些差異的。手動注入是改變了`Deployment`。我們可以看下它具體做了哪些動作: `Deployment`注入前配置: ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: hello spec: replicas: 7 selector: matchLabels: app: hello tier: backend track: stable template: metadata: labels: app: hello tier: backend track: stable spec: containers: - name: hello image: "fake.docker.io/google-samples/hello-go-gke:1.0" ports: - name: http containerPort: 80 ``` `Deployment`注入後配置: ```yaml apiVersion: apps/v1 kind: Deployment metadata: creationTimestamp: null name: hello spec: replicas: 7 selector: matchLabels: app: hello tier: backend track: stable strategy: {} template: metadata: annotations: sidecar.istio.io/status: '{"version":"2343d4598565fd00d328a3388421ee637d25d3f7068e7d5cadef374ee1a06b37","initContainers":["istio-init"],"containers":["istio-proxy"],"volumes":null,"imagePullSecrets":null}' creationTimestamp: null labels: app: hello istio.io/rev: "" security.istio.io/tlsMode: istio tier: backend track: stable spec: containers: - image: fake.docker.io/google-samples/hello-go-gke:1.0 name: hello ports: - containerPort: 80 name: http resources: {} - image: docker.io/istio/proxy_debug:unittest name: istio-proxy resources: {} initContainers: - image: docker.io/istio/proxy_init:unittest-test name: istio-init resources: {} securityContext: fsGroup: 1337 status: {} --- ``` 可以新增了一個容器映象 ```yaml - image: docker.io/istio/proxy_debug:unittest name: istio-proxy resources: {} ``` 那麼注入的內容模板從哪裡獲取,這裡有兩個選項。 1. —injectConfigFile 指定對應的注入檔案 2. —injectConfigMapName 注入配置的 ConfigMap 名稱 如果在操作時發現Sidecar沒有注入成功可以根據注入的方式檢視上面的注入流程來查詢問題。 ## 參考文獻 https://preliminary.istio.io/zh/docs/setup/additional-setup/sidecar-injection/#automatic-sidecar-injection https://kubernetes.io/zh/docs/reference/access-authn-authz/admission-controllers/ https://istio.io/zh/docs/reference/commands/istioctl/#istioctl-kube