一. k8s集群准备
这里不再赘述k8s集群搭建。主要注意参数:
kubectl get po kube-apiserver-server -n kube-system -o yaml | grep plugin
预期结果为:- --enable-admission-plugins=NodeRestriction,MutatingAdmissionWebhook,ValidatingAdmissionWebhook
至少要拥有两个参数:MutatingAdmissionWebhook,ValidatingAdmissionWebhook 这是控制webhook特性启用的。
/etc/kubernetes/manifests/kube-apiserver.yaml
,修改对应的启动项为预期值。#!/bin/bash# 配置参数 NAMESPACE="default" SERVICE_NAME="webhook-svc" SECRET_NAME="webhook-tls" CN="${SERVICE_NAME}.${NAMESPACE}.svc" SAN="DNS:${CN},DNS:${SERVICE_NAME}.${NAMESPACE}.svc.cluster.local"# 清理旧文件并创建目录 rm -rf certs mkdir -p certs cd certs# 步骤1: 生成 CA 私钥和证书 openssl genrsa -out ca.key 2048 openssl req -x509 -new -nodes -key ca.key -days 3650 -out ca.crt -subj "/CN=Webhook Root CA"# 步骤2: 生成服务器私钥和 CSR(含 SAN 扩展) cat <<EOF > openssl.cnf [req] req_extensions = v3_req distinguished_name = req_distinguished_name prompt = no[req_distinguished_name] CN = ${CN}[v3_req] keyUsage = keyEncipherment, digitalSignature extendedKeyUsage = serverAuth subjectAltName = @alt_names[alt_names] DNS.1 = ${SERVICE_NAME} DNS.2 = ${SERVICE_NAME}.${NAMESPACE} DNS.3 = ${SERVICE_NAME}.${NAMESPACE}.svc DNS.4 = ${SERVICE_NAME}.${NAMESPACE}.svc.cluster.local EOF# 生成服务器私钥和 CSR openssl genrsa -out tls.key 2048 openssl req -new -key tls.key -out server.csr -config openssl.cnf# 步骤3: 使用 CA 签发服务器证书 openssl x509 -req -in server.csr \-CA ca.crt \-CAkey ca.key \-CAcreateserial \-out tls.crt \-days 365 \-extensions v3_req \-extfile openssl.cnf# 步骤4: 创建 Secret(包含 CA 和服务器证书) kubectl create secret generic ${SECRET_NAME} \--from-file=tls.crt=tls.crt \--from-file=tls.key=tls.key \--from-file=ca.crt=ca.crt \-n ${NAMESPACE} --dry-run=client -o yaml > ./deploy/secret.yamlecho "Secret YAML 已保存到 certs/secret.yaml" echo "请执行以下步骤:" echo "1. 部署 Secret: kubectl apply -f certs/secret.yaml" echo "2. 在 MutatingWebhookConfiguration 中设置 caBundle: $(cat ca.crt | base64 -w0)"
根据自签发ca生成证书。
目录为./certs,实际有3个文件是我们使用到的。
其中ca.crt是要后续设置在MutatingWebhookConfiguration资源中的caBundle字段(base64 -d)
tls.crt和tls.key作为secret挂载到webhookpod中,作为服务端的tls证书。
3.代码
package mainimport ("encoding/json""flag""fmt""github.com/gin-gonic/gin""github.com/golang/glog""io"admissionv1 "k8s.io/api/admission/v1"admissionregistrationv1 "k8s.io/api/admissionregistration/v1"appsv1 "k8s.io/api/apps/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""k8s.io/apimachinery/pkg/types"utilruntime "k8s.io/apimachinery/pkg/util/runtime""log""net/http""strconv" )var (scheme = runtime.NewScheme()codecs = serializer.NewCodecFactory(scheme)deserializer = codecs.UniversalDeserializer() )const patchType = admissionv1.PatchTypeJSONPatchfunc init() {addToScheme(scheme) } func addToScheme(scheme *runtime.Scheme) {utilruntime.Must(corev1.AddToScheme(scheme))utilruntime.Must(appsv1.AddToScheme(scheme))utilruntime.Must(admissionv1.AddToScheme(scheme))utilruntime.Must(admissionregistrationv1.AddToScheme(scheme)) }type webhookServer struct {router *gin.Engineport inttlsCert stringtlsKey string }func newWebhookServer() *webhookServer {return &webhookServer{} }func (ws *webhookServer) init() {ws.router = gin.Default()flag.IntVar(&ws.port, "port", 8443, "Webhook server port")//默认值先改一下//flag.StringVar(&ws.tlsCert, "tls-cert", "/etc/webhook/tls.cert", "TLS certificate")//flag.StringVar(&ws.tlsKey, "tls-key", "/etc/webhook/tls.key", "TLS key")flag.StringVar(&ws.tlsCert, "tls-cert", "./certs/tls.crt", "TLS certificate")flag.StringVar(&ws.tlsKey, "tls-key", "./certs/tls.key", "TLS key")flag.Parse() }func main() {//初始化了webhook服务器配置包括证书svr := newWebhookServer()svr.init()log.Printf("run server...: port=%d, cert=%s, key=%s\n", svr.port, svr.tlsCert, svr.tlsKey)//注册路由 /mutate 逻辑在mutateHandler函数中svr.router.POST("/mutate", mutateHandler)//run httpserr := svr.router.RunTLS(":"+strconv.Itoa(svr.port), svr.tlsCert, svr.tlsKey)if err != nil {glog.Fatalf("webhook server run failed, error: %v", err)return}}func mutateHandler(c *gin.Context) {// 1. 读取原始请求体body, err := io.ReadAll(c.Request.Body)if err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": "failed to read request body"})return}log.Printf("Raw request body:\n%s", string(body))//校验 请求头中必须要带有 application/jsoncontentType := c.Request.Header.Get("Content-Type")if contentType != "application/json" {log.Printf("contentType=%s, expect application/json", contentType)return}// 2. 反序列化为 AdmissionReview 对象//req请求体反序列化之后是decodeObj runtime.object 所有k8s资源实现的接口decodeObj, gvk, err := deserializer.Decode(body, nil, nil)if err != nil {log.Printf("failed to deserialize object: %v", err)c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request format"})return}//构建响应var responseObj runtime.Object //集群中所有资源实现的接口//依据gvk判断不接受admissionReview以外的请求switch *gvk {case admissionv1.SchemeGroupVersion.WithKind("AdmissionReview")://将接口断言成*admissionReview对象reqAdmissionReview, ok := decodeObj.(*admissionv1.AdmissionReview)if !ok {log.Printf("expect AdmissionReview, but got %#v", gvk)c.JSON(http.StatusBadRequest, gin.H{"error": "failed to deserialize AdmissionReview"})return}respAdmissionReview := &admissionv1.AdmissionReview{Response: &admissionv1.AdmissionResponse{UID: reqAdmissionReview.Request.UID,},}respAdmissionReview.SetGroupVersionKind(*gvk)respAdmissionReview.Response = admitFunc(reqAdmissionReview)responseObj = respAdmissionReviewdefault:log.Printf("Unsupported gvk %#v", gvk)c.JSON(http.StatusBadRequest, gin.H{"error": "failed to deserialize AdmissionReview"})return}responseByte, err := json.Marshal(responseObj)if err != nil {log.Println("Can't encode response:", err)c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to encode response"})return}c.Data(http.StatusOK, "application/json", responseByte) }// 当创建deploy,sts, 控制器的时候,自动将replicas改成3 func admitFunc(ar *admissionv1.AdmissionReview) *admissionv1.AdmissionResponse {if ar.Request == nil {return admissionErrorResponse("empty request.")}var patch []bytevar err errorswitch ar.Request.Kind.Kind {case "Deployment":patch, err = processReplicas(ar.Request.Object.Raw, 3, &appsv1.Deployment{})case "StatefulSet":patch, err = processReplicas(ar.Request.Object.Raw, 3, &appsv1.StatefulSet{})default:admissionAllowResp(ar.Request.UID)}if err != nil {return admissionErrorResponse(err.Error())}var pt admissionv1.PatchTypept = patchTypereturn &admissionv1.AdmissionResponse{Allowed: true,UID: ar.Request.UID,Patch: patch,PatchType: &pt,}}func processReplicas(raw []byte, desiredReplicas int32, obj runtime.Object) ([]byte, error) {_, _, err := deserializer.Decode(raw, nil, obj)if err != nil {return nil, err}var currentReplicas *int32switch v := obj.(type) {case *appsv1.Deployment:currentReplicas = v.Spec.Replicascase *appsv1.StatefulSet:currentReplicas = v.Spec.Replicasdefault:return nil, fmt.Errorf("unknown type: %T", obj)}if *currentReplicas != desiredReplicas {patch := []map[string]interface{}{{"op": "replace","path": "/spec/replicas","value": desiredReplicas,},}return json.Marshal(patch)}return nil, nil}func admissionErrorResponse(msg string) *admissionv1.AdmissionResponse {return &admissionv1.AdmissionResponse{Allowed: false,Result: &metav1.Status{Code: http.StatusBadRequest,Message: msg,},} }func admissionAllowResp(uid types.UID) *admissionv1.AdmissionResponse {return &admissionv1.AdmissionResponse{Allowed: true,UID: uid,} }
这里实现的是将deployment,statefulset控制器的副本数强制改为3的webhook
webhook作为https服务器,接受apiserver的请求,请求体是admissionv1.AdmissionReview
实际的结构为:
type AdmissionReview struct {metav1.TypeMeta `json:",inline"`// Request describes the attributes for the admission request.// +optionalRequest *AdmissionRequest `json:"request,omitempty" protobuf:"bytes,1,opt,name=request"`// Response describes the attributes for the admission response.// +optionalResponse *AdmissionResponse `json:"response,omitempty" protobuf:"bytes,2,opt,name=response"` }
可以看到实际资源其实就是将http的request和response放在一起组合成的。
request和response中的uid指向同一个值,返回的AdmissionReview中的response应该带有和request的同一个uid
四.部署
Dockerfile
FROM alpine:latest USER rootCOPY replicasctrl /usr/local/bin/replicasctrl# set entrypoint ENTRYPOINT ["/usr/local/bin/replicasctrl"]
MutatingWebhookConfiguration.yaml
kind: MutatingWebhookConfiguration apiVersion: admissionregistration.k8s.io/v1 metadata:name: replicas-mutating-webhook webhooks:- admissionReviewVersions:- v1clientConfig:caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURGVENDQWYyZ0F3SUJBZ0lVR3phSXQvQkt5enRlU1pJZVJRL0E3Sy9BMlVnd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0dqRVlNQllHQTFVRUF3d1BWMlZpYUc5dmF5QlNiMjkwSUVOQk1CNFhEVEkxTURNeE5URXlNekF4TUZvWApEVE0xTURNeE16RXlNekF4TUZvd0dqRVlNQllHQTFVRUF3d1BWMlZpYUc5dmF5QlNiMjkwSUVOQk1JSUJJakFOCkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQW1Jbm9SaWVETmQybEFIWkZiUWVOUkJuRWt0d1IKZXFjMUNLbHNTOFpPMDV3b3lFT0RlN2UwMVJWSXRUTU5NV2JsU3M4UVpVWnJNNTB2ZDdxK212Sm44NjlnNlg5cAoxNG0xYkx1c05jaDZQZUtTY2ZYQlJaNjNXWWgzbHFMbXhFMGtiTTVmbWhmZ3hnempHMGRnUGpEOWl3N3JBMzJnCm9wRi9SZFA2aTdqcHZ3bTU2a09UTlAwRWxOQ01MUHdVUEFzWFArdkN1anQxc1FOZUtyUHM5b1BVVlpmeWx4bFUKakhTM1p2bmxXOHpTSm5VSHhjbkkxR1VBbVRSN3UwNCs1TlF6aWpBT1Rxczl1YnNOc1lkV2xXdHJaTEpvSDhYMApPa0p3VzJaZDYydFpKTC9jNSs1OHN0Zm9mYVdmdmg2WkhQMTkwVGlVdTJjLzc2RCszd3dqcTdtckN3SURBUUFCCm8xTXdVVEFkQmdOVkhRNEVGZ1FVMWhJZTI3ejhmODlnVVBkdEI5TndsOHFtSHBRd0h3WURWUjBqQkJnd0ZvQVUKMWhJZTI3ejhmODlnVVBkdEI5TndsOHFtSHBRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBTkJna3Foa2lHOXcwQgpBUXNGQUFPQ0FRRUFjUExIVU93b3Y4ZjRPa3NoNGVVS2JqMTZZS1h2SFlJcnRKY3FZejZJcTdJK0dwUkNZbVp6CnM0MlZOSlpwcEwrUU5YUXY5SVBONXNvU3JjVjhaVWhETnFQSHRHWXIzbnM3aGpEY2QxWlVzSHVJM3lTWEJrQU8KQWNUYXFDeDNOODZLcTdqV0FiemV3b3hwTW9weTBMbnFhMnAxdmhjUWN0bG9Nd0pqNHc2OU1wK0luWjRVTERNdgo1Uk9HZ0RJcGFDWGtDazFzV1F2MWFzMWovRkxWekY0WUx0ZVNZbk5ScVRlMTNOVVVvZUlpTVJUZlR5akIyZlFUCm54dXUyKzFaL0RNMTNoaUgwTGdsYUwxNHV2TE5RM2I3SkVFeGZjV0tuUjlSU2ovNkNHM05zTkFySE01K1pOYkgKOElNL1l5Y3UxWjFnM3EzUHMxWFBOdlN5OTdlZDBLRHNuQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0Kservice:name: webhook-svcnamespace: defaultpath: /mutateport: 8443failurePolicy: FailmatchPolicy: Equivalentname: replicas.mutating.webhooknamespaceSelector: {}objectSelector: {}reinvocationPolicy: Neverrules:- apiGroups:- appsapiVersions:- v1operations:- CREATE- UPDATEresources:- deployments- statefulsetsscope: "*"sideEffects: NonetimeoutSeconds: 10
secret.yaml
apiVersion: v1 data:ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURGVENDQWYyZ0F3SUJBZ0lVR3phSXQvQkt5enRlU1pJZVJRL0E3Sy9BMlVnd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0dqRVlNQllHQTFVRUF3d1BWMlZpYUc5dmF5QlNiMjkwSUVOQk1CNFhEVEkxTURNeE5URXlNekF4TUZvWApEVE0xTURNeE16RXlNekF4TUZvd0dqRVlNQllHQTFVRUF3d1BWMlZpYUc5dmF5QlNiMjkwSUVOQk1JSUJJakFOCkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQW1Jbm9SaWVETmQybEFIWkZiUWVOUkJuRWt0d1IKZXFjMUNLbHNTOFpPMDV3b3lFT0RlN2UwMVJWSXRUTU5NV2JsU3M4UVpVWnJNNTB2ZDdxK212Sm44NjlnNlg5cAoxNG0xYkx1c05jaDZQZUtTY2ZYQlJaNjNXWWgzbHFMbXhFMGtiTTVmbWhmZ3hnempHMGRnUGpEOWl3N3JBMzJnCm9wRi9SZFA2aTdqcHZ3bTU2a09UTlAwRWxOQ01MUHdVUEFzWFArdkN1anQxc1FOZUtyUHM5b1BVVlpmeWx4bFUKakhTM1p2bmxXOHpTSm5VSHhjbkkxR1VBbVRSN3UwNCs1TlF6aWpBT1Rxczl1YnNOc1lkV2xXdHJaTEpvSDhYMApPa0p3VzJaZDYydFpKTC9jNSs1OHN0Zm9mYVdmdmg2WkhQMTkwVGlVdTJjLzc2RCszd3dqcTdtckN3SURBUUFCCm8xTXdVVEFkQmdOVkhRNEVGZ1FVMWhJZTI3ejhmODlnVVBkdEI5TndsOHFtSHBRd0h3WURWUjBqQkJnd0ZvQVUKMWhJZTI3ejhmODlnVVBkdEI5TndsOHFtSHBRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBTkJna3Foa2lHOXcwQgpBUXNGQUFPQ0FRRUFjUExIVU93b3Y4ZjRPa3NoNGVVS2JqMTZZS1h2SFlJcnRKY3FZejZJcTdJK0dwUkNZbVp6CnM0MlZOSlpwcEwrUU5YUXY5SVBONXNvU3JjVjhaVWhETnFQSHRHWXIzbnM3aGpEY2QxWlVzSHVJM3lTWEJrQU8KQWNUYXFDeDNOODZLcTdqV0FiemV3b3hwTW9weTBMbnFhMnAxdmhjUWN0bG9Nd0pqNHc2OU1wK0luWjRVTERNdgo1Uk9HZ0RJcGFDWGtDazFzV1F2MWFzMWovRkxWekY0WUx0ZVNZbk5ScVRlMTNOVVVvZUlpTVJUZlR5akIyZlFUCm54dXUyKzFaL0RNMTNoaUgwTGdsYUwxNHV2TE5RM2I3SkVFeGZjV0tuUjlSU2ovNkNHM05zTkFySE01K1pOYkgKOElNL1l5Y3UxWjFnM3EzUHMxWFBOdlN5OTdlZDBLRHNuQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0Ktls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURuVENDQW9XZ0F3SUJBZ0lVTkswVTNtL0wzYzVwQmwwSFdycWRtRU5kbVlvd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0dqRVlNQllHQTFVRUF3d1BWMlZpYUc5dmF5QlNiMjkwSUVOQk1CNFhEVEkxTURNeE5URXlNekF4TUZvWApEVEkyTURNeE5URXlNekF4TUZvd0lqRWdNQjRHQTFVRUF3d1hkMlZpYUc5dmF5MXpkbU11WkdWbVlYVnNkQzV6CmRtTXdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFERlhOU3Zpa1g4c0R2a2NPR24KSndXdHVTT3JDRWQwRmpxR1FmZ3M3NWtPTjcyclI1My9XSzFRd3VwRnROOHRxcGZLYkJUVHpreURUMCtCZEsvcwp6Z1ZObkRRWVVzS01pVDJmRTNOR2Zuem1rdGRmc2o5bjhKRkdYRW1sZzVFcGFXVUxqL2RZM3N0d1Z6SHI5TkpOCmZTVUlZUlQrMXMrUW5lVktwVkpjaERPU2RlQ0xTL0xmU1p1ckJLamE2S3kzb2FaOGE0T2hHSFEzUWpvTmxlY3cKYnJ1czRrT3hoQTVDYXQ3L2k3Q2kvbHB0WUd5OTdiVjl6amNXejNZQjNEWklUSnVZdlVMbjVUcXUxejVwOXZrNgpEMHpZQUo2ZHo0cjNzTnZVRXh3TWQ5d09DK3hONXJPNkFUeWxhZUVsNHJIdW80aFVxU1ExVENaUGFmU0p6Tmc4Cmhyc3hBZ01CQUFHamdkSXdnYzh3Q3dZRFZSMFBCQVFEQWdXZ01CTUdBMVVkSlFRTU1Bb0dDQ3NHQVFVRkJ3TUIKTUdzR0ExVWRFUVJrTUdLQ0MzZGxZbWh2YjJzdGMzWmpnaE4zWldKb2IyOXJMWE4yWXk1a1pXWmhkV3gwZ2hkMwpaV0pvYjI5ckxYTjJZeTVrWldaaGRXeDBMbk4yWTRJbGQyVmlhRzl2YXkxemRtTXVaR1ZtWVhWc2RDNXpkbU11ClkyeDFjM1JsY2k1c2IyTmhiREFkQmdOVkhRNEVGZ1FVSnlndVBEb2JXRlhBdVJiSFhlNDZacGZFSHFJd0h3WUQKVlIwakJCZ3dGb0FVMWhJZTI3ejhmODlnVVBkdEI5TndsOHFtSHBRd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQgpBQkpkT2ZpbzE5bTBLeUF3VXgyb1VHNkNYemh6MW9rbm1PeUR4eTkrcXg2eTNIZXdTUTdLeWJCNUpJMWNGNUJMClFOYUN1dGVadWxnSGNydVFXUlJONng0L2E3TGF6V0RENWYzRmVPNEczbEppZk1IUVlVaDJOSlBuV3hKSU55b1MKWnlTcGhVL3Zmb2ozMVpuMXlqTFB0V3gzSVllcms4dnlQR0YrbkRRZE1lREM2Q3MyMXMyNDQwa01vaUVmVnhkcApScjRYUHp1R0FiMGNpcTRwdDlBTHpFUEh4RHhTYklsVmdEMFEvZGdIUEpuSkYzVDJ4ZVVyMDZJcmpmSjdQSkhpCkZJdEx1U3VQejhkdldUWEoxV2ZtWE9hN0ZIZUFvejRIbm5EdEhtc1VEZTRBNkR4L1BMVVc1K2kwclc4QTRncXAKRU5zbU9NclRDa3RkeGdUdTczSk9FdWM9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0Ktls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRREZYTlN2aWtYOHNEdmsKY09Hbkp3V3R1U09yQ0VkMEZqcUdRZmdzNzVrT043MnJSNTMvV0sxUXd1cEZ0Tjh0cXBmS2JCVFR6a3lEVDArQgpkSy9zemdWTm5EUVlVc0tNaVQyZkUzTkdmbnpta3RkZnNqOW44SkZHWEVtbGc1RXBhV1VMai9kWTNzdHdWekhyCjlOSk5mU1VJWVJUKzFzK1FuZVZLcFZKY2hET1NkZUNMUy9MZlNadXJCS2phNkt5M29hWjhhNE9oR0hRM1Fqb04KbGVjd2JydXM0a094aEE1Q2F0Ny9pN0NpL2xwdFlHeTk3YlY5empjV3ozWUIzRFpJVEp1WXZVTG41VHF1MXo1cAo5dms2RDB6WUFKNmR6NHIzc052VUV4d01kOXdPQyt4TjVyTzZBVHlsYWVFbDRySHVvNGhVcVNRMVRDWlBhZlNKCnpOZzhocnN4QWdNQkFBRUNnZ0VBRWZyZmkzQnk2TTdiWGZma3J0Z3d2YjlnbnZ1OW1yZE50SjU4OEFjUjhBZ20KK053bzZqTFhjMFNXbUN3ZXF1ZmdOVG84ZVlGUldpTVhFS21qUDFVVGlac0I2ZmRjTHZadnpUYTE2VVdydGt2SgpZRGY2YThzd1NQTXVhR3hBaEwvTHkwNWR6OVJZUDA5S1JuOUN5M2xycnNROVord0U4OTFXbnNMSjZwREdyQUIxCk5CeG1LVW03QXBVNVpMNExCYVVVWi9yZWQraTdJYTVTQU9yc2RQdEsvTDN5WHZmZXJ4KzdYakViSDNMN0RxTloKekN4SEFrT2IyT2Q2Q0o5SGN0eUdrNSsvVytJUERhdENJSzAyZFRDR0NvbXZqVzlyQW9vbDdNcXJtSTBvN0M4NApWeFlDSTV2N0srbW51QjBobnVUVkFJVWdsYU5PQSt6emljbnpjWkVXNHdLQmdRRGlGdXNTSEoxVk9SaUJNM3pzCmg2MEw2UkVjQXV4VEhVTVo2ZGxORmRzQ2tvbEVKQXMwd0JHSGNtYlZudDRDekV4UXk4bVhzM2xHVU5YZnlTSTgKUjlobXlVZEI4QTRaNGZvUHV3REdGYTRXV2VrdUl3Z21KTHBaZ2c1K2FXcW9EQUp1dUtReXBCZnBKTkovVXgxdgpRRDJYT2FUS3pRdTRTZHNOWkR4b2hMQXRBd0tCZ1FEZmVRRTZZMFp6Snh6WDRjSW9zWERzeE9oZjB0dzVzaEJLClFhc1hoa1pKcERPYXI5NzBzWnFFSURHREtHVndhRVVUK0RuNTRQSFREd1kzaGI0UmZodUVVZnpQc2tEejFWeWYKZWJiNG5saDB1Z0RMMHl3TkU3NTRQeXRXeGJwK1hUczZOWnRMT2ZkYlJJY0V5Z0l0NS9FYTZ0d3JKd291NVd4SAo2VEZ6NjFxZXV3S0JnUUNxM1hrd1NmSFpxM25LZ3hnQlJoUlFzUVplTGhOZVNQb2lSbW9VYU5VSW42Z2ZtRUhqCnp0Z3dqaFFMazdIaldYUy9oeFBHa3p1dkdYNVpUdytSa1JhSnI4b3JtZmwrTkJzZzhrb0dhZklVTUVVYXVoejUKZnI1YTBRQ1ZKcVFWZG1ZTU9YeldUTTlKUXF2VzBBQ3B5Rm9EeE91MjNMbmp2K1ZOdkpndXdVREg4d0tCZ0ZTNwpXOFRZdVhDV0J2Q3Y3OTlnRURJbUl2bWFTTmd6ZE12REJHMUNBMHFPME9ZNUF1K0NtOVMzSkM3WDFVWitzcHAwCnh2N0ExTkF5NVNlT05WZ0ttY0pkRjk5a2RnNDkrd1dZcjlDcXNWMW8zVDVyVGt1VERlZ29BM1crT1EwS3FwZFMKbGhRNjRWZ2dycFVaUnlSQ3luOXJSNW14RHNKalNPQW5RaEh5emdSYkFvR0FMNlZYSGFlbnlSM1BtUGM0Rk1hagptM3BkNDRXUmJmVUs2VzV4RXp3R1drREF5cXVwYW1mdm4yTTVYejFEZVh0Tzk5dTlrbU5WWlByWXJubXdVOElNCkZ2TjQyMTRDMlRhL2FlVGdxRHM1Q3FOWmFxTW1aY2F5ejdNR1h5S1ZYNTJmdm41YUloR3BSQjFBdWhiU3RKbFgKb3hKRkNxOURkTXU5NlJuTHFNMitDVjA9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K kind: Secret metadata:creationTimestamp: nullname: webhook-tlsnamespace: default
service.yaml
# webhook-service.yaml apiVersion: v1 kind: Service metadata:name: webhook-svcnamespace: default spec:ports:- port: 8443targetPort: 8443 # selector: # app: admission-webhook-example
如果是在本地调试还未打镜像的情况下,需要配置ep到wsl或者虚拟机内部的监听端口
#调试情况下配置ep apiVersion: v1 kind: Endpoints metadata:name: webhook-svcnamespace: default subsets:- addresses:- ip: 172.21.227.39 # 替换为步骤1获取的WSL IPports:- port: 8443
webhook镜像部署:
deployment.yaml
apiVersion: apps/v1 kind: Deployment metadata:name: webhook spec:progressDeadlineSeconds: 600replicas: 1revisionHistoryLimit: 10selector:matchLabels:service.cpaas.io/name: deployment-webhookstrategy:rollingUpdate:maxSurge: 25%maxUnavailable: 25%type: RollingUpdatetemplate:metadata:creationTimestamp: nulllabels:service.cpaas.io/name: deployment-webhookspec:affinity: {}containers:- args:- --tls-cert=/etc/webhook/certs/tls.crt- --tls-key=/etc/webhook/certs/tls.keyimage: 192.168.8.126:30080/wangao/webhook:v1imagePullPolicy: IfNotPresentname: webhookports:- containerPort: 8443protocol: TCPresources: {}terminationMessagePath: /dev/termination-logterminationMessagePolicy: FilevolumeMounts:- mountPath: /etc/webhook/certsname: certsreadOnly: truednsPolicy: ClusterFirstrestartPolicy: AlwaysschedulerName: default-schedulersecurityContext: {}terminationGracePeriodSeconds: 30volumes:- name: certssecret:defaultMode: 420secretName: replicas-webhook-tls
这样的话,apiserver就会根据mutatingwebhook中的配置,用ca去验证对应service+port的服务器证书,并且发送request,服务器返回符合apiserver要求的response,apiserver根据其response修改请求,最终持久化到etcd中。
另外,validatewebhook的逻辑类似,一般单纯用来验证请求,只是mutatingwebhook优先于validatewebhook生效。