Istio注入SideCar原理

简介

Istio提供一种简单的方式来建立已部署的服务的网络,具备负载均衡,服务到服务认证,监控等等功能,而不需要改动任何服务代码。

简单的说,有了Istio,你的服务就不再需要任何微服务开发框架(典型如Spring Cloud,Dubbo),也不再需要自己动手实现各种复杂的服务治理的功能(很多是Spring Cloud和Dubbo也不能提供的,需要自己动手)。只要服务的客户端和服务器可以进行简单的直接网络访问,就可以通过将网络层委托给Istio,从而获得一系列的完备功能。

可以近似的理解为:Istio = 微服务框架 + 服务治理

这里主要讲解使用istio时,一些sidecar容器的注入原理

sidecar

stio 服务网格目前所需的容器有:
istio-init 用于设置 iptables 规则,以便将入站/出站流量通过 Sidecar 代理。
istio-proxy 这个容器是真正的 Sidecar 代理(基于 Envoy)

向 pod 中注入 Istio Sidecar 的两种方法:

  1. 使用 istioctl 手动注入

  2. 启用 pod 所属命名空间的 Istio Sidecar 注入器自动注入。

这里只讲解自动注入的情况

使用配置

如果想把一个服务纳入 Istio 的网格中,需要在 pod 中注入 Sidecar 进行流量的劫持处理,通用的做法就是在 namespace 上打上 istio-injection=enabled 标签,这样只要在此 namespace 下创建或重启 pod 都会导致 pod 被注入 Sidecar,当然为了不让指定 pod 注入 Sidecar,可以在 pod 的 annotations 里加上 sidecar.istio.io/inject: "false" 。

webhook

Kubernetes 提供了自定义资源类型和自定义控制器来扩展功能,还提供了动态准入控制 Webhook,其实这个动态准入控制就是一个回调,Kubernetes 通过 Webhook 来实现准入控制,分为两种:验证性质的准入 Webhook (Validating Admission Webhook) 和修改性质的准入 Webhook (Mutating Admission Webhook)。
在 Istio 的配置里可以看到回调的 url 路径 /inject,Istio 主要使用的是 Mutating Admission Webhook,在资源持久化到 ETCD 之前进行资源的修改,增加 Init Container 和 Sidecar Container。但是 Istio 在进行资源修改前,需要满足一些条件,这些条件可以通过配置进行修改。

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

通过配置我们看到, namespaceSelector 会去 match 标签为 istio-injection: enabled 的 namespace,并且根据请求规则,去匹配所有 pod 的创建 CREATE 请求。当 apiserver 收到一个符合规则的请求时,apiserver 会给 Webhook 服务发送一个通过审核的请求,Istio 中的这个 Webhook 服务是 Istiod 的 service,请求地址为 /inject。从代码 /pkg/kube/inject/webhook.go,中我们查看 Istio 是如何处理自动注入的,在 Discovery Server 中注册了两个用来处理自动注入的请求 handler, p.Mux.HandleFunc ("/inject", wh.serveInject) 、 p.Mux.HandleFunc ("/inject/", wh.serveInject) , wh.serveInject 就是实现自动注入的主要逻辑。

源码解析

入口函数:

// NewWebhook creates a new instance of a mutating webhook for automatic sidecar injection.
func NewWebhook(p WebhookParameters) (*Webhook, error) {
   if p.Mux == nil {
      return nil, errors.New("expected mux to be passed, but was not passed")
   }

   wh := &Webhook{
      watcher:    p.Watcher,
      meshConfig: p.Env.Mesh(),
      env:        p.Env,
      revision:   p.Revision,
   }

   p.Watcher.SetHandler(wh.updateConfig)
   sidecarConfig, valuesConfig, err := p.Watcher.Get()
   if err != nil {
      return nil, err
   }
   if err := wh.updateConfig(sidecarConfig, valuesConfig); err != nil {
      log.Errorf("failed to process webhook config: %v", err)
   }

   p.Mux.HandleFunc("/inject", wh.serveInject)
   p.Mux.HandleFunc("/inject/", wh.serveInject)

   p.Env.Watcher.AddMeshHandler(func() {
      wh.mu.Lock()
      wh.meshConfig = p.Env.Mesh()
      wh.mu.Unlock()
   })

   return wh, nil
}

从上面可以看到,webhook的回调地址是/inject,对应的处理函数是serveInject

下面来看看这个处理函数

