[译]深入剖析 Kubernetes MutatingAdmissionWebhook

2019/4/28 posted in  Kubernetes

翻译自 《Diving into Kubernetes MutatingAdmissionWebhook》
原文链接:https://medium.com/ibm-cloud/diving-into-kubernetes-mutatingadmissionwebhook-6ef3c5695f74

对于在数据持久化之前,拦截到 Kubernetes API server 的请求,Admission controllers 是非常有用的工具。然而,由于其需要由集群管理员在 kube-apiserver 中编译成二进制文件,所以使用起来不是很灵活。从 Kubernetes 1.7 起,引入了 InitializersExternal Admission Webhooks,用以解决这个问题。在 Kubernetes 1.9 中,Initializers 依然停留在 alpha 版本,然而 External Admission Webhooks 被提升到了 beta 版,且被分成了 MutatingAdmissionWebhookValidatingAdmissionWebhook

MutatingAdmissionWebhookValidatingAdmissionWebhook 二者合起来就是一个特殊类型的 admission controllers,一个处理资源更改,一个处理验证。验证是通过匹配到 MutatingWebhookConfiguration 中定义的规则完成的。

在这篇文章中,我会深入剖析 MutatingAdmissionWebhook 的细节,并一步步实现一个可用的 webhook admission server

Webhooks 的好处

Kubernetes 集群管理员可以使用 webhooks 来创建额外的资源更改及验证准入插件,这些准入插件可以通过 apiserver 的准入链来工作,而不需要重新编译 apiserver。这使得开发者可以对于很多动作都可以自定义准入逻辑,比如对任何资源的创建、更新、删除,给开发者提供了很大的自由和灵活度。可以使用的应用数量巨大。一些常见的使用常见包括:

  1. 在创建资源之前做些更改。Istio 是个非常典型的例子,在目标 pods 中注入 Envoy sidecar 容器,来实现流量管理和规则执行。
  2. 自动配置 StorageClass。监听 PersistentVolumeClaim 资源,并按照事先定好的规则自动的为之增添对应的 StorageClass。使用者无需关心 StorageClass 的创建。
  3. 验证复杂的自定义资源。确保只有其被定义后且所有的依赖项都创建好并可用,自定义资源才可以创建。
  4. namespace 的限制。在多租户系统中,避免资源在预先保留的 namespace 中被创建。

除了以上列出来的使用场景,基于 webhooks 还可以创建更多应用。

Webhooks 和 Initializers

基于社区的反馈,以及对 External Admission WebhooksInitializers 的 alpha 版本的使用案例,Kubernetes 社区决定将 webhooks 升级到 beta 版,并且将其分成两种 webhooks(MutatingAdmissionWebhookValidatingAdmissionWebhook)。这些更新使得 webhooks 与其他 admission controllers 保持一致,并且强制 mutate-before-validateInitializers 可以在 Kubernetes 资源创建前更改,从而实现动态准入控制。如果你对 Initializers 不熟悉,可以参考这篇文章

所以,到底 WebhooksInitializers 之间的区别是什么呢?

  1. Webhooks 可以应用于更多操作,包括对于资源 "增删改" 的 "mutate" 和 "admit";然而 Initializers 不可以对 "删" 资源进行 "admit"。
  2. Webhooks 在创建资源前不允许查询;然而 Initializers 可以监听未初始化的资源,通过参数 ?includeUninitialized=true 来实现。
  3. 由于 Initializers 会把 "预创建" 状态也持久化到 etcd,因此会引入高延迟且给 etcd 带来负担,尤其在 apiserver 升级或失败时;然而 Webhooks 消耗的内存和计算资源更少。
  4. WebhooksInitializers 对失败的保障更强大。Webhooks 的配置中可以配置失败策略,用以避免资源在创建的时候被 hang 住。然而 Initializers 在尝试创建资源的时候可能会 block 住所有的资源。

除了上面列举的不同点,Initializer 在较长一段开发时间内还存在很多已知问题,包括配额补充错误等。Webhooks 升级为 beta 版也就预示着在未来 Webhooks 会是开发目标。如果你需要更稳定的操作,我推荐使用 Webhooks

MutatingAdmissionWebhook 如何工作

MutatingAdmissionWebhook 在资源被持久化到 etcd 前,根据规则将其请求拦截,拦截规则定义在 MutatingWebhookConfiguration 中。MutatingAdmissionWebhook 通过对 webhook server 发送准入请求来实现对资源的更改。而 webhook server 只是一个简单的 http 服务器。

