kubernetes中的AOP - 準入控制

1.Admission Controllers

在整體的流程中可以理解為過濾器,攔截器甚至是AOP中的切面。準入控制是通過一個個插件實現(xiàn),每個插件實現(xiàn)特定功能。例如ServiceAccount插件就是為了完成驗證工作,MutatingAdmissionWebhook 和ValidatingAdmissionWebhook是為了實現(xiàn)動態(tài)注入控制。

2.kubernetes本身有哪些webhook

image.png

其中
NamespaceLifecycle, LimitRanger, ServiceAccount, TaintNodesByCondition, Priority, DefaultTolerationSeconds, DefaultStorageClass, StorageObjectInUseProtection, PersistentVolumeClaimResize, MutatingAdmissionWebhook, ValidatingAdmissionWebhook, ResourceQuota是默認開啟的

  • AlwaysAdmit
    使用這個插件自行通過所有的請求。

  • AlwaysDeny
    拒絕所有的請求。用于測試

  • AlwaysPullImages
    不管鏡像的拉去策略是那種,都將會被改成Always。在多租戶的場景下,即便鏡像被拉去到本地,其他租戶的pod也使用不了

  • ServiceAccount
    做ServiceAccount的驗證

  • NamespaceLifecycle
    這個插件強制不能在一個正在被終止的 Namespace 中創(chuàng)建新對象,和確保使用不存在 Namespace 的請求被拒絕。

  • PodNodeSelector
    通過讀取命名空間注釋和全局配置,這個插件默認并限制了在一個命名空間中使用什么節(jié)點選擇器。

  • other
    其他

3.動態(tài)準入控制 Dynamic Admission Control

上面所說的準入控制都是kubernetes自身的,并且是編譯時織如。對于面向定制化、插件化的kubernetes提供了運行時的動態(tài)實現(xiàn),可以讓開發(fā)人員實現(xiàn)相關(guān)插件,動態(tài)織入并生效。

4.如何實現(xiàn)自己的準入控制插件

首先了解下Admission webhooks,它是接收許可請求并對其執(zhí)行操作的HTTP回調(diào),Admission webhooks有兩個兩種類型validating admission Webhookmutating admission webhook。

  • mutating admission webhook
    mutating 是最先被注入的,并且他可以攔截并修改修改發(fā)送給API server的請求。
  • validating admission Webhook
    在mutating 修改完請求并被apiserver驗證之后,validating 將被注入。根據(jù)校驗規(guī)則可以拒絕強制執(zhí)行自定義策略的請求。

注意:為了保證對象的最終狀態(tài),都應該有一個validating admission Webhook去驗證,因為準入控制產(chǎn)檢有后多個,在后續(xù)進過其他插件式對象數(shù)據(jù)可能會被修改。

Admission webhooks屬于kubernetes控制面的一部分。

4.1、使用Admission webhooks的條件
  • kubernetes版本>1.9
  • MutatingAdmissionWebhook 和ValidatingAdmissionWebhook被啟用,默認啟用
  • admissionregistration.k8s.io/v1或者admissionregistration.k8s.io/v1beta1在APiserver中被啟用
4.2、kubernetes有哪些API

既然webhook是http的回調(diào),那么首先得了解kubernetes中有哪些API接口

[root@node4 pki]# kubectl get --raw=/api/v1 |jq . |grep name\":
      "name": "bindings",
      "name": "componentstatuses",
      "name": "configmaps",
      "name": "endpoints",
      "name": "events",
      "name": "limitranges",
      "name": "namespaces",
      "name": "namespaces/finalize",
      "name": "namespaces/status",
      "name": "nodes",
      "name": "nodes/proxy",
      "name": "nodes/status",
      "name": "persistentvolumeclaims",
      "name": "persistentvolumeclaims/status",
      "name": "persistentvolumes",
      "name": "persistentvolumes/status",
      "name": "pods",
      "name": "pods/attach",
      "name": "pods/binding",
      "name": "pods/eviction",
      "name": "pods/exec",
      "name": "pods/log",
      "name": "pods/portforward",
      "name": "pods/proxy",
      "name": "pods/status",
      "name": "podtemplates",
      "name": "replicationcontrollers",
      "name": "replicationcontrollers/scale",
      "name": "replicationcontrollers/status",
      "name": "resourcequotas",
      "name": "resourcequotas/status",
      "name": "secrets",
      "name": "serviceaccounts",
      "name": "services",
      "name": "services/proxy",
      "name": "services/status",

