Kubebuilder快速上手:手把手教你创建Kubernetes Operator
Kubebuilder快速上手:手把手教你创建Kubernetes Operator
Kubernetes Operator 是一种扩展 Kubernetes API 的方式,它允许你像管理内置资源一样管理应用程序。Kubebuilder 是一个用于构建 Kubernetes Operator 的 SDK,它提供了一套工具和库,可以帮助你快速地创建、测试和部署 Operator。本文将带你一步步使用 Kubebuilder 创建一个简单的 Operator,并实现对 CRD 资源的管理自动化。
1. 准备工作
在开始之前,你需要确保满足以下条件:
- 安装 Go:Kubebuilder 是用 Go 编写的,因此你需要安装 Go 语言环境。
- 安装 Docker:你需要 Docker 来构建 Operator 的镜像。
- 安装 Kubectl:你需要 Kubectl 来与 Kubernetes 集群交互。
- 安装 Kubebuilder:按照 Kubebuilder 官方文档进行安装:https://kubebuilder.io/docs/installation
- 连接到 Kubernetes 集群:确保你的 Kubectl 配置正确,并且可以连接到 Kubernetes 集群。
2. 创建项目
首先,使用 Kubebuilder 创建一个新的项目:
mkdir my-operator
cd my-operator
kubebuilder init --domain example.com --owner "Your Name"
--domain example.com:指定你的域名,用于生成 API 组名。请替换为你自己的域名。--owner "Your Name":指定 Operator 的所有者。
3. 定义 CRD
接下来,我们需要定义一个 CRD(Custom Resource Definition),它描述了我们想要管理的自定义资源。使用 Kubebuilder 创建一个 API:
kubebuilder create api --group apps --version v1alpha1 --kind MyApp
--group apps:指定 API 组名。--version v1alpha1:指定 API 版本。--kind MyApp:指定自定义资源的 Kind(类型)。
执行上述命令后,Kubebuilder 会生成以下文件:
api/v1alpha1/myapp_types.go:定义 MyApp 资源的结构体。controllers/myapp_controller.go:定义 MyApp 资源的控制器逻辑。
4. 修改 CRD 定义
打开 api/v1alpha1/myapp_types.go 文件,修改 MyApp 资源的结构体,添加你需要的字段。例如,我们可以添加一个 Spec 字段来描述 MyApp 的规格,以及一个 Status 字段来描述 MyApp 的状态:
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// MyAppSpec defines the desired state of MyApp
type MyAppSpec struct {
// Replicas is the desired number of replicas
Replicas *int32 `json:"replicas,omitempty"`
// Image is the image to use for the deployment
Image string `json:"image,omitempty"`
}
// MyAppStatus defines the observed state of MyApp
type MyAppStatus struct {
// ReadyReplicas is the number of ready replicas
ReadyReplicas int32 `json:"readyReplicas,omitempty"`
}
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
// MyApp is the Schema for the myapps API
type MyApp struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec MyAppSpec `json:"spec,omitempty"`
Status MyAppStatus `json:"status,omitempty"`
}
//+kubebuilder:object:root=true
// MyAppList contains a list of MyApp
type MyAppList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []MyApp `json:"items"`
}
func init() {
SchemeBuilder.Register(&MyApp{}, &MyAppList{})
}
注意:
//+kubebuilder:object:root=true:这个标记告诉 Kubebuilder 这个类型是一个 CRD。//+kubebuilder:subresource:status:这个标记告诉 Kubebuilder 这个类型有一个状态子资源,用于存储资源的状态信息。json:"replicas,omitempty":omitempty选项表示如果该字段为空,则在序列化为 JSON 时忽略该字段。
修改完 CRD 定义后,需要运行以下命令来更新 CRD:
make manifests
5. 实现控制器逻辑
打开 controllers/myapp_controller.go 文件,实现控制器逻辑。控制器负责监听 MyApp 资源的创建、更新和删除事件,并根据事件执行相应的操作。例如,我们可以创建一个 Deployment 来运行 MyApp:
package controllers
import (
"context"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/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"
appsv1alpha1 "example.com/my-operator/api/v1alpha1"
)
// MyAppReconciler reconciles a MyApp object
type MyAppReconciler struct {
client.Client
Scheme *runtime.Scheme
}
//+kubebuilder:rbac:groups=apps.example.com,resources=myapps,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=apps.example.com,resources=myapps/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=apps.example.com,resources=myapps/finalizers,verbs=update
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch
// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the MyApp object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.1/pkg/reconcile
func (r *MyAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
// TODO(user): your logic here
var myApp appsv1alpha1.MyApp
err := r.Get(ctx, req.NamespacedName, &myApp)
if err != nil {
log.Error(err, "unable to fetch MyApp")
// we'll ignore not-found errors, since they can't be fixed by an immediate
// requeue (we'll need to wait for a new notification), and we can get them
// on deleted requests.
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// Check if the deployment already exists, if not create a new one
var deployment appsv1.Deployment
err = r.Get(ctx, client.ObjectKey{Namespace: myApp.Namespace, Name: myApp.Name}, &deployment)
if err != nil && client.IgnoreNotFound(err) == nil {
// Define a new deployment
deploy := r.deploymentForMyApp(&myApp)
log.Info("Creating a new Deployment", "Deployment.Namespace", deploy.Namespace, "Deployment.Name", deploy.Name)
err = r.Create(ctx, deploy)
if err != nil {
log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", deploy.Namespace, "Deployment.Name", deploy.Name)
return ctrl.Result{}, err
}
// Deployment created successfully - return and requeue
return ctrl.Result{Requeue: true}, nil
} else if err != nil {
log.Error(err, "Failed to get Deployment")
return ctrl.Result{}, err
}
// Ensure the deployment size is the same as the spec
replicas := myApp.Spec.Replicas
if *deployment.Spec.Replicas != *replicas {
log.Info("Updating Deployment", "Deployment.Namespace", deployment.Namespace, "Deployment.Name", deployment.Name)
deployment.Spec.Replicas = replicas
err = r.Update(ctx, &deployment)
if err != nil {
log.Error(err, "Failed to update Deployment", "Deployment.Namespace", deployment.Namespace, "Deployment.Name", deployment.Name)
return ctrl.Result{}, err
}
// Spec updated - return and requeue
return ctrl.Result{Requeue: true}, nil
}
// Update the MyApp status with the pod names
// List the pods for this myapp's deployment
podList := &corev1.PodList{}
lbls := map[string]string{
"app": myApp.Name,
}
listOpts := []client.ListOption{
client.InNamespace(myApp.Namespace),
client.MatchingLabels(lbls),
}
if err = r.List(ctx, podList, listOpts...); err != nil {
log.Error(err, "Failed to list pods", "MyApp.Namespace", myApp.Namespace, "MyApp.Name", myApp.Name)
return ctrl.Result{}, err
}
podNames := getPodNames(podList.Items)
// Update status.Nodes if needed
if !reflect.DeepEqual(podNames, myApp.Status.Nodes) {
myApp.Status.Nodes = podNames
err = r.Status().Update(ctx, &myApp)
if err != nil {
log.Error(err, "Failed to update MyApp status", "MyApp.Namespace", myApp.Namespace, "MyApp.Name", myApp.Name)
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
// deploymentForMyApp returns a myapp Deployment object
func (r *MyAppReconciler) deploymentForMyApp(myApp *appsv1alpha1.MyApp) *appsv1.Deployment {
ls := map[string]string{
"app": myApp.Name,
"controller": myApp.Name,
}
replicas := myApp.Spec.Replicas
dep := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: myApp.Name,
Namespace: myApp.Namespace,
Labels: ls,
},
Spec: appsv1.DeploymentSpec{
Replicas: replicas,
Selector: &metav1.LabelSelector{
MatchLabels: ls,
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: ls,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Image: myApp.Spec.Image,
Name: "myapp",
Ports: []corev1.ContainerPort{{ContainerPort: 8080, Name: "http"}}}},
},
},
},
}
// Set MyApp instance as the owner and controller
ctrl.SetControllerReference(myApp, dep, r.Scheme)
return dep
}
// getPodNames returns the pod names of the array of pods passed in
func getPodNames(pods []corev1.Pod) []string {
var podNames []string
for _, pod := range pods {
podNames = append(podNames, pod.Name)
}
return podNames
}
// SetupWithManager sets up the controller with the Manager.
func (r *MyAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).For(&appsv1alpha1.MyApp{}).Owns(&appsv1.Deployment{}).Complete(r)
}
注意:
//+kubebuilder:rbac:…:这些标记用于声明 Operator 需要的权限。确保你声明了所有需要的权限,否则 Operator 将无法正常工作。Reconcile函数是控制器的主循环。它负责监听资源事件,并执行相应的操作。ctrl.SetControllerReference函数用于设置 Deployment 的 OwnerReference。这可以确保当 MyApp 资源被删除时,Deployment 也会被自动删除。
6. 部署 Operator
在部署 Operator 之前,需要先构建 Operator 的镜像:
make docker-build docker-push IMG="your-docker-repo/my-operator:latest"
your-docker-repo/my-operator:latest:替换为你自己的 Docker 镜像仓库地址和标签。
然后,运行以下命令来部署 Operator:
make deploy IMG="your-docker-repo/my-operator:latest"
7. 创建 MyApp 资源
创建一个 YAML 文件,例如 config/samples/apps_v1alpha1_myapp.yaml,来定义一个 MyApp 资源:
apiVersion: apps.example.com/v1alpha1
kind: MyApp
metadata:
name: myapp-sample
spec:
replicas: 3
image: nginx:latest
然后,使用 Kubectl 创建 MyApp 资源:
kubectl apply -f config/samples/apps_v1alpha1_myapp.yaml
8. 验证 Operator
使用 Kubectl 验证 Operator 是否正常工作:
kubectl get myapps
kubectl get deployments
kubectl get pods
你应该能够看到 MyApp 资源、Deployment 和 Pod 都被成功创建。
9. 总结
本文介绍了如何使用 Kubebuilder 快速创建一个 Kubernetes Operator,并实现对 CRD 资源的管理自动化。通过本文的学习,你应该能够掌握 Kubebuilder 的基本用法,并能够使用 Kubebuilder 创建自己的 Operator。
进一步学习:
- Kubebuilder 官方文档:https://kubebuilder.io/
- Kubernetes Operator 模式:https://kubernetes.io/docs/concepts/extend-kubernetes/operators/
- Operator SDK:https://sdk.operatorframework.io/
希望本文能够帮助你快速入门 Kubernetes Operator 开发!