Operator实战2:实现webhook修改Job的最大重试次数

原创 吴就业 153 0 2023-06-03

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/1d93c9c4d4884537aec6dec50017e1f8

作者:吴就业
链接:https://wujiuye.com/article/1d93c9c4d4884537aec6dec50017e1f8
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

需求背景

terraform-controller是一个实现kubevela整合terraform能力的插件,本质上也是一个Operator,核心能力就是提供自定义的Configuration资源定义+Configuration资源控制器。通过Configuration资源的Spec声明hcl代码,Configuration资源控制器监听Configuration资源的创建/更新/销毁,执行terraform applyterraform destroy命令,实现申请、更新IaC资源(基础设施),和删除IaC资源。

terraform-controller的实现原理就是通过创建一个Job来启动一个Pod,在Pod中启动一个带terraform的镜像容器,来执行terraform applyterraform destroy命令。在terraform-controller一章中我们会详细介绍terraform-controller的工作原理。

由于terraform-controller不支持配置Job的最大重试次数,如果terraform apply执行失败,例如云平台账号没有足够的费用去申请IaC资源,Job的控制器会不断的拉起Pod重试。正常情况,这种重试并不会有什么副作用,在案例中,我们只需要给账号充值,重试还能使得自动恢复IaC的申请,完成应用的部署。

然而,在自研混合云平台的背景下,由于前期研发人员投入有限,很多IaC的申请,背后都是“人工智能”,就是通过提交工单的方式申请,而这种失败的重试,可能会导致提交非常多的工单,不仅是占用数据库资源,还会导致重要工单被这些无限重复的工单覆盖。

这个问题笔记已经向社区反馈(https://github.com/kubevela/terraform-controller/issues/361),并提了PR,只是官方不会立即发布一个新的版本,而我们又不想基于terraform-controller二次开发,主要是后期如果社区更新支持新特性,就很难合并到自己的分支,因此我们想通过webhook去修改Job的最大重试次数。

webhook简介

Kubernetes apiserver提供两种特殊的准入控制器,分别是MutatingAdmissionWebhook和ValidatingAdmissionWebhook。这两种准入控制器,将发送准入请求到外部的HTTP回调服务,并接收准入响应。

准入控制器就是在对象持久化之前用于对 Kubernetes API Server 的请求进行拦截处理,在请求经过身份验证和授权之后放行通过。Mutating准入控制器可以修改被处理的资源对象,Validating准入控制器则不能修改。只要Mutating准入控制器和Validating准入控制器中的任何一个拒绝了请求,都会立即拒绝整个请求,并将错误返回给用户。

来自网络

图片来源于官方文档?

webhook就是准入控制器的http回调服务。k8s提供两种类型的用于创建Webhook的资源定义:MutatingWebhookConfiguration和ValidatingWebhookConfiguration。

使用MutatingWebhookConfiguration资源注册的Webhook可接受或拒绝资源对象请求,并且可以变更资源对象。而使用ValidatingWebhookConfiguration资源注册的Webhook只可在不更改对象的情况下接受或拒绝对象请求,通常用于参数校验。

由于我们的需求是修改Job的最大重试次数,也就是需要修改Job资源对象,因此我们需要使用MutatingWebhookConfiguration资源来注册我们的webhook,并且需要开发webhook服务,提供一个http接口。

开发Webhook

我们先基于kubebuilder脚手架创建一个空的Operator项目,基于这个空项目开发webhook。

1.声明MutatingWebhookConfiguration资源,注册一个webhook。

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  creationTimestamp: null
  name: terraform-job-webhook-configuration
webhooks:
  - admissionReviewVersions:
      - v1
    clientConfig:
      caBundle: |-
        ##这里填自签证书的base64字符串
      service:
        name: terraform-job-webhook-service
        namespace: vela-system
        path: /modify-terraform-job
    failurePolicy: Ignore
    name: modify.terraform.job.kb.io
    rules:
      - apiGroups:
          - "batch"
        apiVersions:
          - "v1"
        operations:
          - CREATE
          - UPDATE
        resources:
          - jobs
    sideEffects: None

注:如果是拦截非自定义资源的请求,建议通过标签选择过滤,或者将failurePolicy设置为Ignore,否则webhook服务故障可能会导致整个k8s集群瘫痪。

2.我们需要编写Service资源,将接口暴露给Kubernetes API Server调用。

---
apiVersion: v1
kind: Service
metadata:
  name: terraform-job-webhook-service
  namespace: vela-system
spec:
  ports:
    - name: webhook-port
      port: 443
      targetPort: 8090 ## webhook服务的端口
      protocol: TCP
  selector:
    app: terraform-daemon

3.我们需要实现admission.Handler接口,这是controller-runtime封装好的API。

type terraformJobHandler struct {
        decoder *admission.Decoder
        client  client.Client
}

func NewTerraformJobHandler(client client.Client) admission.Handler {
        return &terraformJobHandler{
                client: client,
        }
}

func (s *terraformJobHandler) InjectDecoder(d *admission.Decoder) error {
        s.decoder = d
        return nil
}

func (s *terraformJobHandler) Handle(ctx context.Context, request admission.Request) admission.Response {
        logger := log.FromContext(ctx)

        job := &v1.Job{}
        err := s.decoder.Decode(request, job)
        if err != nil {
                logger.Error(err, "decoder job error")
                return admission.Errored(http.StatusBadRequest, err)
        }

        if !k8sutil.IsTerraformJob(job) {
                return admission.Allowed("")
        }

        if *job.Spec.BackoffLimit > 0 {
                bl := int32(0)
                job.Spec.BackoffLimit = &bl
                if patch, err := json.Marshal(job); err != nil {
                        logger.Error(err, fmt.Sprintf("marshal job %s/%s to json error"), job.Namespace, job.Name)
                        return admission.Errored(http.StatusInternalServerError, err)
                } else {
                        logger.Info(fmt.Sprintf("update job %s/%s backoff limit 0", job.Namespace, job.Name))
                        return admission.PatchResponseFromRaw(request.Object.Raw, patch)
                }
        }

        return admission.Allowed("")
}

4.我们需要自签证书,Kubernetes API Server在调用我们的webhook服务时会使用https调用。关于如何自签证书稍后会介绍。

5.在main方法中通过Manager给webhook服务注册接口处理器。

mgr.GetWebhookServer().Register("/modify-terraform-job",                
    &webhook.Admission{Handler: NewTerraformJobHandler(mgr.GetClient())})

6.在main方法中通过Manager给webhook服务配置证书的路径。

mgr.GetWebhookServer().CertDir = webhookCertPath        
mgr.GetWebhookServer().CertName = "cert.pem"        
mgr.GetWebhookServer().KeyName = "key.pem"

7.在main方法中通过Manager给webhook服务配置监听端口。

mgr.GetWebhookServer().Port = port

8.我们需要声明Deployment来部署我们的webhook服务。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: terraform-daemon
  namespace: vela-system
  labels:
    app: terraform-daemon
spec:
  selector:
    matchLabels:
      app: terraform-daemon
  replicas: 1
  template:
    metadata:
      annotations:
        kubectl.kubernetes.io/default-container: terraform-daemon
      labels:
        app: terraform-daemon
    spec:
      containers:
        - name: terraform-daemon
          image: terraform-daemon:1.0.0
          command:
            - ./terraform-daemon
          args:
            - --webhook-bind-port=8090 ## webhook服务的端口
            - --webhook-cert-path=/data/cert ## 自签ca证书的目录

自签证书

这里推荐使用cfssl工具,使用起来比较简单。由于证书的制作不是本篇的重点,这里只推荐一个生成方式,只要按照教程一步步来做就可以制作出来。

1.安装cfssl工具,mac下使用brew安装。

brew install cfssl

2.创建一个目录,用于作为自签证书的workspace。

mkdir /tmp/ca

3.在workspace下添加以下几个配置文件。

ca-config.json

{
  "signing": {
    "default": {
      "expiry": "87600h"
    },
    "profiles": {
      "server": {
        "usages": [
          "signing",
          "key encipherment",
          "server auth",
          "client auth"
        ],
        "expiry": "87600h"
      }
    }
  }
}

ca-csr.json

{
  "CN": "Kubernetes",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "Kubernetes",
      "OU": "CA",
      "ST": "Oregon"
    }
  ]
}

