lxcfs-hostpath-injector

Nov 14, 2019 21:30 · 2632 words · 6 minute read Kubernetes Golang

有关 Kubernetes 集群中利用 LXCFS 提升容器的隔离能力,阿里云提供了一个利用 Initializer 扩展机制对资源创建进行拦截和注入处理的能力的方案 https://github.com/denverdino/lxcfs-initializer,实现对 lxcfs 所托管文件的自动化挂载。快速了解 Initializer,可以查看 https://medium.com/ibm-cloud/kubernetes-initializers-deep-dive-and-tutorial-3bc416e4e13e

但是 Kubernetes 从 1.14 版之后就移除了 Initializer,取而代之的是 admission webhooks

Webhooks vs Initializers

根据 Kubernetest社区的反馈和 External Admission Webhooks 与 Initializers 在 alpha 阶段的使用情况,社区决定将 webhooks 提升至 beta 阶段,并拆分为 MutatingAdmissionWebhook 和 ValidatingAdmissionWebhook。

  • Webhooks 可以 CRUD 和提交资源,而 Initializers 在 DELETE 请求时无法提交资源。
  • 在创建之前 Webhooks 不允许查询资源,而 Initializers 通过查询参数 ?includeUninitialized=true 来观察未初始化的资源,这让资源创建变得透明。
  • Initializers 会将创建前的状态保存至 etcd,相比 Webhooks 更吃资源。
  • Webhooks 的失败策略比 Initializers 更强大。在 Webhooks 配置中设置失败策略来避免资源创建时 hang 住,而 Initializers 的某些 bug 可能会导致资源创建阻塞。

将 Webhooks 提升至 beta 状态可能是一个未来对它更多支持的信号,如果首先考虑稳定性,建议使用 Webhooks。

MutatingAdmissionWebhook 工作原理

MutatingAdmissionWebhook 在 apiserver 将 API 持久化至 etcd 前拦截与 MutatingWebhookConfiguration 定义好的规则相匹配的请求。apiserver 将准入请求发送至 webhook 服务器来执行变异(修改)。webhook 本质上是一个遵循 Kubernetes API 的 HTTP 服务器应用程序。

MutatingAdmissionWebhook 要三样东西:

  1. MutatingWebhookConfiguration 配置对象

    通过 MutatingWebhookConfiguration 将 MutatingAdmissionWebhook 注册到 apiserver:

    • 如何连接 webhook admission server
    • 如何验证 webhook admission server
    • webhook admission server 的 URL 路径
    • 定义哪种资源及其处理方法的规则
    • 如何处理来自 webhook admission server 的一些无法识别的错误
  2. MutatingAdmissionWebhook 本身

    MutatingAdmissionWebhook 是个插件型的准入控制器,从 MutatingWebhookConfiguration 获取准入 webhook。MutatingAdmissionWebhook 会观察 apiserver 并拦截匹配规则的请求,调用 webhook。

  3. Webhook Admission Server 服务器应用程序

    Webhook Admission Server 是遵循 Kubernetes API 的 HTTP 服务器应用程序。每个发送至 apiserver 的请求,MutatingAdmissionWebhook 发送 AdmissionReview 对象至对应的 webhook admission server。webhook admission server 从 AdmissionReview 获取像 objectoldobjectuserInfo 这样的信息,并反馈 AdmissionReview,携带 AllowedResult 字段还有用于变异(修改) Pod 的 patch 等信息。

现在非常火热的的 Service Mesh 应用 istio 就是通过 MutatingAdmissionWebhook 来自动将 Envoy 这个 sidecar 容器注入到 Pod 中去的:https://istio.io/docs/setup/additional-setup/sidecar-injection/

确认 Kubernetes 集群是否支持 MutatingAdmissionWebhook

MutatingAdmissionWebhook 需要版本 1.9.0+ Kubernetes,通过下面的命令来确认:

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

设计与实现

github repo: https://github.com/crazytaxii/lxcfs-hostpath-injector

首先需要构建一个 HTTP 服务器应用来提供 webhook 能力,参考 Kubernetes apiserver,我使用 cobra 来提供 CLI 接口。

webhook 服务器应用程序同时也是一个命令行应用,在启动时提供一些必要的参数:

fs.IntVar(&opt.Port, "port", defaultPort, "Webhook server port")
fs.StringVar(&opt.CertFile, "tls-cert-file", defaultCertFile, "File containing the x509 Certificate for HTTPS")
fs.StringVar(&opt.KeyFile, "tls-key-file", defaultKeyFile, "File containing the x509 private key to --tls-cert-file")
fs.StringVar(&opt.ConfigFile, "sidecar-config-file", webhook.DefaultConfigFile, "File containing sidecar configuration")
}
  • port:服务器应用程序监听的端口,默认为 443
  • tls-cert-file:SSL 证书路径
  • tls-key-file:SSL 私钥路径
  • sidecar-config-file:配置文件路径