func (wh *Webhook) serveInject(w http.ResponseWriter, r *http.Request) {
	// ...省略一万字...

	var reviewResponse *kube.AdmissionResponse
	var obj runtime.Object
	var ar *kube.AdmissionReview
	if out, _, err := deserializer.Decode(body, nil, obj); err != nil {
		handleError(fmt.Sprintf("Could not decode body: %v", err))
		reviewResponse = toAdmissionResponse(err)
	} else {
		log.Debugf("AdmissionRequest for path=%s\n", path)
		ar, err = kube.AdmissionReviewKubeToAdapter(out)
		if err != nil {
			handleError(fmt.Sprintf("Could not decode object: %v", err))
			reviewResponse = toAdmissionResponse(err)
		} else {
			reviewResponse = wh.inject(ar, path)
		}
	}

	// ...省略一万字...
}

func (wh *Webhook) inject(ar *v1beta1.AdmissionReview, path string) *v1beta1.AdmissionResponse {
	// ...省略一万字...
	
 	wh.mu.RLock()
	if !injectRequired(IgnoredNamespaces.UnsortedList(), wh.Config, &pod.Spec, pod.ObjectMeta) {
		log.Infof("Skipping %s/%s due to policy check", pod.ObjectMeta.Namespace, podName)
		totalSkippedInjections.Increment()
		wh.mu.RUnlock()
		return &kube.AdmissionResponse{
			Allowed: true,
		}
	}

	proxyConfig := mesh.DefaultProxyConfig()
	if wh.env.PushContext != nil && wh.env.PushContext.ProxyConfigs != nil {
		if generatedProxyConfig := wh.env.PushContext.ProxyConfigs.EffectiveProxyConfig(
			&model.NodeMetadata{
				Namespace:   pod.Namespace,
				Labels:      pod.Labels,
				Annotations: pod.Annotations,
			}, wh.meshConfig); generatedProxyConfig != nil {
			proxyConfig = generatedProxyConfig
		}
	}
	deploy, typeMeta := kube.GetDeployMetaFromPod(&pod)
	params := InjectionParameters{
		pod:                 &pod,
		deployMeta:          deploy,
		typeMeta:            typeMeta,
		templates:           wh.Config.Templates,
		defaultTemplate:     wh.Config.DefaultTemplates,
		aliases:             wh.Config.Aliases,
		meshConfig:          wh.meshConfig,
		proxyConfig:         proxyConfig,
		valuesConfig:        wh.valuesConfig,
		revision:            wh.revision,
		injectedAnnotations: wh.Config.InjectedAnnotations,
		proxyEnvs:           parseInjectEnvs(path),
	}
	wh.mu.RUnlock()

	patchBytes, err := injectPod(params)
	if err != nil {
		handleError(fmt.Sprintf("Pod injection failed: %v", err))
		return toAdmissionResponse(err)
	}
	// ...省略一万字...
}

主要逻辑就是

  • 解析request请求

  • 检查各项配置,查看是否运用webhook进行sidecar的注入

  • 检查通过则注入sidecar

  • 构造适配kube的返回结果

1. 注入条件

下面来看下需要满足哪些条件才会注入

func injectRequired(ignored []string, config *Config, podSpec *corev1.PodSpec, metadata *metav1.ObjectMeta) bool { 
    // Skip injection when host networking is enabled. The problem is
	// that the iptables changes are assumed to be within the pod when,
	// in fact, they are changing the routing at the host level. This
	// often results in routing failures within a node which can
	// affect the network provider within the cluster causing
	// additional pod failures.
	if podSpec.HostNetwork {
		return false
	}

	// skip special kubernetes system namespaces
	for _, namespace := range ignored {
		if metadata.Namespace == namespace {
			return false
		}
	}

	annos := metadata.GetAnnotations()

	var useDefault bool
	var inject bool

	objectSelector := annos[annotation.SidecarInject.Name]
	if lbl, labelPresent := metadata.GetLabels()[label.SidecarInject.Name]; labelPresent {
		// The label is the new API; if both are present we prefer the label
		objectSelector = lbl
	}
	switch strings.ToLower(objectSelector) {
	// http://yaml.org/type/bool.html
	case "y", "yes", "true", "on":
		inject = true
	case "":
		useDefault = true
	}

	// If an annotation is not explicitly given, check the LabelSelectors, starting with NeverInject
	if useDefault {
		for _, neverSelector := range config.NeverInjectSelector {
			selector, err := metav1.LabelSelectorAsSelector(&neverSelector)
			if err != nil {
				log.Warnf("Invalid selector for NeverInjectSelector: %v (%v)", neverSelector, err)
			} else if !selector.Empty() && selector.Matches(labels.Set(metadata.Labels)) {
				log.Debugf("Explicitly disabling injection for pod %s/%s due to pod labels matching NeverInjectSelector config map entry.",
					metadata.Namespace, potentialPodName(metadata))
				inject = false
				useDefault = false
				break
			}
		}
	}

	// If there's no annotation nor a NeverInjectSelector, check the AlwaysInject one
	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)) {
				log.Debugf("Explicitly enabling injection for pod %s/%s due to pod labels matching AlwaysInjectSelector config map entry.",
					metadata.Namespace, potentialPodName(metadata))
				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
		}
	}

	if log.DebugEnabled() {
		// Build a log message for the annotations.
		annotationStr := ""
		for name := range AnnotationValidation {
			value, ok := annos[name]
			if !ok {
				value = "(unset)"
			}
			annotationStr += fmt.Sprintf("%s:%s ", name, value)
		}

		log.Debugf("Sidecar injection policy for %v/%v: namespacePolicy:%v useDefault:%v inject:%v required:%v %s",
			metadata.Namespace,
			potentialPodName(metadata),
			config.Policy,
			useDefault,
			inject,
			required,
			annotationStr)
	}

	return required
}

