查看原文
其他

给 K8s 中的 Operator 添加 Webhook 功能【保姆级】

油腻中年李大鹅 云原生实验室 2022-11-27


本文转自油腻中年李大鹅,原文:https://juejin.cn/post/7132752189646667784,版权归原作者所有。欢迎投稿,投稿请添加微信好友:cloud-native-yang

简介

准入控制器是在对象持久化之前用于对 Kubernetes API Server 的请求进行拦截的代码段,在请求经过身份验证和授权之后放行通过。准入控制器可能正在 validating、mutating 或者都在执行,Mutating 控制器可以修改他们处理的资源对象,Validating 控制器不会,如果任何一个阶段中的任何控制器拒绝了请求,则会立即拒绝整个请求,并将错误返回给最终的用户。

时序图如下所示:

如果我们想为 CRD 实现 admission webhook,我们唯一要做的就是实现 Defaulter 和 (或)Validator 接口。

Kubebuilder 会我们处理剩余的工作:

  • 创建 webhook 服务器
  • 确保服务器已添加到管理器中
  • 为 webhook 创建处理程序
  • 使用服务器中的路径注册每个处理程序

设计实战场景

这里我们直接在手摸手教你使用 kubebuilder 开发 operator[1]中实战的 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[2]

来管理证书。

直接使用下面命令安装 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.gowebhook_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)
 }
}

接下来我们实现验证功能,由于这里我们需要验证 CreateUpdate, 它们使用同一套逻辑,为了代码简洁以及复用性,我们封装一个 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 即可。

引用链接

[1]

手摸手教你使用 kubebuilder 开发 operator: https://juejin.cn/post/7099354856078442509

[2]

cert-manager: https://link.juejin.cn?target=https%3A%2F%2Fcert-manager.io%2F




你可能还喜欢

点击下方图片即可阅读

镜像仓库 Harbor 对接 MinIO 对象存储

2022-09-06

Kube-Proxy IPVS 模式详解以及故障排查指南

2022-09-05

使用 WireGuard 组建非对称路由以降低延迟

2022-09-04

彻底理解 WireGuard 的路由策略

2022-08-31


云原生是一种信仰 🤘

关注公众号

后台回复◉k8s◉获取史上最方便快捷的 Kubernetes 高可用部署工具,只需一条命令,连 ssh 都不需要!



点击 "阅读原文" 获取更好的阅读体验!


发现朋友圈变“安静”了吗?

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存