https://github.com/crazytaxii/lxcfs-hostpath-injector/blob/master/cmd/injector/app/server.go

// Section 1
cfg, err := webhook.LoadWebhookServerConfig(opt.ConfigFile)
if err != nil {
    klog.Errorf("Failed to laod sidecar config: %v", err)
    return err
}

// Section 2
// TLS
pair, err := tls.LoadX509KeyPair(opt.CertFile, opt.KeyFile)
if err != nil {
    klog.Errorf("Failed to load key pair: %v", err)
    return err
}

whsvr := &webhook.WebhookServer{
    Config: cfg,
    Server: &http.Server{
        Addr: fmt.Sprintf(":%v", opt.Port),
        TLSConfig: &tls.Config{
            Certificates: []tls.Certificate{pair},
        },
    },
}

// Section 3
mux := http.NewServeMux()
mux.HandleFunc("/mutate", whsvr.Mutate)
whsvr.Server.Handler = mux

// Section 4
go func() {
    if err := whsvr.Server.ListenAndServeTLS("", ""); err != nil {
        klog.Errorf("Failed to listen and serve webhook server: %v", err)
    }
}()
  • 载入相应的配置文件
  • apiserver 与 webhook server 之间通讯必须走 HTTPS,需要 SSL 证书
  • mux 用于路由与相应的 Handler 函数绑定
  • 服务器应用程序开始监听

当 Pod 创建请求提交至 apiserver 时,将被拦截并向 webhook server 发送请求,/mutate 路径与下面的函数绑定:

// Mutate method for webhook server.
func (whsvr *WebhookServer) Mutate(w http.ResponseWriter, r *http.Request) {
    var body []byte
    if r.Body != nil {
        if data, err := ioutil.ReadAll(r.Body); err == nil {
            body = data
        }
    }
    if len(body) == 0 {
        klog.Error("empty body")
        http.Error(w, "empty body", http.StatusBadRequest)
        return
    }

    // verify the content type
    contentType := r.Header.Get("Content-Type")
    if contentType != "application/json" {
        klog.Errorf("Content-Type=%s, expect application/json", contentType)
        http.Error(w, "invalid Content-Type, expect `application/json`", http.StatusUnsupportedMediaType)
        return
    }

    // Section 1
    // The AdmissionReview that was sent to the webhook.
    requestedAdmissionReview := &v1beta1.AdmissionReview{}
    var admissionResponse *v1beta1.AdmissionResponse
    if _, _, err := deserializer.Decode(body, nil, requestedAdmissionReview); err != nil {
        klog.Errorf("Can't decode body: %v", err)
        admissionResponse = toAdmissionResponseErr(err)
    } else {
        // Section 2
        // pass to admitFunc
        admissionResponse = whsvr.mutate(requestedAdmissionReview)
    }

    // The AdmissionReview that will be returned.
    responseAdmissionReview := &v1beta1.AdmissionReview{}
    if admissionResponse != nil {
        responseAdmissionReview.Response = admissionResponse
        if requestedAdmissionReview.Request != nil {
            responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID
        }
    }

    // Section 3
    resp, err := json.Marshal(responseAdmissionReview)
    if err != nil {
        klog.Errorf("Can't encode response: %v", err)
        http.Error(w, fmt.Sprintf("could not encode response: %v", err), http.StatusInternalServerError)
    }
    _, err = w.Write(resp)
    if err != nil {
        klog.Errorf("Can't write response: %v", err)
        http.Error(w, fmt.Sprintf("could not encode response: %v", err), http.StatusInternalServerError)
    }
}

Mutate 函数负责处理 HTTP 请求并返回结果:

  • 首先将 HTTP 请求反序列化成 AdmissionReview 对象,其中包含了完整的 Pod 资源信息
  • 调用 mutate() 函数来创建 patch 注入 hostPath 卷
  • 最后序列化包含 patch 的 AdmissionReview,发回 apiserver

