Kubernetes 开发知识–Kubernetes 准入控制与 admission webhook 的使用

一、什么是准入控制

我们都知道 Kubernetes 的最核心组件就是它的 API Server,所有资源的创建、更新和删除都是通过 API Server 进行的。我们可以通过 HTTP 请求或者 kubectl 这样的客户端来和 APIServer 通信,在我们操作对象的请求到达 API Server 之前,会先到达准入控制器这里。准入控制器可以执行“验证”和“变更”操作。因此我们可以认为有两种准入控制器:变更(mutating)准入控制器验证(validating)准入控制器

准入控制过程也同样分为两个阶段。第一阶段,运行变更准入控制器,对对象进行修改操作;第二阶段,运行验证准入控制器。如果任何一个阶段的任何控制器拒绝了该请求,则整个请求将立即被拒绝,并向终端用户返回一个错误。

我们可以通过下面的图来直观的感受一下:

Kubernetes 开发知识--Kubernetes 准入控制与 admission webhook 的使用

二、准入控制能做什么

准入控制存在的目的就是提高 Kubernetes 架构的灵活性。因此 Kubernetes 提供了一些准入控制器,并且允许你打开或关闭一部分,同时你也可以自定义准入控制器,来完成你自己的一些特殊需求。可以自定义的准入控制器也分为两种:一个叫 MutatingAdmissionWebhook,一个叫 ValidatingAdmissionWebhook,其实也是我们上面说的变更准入控制器验证准入控制器

Kubernetes 提供了一些常用的准入控制器,下面举几个例子:

  • AlwaysPullImages: 该准入控制器会修改每一个新创建的 Pod 的镜像拉取策略为 Always。这样在多租户集群里,用户就能保证自己的私有镜像只会在有凭证的情况下使用,而不会出现因为机器上缓存了镜像而被其他用户直接使用的情况。

  • NamespaceLifecycle:该准入控制器禁止在一个正在被终止的 Namespace 中创建新对象,并且确保使用不存在的 Namespace 的请求被拒绝。该准入控制器还会禁止删除三个系统保留的命名空间,即 defaultkube-systemkube-public

除了 Kubernetes 提供的这些准入控制器,我们可以编写 MutatingAdmissionWebhookValidatingAdmissionWebhook。比如我之前有写一个叫做 [lazykube](https://github.com/joyme123/lazykube)MutatingAdmissionWebhook,它的作用是对每一个创建的 Pod 的镜像源都进行校验,如果是 docker hub, gcr.io, quay.io 等地址的镜像,就修改成国内的代理源,这样就可以自动解决镜像需要翻墙下载的问题了。

三、如何使用 admission webhook 编写一个准入控制器

使用 admission webhook 编写一个准入控制器其实很简单,它就和你日常写 web server 一样:

// Start 启动服务
func (whsrv *WebhookServer) Start() error {
    mux := http.NewServeMux()
    mux.HandleFunc("/mutate", whsrv.serve)
    whsrv.server.Handler = mux

    if err := whsrv.server.ListenAndServeTLS("", ""); err != nil {
        return fmt.Errorf("Failed to listen and serve webhook server: %v", err)
    }

    return nil
}

在收到一个 AdmissionReview 请求的时候,将其中的 AdmissionRequest 对象携带的资源对象取出来进行校验或变更:

var body []byte
if r.Body != nil {
    if data, err := ioutil.ReadAll(r.Body); err == nil {
        body = data
    }
}

if len(body) == 0 {
    log.Error("empty body")
    http.Error(w, "empty body", http.StatusBadRequest)
    return
}

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

var admissionResponse *v1beta1.AdmissionResponse
ar := v1beta1.AdmissionReview{}
if _, _, err := deserializer.Decode(body, nil, &ar); err != nil {
    log.Errorf("Can't decode body: %v", err)
    admissionResponse = &v1beta1.AdmissionResponse{
        Result: &metav1.Status{
            Message: err.Error(),
        },
    }
} else {
    admissionResponse = whsrv.mutate(&ar)
}

然后生成 AdmissionResponse, 通过 AdmissionReview 进行响应即可:

admissionReview := v1beta1.AdmissionReview{}
if admissionResponse != nil {
    admissionReview.Response = admissionResponse
    if ar.Request != nil {
        admissionReview.Response.UID = ar.Request.UID
    }
}

resp, err := json.Marshal(admissionReview)
if err != nil {
    log.Errorf("Can't encode response: %v", err)
    http.Error(w, fmt.Sprintf("could not encode response: %v", err), http.StatusInternalServerError)
}
log.Infoln("Ready to write response ...")
if _, err := w.Write(resp); err != nil {
    log.Errorf("Can't write response: %v", err)
    http.Error(w, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError)
}

完整的代码可以参考上面的 lazykube 项目。

这样我们写好之后就可以开始部署了。我们需要告诉 Kubernetes 我们的 admission webhook 的地址,以及我们要操作的资源对象,因此我们创建一个 MutatingWebhookConfiguration 或者 ValidatingWebhookConfiguration 对象,比如 lazykube 就是对创建 Pod 进行变更操作:

apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
  name: lazykube-webhook-cfg
  namespace: kube-system
  labels:
    app: lazykube
webhooks:
  - name: lazykube.myway5.com
    clientConfig:
      service:
        name: lazykube-webhook-svc
        namespace: kube-system
        path: "/mutate"
      caBundle: ××××××××
    rules:
      - operations: [ "CREATE" ]
        apiGroups: [""]
        apiVersions: ["v1"]
        resources: ["pods"]

然后为了让 Kubernetes 通过 HTTPS 进行通信,并信任我们的证书,我们需要让 Kubernetes 集群为我们的签发证书。具体的方法可以参考这里:kubernetes 集群中的证书签发。最后使用 Deployment 部署我们的服务即可。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据