下面这幅图详细描述了 MutatingAdmissionWebhook 如何工作:

MutatingAdmissionWebhook 需要三个对象才能运行:

MutatingWebhookConfiguration

MutatingAdmissionWebhook 需要根据 MutatingWebhookConfiguration 向 apiserver 注册。在注册过程中,MutatingAdmissionWebhook 需要说明:

  1. 如何连接 webhook admission server
  2. 如何验证 webhook admission server
  3. webhook admission server 的 URL path;
  4. webhook 需要操作对象满足的规则;
  5. webhook admission server 处理时遇到错误时如何处理。

MutatingAdmissionWebhook 本身

MutatingAdmissionWebhook 是一种插件形式的 admission controller ,且可以配置到 apiserver 中。MutatingAdmissionWebhook 插件可以从 MutatingWebhookConfiguration 中获取所有感兴趣的 admission webhooks

然后 MutatingAdmissionWebhook 监听 apiserver 的请求,拦截满足条件的请求,并并行执行。

Webhook Admission Server

Webhook Admission Server 只是一个附着到 k8s apiserver 的 http server。对于每一个 apiserver 的请求,MutatingAdmissionWebhook 都会发送一个 admissionReview 到相关的 webhook admission serverwebhook admission server 再决定如何更改资源。

MutatingAdmissionWebhook 教程

编写一个完整的 Webhook Admission Server 可能令人生畏。为了方便起见,我们编写一个简单的 Webhook Admission Server 来实现注入 nginx sidecar 容器以及挂载 volume。完整代码在 kube-mutating-webhook-tutorial。这个项目参考了 Kubernetes webhook 示例Istio sidecar 注入实现

在接下来的段落里,我会向你展示如何编写可工作的容器化 webhook admission server,并将其部署到 Kubernetes 集群中。

前置条件

MutatingAdmissionWebhook 要求 Kubernetes 版本为 1.9.0 及以上,其 admissionregistration.k8s.io/v1beta1 API 可用。确保下面的命令:

kubectl api-versions | grep admissionregistration.k8s.io/v1beta1

其输出为:

admissionregistration.k8s.io/v1beta1

另外,MutatingAdmissionWebhookValidatingAdmissionWebhook 准入控制器需要以正确的顺序加入到 kube-apiserveradmission-control 标签中。

编写 Webhook Server

Webhook Admission Server 是一个简单的 http 服务器,遵循 Kubernetes API。我粘贴部分伪代码来描述主逻辑:

sidecarConfig, err := loadConfig(parameters.sidecarCfgFile)
pair, err := tls.LoadX509KeyPair(parameters.certFile, parameters.keyFile)

whsvr := &WebhookServer {
    sidecarConfig:    sidecarConfig,
    server:           &http.Server {
        Addr:        fmt.Sprintf(":%v", 443),
        TLSConfig:   &tls.Config{Certificates: []tls.Certificate{pair}},
    },
}
    
// define http server and server handler
mux := http.NewServeMux()
mux.HandleFunc("/mutate", whsvr.serve)
whsvr.server.Handler = mux

// start webhook server in new rountine
go func() {
    if err := whsvr.server.ListenAndServeTLS("", ""); err != nil {
        glog.Errorf("Filed to listen and serve webhook server: %v", err)
    }
}()

以上代码的详解:

  1. sidecarCfgFile 包含了 sidecar 注入器模板,其在下面的 ConfigMap 中定义;
  2. certFilekeyFile 是秘钥对,会在 webhook server 和 apiserver 之间的 TLS 通信中用到;
  3. 19 行开启了 https server,以监听 443 端口路径为 '/mutate'

接下来我们关注处理函数 serve 的主要逻辑:

// Serve method for webhook server
func (whsvr *WebhookServer) serve(w http.ResponseWriter, r *http.Request) {
    var body []byte
    if r.Body != nil {
        if data, err := ioutil.ReadAll(r.Body); err == nil {
            body = data
        }
    }

    var reviewResponse *v1beta1.AdmissionResponse
    ar := v1beta1.AdmissionReview{}
    deserializer := codecs.UniversalDeserializer()
    if _, _, err := deserializer.Decode(body, nil, &ar); err != nil {
        glog.Error(err)
        reviewResponse = toAdmissionResponse(err)
    } else {
        reviewResponse = mutate(ar)
    }

    response := v1beta1.AdmissionReview{}
    if reviewResponse != nil {
        response.Response = reviewResponse
        response.Response.UID = ar.Request.UID
    }
    // reset the Object and OldObject, they are not needed in a response.
    ar.Request.Object = runtime.RawExtension{}
    ar.Request.OldObject = runtime.RawExtension{}

    resp, err := json.Marshal(response)
    if err != nil {
        glog.Error(err)
    }
    if _, err := w.Write(resp); err != nil {
        glog.Error(err)
    }
}

函数 serve 是一个简单的 http 处理器,参数为 http requestresponse writer

  1. 首先将请求组装为 AdmissionReview,其中包括 objectoldobjectuserInfo ...
  2. 然后触发 Webhook 主函数 mutate 来创建 patch,以实现注入 sidecar 容器及挂载 volume。
  3. 最后,将 admission decision 和额外 patch 组装成响应,并发送回给 apiserver。

对于函数 mutate 的实现,你可以随意发挥。我就以我的实现方式做个例子:

// main mutation process
func (whsvr *WebhookServer) mutate(ar *v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
    req := ar.Request
    var pod corev1.Pod
    if err := json.Unmarshal(req.Object.Raw, &pod); err != nil {
        glog.Errorf("Could not unmarshal raw object: %v", err)
        return &v1beta1.AdmissionResponse {
            Result: &metav1.Status {
                Message: err.Error(),
            },
        }
    }
    
    // determine whether to perform mutation
    if !mutationRequired(ignoredNamespaces, &pod.ObjectMeta) {
        glog.Infof("Skipping mutation for %s/%s due to policy check", pod.Namespace, pod.Name)
        return &v1beta1.AdmissionResponse {
            Allowed: true, 
        }
    }

    annotations := map[string]string{admissionWebhookAnnotationStatusKey: "injected"}
    patchBytes, err := createPatch(&pod, whsvr.sidecarConfig, annotations)
    
    return &v1beta1.AdmissionResponse {
        Allowed: true,
        Patch:   patchBytes,
        PatchType: func() *v1beta1.PatchType {
            pt := v1beta1.PatchTypeJSONPatch
            return &pt
        }(),
    }
}

从上述代码中可以看出,函数 mutate 请求了 mutationRequired 来决定这个改动是否被允许。对于被允许的请求,函数 mutate 从另一个函数 createPatch 中获取到修改体 'patch'。注意这里函数 mutationRequired 的诡计,我们跳过了带有注解 sidecar-injector-webhook.morven.me/inject: true。这里稍后会在部署 deployment 的时候提到。完整代码请参考这里

编写 Dockerfile 并构建

创建构建脚本:

dep ensure
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o kube-mutating-webhook-tutorial .
docker build --no-cache -t morvencao/sidecar-injector:v1 .
rm -rf kube-mutating-webhook-tutorial

docker push morvencao/sidecar-injector:v1

以下面为依赖编写 Dockerfile 文件:

FROM alpine:latest

ADD kube-mutating-webhook-tutorial /kube-mutating-webhook-tutorial
ENTRYPOINT ["./kube-mutating-webhook-tutorial"]

在手动构建容器前,你需要 Docker ID 账号并将 image name 和 tag (Dockerfile 和 deployment.yaml 文件中)修改成你自己的,然后执行以下命令:

[root@mstnode kube-mutating-webhook-tutorial]# ./build
Sending build context to Docker daemon  44.89MB
Step 1/3 : FROM alpine:latest
 ---> 3fd9065eaf02
Step 2/3 : ADD kube-mutating-webhook-tutorial /kube-mutating-webhook-tutorial
 ---> 432de60c2b3f
Step 3/3 : ENTRYPOINT ["./kube-mutating-webhook-tutorial"]
 ---> Running in da6e956d1755
Removing intermediate container da6e956d1755
 ---> 619faa936145
Successfully built 619faa936145
Successfully tagged morvencao/sidecar-injector:v1
The push refers to repository [docker.io/morvencao/sidecar-injector]
efd05fe119bb: Pushed
cd7100a72410: Layer already exists
v1: digest: sha256:7a4889928ec5a8bcfb91b610dab812e5228d8dfbd2b540cd7a341c11f24729bf size: 739

编写 Sidecar 注入配置

