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

其中
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 Webhook和mutating 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)建MutatingWebhookConfiguration和ValidatingWebhookConfiguration兩個配置文件。
一個配置文件中可以包含多個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,主要有url和service兩種方式。URL方式直接指定webhook服務地址,URL方式必須是https協(xié)議。如果有DNS可以可以通過域名訪問,另外不建議使用localhost或127.0.0.1clientConfig: 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下的所有資源下的子資源statusscope
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打印的日志

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

容器已被注入

5.1測試名稱為demo-webhook的pod
標簽未注入
