Istio Sidecar 注入过程解密

本文中我们会深入到 Sidecar 注入模型中,来更清晰的了解 Sidecar 的注入过程。

作者 Manish Chugtu 译者 崔秀龙 发表于 2019年4月8日

Istio 服务网格架构的概述,通常都是从对数据面和控制面的叙述开始的。

来自 Istio 的官方文档。

Istio 服务网格逻辑上分为数据平面和控制平面。

数据平面由一组以 sidecar 方式部署的智能代理(Envoy)组成。这些代理和 Mixer(一个通用的策略和遥测中心)合作,对所有微服务之间的之间所有的网络通信进行控制。

控制平面负责管理和配置代理来路由流量。此外控制平面配置 Mixer 以实施策略和收集遥测数据。

Sidecar 注入到应用的过程,可以是自动的,也可以是手动的,了解这一过程是很重要的。应用的流量会被重定向进入或流出 Sidecar,开发人员无需关心。应用接入 Istio 服务网格之后,开发者可以开始使用网格功能并从中受益。然而数据平面是如何工作的,以及需要怎样的条件才能完成这种无缝工作?本文中我们会深入到 Sidecar 注入模型中,来更清晰的了解 Sidecar 的注入过程。

Sidecar 注入

简单来说,注入 Sidecar 就是把附加的容器配置插入 Pod 模板的过程。Istio 服务网格所需的附加容器是:

istio-init

这个初始化容器用于设置 iptables 规则,让出入流量都转由 Sidecar 进行处理。和应用容器相比,初始化容器有几点不同:

  • 它在应用容器之前启动,并且会运行到结束。
  • 如果有多个初始化容器,只有在前一个初始化容器成功结束之后才会启动下一个。

不难看出,这种容器是非常适合执行启动或者初始化任务的,并且也无需和实际的应用容器集成到一起。istio-init 只是用来设置 iptables 的规则。

istio-proxy

这个容器是真正的 Sidecar(基于 Envoy)。

手工注入

要进行手工注入,只要用 istioctl 把前面提到的两个容器定义加入到 Pod 的模板之中即可。不论是手工注入还是自动注入,都是从 istio-sidecar-injector 以及 istio 两个 Configmap 对象中获取配置的。

我们先来看看 istio-sidecar-injector Configmap,了解一下其中的内容。

$ kubectl -n istio-system get configmap istio-sidecar-injector -o=jsonpath='{.data.config}'
部分输出内容:

policy: enabled
template: |-
  initContainers:
  - name: istio-init
    image: docker.io/istio/proxy_init:1.0.2
    args:
    - "-p"
    - [[ .MeshConfig.ProxyListenPort ]]
    - "-u"
    - 1337
    .....
    imagePullPolicy: IfNotPresent
    securityContext:
      capabilities:
        add:
        - NET_ADMIN
    restartPolicy: Always

  containers:
  - name: istio-proxy
    image: [[ if (isset .ObjectMeta.Annotations "sidecar.istio.io/proxyImage") -]]
    "[[ index .ObjectMeta.Annotations "sidecar.istio.io/proxyImage" ]]"
    [[ else -]]
    docker.io/istio/proxyv2:1.0.2
    [[ end -]]
    args:
    - proxy
    - sidecar
    .....
    env:
    .....
    - name: ISTIO_META_INTERCEPTION_MODE
      value: [[ or (index .ObjectMeta.Annotations "sidecar.istio.io/interceptionMode") .ProxyConfig.InterceptionMode.String ]]
    imagePullPolicy: IfNotPresent
    securityContext:
      readOnlyRootFilesystem: true
      [[ if eq (or (index .ObjectMeta.Annotations "sidecar.istio.io/interceptionMode") .ProxyConfig.InterceptionMode.String) "TPROXY" -]]
      capabilities:
        add:
        - NET_ADMIN
    restartPolicy: Always
    .....

如你所见,这个 Configmap 中包含了前面提到的两个容器的配置内容:istio-init 初始化容器以及 istio-proxy 代理容器。配置中包含了容器镜像名称以及一些参数,例如拦截模式,权限要求等。