判断条件很多,具体如下

  • 判断 pod 的 spec 中没有设置 hostNetwork:true

  • 判断待注入的 pod 不在系统 namespace 里,如 kube-system 、kube-public

  • 设置三个临时变量, useDefault=false 、 inject=false 、required=false,判断是否配置 sidecar.istio.io/inject

  • 如果 sidecar.istio.io/inject 的值设置为 y, yes, true, on,则 inject=true

  • sidecar.istio.io/inject 为其他值,则 useDefault=true

  • 判断 neverInjectSelector 是否有匹配到的条件,如果匹配到则设置 useDefault = false、inject = false

  • 判断 alwaysInjectSelector 是否有匹配到的条件,如果匹配到则设置 useDefault = false、inject = true, alwaysInjectSelector 优先级高于 neverInjectSelector

  • 判断 policy 的取值

  • 如果 policy 取值为 disabled,并且上述得到的 useDefault=true,则 required=false,不进行注入

  • 如果 policy 取值为 disabled,并且上述得到的 useDefault=false,则需要根据 inject 的值判断是否需要注入

  • 如果 policy 取值为 enabled,并且上述得到的 useDefault=false,则需要根据 inject 的值判断是否需要注入

  • 如果 policy 取值为 enabled,并且上述得到的 useDefault=true,则 required=true,进行注入

  • policy 为其他值,不进行注入

从上面可以看出
是否注入Sidecar的优先级为

Pod Annotations → NeverInjectSelector → AlwaysInjectSelector → Default Policy

留给使用者的控制选项是很多的,非常灵活。

2. 注入过程

下面来看下具体的注入过程

func injectPod(req InjectionParameters) ([]byte, error) {
   checkPreconditions(req)

   // The patch will be built relative to the initial pod, capture its current state
   originalPodSpec, err := json.Marshal(req.pod)
   if err != nil {
      return nil, err
   }

   // Run the injection template, giving us a partial pod spec
   mergedPod, injectedPodData, err := RunTemplate(req)
   if err != nil {
      return nil, fmt.Errorf("failed to run injection template: %v", err)
   }

   mergedPod, err = reapplyOverwrittenContainers(mergedPod, req.pod, injectedPodData)
   if err != nil {
      return nil, fmt.Errorf("failed to re apply container: %v", err)
   }

   // Apply some additional transformations to the pod
   if err := postProcessPod(mergedPod, *injectedPodData, req); err != nil {
      return nil, fmt.Errorf("failed to process pod: %v", err)
   }

   patch, err := createPatch(mergedPod, originalPodSpec)
   if err != nil {
      return nil, fmt.Errorf("failed to create patch: %v", err)
   }

   log.Debugf("AdmissionResponse: patch=%v\n", string(patch))
   return patch, nil
}

func createPatch(pod *corev1.Pod, original []byte) ([]byte, error) {
	reinjected, err := json.Marshal(pod)
	if err != nil {
		return nil, err
	}
	p, err := jsonpatch.CreatePatch(original, reinjected)
	if err != nil {
		return nil, err
	}
	return json.Marshal(p)
}

可以看到,整个注入过程逻辑为:

  • 把原本的Pod配置反解析成Pod对象,

  • 把需要注入的Yaml内容(如:Sidecar)反序列成对象然后append到对应Pod (如:Container)上,

  • 再把修改后的Pod重新解析成yaml 内容返回给k8s的api server,

  • k8s 拿着修改后内容再将这两个容器调度到同一台机器进行部署

https://www.cnblogs.com/haoyunlaile/p/12960441.html

posted on 2023-05-03 13:14  萌兰三太子  阅读(79)  评论(0)    收藏  举报  来源

导航