认识Kubebuilder
在这篇文章中,我将向您介绍如何使用kubebuilder轻松创建Kubernetes Operator。
在快速介绍 kubebuilder 后,我们将逐步创建一个 k8s Operator(以 kube-image-keeper Operator 为例,它是一个用于在 Kubernetes 集群中缓存镜像的开源工具)。最后,我们将讨论 kubebuilder 的优点和局限性。
一、什么是Kubebuilder?
Kubebuilder,K8s operator创建框架
- Kubebuilder是一个旨在简化创建Kubernetes operators的框架。
- 它使您能够直接从终端生成项目样板,包括诸如Makefile、Dockerfile和主要源文件(如main.go)等基本组件。
- 一旦项目创建完成,Kubebuilder还可以帮助生成配置自定义资源定义(CRD)、控制器和Webhook的必要文件。
controller-runtime和controller-tools库
- Kubebuilder基于controller-runtime和controller-tools库。虽然完全可以使用这些库来开发operator,但没有Kubebuilder会使这个过程变得繁琐,因为需要手动创建和编写必要的文件和代码,而Kubebuilder会自动生成这些文件和代码。
二、Kubebuilder,举例来说
让我们看一下使用 kubebuilder 创建 Kubernetes operator kube-image-keeper (kuik) 的具体示例。
开源项目kuik
- Kuik是一个专为Kubernetes中容器镜像进行集群内缓存的operator。例如,在容器镜像注册表暂时不可用或为了避免超过注册表拉取配额(并产生相关费用)时,它非常有用。
- 由于kuik是开源的,您有机会通过访问kuik的公共GitHub存储库来查看使用Kubebuilder生成的项目的整体架构。
- 回到项目架构。简而言之,它包括两个控制器、一个变更Webhook和一个CRD,如下图所示:
- 首先,Webhook重写Pod镜像的URL,将其重定向到缓存注册表。
- 然后,两个控制器监视Pod和CachedImages:第一个根据Pod使用的镜像创建CachedImages,而第二个则通过CachedImages缓存请求的镜像。
- 当kubelet要求容器运行时(例如containerd)启动容器时,运行时通过代理检索镜像,因为镜像已被重写为指向代理。
- 最后,代理根据镜像是否已被缓存决定将镜像拉取请求转发到缓存注册表或原始注册表。
三、使用 kubebuilder init 创建基础项目
现在,大家到你们的终端吧!我们将生成 kubebuilder 项目的基本脚手架(或样板)。为此,请在空文件夹中执行以下命令:
kubebuilder init \
--domain kuik.enix.io \
--repo github.com/enix/kube-image-keeper
其中“domain”对应我们要创建的资源的api组,“repo”对应项目所在的git仓库。
Kubebuilder创建了几个文件和文件夹,包括:
- config:包含项目的kustomize清单的文件夹。
- Dockerfile:一个distroless Dockerfile。
- hack:包含源文件的样板的文件夹(可用于在每个源文件顶部插入许可证)。
- main.go:一个预填充的main函数,允许您立即启动一个功能性的“no-op”操作员。
- Makefile:这个makefile允许您运行控制器,运行测试,甚至生成CRD的yaml清单。
- PROJECT:描述kubebuilder项目及其各个组件的文件。
四、使用kubebuilder create api生成控制器
现在已经生成了基本项目,我们可以开始创建我们的控制器了。
CachedImages控制器
我们将从CachedImageController开始,它的作用是监视CachedImage,并在删除CachedImage时将引用的图像缓存或从缓存中删除它们:
kubebuilder create api \
--kind CachedImage \
--version v1alpha1 \
--namespaced=false \
--resource=true \
--controller=true
- 使用–resource和–controller标志可以让kubebuilder为指定的资源类型生成CRD和相应的控制器。否则,选择将以交互方式进行。标志–namespaced=false表示我们希望CachedImages是非命名空间的。
Pods控制器
为了生成负责监视Pods并创建CachedImages的控制器的基本结构,我们可以使用类似的命令。然而,我们指示Kubebuilder不要生成自定义资源定义(CRD),因为Pods已经是原生的Kubernetes资源。
kubebuilder create api \
--group core \
--kind Pod \
--version v1 \
--resource=false \
--controller=true
运行这两个命令后,我们会得到几个新文件。值得注意的是,在controllers文件夹中,我们可以找到两个文件cachedimage_controller.go和pod_controller.go,以及suite_test.go文件,其中包含了快速设置集成测试所需的内容。CachedImage类型在api/v1alpha1/cachedimage_types.go文件中定义。我们注意到main.go文件已经更新,用于启动我们的两个新控制器。最后,PROJECT文件现在包含有关资源和控制器设置的信息。
为了说明,pod_controller.go文件如下所示:
package controllers
import (
"context"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
)
// PodReconciler reconciles a Pod object
type PodReconciler struct {
client.Client
Scheme *runtime.Scheme
}
//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=core,resources=pods/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=core,resources=pods/finalizers,verbs=update
func (r *PodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = log.FromContext(ctx)
// TODO(user): your logic here
return ctrl.Result{}, nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *PodReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&corev1.Pod{}).
Complete(r)
}
协调函数定义协调循环。这是实现控制器逻辑的地方。
生成与 CRD 相关的文件
一旦实现了控制器并定义了 CachedImage 类型,我们仍然需要生成 CRD 的 yaml 清单以将其安装在 Kubernetes 集群中。
注释
如果你打开cachedimage_types.go,你可以看到一个CachedImage的定义,它看起来像这样(完整的文件有点长,但这部分对你来说是最有趣的):
type CachedImageSpec struct {
Foo string `json:"foo,omitempty"`
}
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:resource:scope=Cluster
type CachedImage struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec CachedImageSpec `json:"spec,omitempty"`
Status CachedImageStatus `json:"status,omitempty"`
}
除了结构体之外,我们还可以看到各种注释。这些注解描述了如何生成相应的CRD。例如,+kubebuilder:resource:scope=Cluster 表示 CachedImage 没有命名空间。其他注释也可用,例如,您可以添加要在 kubectl get 期间显示的列,这要归功于注释 +kubebuilder:printcolumn:name=“Foo”,type=“string”,JSONPath=“.spec.foo”。在这种特定情况下,我们添加“Foo”列,显示 CachedImages 的 .spec.foo 值。
make generate和make manifest命令
为了生成与我们的CRD相关的文件,我们将使用两个命令。
第一个命令make generate在调用kubebuilder create api命令时会自动执行,它会创建api/v1alpha1/zz_generated.deepcopy.go文件,该文件实现了操作符的某些关键函数,例如CachedImage.DeepCopy函数。
第二个命令是我们最感兴趣的命令:make manifests。这个命令会生成config/crd/bases/kuik.enix.io_cachedimages.yaml文件,其中包含了CRD的定义,我们随后可以将其安装到我们的Kubernetes集群中。
使用kubebuilder create webhook生成webhook
我们已经创建了两个控制器和我们的CRD,现在剩下的就是创建我们的变异webhook。
为了做到这一点,通常我们使用kubebuilder create webhook命令。然而,我们有一个小问题:kubebuilder不支持为“核心类型”创建webhook,而我们想要创建的webhook恰好是针对Pods的。因此,我们按照官方kubebuilder文档的说明,使用controller-runtime库自己创建此部分的样板代码。
如果您想为自定义资源创建webhook,就像创建控制器一样简单:
kubebuilder create webhook \
--kind CachedImage \
--version v1alpha1 \
--defaulting
您可以在 --conversion、–defaulting 和 --programmatic-validation 标志之间进行选择,以创建不同类型的 Webhook。必须至少提供这 3 个标志之一,对应于以下功能:
- –conversion:转换 webhook
- –defaulting:实现 Defaulter 接口的准入 Webhook
- –programmatic-validation:实现验证器接口的准入 webhook
使用make test运行测试
在配置控制器的各个组件之后,下一步显然是检查所有东西是否正常工作。为了做到这一点,我们实现了必要的测试,并重点关注suite_test.go文件中的集成测试。
为了执行测试,我们使用make test命令,该命令负责下载测试依赖项并执行它们。我们可以使用ENVTEST_K8S_VERSION环境变量选择要运行集成测试的Kubernetes API版本,例如ENVTEST_K8S_VERSION=1.25。但是要小心,只有控制平面的一部分会启动,即API服务器和etcd,但除我们创建的控制器之外的其他各种控制器不会被执行。因此,行为可能与预期略有不同。您可以在文档中找到更多细节。
在本地运行operator
为了简化开发过程,kubebuilder允许您使用’make run’命令在本地计算机上轻松运行操作符。当然,需要连接到您打算操作的Kubernetes集群。
然而,当您的操作符包含一个webhook时,需要采取一些预防措施。webhook需要一个证书以确保正常运行(在相应的MutatingWebhookConfiguration中指定的证书),并且发送到webhook的请求必须在集群外部发送。在这种情况下,我们将通过URL而不是Kubernetes服务在webhook配置(MutatingWebhookConfiguration.webhooks.clientConfig)中进行配置。文档建议在本地测试代码之前通过设置ENABLE_WEBHOOKS=false环境变量禁用webhook。
五、Kubebuilder: 优势和局限性
现在我们已经看到了使用kubebuilder创建Kubernetes操作符的用法,让我们来看看它的优势和局限性:
Kubebuilder的优势
- 快速的Kubernetes操作符开发:kubebuilder简化了项目的快速启动过程。
- 无需费力创建和维护CRD:它简化了定义和更新自定义资源定义的过程。
- 集成测试的内置脚手架:kubebuilder设置了一个具有指定版本的Kubernetes API和etcd(不包括控制平面控制器)。这对于在GitHub中的测试矩阵非常方便。
- 相对于Operator SDK的简单性:与Operator SDK相比,kubebuilder提供了一种更简单的方法,而Operator SDK本身是建立在kubebuilder之上。
Kubebuilder的局限性
- 繁琐而手动的kubebuilder更新:当升级到较新版本的kubebuilder(因此更新生成的代码以获得改进)时,没有明确的说明如何修改先前生成的文件。仅仅更新kubebuilder是不够的,因为现有文件仍然保持不变。为了将新版本的kubebuilder的增强功能合并到项目中,最简单的方法是在新目录中重新生成项目,然后手动将脚手架更改应用到原始项目中。这项任务可能很繁琐。虽然已经提出了一个命令来简化这项任务,但在撰写本文时尚未实施。
- 对Kustomize的原生支持,但不支持Helm:kubebuilder自动生成Kustomize清单,而不提供用户选择的选项。如果用户计划使用Helm部署应用程序,他必须手动编写Helm清单。
- 非标准的目录结构:控制器和api文件夹应该在pkg或internal文件夹中。
- Kubebuilder、Kubernetes和Golang之间的版本依赖关系:更新这三个元素中的一个可能需要更新其他元素。例如,更新golang版本可能需要更新Kubernetes版本,并放弃对旧版本的支持。特定版本的kubebuilder被设计为与特定的Kubernetes版本一起使用,依此类推。
- 使用dependabot(用于自动更新依赖项的GitHub bot):您可能需要要求dependabot忽略由kubebuilder管理的某些依赖项,因为更新它们可能会导致问题。如前所述,kubebuilder、Kubernetes和golang的版本密切相关,并且通常需要协调更新。
- gitignore文件夹中的二进制文件:makefile在被git忽略的文件夹中安装二进制文件。在更新项目时,请记得删除这些二进制文件,以便kubebuilder可以重新安装它们。忘记这一步可能导致二进制文件与kubebuilder版本不匹配,可能导致项目故障。
- 核心类型的脚手架:kubebuilder无法为Kubernetes核心类型(如Pods)生成脚手架。这方面需要手动配置。在kube-image-keeper项目的示例中,我们在Pods上使用了一个变异webhook。kubebuilder无法为此webhook生成所需的文件,因此我们必须手动创建它们。
六、我们对kubebuilder的看法
Kubebuilder可以显著简化您的Kubernetes操作符的开发。它在启动新项目时似乎特别有价值。然而,根据操作符的不同,它也有一些限制,这使得它的使用变得复杂,并需要偏离工具的初始操作和文件组织。在这种情况下,您需要仔细评估是否继续使用Kubebuilder或转换为手动操作符更新。