简介
准入控制器是在对象持久化之前用于对 Kubernetes API Server 的请求进行拦截的代码段,在请求经过身份验证和授权之后放行通过。准入控制器可能正在 validating、mutating 或者都在执行,Mutating 控制器可以修改他们处理的资源对象,Validating 控制器不会,如果任何一个阶段中的任何控制器拒绝了请求,则会立即拒绝整个请求,并将错误返回给最终的用户。
时序图如下所示:
如果我们想为CRD实现admission webhook,我们唯一要做的就是实现Defaulter和(或)Validator接口。
Kubebuilder会我们处理剩余的工作:
- 创建webhook服务器
- 确保服务器已添加到管理器中
- 为webhook创建处理程序
- 使用服务器中的路径注册每个处理程序
设计实战场景
这里我们直接在手摸手教你使用kubebuilder开发operator中实战的app-operator项目上来扩展功能。
这里我们扩展两个功能:
1、假设用户的CR中没有设置总QPS,我们可以通过webhook来设置一个默认值,比如1300;
2、为了保护系统,给单个Pod的QPS设置上限,比如1000,如果用户的CR中singlePodsQPS的值超过1000,webhook进行拦截,创建资源对象失败。
部署证书管理器
和controller类似,webhook既可以在kubernetes环境中运行,也可以在kubernetes环境外运行,如果webhook在kuberneetes环境之外运行,需要将证书放在所在的环境中,默认路径:
/tmp/k8s-webhook-server/serving-certs/tls.{crt,key}
这里选择是将webhook部署在kubernetes环境中,使用cert-manager
来管理证书。
直接使用下面命令安装cert-manager组件即可:
$ kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.9.1/cert-manager.yaml
命令执行成功后,会自动创建cert-manager命名空间,以及rbac、pod、service等资源:
$ kubectl get all -n cert-manager
NAME READY STATUS RESTARTS AGE
pod/cert-manager-6544c44c6b-gf6nk 1/1 Running 0 20h
pod/cert-manager-cainjector-5687864d5f-22dzw 1/1 Running 0 20h
pod/cert-manager-webhook-785bb86798-nh2gc 1/1 Running 0 20h
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/cert-manager ClusterIP 10.109.226.106 <none> 9402/TCP 20h
service/cert-manager-webhook ClusterIP 10.106.116.190 <none> 443/TCP 20h
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/cert-manager 1/1 1 1 20h
deployment.apps/cert-manager-cainjector 1/1 1 1 20h
deployment.apps/cert-manager-webhook 1/1 1 1 20h
NAME DESIRED CURRENT READY AGE
replicaset.apps/cert-manager-6544c44c6b 1 1 1 20h
replicaset.apps/cert-manager-cainjector-5687864d5f 1 1 1 20h
replicaset.apps/cert-manager-webhook-785bb86798 1 1 1 20h
开发webhook
进入app-operator工程下,在终端执行以下命令创建webhook:
$ kubebuilder create webhook \
--group elasticweb \
--version v1 \
--kind ElasticWeb \
--defaulting \
--programmatic-validation
上述命令执行完毕,kubebuilder会为我们创建webhook的处理程序。
首先来查看main.go文件,不难发现,kuberbuilder为我们新增了如下代码:
if err = (&elasticwebv1.ElasticWeb{}).SetupWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "ElasticWeb")
os.Exit(1)
}
同时不难发现,在api/v1下,新增了elasticweb_webhook.go和webhook_suite_test.go两个文件,同时在config目录下,也新增了webhook的相关配置。
这里我们最为关心的就是elasticweb_webhook.go这个文件了,因为我们的主要逻辑都是要在这里实现。
配置
在进行编写业务逻辑之前,我们需要对config/default/kustomization.yaml文件做一些修改,原文件中对webhook的配置部分都是注释掉的,我们要做的是就是启用这些配置。
这些配置分别是- ../webhook,- ../certmanager,- manager_webhook_patch.yaml,- webhookcainjection_patch.yaml以及vars下的全部内容。
# config/default/kustomization.yaml
# Adds namespace to all resources.
namespace: elasticweb-system
# Value of this field is prepended to the
# names of all resources, e.g. a deployment named
# "wordpress" becomes "alices-wordpress".
# Note that it should also match with the prefix (text before '-') of the namespace
# field above.
namePrefix: elasticweb-
# Labels to add to all resources and selectors.
#commonLabels:
# someName: someValue
bases:
- ../crd
- ../rbac
- ../manager
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
- ../webhook
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
- ../certmanager
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
#- ../prometheus
patchesStrategicMerge:
# Protect the /metrics endpoint by putting it behind auth.
# If you want your controller-manager to expose the /metrics
# endpoint w/o any authn/z, please comment the following line.
- manager_auth_proxy_patch.yaml
# Mount the controller config file for loading manager configurations
# through a ComponentConfig type
#- manager_config_patch.yaml
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
- manager_webhook_patch.yaml
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'.
# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks.
# 'CERTMANAGER' needs to be enabled to use ca injection
- webhookcainjection_patch.yaml
# the following config is for teaching kustomize how to do var substitution
vars:
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR
objref:
kind: Certificate
group: cert-manager.io
version: v1
name: serving-cert # this name should match the one in certificate.yaml
fieldref:
fieldpath: metadata.namespace
- name: CERTIFICATE_NAME
objref:
kind: Certificate
group: cert-manager.io
version: v1
name: serving-cert # this name should match the one in certificate.yaml
- name: SERVICE_NAMESPACE # namespace of the service
objref:
kind: Service
version: v1
name: webhook-service
fieldref:
fieldpath: metadata.namespace
- name: SERVICE_NAME
objref:
kind: Service
version: v1
name: webhook-service
编码
一切就绪后,现在开始实现业务逻辑。
打开api/v1/elasticweb_webhook.go文件。
新增依赖:
apierrors "k8s.io/apimachinery/pkg/api/errors"
首先我们来实现Default方法。
Default就是来判断CR中的TotalQPS的值是否为空,如果为空值,那么就给它设置个默认值:
// Default implements webhook.Defaulter so a webhook will be registered for the type
func (r *ElasticWeb) Default() {
elasticweblog.Info("default", "name", r.Name)
// TODO(user): fill in your defaulting logic.
// 如果创建的时候没有输入总QPS,就设置个默认值
if r.Spec.TotalQPS == nil {
r.Spec.TotalQPS = new(int32)
*r.Spec.TotalQPS = 1300
elasticweblog.Info("a. TotalQPS is nil,set default value now", "totalQPS", *r.Spec.TotalQPS)
} else {
elasticweblog.Info("b. TotalQPS exists", "TotalQPS", r.Spec.TotalQPS)
}
}
接下来我们实现验证功能,由于这里我们需要验证Create和Update,它们使用同一套逻辑,为了代码简洁以及复用性,我们封装一个validateElasticWeb方法。
我们主要是验证singlePodsQPS是否大于1000,如果是,则直接拦截。
func (r *ElasticWeb) validateElasticWeb() error {
var allErrs field.ErrorList
if *r.Spec.SinglePodsQPS > 1000 {
elasticweblog.Info("c. Invalid SinglePodQPS")
err := field.Invalid(field.NewPath("spec").Child("singlePodQPS"),
*r.Spec.SinglePodsQPS,
"d. must be less than 1000")
allErrs = append(allErrs, err)
return apierrors.NewInvalid(
schema.GroupKind{
Group: "elasticweb.example.com",
Kind: "ElasticWeb",
},
r.Name,
allErrs)
} else {
elasticweblog.Info("e. SinglePodQPS is valid")
return nil
}
}
通过上面的代码可见,最终是调用apierrors.NewInvalid生成错误实例,而此方法接收的是多个错误,因此需要为期准备切片作为入参,如果是多个参数校验失败,可以直接放入切片。
最终我们需要在ValidateCreate()和ValidateUpdate()方法中调用我们上面封装的validateElasticWeb。
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *ElasticWeb) ValidateCreate() error {
elasticweblog.Info("validate create", "name", r.Name)
// TODO(user): fill in your validation logic upon object creation.
return r.validateElasticWeb()
}
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *ElasticWeb) ValidateUpdate(old runtime.Object) error {
elasticweblog.Info("validate update", "name", r.Name)
// TODO(user): fill in your validation logic upon object update.
return r.validateElasticWeb()
}
测试
部署
由于我们选择使用cert-manager来管理证书,所以我们测试的时候需要将operator部署到集群。
1、部署CRD:
make install
2、构建镜像并推送到镜像仓库
make docker-build docker-push IMG=huiyichanmian/elasitcweb:v0.0.2
3、部署集成了webhook功能的controller
make deploy IMG=huiyichanmian/elasitcweb:v0.0.2
4、查看,确保启动成功
$ kubectl get pod -n elasticweb-system
NAME READY STATUS RESTARTS AGE
elasticweb-controller-manager-5dc874656b-dsqt5 2/2 Running 0 4h41m
验证Defaulter
修改config/samples/elasticweb_v1_elasticweb.yaml,将totalQPS字段进行注释,完整文件如下所示:
apiVersion: elasticweb.example.com/v1
kind: ElasticWeb
metadata:
name: elasticweb-sample
namespace: dev
spec:
image: nginx:1.17.1
port: 30003
singlePodsQPS: 800
# totalQPS: 2400
此时我们设置的单个Pod的QPS是800,如果webhook生效,总QPS就是1500,那么对应的Pod应该是两个,我们直接创建上面的资源对象后,查看是否符合我们的预期。
$ kubectl apply -f config/samples/elasticweb_v1_elasticweb.yaml
elasticweb.elasticweb.example.com/elasticweb-sample created
$ kubectl get pod -n dev
NAME READY STATUS RESTARTS AGE
elasticweb-sample-566d5dd6d9-6dzdj 1/1 Running 0 7s
elasticweb-sample-566d5dd6d9-6hqjd 1/1 Running 0 7s
可见此时的Pod的数量是2。符合我们的预期。
接着使用kubectl describe命令来查看elasticweb资源对象的详情。
$ kubectl describe elasticweb elasticweb-sample -n dev
Name: elasticweb-sample
Namespace: dev
Labels: <none>
Annotations: <none>
API Version: elasticweb.example.com/v1
Kind: ElasticWeb
Metadata:
Creation Timestamp: 2022-08-17T07:18:25Z
Generation: 1
......
Spec:
Image: nginx:1.17.1
Port: 30003
Single Pods QPS: 800
Total QPS: 1300
Events: <none>
可以看到Total QPS字段被webhook设置为1300,RealQPS也计算正确。
再来看Controller的日志。
其中的webhook部分是否符合预期,如上图红框所示,发现TotalQPS字段为空,就将设置为默认值,并且在检测的时候SinglePodQPS的值也没有超过1000。
验证Validator
接下来就是验证webhook的参数校验功能了,其实创建时的逻辑已经在上面验证过了,我们现在的singlePodsQPS为800,小于1000,符合要求,从controller日志中可以看到"e. SinglePodQPS is valid"。
那么我们直接修改config/samples/elasticweb_v1_elasticweb.yaml中singlePodsQPS的值,将800改为1200。
$ kubectl apply -f config/samples/elasticweb_v1_elasticweb.yaml
The ElasticWeb "elasticweb-sample" is invalid: spec.singlePodQPS: Invalid value: 1200: d. must be less than 1000
发现直接在终端抛出了错误。
使用kubectl describe命令来查看elasticweb资源对象的详情,可以发现Single Pods QPS的值依然是800。
查看日志:
如何在本地测试
1、获取证书:
$ kubectl get secrets webhook-server-cert -n elasticweb-system -o jsonpath='{..tls\.crt}' |base64 -d > certs/tls.crt\n
$ kubectl get secrets webhook-server-cert -n elasticweb-system -o jsonpath='{..tls\.key}' |base64 -d > certs/tls.key
2、修改main.go,让webhook server使用指定证书:
if os.Getenv("ENVIRONMENT") == "DEV" {
path, err := os.Getwd()
if err != nil {
setupLog.Error(err, "unable to get work dir")
os.Exit(1)
}
options.CertDir = path + "/certs"
}
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), options)
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
}
3、运行
$ make run ENVIRONMENT=DEV
补充
controller有两种运行方式,一种是在kubernetes环境内,一种是在kubernetes环境外独立运行,在编码阶段我们通常会在开发环境上运行controller,但是如果使用了webhook,由于其特殊的鉴权方式,需要将kubernetes签发的证书放置在本地的tmp/k8s-webhook-server/serving-crets/目录。
面对这种问题,官方给出的建议是:如果在开发阶段暂时用不到webhook,那么
在本地运行controller时屏蔽webhook的功能。
具体操作是首先修改main.go文件,其实就是在webhook控制器这块添加一个环境变量的判断:
if os.Getenv("ENABLE_WEBHOOK") != "false" {
if err = (&elasticwebv1.ElasticWeb{}).SetupWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "ElasticWeb")
os.Exit(1)
}
}
在本地启动controller得时候,使用make run ENABLE_WEBHOOK=false即可。