从安全角度看来,要注意 istio-init 需要 NET_ADMIN 权限,以便在 Pod 的命名空间中修改 iptables;如果 istio-proxy 设置为 TPROXY 模式,也会需要这一权限。这一权限是限制在 Pod 的命名空间之内的,这应该没问题。然而我注意到最近的 open-shift 版本好像在这方面有点问题,需要进一步确认。这方面的选项会在文末继续讨论。

要修改当前的 Pod 模板来进行注入,可以:

 $ istioctl kube-inject -f demo-red.yaml | kubectl apply -f -

或者

想要使用修改过的 Configmap 或者本地 Configmap:

  • 从 Configmap 中创建 inject-config.yaml and mesh-config.yaml

    $ kubectl -n istio-system get configmap istio-sidecar-injector -o=jsonpath='{.data.config}' > inject-config.yaml
    $ kubectl -n istio-system get configmap istio -o=jsonpath='{.data.mesh}' > mesh-config.yaml
    
  • 修改现存的 Pod 模板,这里假设文件名为 demo-red.yaml

    $ istioctl kube-inject --injectConfigFile inject-config.yaml --meshConfigFile mesh-config.yaml --filename demo-red.yaml --output demo-red-injected.yaml
    
  • 提交 demo-red-injected.yaml

    $ kubectl apply -f demo-red-injected.yaml
    

上面的几个步骤中,我们使用 sidecar-injector 以及 istio 两个 Configmap 的内容修改了 Pod 模板并使用 kubectl 命令进行了提交。如果查看一下注入后的 YAML 文件,会看到前面讨论过的 Istio 的专属容器,提交到集群上之后,会看到两个容器在运行:一个是实际的应用容器,另一个则是 istio-proxy Sidecar。

$ kubectl get pods | grep demo-red

demo-red-pod-8b5df99cc-pgnl7   2/2       Running   0          3d

这里没有 3 个 Pod,这是因为 istio-init 容器是一个初始化容器,它完成任务之后就会退出——他的任务就是设置 Pod 内的 iptables 规则。要确认退出的初始化容器,可以看看 kubectl describe 的输出:

$ kubectl describe pod demo-red-pod-8b5df99cc-pgnl7

SNIPPET from the output:

Name:               demo-red-pod-8b5df99cc-pgnl7
Namespace:          default
.....
Labels:             app=demo-red
                    pod-template-hash=8b5df99cc
                    version=version-red