现在我们来创建一个 Kubernetes ConfigMap,包含需要注入到目标 pod 中的容器和 volume 信息:

apiVersion: v1
kind: ConfigMap
metadata:
  name: sidecar-injector-webhook-configmap
data:
  sidecarconfig.yaml: |
    containers:
      - name: sidecar-nginx
        image: nginx:1.12.2
        imagePullPolicy: IfNotPresent
        ports:
          - containerPort: 80
        volumeMounts:
          - name: nginx-conf
            mountPath: /etc/nginx
    volumes:
      - name: nginx-conf
        configMap:
          name: nginx-configmap

从上面的清单中看,这里需要另一个包含 nginx confConfigMap。完整 yaml 参考 nginxconfigmap.yaml

然后将这两个 ConfigMap 部署到集群中:

[root@mstnode kube-mutating-webhook-tutorial]# kubectl create -f ./deployment/nginxconfigmap.yaml
configmap "nginx-configmap" created
[root@mstnode kube-mutating-webhook-tutorial]# kubectl create -f ./deployment/configmap.yaml
configmap "sidecar-injector-webhook-configmap" created

创建包含秘钥对的 Secret

由于准入控制是一个高安全性操作,所以对外在的 webhook server 提供 TLS 是必须的。作为流程的一部分,我们需要创建由 Kubernetes CA 签名的 TLS 证书,以确保 webhook server 和 apiserver 之间通信的安全性。对于 CSR 创建和批准的完整步骤,请参考 这里

简单起见,我们参考了 Istio 的脚本并创建了一个类似的名为 webhook-create-signed-cert.sh 的脚本,来自动生成证书及秘钥对并将其加入到 secret 中。

#!/bin/bash
while [[ $# -gt 0 ]]; do
    case ${1} in
        --service)
            service="$2"
            shift
            ;;
        --secret)
            secret="$2"
            shift
            ;;
        --namespace)
            namespace="$2"
            shift
            ;;
    esac
    shift
done

[ -z ${service} ] && service=sidecar-injector-webhook-svc
[ -z ${secret} ] && secret=sidecar-injector-webhook-certs
[ -z ${namespace} ] && namespace=default

csrName=${service}.${namespace}
tmpdir=$(mktemp -d)
echo "creating certs in tmpdir ${tmpdir} "

cat <<EOF >> ${tmpdir}/csr.conf
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = ${service}
DNS.2 = ${service}.${namespace}
DNS.3 = ${service}.${namespace}.svc
EOF

openssl genrsa -out ${tmpdir}/server-key.pem 2048
openssl req -new -key ${tmpdir}/server-key.pem -subj "/CN=${service}.${namespace}.svc" -out ${tmpdir}/server.csr -config ${tmpdir}/csr.conf

# clean-up any previously created CSR for our service. Ignore errors if not present.
kubectl delete csr ${csrName} 2>/dev/null || true

# create  server cert/key CSR and  send to k8s API
cat <<EOF | kubectl create -f -
apiVersion: certificates.k8s.io/v1beta1
kind: CertificateSigningRequest
metadata:
  name: ${csrName}
spec:
  groups:
  - system:authenticated
  request: $(cat ${tmpdir}/server.csr | base64 | tr -d '\n')
  usages:
  - digital signature
  - key encipherment
  - server auth
EOF

# verify CSR has been created
while true; do
    kubectl get csr ${csrName}
    if [ "$?" -eq 0 ]; then
        break
    fi
done

# approve and fetch the signed certificate
kubectl certificate approve ${csrName}
# verify certificate has been signed
for x in $(seq 10); do
    serverCert=$(kubectl get csr ${csrName} -o jsonpath='{.status.certificate}')
    if [[ ${serverCert} != '' ]]; then
        break
    fi
    sleep 1
done
if [[ ${serverCert} == '' ]]; then
    echo "ERROR: After approving csr ${csrName}, the signed certificate did not appear on the resource. Giving up after 10 attempts." >&2
    exit 1
fi
echo ${serverCert} | openssl base64 -d -A -out ${tmpdir}/server-cert.pem


# create the secret with CA cert and server cert/key
kubectl create secret generic ${secret} \
        --from-file=key.pem=${tmpdir}/server-key.pem \
        --from-file=cert.pem=${tmpdir}/server-cert.pem \
        --dry-run -o yaml |
    kubectl -n ${namespace} apply -f -

运行脚本后,包含证书和秘钥对的 secret 就被创建出来了:

[root@mstnode kube-mutating-webhook-tutorial]# ./deployment/webhook-create-signed-cert.sh
creating certs in tmpdir /tmp/tmp.wXZywp0wAF
Generating RSA private key, 2048 bit long modulus
...........................................+++
..........+++
e is 65537 (0x10001)
certificatesigningrequest "sidecar-injector-webhook-svc.default" created
NAME                                   AGE       REQUESTOR                                           CONDITION
sidecar-injector-webhook-svc.default   0s        https://mycluster.icp:9443/oidc/endpoint/OP#admin   Pending
certificatesigningrequest "sidecar-injector-webhook-svc.default" approved
secret "sidecar-injector-webhook-certs" created

创建 Sidecar 注入器的 Deployment 和 Service

deployment 带有一个 pod,其中运行的就是 sidecar-injector 容器。该容器以特殊参数运行:

  1. sidecarCfgFile 指的是 sidecar 注入器的配置文件,挂载自上面创建的 ConfigMap sidecar-injector-webhook-configmap
  2. tlsCertFiletlsKeyFile 是秘钥对,挂载自 Secret injector-webhook-certs
  3. alsologtostderrv=42>&1 是日志参数。
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: sidecar-injector-webhook-deployment
  labels:
    app: sidecar-injector
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: sidecar-injector
    spec:
      containers:
        - name: sidecar-injector
          image: morvencao/sidecar-injector:v1
          imagePullPolicy: IfNotPresent
          args:
            - -sidecarCfgFile=/etc/webhook/config/sidecarconfig.yaml
            - -tlsCertFile=/etc/webhook/certs/cert.pem
            - -tlsKeyFile=/etc/webhook/certs/key.pem
            - -alsologtostderr
            - -v=4
            - 2>&1
          volumeMounts:
            - name: webhook-certs
              mountPath: /etc/webhook/certs
              readOnly: true
            - name: webhook-config
              mountPath: /etc/webhook/config
      volumes:
        - name: webhook-certs
          secret:
            secretName: sidecar-injector-webhook-certs
        - name: webhook-config
          configMap:
            name: sidecar-injector-webhook-configmap

Service 暴露带有 app=sidecar-injector label 的 pod,使之在集群中可访问。这个 Service 会被 MutatingWebhookConfiguration 中定义的 clientConfig 部分访问,默认的端口 spec.ports.port 需要设置为 443。

apiVersion: v1
kind: Service
metadata:
  name: sidecar-injector-webhook-svc
  labels:
    app: sidecar-injector
spec:
  ports:
  - port: 443
    targetPort: 443
  selector:
    app: sidecar-injector

然后将上述 Deployment 和 Service 部署到集群中,并且验证 sidecar 注入器的 webhook server 是否 running:

[root@mstnode kube-mutating-webhook-tutorial]# kubectl create -f ./deployment/deployment.yaml
deployment "sidecar-injector-webhook-deployment" created
[root@mstnode kube-mutating-webhook-tutorial]# kubectl create -f ./deployment/service.yaml
service "sidecar-injector-webhook-svc" created
[root@mstnode kube-mutating-webhook-tutorial]# kubectl get deployment
NAME                                  DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
sidecar-injector-webhook-deployment   1         1         1            1           2m
[root@mstnode kube-mutating-webhook-tutorial]# kubectl get pod
NAME                                                  READY     STATUS    RESTARTS   AGE
sidecar-injector-webhook-deployment-bbb689d69-fdbgj   1/1       Running   0          3m

动态配置 webhook 准入控制器

MutatingWebhookConfiguration 中具体说明了哪个 webhook admission server 是被使用的并且哪些资源受准入服务器的控制。建议你在创建 MutatingWebhookConfiguration 之前先部署 webhook admission server,并确保其正常工作。否则,请求会被无条件接收或根据失败规则被拒。

现在,我们根据下面的内容创建 MutatingWebhookConfiguration

apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
  name: sidecar-injector-webhook-cfg
  labels:
    app: sidecar-injector
webhooks:
  - name: sidecar-injector.morven.me
    clientConfig:
      service:
        name: sidecar-injector-webhook-svc
        namespace: default
        path: "/mutate"
      caBundle: ${CA_BUNDLE}
    rules:
      - operations: [ "CREATE" ]
        apiGroups: [""]
        apiVersions: ["v1"]
        resources: ["pods"]
    namespaceSelector:
      matchLabels:
        sidecar-injector: enabled

第 8 行:name - webhook 的名字,必须指定。多个 webhook 会以提供的顺序排序;
第 9 行:clientConfig - 描述了如何连接到 webhook admission server 以及 TLS 证书;
第 15 行:rules - 描述了 webhook server 处理的资源和操作。在我们的例子中,只拦截创建 pods 的请求;
第 20 行:namespaceSelector - namespaceSelector 根据资源对象是否匹配 selector 决定了是否针对该资源向 webhook server 发送准入请求。

在部署 MutatingWebhookConfiguration 前,我们需要将 ${CA_BUNDLE} 替换成 apiserver 的默认 caBundle。我们写个脚本来自动匹配:

#!/bin/bash
set -o errexit
set -o nounset
set -o pipefail

ROOT=$(cd $(dirname $0)/../../; pwd)

export CA_BUNDLE=$(kubectl get configmap -n kube-system extension-apiserver-authentication -o=jsonpath='{.data.client-ca-file}' | base64 | tr -d '\n')

if command -v envsubst >/dev/null 2>&1; then
    envsubst
else
    sed -e "s|\${CA_BUNDLE}|${CA_BUNDLE}|g"
fi

然后执行:

[root@mstnode kube-mutating-webhook-tutorial]# cat ./deployment/mutatingwebhook.yaml |\
>   ./deployment/webhook-patch-ca-bundle.sh >\
>   ./deployment/mutatingwebhook-ca-bundle.yaml

我们看不到任何日志描述 webhook server 接收到准入请求,似乎该请求并没有发送到 webhook server 一样。所以有一种可能性是这是被 MutatingWebhookConfiguration 中的配置触发的。再确认一下 MutatingWebhookConfiguration 我们会发现下面的内容:

namespaceSelector:
      matchLabels:
        sidecar-injector: enabled

通过 namespaceSelector 控制 sidecar 注入器

我们在 MutatingWebhookConfiguration 中配置了 namespaceSelector,也就意味着只有在满足条件的 namespace 下的资源能够被发送到  webhook server。于是我们将 default 这个 namespace 打上标签 sidecar-injector=enabled

[root@mstnode kube-mutating-webhook-tutorial]# kubectl label namespace default sidecar-injector=enabled
namespace "default" labeled
[root@mstnode kube-mutating-webhook-tutorial]# kubectl get namespace -L sidecar-injector
NAME          STATUS    AGE       sidecar-injector
default       Active    1d        enabled
kube-public   Active    1d
kube-system   Active    1d

现在我们配置的 MutatingWebhookConfiguration 会在 pod 创建的时候就注入 sidecar 容器。将运行中的 pod 删除并确认是否创建了新的带 sidecar 容器的 pod:

[root@mstnode kube-mutating-webhook-tutorial]# kubectl delete pod sleep-6d79d8dc54-r66vz
pod "sleep-6d79d8dc54-r66vz" deleted
[root@mstnode kube-mutating-webhook-tutorial]# kubectl get pods
NAME                                                  READY     STATUS              RESTARTS   AGE
sidecar-injector-webhook-deployment-bbb689d69-fdbgj   1/1       Running             0          29m
sleep-6d79d8dc54-b8ztx                                0/2       ContainerCreating   0          3s
sleep-6d79d8dc54-r66vz                                1/1       Terminating         0          11m
[root@mstnode kube-mutating-webhook-tutorial]# kubectl get pod sleep-6d79d8dc54-b8ztx -o yaml
apiVersion: v1
kind: Pod
metadata:
  annotations:
    kubernetes.io/psp: default
    sidecar-injector-webhook.morven.me/inject: "true"
    sidecar-injector-webhook.morven.me/status: injected
  labels:
    app: sleep
    pod-template-hash: "2835848710"
  name: sleep-6d79d8dc54-b8ztx
  namespace: default
spec:
  containers:
  - command:
    - /bin/sleep
    - infinity
    image: tutum/curl
    imagePullPolicy: IfNotPresent
    name: sleep
    resources: {}
    volumeMounts:
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: default-token-d7t2r
      readOnly: true
  - image: nginx:1.12.2
    imagePullPolicy: IfNotPresent
    name: sidecar-nginx
    ports:
    - containerPort: 80
      protocol: TCP
    resources: {}
    terminationMessagePath: /dev/termination-log
    terminationMessagePolicy: File
    volumeMounts:
    - mountPath: /etc/nginx
      name: nginx-conf
  volumes:
  - name: default-token-d7t2r
    secret:
      defaultMode: 420
      secretName: default-token-d7t2r
  - configMap:
      defaultMode: 420
      name: nginx-configmap
    name: nginx-conf