https://github.com/crazytaxii/lxcfs-hostpath-injector/blob/master/pkg/webhook/webhook.go

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 {
        klog.Errorf("Could not unmarshal raw object: %v", err)
        return toAdmissionResponseErr(err)
    }

    klog.Infof("AdmissionReview for Kind=%v, Namespace=%v Name=%v UID=%v patchOperation=%v UserInfo=%v",
        req.Kind, req.Namespace, req.Name, req.UID, req.Operation, req.UserInfo)

    // Determine whether to perform mutation.
    if !whsvr.mutationRequired(&pod.ObjectMeta) {
        klog.Info("Skipping mutation")
        return &v1beta1.AdmissionResponse{
            Allowed: true,
        }
    }

    annotations := map[string]string{admissionWebhookAnnotationStatusKey: "injected"}
    patchBytes, err := createPatch(&pod, whsvr.Config.SidecarConfig, annotations)
    if err != nil {
        klog.Errorf("Could not create patch: %v", err)
        return toAdmissionResponseErr(err)
    }

    return &v1beta1.AdmissionResponse{
        Allowed: true,
        Patch:   patchBytes,
        PatchType: func() *v1beta1.PatchType {
            pt := v1beta1.PatchTypeJSONPatch
            return &pt
        }(),
    }
}

mutate 方法调用了 mutationRequired 根据 Pod 携带的注解(annotations)来判断是否需要修改。如果注解中携带了 sidecar-injector.lxcfs/inject: truecreatePatch 生成 patch,如果没有或者值非 true 都会被忽略。完整的处理逻辑查看 https://github.com/crazytaxii/lxcfs-hostpath-injector/blob/master/pkg/webhook/webhook.go

编译与构建镜像

$ git clone https://github.com/crazytaxii/lxcfs-sidecar-injector.git
$ cd lxcfs-sidecar-injector
$ export GO111MODULE=on
$ make build
$ make image

部署

在 Kubernetes 集群中创建相关 DeploymentService 资源对象来部署 webhook server。HTTPS 服务器需要预先提供 SSL 证书和私钥,程序启动参数中读取的证书和私钥文件是通过一个 secret 对象挂载进来:

spec:
  containers:
    - name: lxcfs-sidecar-injector
      image: crazytaxii/lxcfs-sidecar-injector:latest
      imagePullPolicy: IfNotPresent
      args:
        - --tls-cert-file=/etc/webhook/certs/cert.pem
        - --tls-key-file=/etc/webhook/certs/key.pem
      ports:
        - containerPort: 443
      volumeMounts:
        - name: webhook-certs
          mountPath: /etc/webhook/certs
          readOnly: true
  volumes:
    - name: webhook-certs
      secret:
        secretName: lxcfs-sidecar-injector-certs

在生产环境中要走 HTTPS,我们可以使用类似于 cert-manager 之类的工具来自动处理 SSL 证书。需要注意的是这里设置的 CA 证书是需要让 apiserver 能够验证的,这里重用了 istio 项目中的证书签名请求脚本。通过发送请求到 apiserver,获取认证信息,然后使用获得的结果来创建需要的 Secret。

$ ./kubernetes/webhook-create-signed-cert.sh
$ kubectl apply -f ./kubernetes/deployment.yaml
$ kubectl apply -f ./kubernetes/service.yaml

现在 webhook 服务运行起来了,它已经可以接收来自 apiserver 的请求。但是我们还需要在 kubernetes 上创建 MutatingWebhookConfiguration。查看 MutatingWebhookConfiguration,我注意到它里面包含一个 CA_BUNDLE 的占位符。

webhooks:
  - name: lxcfs-sidecar-injector-svc.default.svc.cluster.local
    clientConfig:
      service:
        name: lxcfs-sidecar-injector-svc
        namespace: default
        path: /mutate
      caBundle: ${CA_BUNDLE}

CA 证书应提供给 admission webhook 配置 MutatingWebhookConfiguration 对象,这样 apiserver 才可以信任 webhook server 提供的 SSL 证书。因为上面已经使用 Kubernetes API 签署了证书,所以就用 kubeconfig 中的 CA 证书来简化操作。这里提供了一个小脚本用来替换 CA_BUNDLE 这个占位符,运行下面命令即可:

$ cat ./kubernetes/mutatingwebhook.yaml |\
    ./kubernetes/webhook-patch-ca-bundle.sh >\
    ./kubernetes/mutatingwebhookconfigurations.yaml
$ kubectl apply -f ./kubernetes/mutatingwebhookconfigurations.yaml

这样整个 admission webhook server 就部署好了。

测试

在注解中添加约定的 sidecar-injector.lxcfs/inject: "true" 并带上 requests 和 limits 字段(资源限制):

spec:
  template:
    metadata:
      annotations:
        sidecar-injector.lxcfs/inject: "true"
$ kubectl apply -f ./kubernetes/test-deloyment.yaml
$ kubectl exec -it $(kubectl get pod -l app=web -o jsonpath="{.items[0].metadata.name}") -- free -h
             total       used       free     shared    buffers     cached
Mem:          256M       2.9M       253M         0B         0B       364K
-/+ buffers/cache:       2.5M       253M
Swap:           0B         0B         0B

容器内部读取的资源配置被成功限制。

参考