Annotations:        sidecar.istio.io/status={"version":"3c0b8d11844e85232bc77ad85365487638ee3134c91edda28def191c086dc23e","initContainers":["istio-init"],"containers":["istio-proxy"],"volumes":["istio-envoy","istio-certs...
Status:             Running
IP:                 10.32.0.6
Controlled By:      ReplicaSet/demo-red-pod-8b5df99cc
Init Containers:
  istio-init:
    Container ID:  docker://bef731eae1eb3b6c9d926cacb497bb39a7d9796db49cd14a63014fc1a177d95b
    Image:         docker.io/istio/proxy_init:1.0.2
    Image ID:      docker-pullable://docker.io/istio/[email protected]:e16a0746f46cd45a9f63c27b9e09daff5432e33a2d80c8cc0956d7d63e2f9185
    .....
    State:          Terminated
      Reason:       Completed
    .....
    Ready:          True
Containers:
  demo-red:
    Container ID:   docker://8cd9957955ff7e534376eb6f28b56462099af6dfb8b9bc37aaf06e516175495e
    Image:          chugtum/blue-green-image:v3
    Image ID:       docker-pullable://docker.io/chugtum/[email protected]:274756dbc215a6b2bd089c10de24fcece296f4c940067ac1a9b4aea67cf815db
    State:          Running
      Started:      Sun, 09 Dec 2018 18:12:31 -0800
    Ready:          True
  istio-proxy:
    Container ID:  docker://ca5d690be8cd6557419cc19ec4e76163c14aed2336eaad7ebf17dd46ca188b4a
    Image:         docker.io/istio/proxyv2:1.0.2
    Image ID:      docker-pullable://docker.io/istio/[email protected]:54e206530ba6ca9b3820254454e01b7592e9f986d27a5640b6c03704b3b68332
    Args:
      proxy
      sidecar
      .....
    State:          Running
      Started:      Sun, 09 Dec 2018 18:12:31 -0800
    Ready:          True
    .....

从上文的输出可以看出,istio-init 容器的 State 字段值为 TerminatedReasonCompleted。正在运行的两个 Pod 是应用容器 demo-red 以及 istio-proxy

自动注入

多数时候,用户不想在每次部署应用的时候都用 istioctl 命令进行手工注入,这时候就可以使用 Istio 的自动注入功能来应对了。只要给用于部署应用的命名空间打个 istio-injection=enabled 标签就可以了。

打上标签之后,这个命名空间中新建的任何 Pod 都会被 Istio 注入 Sidecar。下面的例子里,istio-dev 命名空间中部署的 Pod 被自动注入了 Sidecar:

$ kubectl get namespaces --show-labels

NAME           STATUS    AGE       LABELS
default        Active    40d       <none>
istio-dev      Active    19d       istio-injection=enabled
istio-system   Active    24d       
kube-public    Active    40d       
kube-system    Active    40d       

这是怎么完成的呢?要回答这一问题,首先要了解一下 Kubernetes 的准入控制器。

来自 Kubernetes 文档

准入控制器是一段代码,会拦截 Kubernetes API Server 收到的请求,拦截发生在认证和鉴权完成之后,对象进行持久化之前。可以定义两种类型的 Admission webhook:Validating 和 Mutating。Validating 类型的 Webhook 可以根据自定义的准入策略决定是否拒绝请求;Mutating 类型的 Webhook 可以根据自定义配置来对请求进行编辑。

Istio 的自动注入过程中会依赖 Mutating webhook。我们看看 istio-sidecar-injector 中的配置详情:

$ kubectl get mutatingwebhookconfiguration istio-sidecar-injector -o yaml

输出内容节选:

apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"admissionregistration.k8s.io/v1beta1","kind":"MutatingWebhookConfiguration","metadata":{"annotations":{},"labels":{"app":"istio-sidecar-injector","chart":"sidecarInjectorWebhook-1.0.1","heritage":"Tiller","release":"istio-remote"},"name":"istio-sidecar-injector","namespace":""},"webhooks":[{"clientConfig":{"caBundle":"","service":{"name":"istio-sidecar-injector","namespace":"istio-system","path":"/inject"}},"failurePolicy":"Fail","name":"sidecar-injector.istio.io","namespaceSelector":{"matchLabels":{"istio-injection":"enabled"}},"rules":[{"apiGroups":[""],"apiVersions":["v1"],"operations":["CREATE"],"resources":["pods"]}]}]}
  creationTimestamp: 2018-12-10T08:40:15Z
  generation: 2
  labels:
    app: istio-sidecar-injector
    chart: sidecarInjectorWebhook-1.0.1
    heritage: Tiller
    release: istio-remote
  name: istio-sidecar-injector
  .....
webhooks:
- clientConfig:
    service:
      name: istio-sidecar-injector
      namespace: istio-system
      path: /inject
  name: sidecar-injector.istio.io
  namespaceSelector:
    matchLabels:
      istio-injection: enabled
  rules:
  - apiGroups:
    - ""
    apiVersions:
    - v1
    operations:
    - CREATE
    resources:
    - pods

这里可以看到一个 namespaceSelector,其中的定义表明它的工作是针对带有 istio-injection: enabled 标签的命名空间的。这种情况下,你还会看到 Pod 创建期间进行注入时的一些其它工作和相关资源的内容。当 apiserver 收到一个符合规则的请求时,apiserver 会给 Webhook 服务发送一个准入审核的请求,Webhook 服务的定义包含在 clientConfig 配置中的 name: istio-sidecar-injector 字段里。我们会看到,这个服务正在 istio-system 命名空间里运行。

$ kubectl get svc --namespace=istio-system | grep sidecar-injector

istio-sidecar-injector   ClusterIP   10.102.70.184   <none>        443/TCP             24d

这个配置总体上来说,完成了我们手工注入所需完成的工作。只不过是它是在 Pod 创建的过程中自动完成的,所以你也不会看到 Deployment 对象发生了任何变化。可以用 kubectl describe 命令来查看 Sidecar 和初始化容器。如果想要修改注入逻辑,例如 Istio 自动注入的生效范围,可以编辑 MutatingWebhookConfiguration,然后重启 Sidecar injector Pod。

Sidecar 的自动注入过程除了根据 namespaceSelector 选择命名空间之外,还受到缺省注入策略以及 Pod 自身注解的影响。

再看看 istio-sidecar-injector ConfigMap 中的缺省策略定义。可以看到,缺省是启用的。

$ kubectl -n istio-system get configmap istio-sidecar-injector -o=jsonpath='{.data.config}'

SNIPPET from the output:

policy: enabled
template: |-
  initContainers:
  - name: istio-init
    image: "gcr.io/istio-release/proxy_init:1.0.2"
    args:
    - "-p"
    - [[ .MeshConfig.ProxyListenPort ]]

还可以在 Pod 模板中使用注解 sidecar.istio.io/inject 覆盖缺省策略。下面的例子中的 Deployment 用这种方式禁用了自动注入:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: ignored
spec:
  template:
    metadata:
      annotations:
        sidecar.istio.io/inject: "false"
    spec:
      containers:
      - name: ignored
        image: tutum/curl
        command: ["/bin/sleep","infinity"]

这个例子展示了很多可能性,可以从命名空间标签、ConfigMap 或者 Pod 中分别进行控制:

  • Webhooks 定义中的 namespaceSelectoristio-injection: enabled
  • 缺省策略(在 ConfigMap istio-sidecar-injector 中定义)
  • Pod 注解(sidecar.istio.io/inject

注入状态表中,根据上面三个因素的不同,可以看到有不同的注入结果。

从应用容器到 Sidecar 代理的通信

现在我们知道了 Sidecar 容器和初始化容器被注入到应用中的过程了,Sidecar 代理是如何截获进出容器的流量呢?我们前面提到过,这是通过对 Pod 命名空间中 iptable 规则的设置来完成的,这个设置过程由 istio-init 容器来控制。现在可以看看命名空间中到底更新了些什么。

进入前面我们部署的应用 Pod 的命名空间,看看配置完成的 iptables。我会使用 nsenter。也可以用特权模式进入容器获得同样的信息。如果无法访问节点,可以用 exec 进入 Sidecar 来执行 iptables 指令。

$ docker inspect b8de099d3510 --format '{{ .State.Pid }}'

4125
$ nsenter -t 4215 -n iptables -t nat -S

-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N ISTIO_INBOUND
-N ISTIO_IN_REDIRECT
-N ISTIO_OUTPUT
-N ISTIO_REDIRECT
-A PREROUTING -p tcp -j ISTIO_INBOUND
-A OUTPUT -p tcp -j ISTIO_OUTPUT
-A ISTIO_INBOUND -p tcp -m tcp --dport 80 -j ISTIO_IN_REDIRECT
-A ISTIO_IN_REDIRECT -p tcp -j REDIRECT --to-ports 15001
-A ISTIO_OUTPUT ! -d 127.0.0.1/32 -o lo -j ISTIO_REDIRECT
-A ISTIO_OUTPUT -m owner --uid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -m owner --gid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -d 127.0.0.1/32 -j RETURN
-A ISTIO_OUTPUT -j ISTIO_REDIRECT
-A ISTIO_REDIRECT -p tcp -j REDIRECT --to-ports 15001

上面展示的内容中,可以清楚的看到,所有从 80 端口(red-demo 应用监听的端口)进入的流量,被 REDIRECTED 到了端口 15001,这是 istio-proxy(Envoy 代理)监听的端口。外发流量也是如此。

本文进入尾声。希望能够让读者了解到 Istio 将 Sidecar 注入到 Pod 中的过程,以及 Istio 将流量路由到代理服务器的过程。

更新:在 istio-init 阶段,现在有了一个新的使用 CNI 的方式,这种方式无需初始化容器的帮助,也不需要对应的权限。istio-cni 插件会设置 Pod 的网络来满足这一要求,可以代替 istio-init 来完成任务。

Tags: