K8S CRD开发简介

1、Kubernetes CRD开发

1.1 kubernetes 自定义资源(CRD)

K8S CRD是为用户提供出来用做扩展K8S能力的一种方式,这种方式使部署在 k8s 集群上的服务的管理方式更趋向一致。很多开源项目,比如Istio、Rancher都是用这种方式对K8S进行的扩展,Istio 很多参数都通过 kubernetes CRD 来管理,例如 VirtualService 和 DestinationRule,
Kubernetes 的资源管理方式和声明式 API 的良好设计使得在这个平台上的功能扩展变得异常容易。例如 CoreOS 推出的 Operator 框架就是一个很好的例子。这篇文章通过一个简短的示例来演示如何创建自定义资源(CRD)。

1.1.1 创建 CRD(CustomResourceDefinition)

这里以创建一个简单的弹性伸缩配置的 CRD 为例。将下面的内容保存在 scaling_crd.yaml 文件中。

metadata:
  # name must match the spec fields below, and be in the form: <plural>.<group>
  name: scalings.control.example.com
spec:
  # group name to use for REST API: /apis/<group>/<version>
  group: control.example.com
  # list of versions supported by this CustomResourceDefinition
  versions:
    - name: v1
      schema:
        openAPIV3Schema:
          properties:
          type:object
      # Each version can be enabled/disabled by Served flag.
      served: true
      # One and only one version must be marked as the storage version.
      storage: true
  # either Namespaced or Cluster
  scope: Namespaced
  names:
    # plural name to be used in the URL: /apis/<group>/<version>/<plural>
    plural: scalings
    # singular name to be used as an alias on the CLI and for display
    singular: scaling
    # kind is normally the CamelCased singular type. Your resource manifests use this.
    kind: Scaling
    # shortNames allow shorter string to match your resource on the CLI
    shortNames:
    - sc

通过 kubectl 创建这个 CRD:

kubectl apply -f scaling_crd.yaml

1.1.2 创建自定义资源的对象

我们编写一个 test.yaml 文件来创建一个自定义的 Scaling 对象。

apiVersion: “control.example.com/v1"
kind: Scaling
metadata:
  name: test
spec:
  targetDeployment: test
  minReplicas: 1
  maxReplicas: 5
  metricType: CPU
  step: 1
  scaleUp: 80
  scaleDown: 40

通过 kubectl 创建:

kubectl apply -f test.yaml —validate=false

你可以通过 kubectl 查看已经创建的名为 test 的 Scaling 对象

kubectl get scalings.control.example.com test -o yaml

会输出类似如下的结果:

apiVersion: control.example.com/v1
kind: Scaling
metadata:
  annotations:
    kubectl.kubernetes.com/last-applied-configuration: |
      {"apiVersion":"control.example.io/v1","kind":"Scaling","metadata":{"annotations":{},"name":"test","namespace":"default"},"spec":{"maxReplicas":5,"metricType":"CPU","minReplicas":1,"scaleDown":40,"scaleUp":80,"step":1,"targetDeployment":"test"}}
  creationTimestamp: "2019-01-09T12:22:36Z"
  generation: 1
  name: test
  namespace: default
  resourceVersion: "1316610"
  selfLink: /apis/control.example.io/v1/namespaces/default/scalings/test
  uid: 28717b37-5ac2-11e9-89f8-080027a9fd96
spec:
  maxReplicas: 5
  metricType: CPU
  minReplicas: 1
  scaleDown: 40
  scaleUp: 80
  step: 1
  targetDeployment: test

我们可以像操作 k8s 内置的 Deployment 资源一样操作我们创建的 Scaling 资源,同样可以对它进行更新和删除的操作。

1.1.3 参数校验

上面的 CRD 配置中我们并没有指定这个资源的 Spec,也就是说用户可以使用任意的 Spec 创建这个 Scaling 资源,这并不符合我们的要求。我们希望在用户创建 Scaling 对象时,可以像 k8s 的原生资源一样进行参数校验,如果出错的情况下,就不会去创建或更新这个对象,而是给用户错误提示。

K8s 目前提供了两种方式来实现参数校验,OpenAPI v3 schema 和 validatingadmissionwebhook。
这里主要使用比较简单的 OpenAPI v3 schema 来实现。validatingadmissionwebhook 需要用户自己提供一个检查服务,通过创建 ValidatingWebhookConfiguration 让 APIServer 将指定的操作请求转发给这个检查服务,检查服务返回 true 或者 false,决定参数校验是否成功。
我们将之前的 CRD 配置文件 scaling_crd.yaml 做一下修改,增加参数校验的部分:

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  # name must match the spec fields below, and be in the form: <plural>.<group>
  name: scalings.control.example.com
spec:
  # group name to use for REST API: /apis/<group>/<version>
  group: control.example.com
  # list of versions supported by this CustomResourceDefinition
  versions:
    - name: v1
      # Each version can be enabled/disabled by Served flag.
      served: true
      # One and only one version must be marked as the storage version.
      storage: true
      # 使用V3定义创建容器的属性 targetDeployment minReplicas maxReplicas metricType step scaleup scaledown
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              required:
              - targetDeployment
              - minReplicas
              - maxReplicas
              - metricType
              - step
              - scaleUp
              - scaleDown
              properties:
                targetDeployment:
                  type: string
                minReplicas:
                  type: integer
                  minimum: 2
                maxReplicas:
                  type: integer
                  maximum: 5
                metricType:
                  type: string
                  enum:
                    - CPU
                    - MEMORY
                    - REQUESTS
                step:
                  type: integer
                  minimum: 1
                  maximum: 5
                scaleUp:
                  type: integer
                scaleDown:
                  type: integer
  # either Namespaced or Cluster
  scope: Namespaced
  names:
    # plural name to be used in the URL: /apis/<group>/<version>/<plural>
    plural: scalings
    # singular name to be used as an alias on the CLI and for display
    singular: scaling
    # kind is normally the CamelCased singular type. Your resource manifests use this.
    kind: Scaling
    # shortNames allow shorter string to match your resource on the CLI
    shortNames:
    - sc

可以看到 spec 中增加了 schema 字段,其中定义了对各个参数的检验要求。

  • required 表示数组中的参数必须要设置。
  • type string 和 type integer 表示限制参数类型。
  • minimum: 0 表示数字最小值为 0。
  • enum 表示参数只能在指定的值中。

具体支持哪些校验方法可以通过 https://github.com/OAI/OpenAPI-Specification 查看。
更新 CRD 资源:

kubectl apply -f scaling_crd.yaml

再次修改 test.yaml 测试我们的参数校验是否生效,将 targetDeployment: test 这一行删除。
更新 Name 为 test 的 Scaling 对象。

kubectl apply -f test.yaml

可以看到错误提示输出如下:

validation failure list:
spec.targetDeployment in body is required

当然也可以尝试不符合要求的其他参数,同样也会提示相应的错误信息。现在,不符合我们要求的Scaling对象将不被允许创建。

1.2 kubernetes 自定义控制器

Kubernetes 的 controller-manager 通过 APIServer 实时监控内部资源的变化情况,通过各种操作将系统维持在一个我们预期的状态上。比如当我们将 Deployment 的副本数增加时,controller-manager 会监听到此变化,主动创建新的 Pod。

对于通过 CRD 创建的资源,也可以创建一个自定义的 controller 来管理。

1.2.1 目的

在上文中我们创建了自己的 Scaling 资源,如果我们想要通过监听该资源的变化来实现实时的弹性伸缩,就需要自己写一个控制器,通过 APIserver watch 该资源的变化。当我们创建了一个 Scaling 对象,自定义控制器都能获得其参数,之后执行相关的检查,根据结果决定是否需要扩容或缩容相关的实例。

1.2.2 实现

