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 本身有任何更改 。

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 文檔。

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

// 創建一個用於自動注入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邏輯

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函數

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函數

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 自動注入器

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

使用手動注入前先關閉自動注入

kubectl label namespace default istio-injection=disabled

使用istioctl手動注入

istioctl kube-inject -f samples/sleep/sleep.yaml | kubectl apply -f -

我們可以查看對應的deployment 明細

describe deployment sleep
Name:                   sleep
Namespace:              default
CreationTimestamp:      Wed, 27 May 2020 10:45:23 +0800
Annotations:            deployment.kubernetes.io/revision: 1
Selector:               app=sleep
Pod Template:
  Labels:           app=sleep
                    istio.io/rev=
                    security.istio.io/tlsMode=istio
  Annotations:      sidecar.istio.io/interceptionMode: REDIRECT
                    sidecar.istio.io/status:
                      {"version":"d36ff46d2def0caba37f639f09514b17c4e80078f749a46aae84439790d2b560","initContainers":["istio-init"],"containers":["istio-proxy"]...
                    traffic.sidecar.istio.io/excludeInboundPorts: 15020
                    traffic.sidecar.istio.io/includeOutboundIPRanges: *
  Service Account:  sleep
  Init Containers:
   istio-init:
    Image:      docker.io/istio/proxyv2:1.6.0
    Port:       <none>
    Host Port:  <none>
    Args:
      istio-iptables
      -p
      15001
      -z
      15006
      -u
      1337
      -m
      REDIRECT
      -i
      *
      -x
      
      -b
      *
      -d
      15090,15021,15020
  Containers:
   sleep:
    Image:      governmentpaas/curl-ssl
    Port:       <none>
    Host Port:  <none>
    Command:
      /bin/sleep
      3650d
    Environment:  <none>
    Mounts:
      /etc/sleep/tls from secret-volume (rw)
   istio-proxy:
    Image:      docker.io/istio/proxyv2:1.6.0
    Port:       15090/TCP
    Host Port:  0/TCP
    Args:
      proxy
      sidecar
      --domain
      $(POD_NAMESPACE).svc.cluster.local
      --serviceCluster
      sleep.$(POD_NAMESPACE)
      --proxyLogLevel=warning
      --proxyComponentLogLevel=misc:error
      --trust-domain=cluster.local
      --concurrency
      2

可以看到,相比原始的deployment.yaml文件多出了兩個容器,這兩個容器的作用后面單獨寫一篇文章來分析:

  1. Init Containers下的 istio-init
  2. Containers下的istio-proxy

上面兩個容器的注入,默認情況下將使用集群內的配置,或者使用該配置的本地副本來完成注入。

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 並部署

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 中

kubectl get pod  -l app=sleep
NAME                     READY   STATUS    RESTARTS   AGE
sleep-64c6f57bc8-f5n4x   2/2     Running   0          24s

查看對應pod中的容器

kubectl get pods  -o jsonpath="{.items[*].spec.containers[*].image}" | tr -s '[[:space:]]' '\n' | sort
docker.io/istio/proxyv2:1.6.0
governmentpaas/curl-ssl

手動注入的代碼入口在 istioctl/cmd/kubeinject.go

手工注入跟自動注入還是有些差異的。手動注入是改變了Deployment。我們可以看下它具體做了哪些動作:

Deployment注入前配置:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sleep
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sleep
  template:
    metadata:
      labels:
        app: sleep
    spec:
      serviceAccountName: sleep
      containers:
      - name: sleep
        image: governmentpaas/curl-ssl
        command: ["/bin/sleep", "3650d"]
        imagePullPolicy: IfNotPresent
        volumeMounts:
        - mountPath: /etc/sleep/tls
          name: secret-volume
      volumes:
      - name: secret-volume
        secret:
          secretName: sleep-secret
          optional: true

Deployment注入后配置:

kubectl get deployment sleep -o yaml > sleep.yaml
less sleep.yaml

這里只保留跟注入容器有關的部分內容

apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    deployment.kubernetes.io/revision: "1"
  generation: 1
  name: sleep
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sleep
  template:
    metadata:
      annotations:
        sidecar.istio.io/interceptionMode: REDIRECT
      labels:
        app: sleep
        istio.io/rev: ""
        security.istio.io/tlsMode: istio
    spec:
      containers:
      - command:
        - /bin/sleep
        - 3650d
        image: governmentpaas/curl-ssl
        imagePullPolicy: IfNotPresent
        name: sleep
        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
        volumeMounts:
        - mountPath: /etc/sleep/tls
          name: secret-volume
      - args:
        - proxy
        - sidecar
        - --domain
        - $(POD_NAMESPACE).svc.cluster.local
        - --serviceCluster
        - sleep.$(POD_NAMESPACE)
        - --proxyLogLevel=warning
        - --proxyComponentLogLevel=misc:error
        - --trust-domain=cluster.local
        - --concurrency
        - "2"
        image: docker.io/istio/proxyv2:1.6.0
        imagePullPolicy: Always
        name: istio-proxy
        ports:
        - containerPort: 15090
          name: http-envoy-prom
          protocol: TCP
      dnsPolicy: ClusterFirst
      initContainers:
      - args:
        - istio-iptables
        - -p
        - "15001"
        - -z
        - "15006"
        - -u
        - "1337"
        - -m
        - REDIRECT
        - -i
        - '*'
        - -x
        - ""
        - -b
        - '*'
        - -d
        - 15090,15021,15020
        env:
        - name: DNS_AGENT
        image: docker.io/istio/proxyv2:1.6.0
        imagePullPolicy: Always
        name: istio-init
      restartPolicy: Always

可見新增了一個容器鏡像

 image: docker.io/istio/proxyv2:1.6.0

那么注入的內容模板從哪里獲取,這里有兩個選項。

  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-inject


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM