1.Admission Controllers
在整体的流程中可以理解为过滤器,拦截器甚至是AOP中的切面。准入控制是通过一个个插件实现,每个插件实现特定功能。例如ServiceAccount插件就是为了完成验证工作,MutatingAdmissionWebhook 和ValidatingAdmissionWebhook是为了实现动态注入控制。
2.kubernetes本身有哪些webhook
其中
NamespaceLifecycle, LimitRanger, ServiceAccount, TaintNodesByCondition, Priority, DefaultTolerationSeconds, DefaultStorageClass, StorageObjectInUseProtection, PersistentVolumeClaimResize, MutatingAdmissionWebhook, ValidatingAdmissionWebhook, ResourceQuota
是默认开启的
AlwaysAdmit
使用这个插件自行通过所有的请求。AlwaysDeny
拒绝所有的请求。用于测试AlwaysPullImages
不管镜像的拉去策略是那种,都将会被改成Always。在多租户的场景下,即便镜像被拉去到本地,其他租户的pod也使用不了ServiceAccount
做ServiceAccount的验证NamespaceLifecycle
这个插件强制不能在一个正在被终止的 Namespace 中创建新对象,和确保使用不存在 Namespace 的请求被拒绝。PodNodeSelector
通过读取命名空间注释和全局配置,这个插件默认并限制了在一个命名空间中使用什么节点选择器。other
其他
3.动态准入控制 Dynamic Admission Control
上面所说的准入控制都是kubernetes自身的,并且是编译时织如。对于面向定制化、插件化的kubernetes提供了运行时的动态实现,可以让开发人员实现相关插件,动态织入并生效。
4.如何实现自己的准入控制插件
首先了解下Admission webhooks,它是接收许可请求并对其执行操作的HTTP回调,Admission webhooks有两个两种类型validating admission Webhook
和mutating admission webhook
。
- mutating admission webhook
mutating 是最先被注入的,并且他可以拦截并修改修改发送给API server
的请求。 - validating admission Webhook
在mutating 修改完请求并被apiserver验证之后,validating 将被注入。根据校验规则可以拒绝强制执行自定义策略的请求。
注意:
为了保证对象的最终状态,都应该有一个validating admission Webhook去验证,因为准入控制产检有后多个,在后续进过其他插件式对象数据可能会被修改。
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的回调,那么首先得了解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、针对特定的接口添加回调函数
以下两个场景来举例说明。创建namespaces时通过webhook添加label,创建pod是为其注入新的容器(istio中的sidecar实现类似)。
注册回调事件
http.HandleFunc("/namespaces", serveNamespaces)
http.HandleFunc("/pods", servePods)
server := &http.Server{
Addr: ":443",
TLSConfig: configTLS(config),
}
将请求报文体转换为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/-",//此处虚注意,如果是数组类型,非第一个需要加上“/-”
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需要创建MutatingWebhookConfiguration
和ValidatingWebhookConfiguration
两个配置文件。
一个配置文件中可以包含多个webhook的配置,多个配置通过名称区分。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"
}]
}
生成配置证书文件
# 产生根证书
[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
部署在集群内也可以部署在集群外,此处为了调试,直接在外部启动(注意证书生成时要指定外部的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规则
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......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......tLS0K"
rules:
- operations: [ "CREATE" ]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
scope: "*"
-
clientConfig
求达到API server之后规则匹配后,就需要知道发给哪些webhook进行处理。clientConfig字段就是来指定的webhook,主要有url
和service
两种方式。URL方式
直接指定webhook服务地址,URL方式必须是https协议。如果有DNS可以可以通过域名访问,另外不建议使用localhost或127.0.0.1clientConfig: url: "https://10.48.17.175:443/pods" caBundle: "Ci0tLS0tQk...
...tLS0K" Service方式
主要借助于内部域名的方式clientConfig: caBundle: "Ci0tLS0tQk...
...tLS0K" service: namespace: my-service-namespace name: my-service-name path: /my-path port: 1234 注意:
caBundle
是对证书进行base64
处理的结果,其中ca.pem是生成证书是产生的文件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:
集群级别的资源都匹配这个规则
Namespaced:
Namespace级别匹配
*:
匹配所有,无限制
除此之外,在1.15+
版本中,可以规则还可以增加对象标签匹配、命名空间匹配,策略匹配(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
只有匹配上述规则的请求才会被发送到对应的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
标签未注入
demo源码地址