kubernetes中資源是有類型和版本的,不同的資源類型和版本API也不同,其他可自行查看,常用的如下

[root@node4 pki]# kubectl get --raw=/apis/extensions/v1beta1 |jq . |grep name\":
[root@node4 pki]# kubectl get --raw=/apis/batch/v1 |jq . |grep name\":
[root@node4 pki]# kubectl get --raw=/apis/apps/v1 |jq . |grep name\":
4.3、針對特定的接口添加回調(diào)函數(shù)

以下兩個場景來舉例說明。創(chuàng)建namespaces時通過webhook添加label,創(chuàng)建pod是為其注入新的容器(istio中的sidecar實現(xiàn)類似)。

注冊回調(diào)事件

http.HandleFunc("/namespaces", serveNamespaces)
http.HandleFunc("/pods", servePods)
server := &http.Server{
    Addr:      ":443",
    TLSConfig: configTLS(config),
}

將請求報文體轉(zhuǎn)換為AdmissionReview

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

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

    klog.V(2).Info(fmt.Sprintf("handling request: %s", body))

    // The AdmissionReview that was sent to the webhook
    requestedAdmissionReview := v1beta1.AdmissionReview{}

    // The AdmissionReview that will be returned
    responseAdmissionReview := v1beta1.AdmissionReview{}

    deserializer := codecs.UniversalDeserializer()
    if _, _, err := deserializer.Decode(body, nil, &requestedAdmissionReview); err != nil {
        klog.Error(err)
        responseAdmissionReview.Response = toAdmissionResponse(err)
    } else {
        // pass to admitFunc
        responseAdmissionReview.Response = admit(requestedAdmissionReview)
    }

給pod注入label和sidecar 容器

func mutatePods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
    data,_ := json.Marshal(ar.Request.Object)
    fmt.Println(string(data))
    klog.V(2).Info("mutating pods")
    podResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
    if ar.Request.Resource != podResource {
        klog.Errorf("expect resource to be %s", podResource)
        return nil
    }

    raw := ar.Request.Object.Raw
    pod := corev1.Pod{}
    deserializer := codecs.UniversalDeserializer()
    if _, _, err := deserializer.Decode(raw, nil, &pod); err != nil {
        klog.Error(err)
        return toAdmissionResponse(err)
    }
    reviewResponse := v1beta1.AdmissionResponse{}
    reviewResponse.Allowed = true
    fmt.Println("為了測試,此處指定只有pod name=demo時才注入")
    if pod.Name == "demo" {
        saMountName,saMount := getSAMount(pod.Spec.Containers)
        fmt.Println("原pod的secretMount的掛載名稱:"+saMountName)

        needAddedContainers := injectPod(saMount);
        patch := injectLabels()
        for _, container := range needAddedContainers{
            patch = append(patch, PatchOperation{
                Op:    "add",
                Path:  "/spec/containers/-",//此處虛注意,如果是數(shù)組類型,非第一個需要加上“/-”
                Value: container,
            })
        }
        patchBytes ,_ := json.Marshal(patch)
        reviewResponse.Patch = patchBytes
        fmt.Println("==========" + string(reviewResponse.Patch))
        pt := v1beta1.PatchTypeJSONPatch
        reviewResponse.PatchType = &pt
    }
    return &reviewResponse
}

func injectPod(secretMount corev1.VolumeMount) []corev1.Container{
    return []corev1.Container{
        {
            Name: "agent",
            Image: "prima/filebeat:6",
            ImagePullPolicy: corev1.PullIfNotPresent,
            VolumeMounts: []corev1.VolumeMount{secretMount},
        },
    }
}


//獲取原容器中serviceaccount的掛載目錄
func getSAMount(originContainer []corev1.Container)(string,corev1.VolumeMount){
    mountName := ""
    var mount corev1.VolumeMount
    // find service account secret volume mount(/var/run/secrets/kubernetes.io/serviceaccount,
    // https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/#service-account-automation) from app container
    for _, add := range originContainer {
        for _, vmount := range add.VolumeMounts {
            if vmount.MountPath == "/var/run/secrets/kubernetes.io/serviceaccount" {
                mountName = vmount.Name
                mount = vmount
            }
        }
    }
    return mountName,mount
}