client-go 这个 repo 封装了对 k8s 内置资源的一些常用操作,包括了 clients/listers/informer 等对象和函数,可以 通过 Watch 或者 Get List 获取对应的 Object,并且通过 Cache,可以有效避免对 APIServer 频繁请求的压力。
但是对于我们自己创建的 CRD,没有办法直接使用这些代码。
通过 code-generator 这个 repo,我们可以提供自己的 CRD 相关的结构体,轻松的生成 client-go 中类似的代码,方便我们编写自己的控制器。

1.2.3 在自己的项目中使用 code-generator

这里主要参考了 sample-controller 这个项目,可以在github上 kubernetes/sample-controller 找到这个项目

1.2.3.1 创建自定义 CRD 结构体

假设我们有一个 test repo,在根目录创建一个 pkg 目录,用于存放我们自定义资源的 Spec 结构体。
这里我们要知道自己创建的自定义资源的相关内容:

  • API Group: 我们使用的是 control.example.com。
  • Version: 我们用的是 v1,但是可以同时存在多个版本。
  • 资源名称: 这里是 Scaling。

接着创建如下的目录结构:

mkdir -p pkg/apis/control/v1

在 pkg/apis/control 目录下创建一个 register.go 文件。内容如下:

package control

const (
    GroupName = "control.example.com"
)

创建 pkg/apis/control/v1/types.go 文件,内容如下:

package v1

import (
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// +genclient
// +genclient:noStatus
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

type Scaling struct {
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`

    Spec ScalingSpec `json:"spec"`
}

type ScalingSpec struct {
    TargetDeployment string `json:"targetDeployment"`
    MinReplicas      int    `json:"minReplicas"`
    MaxReplicas      int    `json:"maxReplicas"`
    MetricType       string `json:"metricType"`
    Step             int    `json:"step"`
    ScaleUp          int    `json:"scaleUp"`
    ScaleDown        int    `json:"scaleDown"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

type ScalingList struct {
    metav1.TypeMeta `json:",inline"`
    metav1.ListMeta `json:"metadata,omitempty"`

    Items []Scaling `json:"items"`
}

这个文件中我们定义了 Scaling 这个自定义资源的结构体。
其中,类似 // +<tag_name>[=value] 这样格式的注释,可以控制代码生成器的一些行为。

  • +genclient: 为这个 package 创建 client。
  • +genclient:noStatus: 当创建 client 时,不存储 status。
  • +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object: 为结构体生成 deepcopy 的代码,实现了 runtime.Object 的 Interface。

创建 doc 文件,pkg/apis/control/v1/doc.go:

// +k8s:deepcopy-gen=package
// +groupName=control.example.com

package v1

最后 client 对于自定义资源结构还需要一些接口,例如 AddToScheme 和 Resource,这些函数负责将结构体注册到 schemes 中去。
为此创建 pkg/apis/control/v1/register.go 文件:

package v1

import (
    "test/pkg/apis/control"

    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/runtime/schema"
)

var SchemeGroupVersion = schema.GroupVersion{
    Group:   control.GroupName,
    Version: "v1",
}

func Resource(resource string) schema.GroupResource {
    return SchemeGroupVersion.WithResource(resource).GroupResource()
}

var (
    // localSchemeBuilder and AddToScheme will stay in k8s.io/kubernetes.
    SchemeBuilder      runtime.SchemeBuilder
    localSchemeBuilder = &SchemeBuilder
    AddToScheme        = localSchemeBuilder.AddToScheme
)

func init() {
    // We only register manually written functions here. The registration of the
    // generated functions takes place in the generated files. The separation
    // makes the code compile even when the generated files are missing.
    localSchemeBuilder.Register(addKnownTypes)
}

// Adds the list of known types to api.Scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
    scheme.AddKnownTypes(SchemeGroupVersion,
        &Scaling{},
        &ScalingList{},
    )
    metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
    return nil
}

至此,初期的准备工作已近完成,可以通过代码生成器来自动帮助我们生成相关的 client, informer, lister 的代码。

1.2.3.2 生成代码

通常我们通过创建一个 hack/update-codegen.sh 脚本来固化生成代码的步骤。

set -o errexit
set -o nounset
set -o pipefail

SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/..
CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo ../code-generator)}

