kubernetes CustomResourceDefinition(CRD, 自定义资源) 实践

1. 为什么需要 CRD

我们可以把 kubernetes 看作一个巨大的操作系统,它的作用就是管理各种各样的资源:Pod、CronJob…,但是现有的资源可能无法满足用户的需求。在 kubernetes v1.7 之前,如果需要 kubernetes 管理其他资源的话,需要修改 kubernetes 的代码,非常复杂。在 kubernetes v1.7 之后,kubernetes 推出了 CRD,它允许用户动态添加自定义的资源。

2. CRD 实践

接下来,我们通过定义一个 Network 类型的自定义资源,来理解整个自定义资源的原理和使用方法。

2.1. 定义 Network CRD

在使用 Network 资源之前,我们需要让 kubernetes 知道我们的自定义资源到底是什么,比如我们要让它识别一类工具,就需要告诉它这个工具的外貌、作用等信息,这就是 CustomResourceDefinition。

如下是 Network 的 CRD:

# network.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: networks.samplecrd.k8s.io
  annotations:
    "api-approved.kubernetes.io""https://github.com/kubernetes/kubernetes/pull/78458"
spec:
  group: samplecrd.k8s.io
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                cidr:
                  type: string
                gateway:
                  type: string
  names:
    kind: Network
    plural: networks
  scope: Namespaced

这个 CRD 的结构和 Pod 等定义很像,不同的是 CRD 需要在 spec.versions[:] 中定义资源需要的字段,而这些字段会在创建 Network 实例时使用,换句话说,创建 CRD 对应的 CustomResource(CR) 时会用到这些字段。

在 names 字段中定义了 Network 的复数形式,这样 kubernetes 就可以识别 networks 这个类型。

CR 的定义如下,即定义了一个 Network 类型资源的实例。

# example-network.yaml
apiVersion: samplecrd.k8s.io/v1
kind: Network
metadata:
  name: example-network
spec:
  cidr: "192.168.1.0/16"
  gateway: "192.168.1.1"

可以看到这个 CR 的 yaml 中,spec 下有 cidr 和 gateway 两个字段,这两个字段在上面的 CRD 中也声明了,并且声明其类型都为 string。

有了这两个声明,我们就可以使用 CRD 了:

  1. 创建 Network 的 CRD
➜  crd git:(master) ✗ kubectl apply -f network.yaml
customresourcedefinition.apiextensions.k8s.io/networks.samplecrd.k8s.io created
// 查看 crd 
➜  crd git:(master) ✗ kubectl get crd
NAME                        CREATED AT
networks.samplecrd.k8s.io   2024-02-28T13:00:06Z

这时说明 kubernetes 能够识别出来 Network 类型的资源了。

  1. 使用 example-network.yaml 创建一个 Network 对象
➜  example git:(master) ✗ kubectl get networks.samplecrd.k8s.io
No resources found in default namespace.
➜  example git:(master) ✗ kubectl apply -f example-network.yaml
network.samplecrd.k8s.io/example-network created
➜  example git:(master) ✗ kubectl get networks.samplecrd.k8s.io
NAME              AGE
example-network   4s

至此,kubernetes 已经可以正确识别 Network 的实例配置,并且在 APIServer 中设置具体的实例数据了。

2.2. 自定义控制器

我们创建了一个 Network 对象之后,有什么用?它现在只是一坨存在 etcd 的数据。

要想让这坨数据发生作用,例如真正在集群中创建一个网络组件,就需要我们实现一个 controller 来监听 Network 资源的变更,并实时调整我们集群中的网络组件。

有了 controller 之后,CRD 才是完整的、有效的。

所以接下来搞定 controller。

2.2.1. 控制器项目结构

