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 了:
-
创建 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 类型的资源了。
-
使用 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 原理和实现
接下来我们介绍下这个自定义控制器的工作原理。
自定义控制器的核心是 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