server-csr.json

{
  "CN": "admission",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "Kubernetes",
      "OU": "Kubernetes",
      "ST": "Oregon"
    }
  ]
}

4.编写脚本

create_cert.sh
cfssl gencert -initca ca-csr.json | cfssljson -bare ca

cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -hostname=192.168.11.11 \
  -profile=server \
  server-csr.json | cfssljson -bare server

5.可修改ca-config.json配置文件设置过期时间,即修改signing.default.expirysigning.profiles.server.expiry

6.需要修改create_cert.sh脚本的hostname参数,hostname值为“{service-name}.{service-namespace}.svc”,就是webhook的Service的名称和部署的namespace,如果是本地开发环境debug可以填自己的ip地址。

7.执行create_cert.sh脚本,我们将在workspace得到ca.pem、ca-key.pem、server.pem、server-key.pem,其中server.pem、server-key.pem需要我们在构建docker镜像的时候跟程序一起打包进去,并放在/data/cert目录下(本案例),并更名为cert.pem、key.pem。

8.然后执行cat ca.pem | base64命令,将命令输出的字符串copy,填入MutatingWebhookConfiguration资源的caBundle字段。

#云原生

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

terraform篇01:基础设施即代码,初识terraform,用代码减少沟通成本

通常申请基础设施,我们需要向运维描述我们需要什么基础设施、什么规格,运维根据我们的描述去检查是否已经申请过这样的资源,有就会直接给我们使用基础设施的信息,没有再帮我们申请,然后告诉我们使用基础设施的信息,例如mysql的jdbc和用户名、密码。如果将描述代码化,基础设施的申请自动化,就能实现“基础设施即代码”。而terraform就是实现“将描述代码化”的工具软件。

Operator实战4:如何获取已经被删除的pod的日记

在Job场景,如果Job达到backoffLimit还是失败,而且backoffLimit值很小,很快就重试完,没能及时的获取到容器的日记。而达到backoffLimit后,job的controller会把pod删掉,这种情况就没办法获取pod的日记了,导致无法得知job执行失败的真正原因,只能看到job给的错误:"Job has reached the specified backoff limit"。

Operator实战3:Operator开发过程遇到的问题

kubebuilder使用helm代替kustomize;代码改了但似乎没生效-镜像拉取问题; 使用ConfigMap替代Apollo配置中心的最少改动方案;环境变量的注入以及传递;Kubebuilder单测跑不起来;Helm chart和finalizer特性冲突问题。

Operator实战1:使用kubebuilder开发一个部署web服务的Operator

举一个非常简单的需求场景,仅用于介绍如何使用kubebuilder开发一个Operator,非真实需求场景。

中间件云原生利器:Operator,Operator是什么?

新的云原生中间件很难短时间内覆盖到企业项目中,企业走云原生这条道路,还需要考虑传统中间件如何上云的问题。最需要解决的是如何容器化部署,以及自动化运维。这就不得不借助Operator了。

中间件容器化部署实现方案的前期调研

中间件容器化部署是为了实现GitOps模式的持续交付,实现部署即代码。痛点在于大多数中间件都是有状态的,本篇介绍如何实现有状态中间件的容器化部署。