# Istio Sidecar 注入：例外和除错

原文：[Istio sidecar injection: enabling automatic injection, adding exceptions and debugging](https://bani.com.br/2018/09/istio-sidecar-injection-enabling-automatic-injection-adding-exceptions-and-debugging/)

作者：[Jonh Wendell](https://twitter.com/jwendell)

Kubernetes 环境下的 Istio 使用了 Sidecar 模型进行部署，把一个辅助容器（也就是 Sidecar）附加到业务 Pod 之中。这一过程让 Sidecar 容器和业务容器共享同样的网络栈，可以视为同一主机上的两个进程。这样一来，Istio 就能够接管业务应用的所有网络调用，就有了增强服务间通信能力的基础。

这个 Sidecar 容器命名为 `istio-proxy`，能够用手工或者自动方式进行注入。其实这个手工注入也不是 100% 徒手完成的。

## 手工注入

Istio 的发行版本中会带有一个 `istioctl` 工具。看名字就知道这工具很棒：）。它的一个能力就是把 `istio-proxy` Sidecar 注入到业务容器之中。我们可以用一个简单的 `busybox` Pod 来看看它的功能：

~~~shell
$ cat busybox.yaml

apiVersion: v1
kind: Pod
metadata:
  name: busybox-test
spec:
  containers:
  - name: busybox-container
    image: busybox
    command: ['sh', '-c', 'echo Hello Kubernetes! && sleep infinity']

$ istioctl kube-inject -f busybox.yaml > busybox-injected.yaml
$ cat busybox-injected.yaml
apiVersion: v1
kind: Pod
metadata:
  labels:
  name: busybox-test
spec:
  containers:
  - command:
    - sh
    - -c
    - echo Hello Kubernetes! && sleep infinity
    image: busybox
    name: busybox-container

  - image: docker.io/istio/proxyv2:1.0.2
    imagePullPolicy: IfNotPresent
    name: istio-proxy
    args:
      - proxy
      - sidecar
   ...
~~~

上文可见，这个命令生成了一个 YAML 文件，和原始文件有点像，但是 Pod 中加入了一个 Sidecar（istio-proxy）。这的确不是纯手工吧——至少让我们少打了不少字。然后就可以使用 `kubectl apply` 命令把这个修改后的文件提交给 Kubernetes 集群了：

~~~bash
$ kubectl apply -f busybox-injected.yaml
# 如果不想生成中间文件，可以直接用管道完成全部操作。
$ kubectl apply -f <(istioctl kube-inject -f busybox.yaml)
~~~

随之而来的一个问题就是：这些数据是哪来的？它怎么知道 Sidecar 镜像是 `docker.io/istio/proxyv2:1.0.2` 的？答案很简单：所有数据都来自于一个 ConfigMap，这个对象保存在 `istio-system` 命名空间：

~~~shell
$ kubectl -n istio-system describe configmap istio-sidecar-injector
Name:         istio-sidecar-injector
Namespace:    istio-system
 
Data
====
config:
----
policy: enabled
template: |-
  initContainers:
  - name: istio-init
    image: "docker.io/istio/proxy_init:1.0.2"
...
~~~

可以对这个 ConfigMap 进行编辑，修改一些你需要的值，用来进行注入。不难发现，这主要是一个用于向 Pod 加入内容的模板。如果想要为 `istio-proxy` 容器使用其它对象，只要修改这个字段的内容就可以了，或者还可以调整任何其它要注入的东西。这个 ConfigMap 是用来完成网格中所有 Pod 的注入工作的，所以要小心从事。

因为 `istioctl` 要根据 ConfigMap 来获知注入内容，也就是说执行 `istioctl` 的用户必须能够访问到安装了 Istio 的 Kubernetes 集群的这一对象。如果因为某些原因无法访问到这一 ConfigMap，还可以在 `istioctl` 中使用一个本地的配置文件。

~~~bash
# 首先使用有权限的用户运行这一命令
$ kubectl -n istio-system get configmap istio-sidecar-injector -o=jsonpath='{.data.config}' > inject-config.yaml
# 这个文件可以随时进行任意修改。
$ istioctl kube-inject --injectConfigFile inject-config.yaml ...
~~~

## 自动注入

注入 `istio-proxy` 还有另一个方式，就是要求 Istio 进行自动注入。为命名空间设置标签 istio-injection=enabled 就能满足这一需要了。如果命名空间加上了这个标签，所有其中的 Pod 都会被注入 `istio-proxy` Sidecar，也就无需手工执行 `istioctl` 处理 YAML 文件了。

工作方式很简单：它使用了 Kubernetes 的 `MutatingWebhook`，新 Pod 创建之前，这一功能会通知 Istio，让 Istio 有机会对新 Pod 进行就地修改，stio 会使用在 ConfigMap 中的模板把 `istio-proxy` 注入到 Pod 中。

## 看起来不错？

自动注入过程有很大的弹性：

- `istio-sidecar-injector` ConfigMap 中有一个布尔值用来指定自动注入是否启用。
- 只有使用标签进行标注的命名空间才会进行自动注入，也就是说用命名空间标签能够对自动注入进行控制。
- 还可以使用 `kubectl -n istio-system edit MutatingWebhookConfiguration istio-sidecar-injector` 命令，修改其中的 `namespaceSelector` 字段来调整这个标签的用法，甚至移除这个限制（也就是为所有命名空间使用自动注入，慎用）。
- 可以禁用特定 Pod 的自动注入。如果 Pod 包含注解 `sidecar.istio.io/inject: "false"`，Istio 就不会为在这一 Pod 中注入 Sidecar。
- 还有更保守的方式：只对选择的 Pod 进行注入——也就是白名单方式。设置策略为 false（`kubectl -n istio-system edit configmap istio-sidecar-injector`），只对包含注解 `sidecar.istio.io/inject: "true"` 进行注入。

## 想要更大弹性？

还有些上面提到的方法无法满足的弹性要求：

如果不熟悉 Openshift 的用户，它有一个功能叫做 `source-to-image`（代码到镜像，`s2i`），这一功能会将代码构建为容器镜像。提供一个 git 仓库作为输入（支持多种语言），就会输出镜像并运行到 Openshift 集群上。

这是一个神奇的功能。这里不会介绍很多细节，我只会告诉你本文中需要了解的事情：在这一过程中 Openshift 会创建一或更多个的用于进行构建的中间、辅助 Pod。构建过程结束之后，二进制工件被构建为容器镜像，而辅助 Pod 会被销毁。

如果我们为指定命名空间启用了自动注入，并使用 Openshift 的 s2i 功能，会让这些辅助 Pod 也被注入 Sidecar，这些 Pod 是 Openshift 创建的，我们无法对其进行注解以阻止 Sidecar 的注入。辅助 Pod 无需进行注入，怎么办呢？

一个可能的解决方法就是用上面提到的保守方案，只对特定 Pod 启用注入，但是就要对 Pod 进行特别的注解。还有别的方式。

### 新方案

1.1.0 中，Istio 自动注入可以根据标签进行例外设置：不管命名空间标签如何，策略如何设置，对符合标签选择器要求的 Pod 都不进行注入。可以在 `istio-sidecar-injector` 中加入例外设置。

~~~shell
$ kubectl -n istio-system describe configmap istio-sidecar-injector
apiVersion: v1
kind: ConfigMap
metadata:
  name: istio-sidecar-injector
data:
  config: |-
    policy: enabled
    neverInjectSelector:
      - matchExpressions:
        - {key: openshift.io/build.name, operator: Exists}
      - matchExpressions:
        - {key: openshift.io/deployer-pod-for.name, operator: Exists}
    template: |-
      initContainers:
...
~~~

可以看到上面的 `neverInjectSelector` 字段，它是一个 Kubernetes 标签选择器的数组。不同元素之间是“或”关系，第一次发现有符合条件的标签之后就会跳过其它判断。上面的语句意味着：包含 `openshift.io/build.name` 或者 `openshift.io/deployer-pod-for.name` 标签的 Pod，不管标签值如何，都不会进行注入。

为了完整性起见，可以使用 `alwaysInjectSelector` 字段，这个字段会无视全局策略，向符合条件的 Pod 进行注入。

标签选择器方式产生了很大的弹性，能够处理更多的例外情况。阅读 [Kubernetes 文档](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#resources-that-support-set-based-requirements)可以了解更多这方面的内容。

值得注意的是，Pod 注解还是有更高的优先级，如果 Pod 注解包含了 `sidecar.istio.io/inject: "true/false"`，会被优先处理，所以自动注入的评估顺序是：

Pod 注解 → NeverInjectSelector → AlwaysInjectSelector → 命名空间策略。

注入选择器是新特性，这方面的文档还在更新，但是其它部分的文档和例子，都可以在[官方文档](https://istio.io/docs/setup/kubernetes/sidecar-injection/)中查看。

## Pod 为什么没注入？

这是个常见问题。按照前面的介绍（例如给命名空间打标签）进行操作，结果 Pod 还没有被注入。

或者刚好相反，Pod 明明注解为 `sidecar.istio.io/inject: "false`，还是被注入了，为什么？

可以看看 `sidecar-injector` Pod 的日志：

~~~shell
$ pod=$(kubectl -n istio-system get pods -l istio=sidecar-injector -o jsonpath='{.items[0].metadata.name}')
$ kubectl -n istio-system logs -f $pod
~~~

然后可以创建业务 Pod，看看日志输出的具体内容。要看到更详细的日志（经常会很有用），可以编辑 `sidecar-injector` Deployment 对象，给它加上参数 `--log_output_level=default:debug`：

~~~shell
$ kubectl -n istio-system edit deployment istio-sidecar-injector
...
      containers:
      - args:
        - --caCertFile=/etc/istio/certs/root-cert.pem
        - --tlsCertFile=/etc/istio/certs/cert-chain.pem
        - --tlsKeyFile=/etc/istio/certs/key.pem
        - --injectConfig=/etc/istio/inject/config
        - --meshConfig=/etc/istio/config/mesh
        - --healthCheckInterval=2s
        - --healthCheckFile=/health
        - --log_output_level=default:debug
        image: docker.io/istio/sidecar_injector:1.0.2
        imagePullPolicy: IfNotPresent
...
~~~

编辑成功之后 Pod 会重启，完成之后就可以重新查看日志了：

~~~shell
$ pod=$(kubectl -n istio-system get pods -l istio=sidecar-injector -o jsonpath='{.items[0].metadata.name}')
$ kubectl -n istio-system logs -f $pod
~~~

### 提示

如果在日志中还是找不到问题原因，就代表 `sidecar-injector` Pod 没有收到 Pod 创建的通知，也就不会触发自动注入的操作。这可能是因为命名空间没有正确标签导致的，因此需要检查一下命名空间的标签以及 `MutatingWebhookConfiguration` 中的配置。缺省情况下，命名空间应该设置 `istio-injection=enabled` 标签。可以使用 `kubectl -n istio-system edit MutatingWebhookConfiguration istio-sidecar-injector` 命令检查其中的 `namespaceSelector` 字段内容。

完成排查之后，可以再次编辑 `sidecar-injector` Deployment 对象，清除新加入的参数。
