kubebuilder是一个快速实现kubernetes Operator的框架,通过kubebuilder我们能够快速定义CRD资源和实现controller逻辑。我们可以简单地将operator理解为:operator=crd+controller。
kubebuilder自身携带了两个工具:controller-runtime与controller-tools,我们可以通过controller-tools来生成大部分的代码,从而我们可以将我们的主要精力放置在CRD的定义和编写controller的reconcile逻辑上。所谓的reconcile,即调谐,是指将资源的status通过一系列的操作调整至与定义的spec一致。
从整体来看,通过kubebuilder来实现operator大致可分为以下步骤:
我们以创建一个mysql的operator来作为最佳实践的例子。我们期望这个mysql-operator能够实现deployment创建和更新,以及mysql实例数的扩缩容。
确保实践环境中拥有以下组件:
curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)
chmod +x kubebuilder && mv kubebuilder /usr/local/bin/
kubebuilder init --domain tutorial.kubebuilder.io
创建kubebuilder项目之前必须要先有go project,否则会报错。
创建完成之后,我们可以看到此时项目中多了许多的代码,其中:
kubebuilder create api --group batch --version v1 --kind Mysql
Create Resource [y/n]
y
Create Controller [y/n]
y
编写CRD资源的定义主要是在mysql_types.go文件中,编写controller逻辑主要是在mysql_controller.go文件中。
我们首先编写CRD的定义。我们简单地用一个yaml文件来描述我们所期望的定义:
apiVersion: batch.tutorial.kubebuilder.io/v1
kind: Mysql
metadata:
name: mysql-sample
spec:
replicas: 1
image: flyer103/mysql:5.7
command: [ "/bin/bash", "-ce", "tail -f /dev/null" ]
我们需要在mysql_types.go文件中定义我们的spec和status。
// MysqlSpec defines the desired state of Mysql
type MysqlSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file
Name string `json:"name,omitempty"`
Image string `json:"image,omitempty"`
Command []string `json:"command,omitempty" protobuf:"bytes,3,rep,name=command"`
// +optional
//+kubebuilder:default:=1
//+kubebuilder:validation:Minimum:=1
Replicas *int32 `json:"replicas,omitempty" protobuf:"varint,1,opt,name=replicas"`
}
我们在这里针对code重新定义了string类型。
type MySqlCode string
const (
SuccessCode MySqlCode = "success"
FailedCode MySqlCode = "failed"
)
// MysqlStatus defines the observed state of Mysql
type MysqlStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
Code MySqlCode `json:"code,omitempty"`
Replicas int32 `json:"replicas"`
ReadyReplicas int32 `json:"readyReplicas"`
}
像 //+kubebuilder:object:root
这样的以//+
开头的注释在kubebuilder
中被称为元数据标记,这些标记的作用就是告诉controller-tools生成额外的信息。
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas,selectorpath=.status.labelSelector
//+kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.code",description="The phase of game."
//+kubebuilder:printcolumn:name="DESIRED",type="integer",JSONPath=".spec.replicas",description="The desired number of pods."
//+kubebuilder:printcolumn:name="CURRENT",type="integer",JSONPath=".status.replicas",description="The number of currently all pods."
//+kubebuilder:printcolumn:name="READY",type="integer",JSONPath=".status.readyReplicas",description="The number of pods ready."
kubebuilder为我们在mysql_controller.go文件中预先生成了两个函数,分别是Reconcile()和SetupWithManager()。关于这两个函数的区别,我们可以简单地理解为将 Reconcile 添加到 manager 中,这样当 manager 启动时它就会被启动。
kubebuilder自动为我们生成了Reconcile函数,我们只需编写它的业务逻辑即可。我们希望能够通过mysql-operator来管理具体的deployment,因此我们具体需要调谐的逻辑其实是deployment的spec和status的一致。
在Reconcile函数中,我们主要实现以下逻辑:
func (r *MysqlReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
defer utilruntime.HandleCrash()
logger := log.FromContext(ctx)
logger.Info("revice reconcile event", "name", req.String())
// 获取mysql对象
logger.Info("get mysql object", "name", req.String())
mysql := &batchv1.Mysql{}
if err := r.Get(ctx, req.NamespacedName, mysql); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// 如果mysql在删除,则跳过
if mysql.DeletionTimestamp != nil {
logger.Info("mysql in deleting", "name", req.String())
return ctrl.Result{}, nil
}
// 同步资源状态
logger.Info("begin to sync mysql", "name", req.String())
if err := r.syncMysql(ctx, mysql); err != nil {
logger.Error(err, "failed to sync mysql", "name", req.String())
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
我们将具体的同步逻辑抽象为syncMysql函数,我们希望该函数能够实现以下逻辑:
const (
mysqlLabelName = "tutorial.kubebuilder.io/mysql"
)
func (r *MysqlReconciler) syncMysql(ctx context.Context, obj *batchv1.Mysql) error {
logger := log.FromContext(ctx)
mysql := obj.DeepCopy()
name := types.NamespacedName{
Namespace: mysql.Namespace,
Name: mysql.Name,
}
// 构造owner
owner := []metav1.OwnerReference{
{
APIVersion: mysql.APIVersion,
Kind: mysql.Kind,
Name: mysql.Name,
Controller: pointer.BoolPtr(true),
BlockOwnerDeletion: pointer.BoolPtr(true),
UID: mysql.UID,
},
}
labels := map[string]string{
mysqlLabelName: mysql.Name,
}
meta := metav1.ObjectMeta{
Name: mysql.Name,
Namespace: mysql.Namespace,
Labels: labels,
OwnerReferences: owner,
}
deploy := &appsv1.Deployment{}
if err := r.Get(ctx, name, deploy); err != nil {
logger.Info("get deployment success")
if !errors.IsNotFound(err) {
return err
}
deploy = &appsv1.Deployment{
ObjectMeta: meta,
Spec: getDeploymentSpec(mysql, labels),
}
if err := r.Create(ctx, deploy); err != nil {
return nil
}
logger.Info("create Deployment success", "name", name.String())
} else {
want := getDeploymentSpec(mysql, labels)
get := getSpecFromDeployment(deploy)
if !reflect.DeepEqual(want, get) {
new := deploy.DeepCopy()
new.Spec = want
if err := r.Update(ctx, new); err != nil {
return err
}
logger.Info("update deployment success", "name", name.String())
}
}
if *mysql.Spec.Replicas == deploy.Status.ReadyReplicas {
mysql.Status.Code = batchv1.SuccessCode
} else {
mysql.Status.Code = batchv1.FailedCode
}
r.Client.Status().Update(ctx, mysql)
return nil
}
func getDeploymentSpec(mysql *batchv1.Mysql, labels map[string]string) appsv1.DeploymentSpec {
return appsv1.DeploymentSpec{
Replicas: mysql.Spec.Replicas,
Selector: metav1.SetAsLabelSelector(labels),
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: labels,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "main",
Image: mysql.Spec.Image,
Command: mysql.Spec.Command,
},
},
},
},
}
}
func getSpecFromDeployment(deploy *appsv1.Deployment) appsv1.DeploymentSpec {
container := deploy.Spec.Template.Spec.Containers[0]
return appsv1.DeploymentSpec{
Replicas: deploy.Spec.Replicas,
Selector: deploy.Spec.Selector,
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: deploy.Spec.Template.Labels,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: container.Name,
Image: container.Image,
Command: container.Command,
},
},
},
},
}
}
// SetupWithManager sets up the controller with the Manager.
func (r *MysqlReconciler) SetupWithManager(mgr ctrl.Manager) error {
ctrl.NewControllerManagedBy(mgr).
WithOptions(controller.Options{
MaxConcurrentReconciles: 3,
}).
For(&batchv1.Mysql{}).
Owns(&appsv1.Deployment{}).
Complete(r)
return nil
}
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=apps,resources=deployments/status,verbs=get;update;patch
每次修改mysql_types.go文件之后,都需要重新执行以下命令
make install
每次修改mysql_controller.go文件之后,都需要重新执行以下命令
make run
部署我们在前边定义好的yaml文件都本地集群当中
➜ kubectl apply -f batch_v1_mysql.yaml
mysql.batch.tutorial.kubebuilder.io/mysql-sample created
查看是否创建成功
➜ kubectl get mysql
NAME PHASE AGE
mysql-sample success 19s
➜ kubectl get deployment
NAME READY UP-TO-DATE AVAILABLE AGE
mysql-sample 1/1 1 1 27s
➜ kubectl get pod | grep mysql-sample
mysql-sample-6dd55bb569-pgzhz 1/1 Running 0 31s
➜ kubectl scale mysqls.batch.tutorial.kubebuilder.io mysql-sample --replicas 2
mysql.batch.tutorial.kubebuilder.io/mysql-sample scaled