...

可以看到,sidecar 容器和 volume 被成功注入到应用中。至此,我们成功创建了可运行的带 MutatingAdmissionWebhook 的 sidecar 注入器。通过 namespaceSelector,我们可以轻易的控制在特定的 namespace 中的 pods 是否需要被注入 sidecar 容器。

但这里有个问题,根据以上的配置,在 default 这个 namespace 下的所有 pods 都会被注入 sidecar 容器,无一例外。

通过注解控制 sidecar 注入器

多亏了 MutatingAdmissionWebhook 的灵活性,我们可以轻易的自定义变更逻辑来筛选带有特定注解的资源。还记得上面提到的注解 sidecar-injector-webhook.morven.me/inject: "true" 吗?在 sidecar 注入器中这可以当成另一种控制方式。在 webhook server 中我写了一段逻辑来跳过那行不带这个注解的 pod。

我们来尝试一下。在这种情况下,我们创建另一个 sleep 应用,其 podTemplateSpec 中不带注解 sidecar-injector-webhook.morven.me/inject: "true"

[root@mstnode kube-mutating-webhook-tutorial]# kubectl delete deployment sleep
deployment "sleep" deleted
[root@mstnode kube-mutating-webhook-tutorial]# cat <<EOF | kubectl create -f -
apiVersion: extensions/v1beta1
> kind: Deployment
> metadata:
>   name: sleep
> spec:
>   replicas: 1
>   template:
>     metadata:
>       labels:
>         app: sleep
>     spec:
>       containers:
>       - name: sleep
>         image: tutum/curl
>         command: ["/bin/sleep","infinity"]
>         imagePullPolicy: IfNotPresent
> EOF
deployment "sleep" created

然后确认 sidecar 注入器是否跳过了这个 pod:

[root@mstnode kube-mutating-webhook-tutorial]# kubectl get deployment
NAME                                  DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
sidecar-injector-webhook-deployment   1         1         1            1           45m
sleep                                 1         1         1            1           17s
[root@mstnode kube-mutating-webhook-tutorial]# kubectl get pod
NAME                                                  READY     STATUS        RESTARTS   AGE
sidecar-injector-webhook-deployment-bbb689d69-fdbgj   1/1       Running       0          45m
sleep-776b7bcdcd-4bz58                                1/1       Running       0          21s

结果显示,这个 sleep 应用只包含一个容器,没有额外的容器和 volume 注入。然后我们将这个 deployment 增加注解,并确认其重建后是否被注入 sidecar:

[root@mstnode kube-mutating-webhook-tutorial]# kubectl patch deployment sleep -p '{"spec":{"template":{"metadata":{"annotations":{"sidecar-injector-webhook.morven.me/inject": "true"}}}}}'
deployment "sleep" patched
[root@mstnode kube-mutating-webhook-tutorial]# kubectl delete pod sleep-776b7bcdcd-4bz58
pod "sleep-776b7bcdcd-4bz58" deleted
[root@mstnode kube-mutating-webhook-tutorial]# kubectl get pods
NAME                                                  READY     STATUS              RESTARTS   AGE
sidecar-injector-webhook-deployment-bbb689d69-fdbgj   1/1       Running             0          49m
sleep-3e42ff9e6c-6f87b                                0/2       ContainerCreating   0          18s
sleep-776b7bcdcd-4bz58                                1/1       Terminating         0          3m

与预期一致,pod 被注入了额外的 sidecar 容器。至此,我们就获得了一个可工作的 sidecar 注入器,可由 namespaceSelector 或更细粒度的由注解控制。

总结

MutatingAdmissionWebhook 是 Kubernetes 扩展功能中最简单的方法之一,工作方式是通过全新规则控制、资源更改。

此功能使得更多的工作模式变成了可能,并且支持了更多生态系统,包括服务网格平台 Istio。从 Istio 0.5.0 开始,Istio 的自动注入部分的代码被重构,实现方式从 initializers 变更为 MutatingAdmissionWebhook

参考文献