bash "${CODEGEN_PKG}"/generate-groups.sh "deepcopy,client,informer,lister"  \
  k8s.io/sample-controller/pkg/generated k8s.io/sample-controller/pkg/apis \
  control:v1 \
  --output-base "$(dirname "${BASH_SOURCE[0]}")/../../.." \
  --go-header-file "${SCRIPT_ROOT}"/hack/boilerplate.go.txt

可以看到,执行这个脚本,需要使用 code-generator 中的的脚本,所以需要先通过 go get 将 code-generator 这个 repo 的内容下载到本地,并且编译出相关的二进制文件(client-gen, informer-gen, lister-gen)。
在$GOPATH 下面的src下建立k8s.io 文件夹,将code-generator 、 sample-controller两个repo clone 到这里,然后编辑上面的文件后,在sample-controller目录下执行编译
sudo /bin/bash ./hack/update-codegen.sh
执行完成后,可以看到 pkg 目录下多了一个 generated 目录,其中就包含了 informer 和 lister 相关的代码。
并且在 pkg/apis/control/v1 目录下,会多一个 zz_generated.deepcopy.go 文件,用于 deepcopy 相关的处理。

1.2.3.3 创建自定义控制器代码

这里只创建一个 main.go 文件用于简单示例,通过我们刚刚自动生成的代码,每隔一段时间,自动通过 lister 获取所有的 Scaling 对象。

package main

import (
   "fmt"
   "log"
   "os"
   "time"

   "k8s.io/apimachinery/pkg/labels"
   "k8s.io/client-go/tools/clientcmd"
   clientset "k8s.io/sample-controller/pkg/generated/clientset/versioned"
   informers "k8s.io/sample-controller/pkg/generated/informers/externalversions"
)

func main() {
   client, err := newCustomKubeClient()
   if err != nil {
      log.Fatalf("new kube client error: %v", err)
   }

   factory := informers.NewSharedInformerFactory(client, 30*time.Second)
   informer := factory.Control().V1().Scalings()
   lister := informer.Lister()

   stopCh := make(chan struct{})
   factory.Start(stopCh)

   for {
      ret, err := lister.List(labels.Everything())
      if err != nil {
         log.Printf("list error: %v", err)
      } else {
         for _, scaling := range ret {
            log.Println(scaling)
         }
      }

      time.Sleep(5 * time.Second)
   }
}

func newCustomKubeClient() (clientset.Interface, error) {
   kubeConfigPath := os.Getenv("HOME") + "/.kube/config"

   config, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath)
   if err != nil {
      return nil, fmt.Errorf("failed to create out-cluster kube cli configuration: %v", err)
   }

   cli, err := clientset.NewForConfig(config)
   if err != nil {
      return nil, fmt.Errorf("failed to create custom kube client: %v", err)
   }
   return cli, nil
}

编译并执行此代码,每隔 5 秒钟,会在标准输出中输出我们创建的所有 Scaling 对象的具体内容。
需要注意的是,这里生成的 kube client 只能用于操作我们自己的 Scaling 对象。如果需要操作 Deployment 这一类的内置的资源,仍然需要使用 client-go 中的代码,因为不同的 clientset.Interface 实现的接口也是不同的。