func injectLabels() []PatchOperation{
    return []PatchOperation{
        {
            Op: "add",
            Path: "/metadata/labels",
            Value: map[string]string{
                "webhook-label": "HelloWebhook",
            },
        },
    }
}
4.4、如何部署admission webhook服務

部署admission webhook需要創(chuàng)建MutatingWebhookConfigurationValidatingWebhookConfiguration兩個配置文件。

一個配置文件中可以包含多個webhook的配置,多個配置通過名稱區(qū)分。Configuration的版本目前有v1和v1beta1,相比v1,v1beta1功能更加健壯,同時審核日志和度量更易于與激活的配置匹配。

①為webhook生成證書,此處使用cfssl

準備生成證書的配置文件
ca-csr.json

{
    "CN": "etcd",
    "key": {
        "algo": "rsa",
        "size": 2048
    },
    "names": [{
        "C": "CN",
        "ST": "NanJing",
        "L": "NanJing",
        "O": "Kubernetes",
        "OU": "Kubernetes-manual"
    }]
}

webhook-csr.json

{
    "CN": "webhook",
        "hosts": [
      "webhook.default.svc",
      "10.48.17.175",
          "localhost",
          "kubernetes",
          "kubernetes.default",
          "kubernetes.default.svc",
          "kubernetes.default.svc.cluster",
          "kubernetes.default.svc.cluster.local"
        ],

    "key": {
        "algo": "rsa",
        "size": 2048
    },
    "names": [{
        "C": "CN",
        "ST": "NanJing",
        "L": "NanJing",
        "O": "Kubernetes",
        "OU": "Kubernetes-manual"
    }]
}

生成配置證書文件

# 產(chǎn)生根證書
[root@node4 pki]# cfssl gencert -initca ca-csr.json | cfssljson -bare ca
[root@node4 pki]# cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -profile=kubernetes \
  webhook-csr.json | cfssljson -bare webhook
[root@node4 pki]# ll
total 36
-rw-r--r-- 1 root root  342 Nov 20 11:31 ca-config.json
-rw-r--r-- 1 root root 1017 Nov 20 11:27 ca.csr
-rw-r--r-- 1 root root  250 Nov 20 11:31 ca-csr.json
-rw------- 1 root root 1675 Nov 20 11:31 ca-key.pem
-rw-r--r-- 1 root root 1391 Nov 20 11:27 ca.pem
-rw-r--r-- 1 root root 1289 Nov 20 11:31 webhook.csr
-rw-r--r-- 1 root root  550 Nov 20 11:31 webhook-csr.json
-rw------- 1 root root 1675 Nov 20 11:31 webhook-key.pem
-rw-r--r-- 1 root root 1675 Nov 20 11:31 webhook.pem

②部署為webhook server

webhook server的部署可以通過deployment部署在集群內(nèi)也可以部署在集群外,此處為了調(diào)試,直接在外部啟動(注意證書生成時要指定外部的IP),可以通過tls-cert-file和tls-private-key-file指定證書所在位置。

    var config Config
    config.CertFile ="F:/pki/webhook/webhook.pem"
    config.KeyFile="F:/pki/webhook/webhook-key.pem"
    config.addFlags()

    flag.Parse()
    http.HandleFunc("/namespaces", serveNamespaces)
    http.HandleFunc("/pods", servePods)
    http.HandleFunc("/ping", pong)
    server := &http.Server{
        Addr:      ":443",
        TLSConfig: configTLS(config),
    }
    fmt.Println("start ......")
    server.ListenAndServeTLS("", "")

③配置ValidatingWebhookConfiguration和MutatingWebhookConfiguration規(guī)則

MutatingWebhookConfiguration.yaml

kind: MutatingWebhookConfiguration
metadata:
  name: pod-webhook
webhooks:
  - name: pod.cloud.org
    failurePolicy: Fail
    clientConfig:
      url: "https://10.48.17.175:443/pods"
      caBundle: "Ci0tLS0tQk...<base64-encoded PEM bundle containing the CA that signed the webhook's serving certificate>...tLS0K"
    rules:
      - operations: [ "CREATE" ]
        apiGroups: [""]
        apiVersions: ["v1"]
        resources: ["pods"]
        scope: "*"

