一、为什么需要自定义准入控制Webhook

在Kubernetes集群中,默认的准入控制器已经能处理大部分场景,比如资源配额检查、Pod安全策略等。但实际生产环境中,我们经常需要实现一些特定的业务规则验证,这时候就需要开发自定义的准入控制Webhook了。

举个例子,假设我们有个电商平台,所有部署的微服务都需要打上特定的业务标签(比如department: ecommerce),否则就不允许部署。这种业务特定的规则,Kubernetes原生的准入控制器可帮不上忙,就得靠我们自己来实现。

二、Webhook工作原理浅析

准入控制Webhook本质上就是个HTTP回调服务,Kubernetes API Server在遇到需要准入控制的请求时,会向这个Webhook发起HTTP请求,询问"这个操作能不能通过?"。

整个过程分为两个阶段:

  1. 变更阶段(Mutating):可以修改请求内容,比如自动给Pod注入sidecar容器
  2. 验证阶段(Validating):只能回答"允许"或"拒绝",不能修改请求内容

这就像公司门禁系统,变更阶段相当于保安帮你整理下着装(比如提醒你戴工牌),验证阶段则是检查你是否真的有权限进入。

三、手把手开发Validating Webhook

下面我们用Go语言开发一个简单的Validating Webhook,它会检查所有新创建的Pod是否带有特定标签。

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"os"

	admissionv1 "k8s.io/api/admission/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/serializer"
)

var (
	runtimeScheme = runtime.NewScheme()
	codecs        = serializer.NewCodecFactory(runtimeScheme)
	deserializer  = codecs.UniversalDeserializer()
)

// 准入控制响应结构体
type AdmissionResponse struct {
	Allowed bool   `json:"allowed"`
	Message string `json:"message,omitempty"`
}

// 处理准入控制请求
func handleAdmissionReview(w http.ResponseWriter, r *http.Request) {
	// 1. 读取请求体
	body, err := os.ReadAll(r.Body)
	if err != nil {
		http.Error(w, fmt.Sprintf("读取请求失败: %v", err), http.StatusBadRequest)
		return
	}

	// 2. 反序列化请求
	var admissionReview admissionv1.AdmissionReview
	if _, _, err := deserializer.Decode(body, nil, &admissionReview); err != nil {
		http.Error(w, fmt.Sprintf("反序列化失败: %v", err), http.StatusBadRequest)
		return
	}

	// 3. 处理请求
	response := validatePod(admissionReview.Request)

	// 4. 构造响应
	admissionReview.Response = response
	respBytes, err := json.Marshal(admissionReview)
	if err != nil {
		http.Error(w, fmt.Sprintf("序列化响应失败: %v", err), http.StatusInternalServerError)
		return
	}

	// 5. 返回响应
	w.Header().Set("Content-Type", "application/json")
	w.Write(respBytes)
}