上述的方法也是最顶层的实现方式,下面介绍两种可以快速搭建CRD开发的工具:一种是kubebuilder,另一种是operader-sdk,该工具目前正在与kubebuilder融合,其中kubebuilder是一个官方提供的快速实现Operator的工具包,可以快速生成k8s的CRD、Controller、Webhook,我们只需要实现业务逻辑。
kubebuilder封装了controller-runtime和controller-tools工具,通过controller-gen来生成代码,提供脚手架工具初始化 CRDs 工程,自动生成 boilerplate 代码和配置;提供代码库封装底层的 K8s client-go;简化了用户创建Operator的步骤:

  1. 创建工作目录,初始化项目
  2. 创建API,填充字段
  3. 定义 CRD
  4. 编写 Controller 逻辑
  5. 验证测试
  6. 发布到集群中

1.3 kubebuilder 开发自定义资源(CRD)

1.3.1 创建脚手架工程

kubebuilder init --domain edas.io1

这一步创建了一个 Go module 工程,引入了必要的依赖,创建了一些模板文件。

1.3.2 创建 API

kubebuilder create api --group apps --version v1alpha1 --kind Application1

这一步创建了对应的 CRD 和 Controller 模板文件,经过 1、2 两步,现有的工程结构如图 所示:
在这里插入图片描述

1.3.3 定义 CRD

在上图中对应的文件edasapplication_types.go定义 Spec 和 Status。

1.3.4 编写 Controller 逻辑

在上图中对应的文件edasapplication_controller.go实现 Reconcile 逻辑。

1.3.5 测试发布

本地测试完之后使用 Kubebuilder 的 Makefile 构建镜像,部署我们的 CRDs 和 Controller 即可。

1.4 operator-sdk 开发自定义资源(CRD)

该 SDK 提供了一个工作流程,用于使用 Go、 Ansible 或 Helm来开发operators。
下面的工作流用于创建新的 Go operator:

  1. 创建新的 operator project,使用 SDK Command Line Interface(CLI)。
  2. 定义新的resource APIs,通过添加Custom Resource Definitions(CRD)。
  3. 定义 Controllers 观察和协调资源。
  4. 编写协调逻辑,使用 SDK 和 controller-runtime APIs。
  5. 使用 SDK CLI 构建和生成 operator deployment manifests。
    下面的工作流用于创建新的Ansible operator:
  6. 创建新的 operator project,使用SDK Command Line Interface(CLI)。
  7. 编写协调逻辑,为自己的对象,使用ansible playbooks 和 roles。
  8. 使用 SDK CLI 构建和生成 operator deployment manifests。
  9. 可选添加额外的 CRD’s,使用 SDK CLI,重复步骤2、3。
    下面的工作流用于创建新的Helm operator:
  10. 创建新的 operator project,使用 SDK Command Line Interface(CLI)。
  11. 创建新的 (或添加已有的) Helm chart,用于 operator’s 协调逻辑使用。
  12. 使用SDK CLI 构建和生成operator deployment manifests。
  13. 可选添加额外的CRD’s,使用SDK CLI,重复步骤 2 和 3。
    下面就以Go来创建一个operators开发示例

1.4.1 创建脚手架工程

$ operator-sdk new app-operator
$ cd app-operator

1.4.2 创建API

$ operator-sdk add api --api-version=app.example.com/v1alpha1 --kind=AppService

1.4.3 创建控制器

$ operator-sdk add controller --api-version=app.example.com/v1alpha1 --kind=AppService

1.4.4 编译并PUSH镜像

$ operator-sdk build quay.io/example/app-operator
$ docker push quay.io/example/app-operator

1.4.5 测试发布

$ kubectl create -f deploy/

1.5 总结

通过上述介绍来看Kubernetes 中CRD 的开发方式有多种,其中第一种不借用工具的开发方式其实使用的方法是调用client-go 和 code-generate两个工具库中的方法实现CRD资源的管理,涉及的知识点也相对底层,如果阅读了k8s kube-apiserver源码的人更加容易理解这种开发方式;通过kubebuilder和operator-sdk这两种工具的开发方式其实都是将client-go、controller-runtime和controller-tools代码进行了再封装,封装后的库为controller-gen,其目的是简化用户在不理解kube-apiserver等实现的基础上开发CRD的流程。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值