创建如下结构的项目(项目内容上传至:https://github.com/crazyStrome/crd_network):

➜  network git:(master) ✗ tree .
.
├── controller.go
├── crd
│   └── network.yaml
├── example
│   └── example-network.yaml
├── go.mod
├── go.sum
├── hack
│   ├── boilerplate.go.txt
│   ├── tools.go
│   ├── update-codegen.sh
│   └── verify-codegen.sh
├── main.go
└── pkg
    └── apis
        └── samplecrd
            ├── register.go
            └── v1
                ├── doc.go
                ├── register.go
                └── types.go

其中:

  • crd 中即为刚才定义的 CRD 内容
  • example 中即为刚才定义的 network 对象
  • hack 中是一些用于生成代码的脚本
  • pkg 中则是对 Network 这个 CRD 的定义

pkg/apis/samplecrd 就是 API 组的名字。我们最初注册到 kubernetes 的版本就是 v1,因此在文件夹中创建一个 v1 的文件夹。

其中 doc.go 文件内容如下。文件中的注释是用户 kubernetes 生成代码时用的。+k8s:deepcopy-gen=package 表示为 v1 包中的所有类型定义生成 DeepCopy 方法;+groupName 则定义了这个包对应的 API 组名。

// +k8s:deepcopy-gen=package

// +groupName=samplecrd.k8s.io
package v1

types.go 中则定义了 Network 和 Networks 的结构,这样 kubernetes 拿到 Network 的配置后可以根据这些类型解析出实际对象。

其中也存在一些注释,+genclient 表示为这个 API 资源生成 Client 代码;+genclient:noStatus 表示这个 Network 没有 Status 字段;+k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 表示在生成 DeepCopy 的时候,实现 kubernetes 提供的 runtime.Object 接口

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 Network struct {
 metav1.TypeMeta   `json:",inline"`
 metav1.ObjectMeta `json:"metadata,omitempty"`

 Spec networkspec `json:"spec"`
}

type networkspec struct {
 Cidr    string `json:"cidr"`
 Gateway string `json:"gateway"`
}

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

// NetworkList is a list of Network resources
type NetworkList struct {
 metav1.TypeMeta `json:",inline"`
 metav1.ListMeta `json:"metadata"`

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

register.go 中的代码就是把 Network 和 Networks 注册给 APIServer。现在可能还有报错,不过等一会儿代码生成之后就可以了。

package v1

import (
 "crazyStrome/k8s-controller-custom-resource/pkg/apis/samplecrd"

 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

 "k8s.io/apimachinery/pkg/runtime"
 "k8s.io/apimachinery/pkg/runtime/schema"
)

// GroupVersion is the identifier for the API which includes
// the name of the group and the version of the API
var SchemeGroupVersion = schema.GroupVersion{
 Group:   samplecrd.GroupName,
 Version: samplecrd.Version,
}

var (
 SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
 AddToScheme   = SchemeBuilder.AddToScheme
)

// Resource takes an unqualified resource and returns a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
 return SchemeGroupVersion.WithResource(resource).GroupResource()
}

// Kind takes an unqualified kind and returns back a Group qualified GroupKind
func Kind(kind string) schema.GroupKind {
 return SchemeGroupVersion.WithKind(kind).GroupKind()
}

// addKnownTypes adds our types to the API scheme by registering
// Network and NetworkList
func addKnownTypes(scheme *runtime.Scheme) error {
 scheme.AddKnownTypes(
  SchemeGroupVersion,
  &Network{},
  &NetworkList{},
 )

 // register the type in the scheme
 metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
 return nil
}

2.2.2. 代码生成

接下来我们需要用一个脚本 update-codegen.sh 来生成代码,这个脚本里用到了 code-generator 这个工具,这是 kubernetes 用来生成相关代码的。

为了方便我们使用,在 tool.go 中导入了这个包,这样后续我们可以通过 go mod vendor来使用该工具。

tool.go 的代码如下:

package tools

import _ "k8s.io/code-generator"

update-codegen.sh 的内容如下,文件中的 –input-pkg-root 和 –output-base 共同构成该项目的绝对路径,在使用时需要注意修改。

#!/usr/bin/env bash

# Copyright 2017 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

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)}

source "
${CODEGEN_PKG}/kube_codegen.sh"

# generate the code with:
# --output-base    because this script should also be able to run inside the vendor dir of
#                  k8s.io/kubernetes. The output-base is needed for the generators to output into the vendor dir
#                  instead of the $GOPATH directly. For normal projects this can be dropped.

kube::codegen::gen_helpers 
    --input-pkg-root network/pkg/apis 
    --output-base "
$(dirname "${BASH_SOURCE[0]}")/../..
    --boilerplate "
${SCRIPT_ROOT}/hack/boilerplate.go.txt"

kube::codegen::gen_client 
    --with-watch 
    --input-pkg-root network/pkg/apis 
    --output-pkg-root network/pkg/generated 
    --output-base "
$(dirname "${BASH_SOURCE[0]}")/../..
    --boilerplate "
${SCRIPT_ROOT}/hack/boilerplate.go.txt"

生成代码时,首先 go mod vendor 获取 code-generator 工具到 vendor 目录中,然后给 vendor 添加执行权限,之后使用 hack 中的脚本进行代码生成。

➜  network git:(master) ✗ go mod vendor
➜  network git:(master) ✗ chmod 777 -R vendor
➜  network git:(master) ✗ bash hack/update-codegen.sh

生成后的文件目录如下(不包括 vendor):

➜  network git:(master) ✗ tree .
.
├── controller.go
├── crd
│   └── network.yaml
├── example
│   └── example-network.yaml
├── go.mod
├── go.sum
├── hack
│   ├── boilerplate.go.txt
│   ├── tools.go
│   ├── update-codegen.sh
│   └── verify-codegen.sh
├── main.go
├── network
└── pkg
    ├── apis
    │   └── samplecrd
    │       ├── register.go
    │       └── v1
    │           ├── doc.go
    │           ├── register.go
    │           ├── types.go
    │           └── zz_generated.deepcopy.go
    └── generated
        ├── clientset
        │   └── versioned
        │       ├── clientset.go
        │       ├── fake
        │       │   ├── clientset_generated.go
        │       │   ├── doc.go
        │       │   └── register.go
        │       ├── scheme
        │       │   ├── doc.go
        │       │   └── register.go
        │       └── typed
        │           └── samplecrd
        │               └── v1
        │                   ├── doc.go
        │                   ├── fake
        │                   │   ├── doc.go
        │                   │   ├── fake_network.go
        │                   │   └── fake_samplecrd_client.go
        │                   ├── generated_expansion.go
        │                   ├── network.go
        │                   └── samplecrd_client.go
        ├── informers
        │   └── externalversions
        │       ├── factory.go
        │       ├── generic.go
        │       ├── internalinterfaces
        │       │   └── factory_interfaces.go
        │       └── samplecrd
        │           ├── interface.go
        │           └── v1
        │               ├── interface.go
        │               └── network.go
        └── listers
            └── samplecrd
                └── v1
                    ├── expansion_generated.go
                    └── network.go

2.2.3. 实现 main 函数

在 main 函数中,主要做的就是初始化一个自定义控制器,然后启动。

func main() {
  ...
  
  cfg, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig)
  ...
  kubeClient, err := kubernetes.NewForConfig(cfg)
  ...
  networkClient, err := clientset.NewForConfig(cfg)
  ...
  
  networkInformerFactory := informers.NewSharedInformerFactory(networkClient, ...)
  
  controller := NewController(kubeClient, networkClient,
  networkInformerFactory.Samplecrd().V1().Networks())
  
  go networkInformerFactory.Start(stopCh)
 
  if err = controller.Run(2, stopCh); err != nil {
    glog.Fatalf("Error running controller: %s", err.Error())
  }
}

在 main 函数中,首先会根据 masterURL 和 kubeconfig 配置创建一个 kubernetes 的 client 和 networkClient。这两个 client 都是用于和 APIServer 通信用的。

然后使用 networkClient 创建一个 InformerFactory,并使用它生成一个 Network 对象并传递给控制器。

接下来启动这个 Informer,然后再启动自定义控制器。

2.2.4. Controller 原理和实现

接下来我们介绍下这个自定义控制器的工作原理。

kubernetes CustomResourceDefinition(CRD, 自定义资源) 实践

自定义控制器的核心是 Informer,它通过 networkClient 与 APIServer 通信。Informer 中的 Reflector 包会通过 networkClient,使用 ListAndWatch 方法来获取监听 Network 对象实例的变化。

当 APIServer 端有新的 Network 实例被创建、删除或更新,Reflector 都会收到事件。此时它会将事件及相关数据,放进一个 FIFO 队列中。

Informer 会不断从 FIFO 队列读取数据,每拿到一个数据,Informer 会判断事件类型,然后创建或更新本地对象的缓存。而且,Informer 会根据事件类型,触发实现注册好的 ResourceEventHandler,这些 Handler 会分类处理事件。

接下来看看控制器如何实现。

func NewController(
  kubeclientset kubernetes.Interface,
  networkclientset clientset.Interface,
  networkInformer informers.NetworkInformer) *Controller {
  ...
  controller := &Controller{
    kubeclientset:    kubeclientset,
    networkclientset: networkclientset,
    networksLister:   networkInformer.Lister(),
    networksSynced:   networkInformer.Informer().HasSynced,
    workqueue:        workqueue.NewNamedRateLimitingQueue(...,  "Networks"),
    ...
  }
    networkInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
    AddFunc: controller.enqueueNetwork,
    UpdateFunc: func(old, new interface{}) {
      oldNetwork := old.(*samplecrdv1.Network)
      newNetwork := new.(*samplecrdv1.Network)
      if oldNetwork.ResourceVersion == newNetwork.ResourceVersion {
        return
      }
      controller.enqueueNetwork(new)
    },
    DeleteFunc: controller.enqueueNetworkForDelete,
 return controller
}

可以看到在 NewController 中注册了三个 Handler 到 Informer 中,分别处理新增、更新、删除 Network 对象这三种事件。

初始完 Controller 之后,需要 Run 起来。

func (c *Controller) Run(threadiness int, stopCh <-chan struct{}) error {
 ...
  if ok := cache.WaitForCacheSync(stopCh, c.networksSynced); !ok {
    return fmt.Errorf("failed to wait for caches to sync")
  }
  
  ...
  for i := 0; i < threadiness; i++ {
    go wait.Until(c.runWorker, time.Second, stopCh)
  }
  
  ...
  return nil
}

在 Run 方法中,首先等待 Informer 完成一次本地缓存的数据同步操作;然后并发启动多个 Loop 来执行任务。

func (c *Controller) runWorker() {
  for c.processNextWorkItem() {
  }
}
 
func (c *Controller) processNextWorkItem() bool {
  obj, shutdown := c.workqueue.Get()
  
  ...
  
  err := func(obj interface{}) error {
    ...
    if err := c.syncHandler(key); err != nil {
     return fmt.Errorf("error syncing '%s': %s", key, err.Error())
    }
    
    c.workqueue.Forget(obj)
    ...
    return nil
  }(obj)
  
  ...
  
  return true
}
 
func (c *Controller) syncHandler(key string) error {
 
  namespace, name, err := cache.SplitMetaNamespaceKey(key)
  ...
  
  network, err := c.networksLister.Networks(namespace).Get(name)
  if err != nil {
    if errors.IsNotFound(err) {
      glog.Warningf("Network does not exist in local cache: %s/%s, will delete it from Neutron ...",
      namespace, name)
      
      glog.Warningf("Network: %s/%s does not exist in local cache, will delete it from Neutron ...",
    namespace, name)
    
     // FIX ME: call Neutron API to delete this network by name.
     //
     // neutron.Delete(namespace, name)
     
     return nil
  }
    ...
    
    return err
  }
  
  glog.Infof("[Neutron] Try to process network: %#v ...", network)
  
  // FIX ME: Do diff().
  //
  // actualNetwork, exists := neutron.Get(namespace, name)
  //
  // if !exists {
  //   neutron.Create(namespace, name)
  // } else if !reflect.DeepEqual(actualNetwork, network) {
  //   neutron.Update(namespace, name)
  // }
  
  return nil
}

在每次循环中(processNextWorkItem),都会从 workqueue 获取一个成员,即一个 key(Network 对象的 namespace/name)。

然后在 syncHandler 中,从 Informer 的缓存中拿到对应的 Network 对象。拿到 Network 对象之后,这个就是我们期望的结果。因此我们需要从我们的集群中获取该对应对应 Network 设备的当前状态,经过与期望状态 diff 之后,将其调整为期望状态。

当我们处理完 syncHandler 之后,会将该 key 移除队列,这样下次就会处理新的事件。

至此,Controller 的整个工作流程就描述完了。

2.2.5. 运行 Controller

接下来,我们编译该项目并运行:

➜  crd git:(master) ✗ go build -v
➜  network git:(master) ✗ ./network -kubeconfig=$HOME/.kube/config -alsologtostderr=t

此时,可以看到 Controller 正常运转的日志数据。可以重新新建 Network 的 CRD,并创建一个对象,来观察 Controller 是否获取到了对应的事件。


原文始发于微信公众号(crazstom):kubernetes CustomResourceDefinition(CRD, 自定义资源) 实践

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/235804.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!