// 验证Pod标签
func validatePod(request *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse {
	// 只处理Pod创建请求
	if request.Kind.Kind != "Pod" || request.Operation != "CREATE" {
		return &admissionv1.AdmissionResponse{Allowed: true}
	}

	// 解析Pod对象
	pod := corev1.Pod{}
	if err := json.Unmarshal(request.Object.Raw, &pod); err != nil {
		return &admissionv1.AdmissionResponse{
			Allowed: false,
			Message: fmt.Sprintf("解析Pod失败: %v", err),
		}
	}

	// 检查必须的标签
	if pod.Labels["department"] != "ecommerce" {
		return &admissionv1.AdmissionResponse{
			Allowed: false,
			Message: "所有Pod必须包含标签: department=ecommerce",
		}
	}

	return &admissionv1.AdmissionResponse{Allowed: true}
}

func main() {
	http.HandleFunc("/validate", handleAdmissionReview)
	fmt.Println("Starting webhook server on :8443")
	http.ListenAndServeTLS(
		":8443",
		"/etc/webhook/certs/tls.crt",
		"/etc/webhook/certs/tls.key",
		nil,
	)
}

这个Webhook做了以下几件事:

  1. 监听8443端口的/validate路径
  2. 只拦截Pod的创建请求
  3. 检查Pod是否带有department=ecommerce标签
  4. 没有标签就拒绝创建

四、部署Webhook到Kubernetes

开发完Webhook服务后,我们需要把它部署到集群中。这里给出对应的Kubernetes资源配置:

# webhook-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: admission-webhook
  labels:
    app: admission-webhook
spec:
  replicas: 1
  selector:
    matchLabels:
      app: admission-webhook
  template:
    metadata:
      labels:
        app: admission-webhook
    spec:
      containers:
      - name: webhook
        image: my-registry/admission-webhook:v1
        ports:
        - containerPort: 8443
        volumeMounts:
        - name: webhook-certs
          mountPath: /etc/webhook/certs
          readOnly: true
      volumes:
      - name: webhook-certs
        secret:
          secretName: webhook-certs

---
# webhook-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: admission-webhook
  labels:
    app: admission-webhook
spec:
  ports:
  - port: 443
    targetPort: 8443
  selector:
    app: admission-webhook

---
# webhook-configuration.yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: pod-label-validator
webhooks:
- name: validator.example.com
  rules:
  - apiGroups: [""]
    apiVersions: ["v1"]
    operations: ["CREATE"]
    resources: ["pods"]
    scope: "*"
  clientConfig:
    service:
      name: admission-webhook
      path: "/validate"
      port: 443
    caBundle: ${CA_BUNDLE}
  admissionReviewVersions: ["v1"]
  sideEffects: None
  timeoutSeconds: 5

部署步骤:

  1. 先创建包含证书的Secret
  2. 部署Webhook服务
  3. 创建ValidatingWebhookConfiguration资源

五、Mutating Webhook开发实战

有时候我们不仅想验证请求,还想自动修改请求内容。下面展示一个Mutating Webhook的示例,它会自动给Pod注入一个日志收集的sidecar容器。

func mutatePod(request *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse {
	// 只处理Pod创建请求
	if request.Kind.Kind != "Pod" || request.Operation != "CREATE" {
		return &admissionv1.AdmissionResponse{Allowed: true}
	}

	// 解析Pod对象
	pod := corev1.Pod{}
	if err := json.Unmarshal(request.Object.Raw, &pod); err != nil {
		return &admissionv1.AdmissionResponse{
			Allowed: false,
			Message: fmt.Sprintf("解析Pod失败: %v", err),
		}
	}

	// 创建patch操作
	var patches []map[string]interface{}
	
	// 1. 添加sidecar容器
	sidecar := corev1.Container{
		Name:  "log-collector",
		Image: "fluentd:latest",
		VolumeMounts: []corev1.VolumeMount{
			{
				Name:      "varlog",
				MountPath: "/var/log",
			},
		},
	}
	patches = append(patches, map[string]interface{}{
		"op":    "add",
		"path":  "/spec/containers/-",
		"value": sidecar,
	})

	// 2. 添加共享volume
	volume := corev1.Volume{
		Name: "varlog",
		VolumeSource: corev1.VolumeSource{
			HostPath: &corev1.HostPathVolumeSource{
				Path: "/var/log",
			},
		},
	}
	patches = append(patches, map[string]interface{}{
		"op":    "add",
		"path":  "/spec/volumes/-",
		"value": volume,
	})

	// 序列化patch
	patchBytes, err := json.Marshal(patches)
	if err != nil {
		return &admissionv1.AdmissionResponse{
			Allowed: false,
			Message: fmt.Sprintf("创建patch失败: %v", err),
		}
	}

	// 返回允许并携带patch
	return &admissionv1.AdmissionResponse{
		Allowed: true,
		Patch:   patchBytes,
		PatchType: func() *admissionv1.PatchType {
			pt := admissionv1.PatchTypeJSONPatch
			return &pt
		}(),
	}
}

这个Mutating Webhook会:

  1. 检查到Pod创建请求
  2. 自动注入fluentd sidecar容器
  3. 添加共享volume用于日志收集

六、应用场景与技术选型

自定义准入控制Webhook的典型应用场景包括:

  1. 资源验证:确保所有资源都有必要的标签或注解
  2. 安全策略:检查容器是否以非root用户运行
  3. 配置管理:自动注入sidecar或环境变量
  4. 成本控制:限制资源请求量过大

技术选型建议:

  • 语言:Go是首选,因为Kubernetes本身就是用Go写的,生态完善
  • 框架:可以使用controller-runtime等专业库简化开发
  • 部署:建议使用Kubernetes Deployment部署Webhook服务

七、注意事项与最佳实践

在开发和使用Webhook时,需要注意以下几点:

  1. 性能考虑:

    • Webhook会增加API Server的延迟
    • 保持处理逻辑简单高效
    • 设置合理的超时时间(默认10秒)
  2. 高可用:

    • Webhook服务必须有多个副本
    • 正确处理启动顺序问题(Webhook可能依赖其他服务)
  3. 幂等性:

    • Mutating Webhook要确保多次应用patch结果一致
    • 避免造成无限递归(比如Webhook修改了自己的Deployment)
  4. 测试策略:

    • 单元测试:测试业务逻辑
    • 集成测试:使用kind或minikube测试完整流程
    • 混沌测试:模拟Webhook服务不可用的情况

八、总结

自定义准入控制Webhook是扩展Kubernetes能力的强大工具,就像给Kubernetes装上了"业务规则检查器"。通过本文的讲解,你应该已经掌握了:

  1. Webhook的基本工作原理
  2. 如何开发Validating和Mutating Webhook
  3. 如何部署和配置Webhook
  4. 实际生产中的注意事项

记住,能力越大责任越大。Webhook可以拦截所有API请求,一旦出问题可能导致整个集群不可用。所以一定要充分测试,并准备好回滚方案。