ValidatingWebhookConfiguration.yaml

kind: ValidatingWebhookConfiguration
metadata:
  name: pod-webhook
webhooks:
  - name: pod.cloud.org
    failurePolicy: Fail
    clientConfig:
      url: "https://10.48.17.175:443/pods"
      caBundle: "Ci0tLS0tQk...<base64-encoded PEM bundle containing the CA that signed the webhook's serving certificate>...tLS0K"
    rules:
      - operations: [ "CREATE" ]
        apiGroups: [""]
        apiVersions: ["v1"]
        resources: ["pods"]
        scope: "*"
  • clientConfig
    求達到API server之后規(guī)則匹配后,就需要知道發(fā)給哪些webhook進行處理。clientConfig字段就是來指定的webhook,主要有urlservice兩種方式。

    URL方式直接指定webhook服務地址,URL方式必須是https協(xié)議。如果有DNS可以可以通過域名訪問,另外不建議使用localhost或127.0.0.1

     clientConfig:
      url: "https://10.48.17.175:443/pods"
      caBundle: "Ci0tLS0tQk...<base64-encoded PEM bundle containing the CA that signed the webhook's serving certificate>...tLS0K"
    

    Service方式主要借助于內(nèi)部域名的方式

    clientConfig:
      caBundle: "Ci0tLS0tQk...<base64-encoded PEM bundle containing the CA that signed the webhook's serving certificate>...tLS0K"
      service:
        namespace: my-service-namespace
        name: my-service-name
        path: /my-path
        port: 1234
    

    注意: caBundle是對證書進行base64處理的結(jié)果,其中ca.pem是生成證書是產(chǎn)生的文件

     cat  ca.pem | base64 | tr -d '\n'
    
  • operations
    指一系列的方法(操作)類型,可選值CREATE, UPDATE, DELETE, CONNECT, *

  • apiGroups
    資源所屬組,kubernetes中資源組分為核心組(core)和其他類型。核心組路徑為/api/v1,組為空。其他組一般是指擴展組,路勁/apis/$GROUP_NAME,如extensions,apps,batch,autoscaling,policy等。*表示匹配所有資源組。

  • apiVersions
    指資源的版本,如v1, v1beta1,*表示匹配所有版本

  • resources
    指具體的資源類型,支持簡單的表達式配置,如下:
    * 匹配所有資源,但不包括子資源
    */* 匹配所有資源,并且包括子資源
    pods/* 匹配pods下的所有子資源.
    */status 匹配pods下的所有資源下的子資源status

  • scope
    webhook的作用域,可選值如下:
    Cluster:集群級別的資源都匹配這個規(guī)則
    Namespaced:Namespace級別匹配
    *: 匹配所有,無限制

除此之外,在1.15+版本中,可以規(guī)則還可以增加對象標簽匹配、命名空間匹配,策略匹配(matchPolicy)。

apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
webhooks:
- name: my-webhook.example.com
  objectSelector:
    matchLabels:
      foo: bar
  namespaceSelector:
     matchExpressions:
     - key: runlevel
       operator: NotIn
       values: ["0","1"]
  rules:
  - operations: ["CREATE"]
    apiGroups: ["*"]
    apiVersions: ["*"]
    resources: ["*"]
    scope: "*"

④部署配置

[root@node4 webhook]# kubectl apply -f MutatingWebhookConfiguration.yaml
[root@node4 webhook]# kubectl apply -f ValidatingWebhookConfiguration.yaml

只有匹配上述規(guī)則的請求才會被發(fā)送到對應的webhook

5.webhook測試

5.1測試名稱為demo的pod

編寫pod編排文件

apiVersion: v1
kind: Pod
metadata:
  name: demo
  namespace: webhook
spec:
  containers:
  - name: demo
    image: nginx:1.11.7
    imagePullPolicy: IfNotPresent

部署pod測試

[root@node4 webhook]# kubectl create ns webhook
namespace/webhook created
[root@node4 webhook]# kubectl apply -f nginx-pod.yaml 
pod/demo created

查看webhook打印的日志

image.png

查看標簽和容器是否被注入

標簽已被注入

image.png

容器已被注入

image.png

5.1測試名稱為demo-webhook的pod

標簽未注入


image.png

demo源碼地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容