随着Kubernetes越发成熟并且被各大公司所应用,云原生(Cloud Native)这个名词也成为一个大家口头时常提起的名词,不能说没有k8s就没有云原生的今天,毕竟在这之前就已经有了云原生的所谓理念。但是显而易见的是,是k8s推动了云原生从理念到实现, 无论是微服务,容器化,编排,自恢复,IaC还是CI/CD,GitOps,Observability,我们今天所倡导的一切最佳实践,在k8s的世界里,可能就是一行命令,一个标签,一组配置。让我看看如何来实现一个自己的k8s Operator吧。

准备阶段

首先需要Install Kubebuilder, 以Mac为例,亦可以参考官方Quick Start

curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)
chmod +x kubebuilder && sudo mv kubebuilder /usr/local/bin/

生成阶段

接着就可以创建operator的工程了

mkdir my-operator && cd my-operator
kubebuilder init --domain=mydomain.com --repo=mydomain.com/my-operator

继续添加要自定义的CRD,没错,这玩意竟然也可以自动生成。

kubebuilder create api --group apps --version v1alpha1 --kind App

你自己需要做的就是,定义你的CRD里面的字段。比如在api/v1alpha1/app_types.go定义俩个。

type AppSpec struct {
    // Image to deploy
    Image string `json:"image"`
    // Number of replicas
    Replicas *int32 `json:"replicas"`
}

type AppStatus struct {
    AvailableReplicas int32 `json:"availableReplicas"`
}

然后轻轻的敲个命令就CRD就生成好了。

make generate
make manifests

没想到吧,这才没几秒钟,恭喜你,你已经获得了一个完整的Operator以及自己期望定义的CRD全套资源了。

实现阶段

其实这一切才刚刚开始,因为这个Controller还是空空如也,需要我们根据CRD的字段,来实现我们的业务逻辑。

拿我们这次定义的CRD来说,我们想简化k8s App的部署,只需要你提供docker image的名字与节点个数,就可以完成Deploy,所以我们就需要从CRD里面拿到字段的指,来映射到具体的资源里,比如这里我们需要Deployment。大家甚至可以大胆的想想,如果映射到AWS ACK上,那你是不是就可以封装自己的CRD来更简单的部署符合自己公司需要的AWS Infrastructure了。

不过这次我们还是先试试简单的,来创建一个k8s Deployment,在controllers/app_controller.go里填入代码。

func (r *AppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    log := log.FromContext(ctx)
    
    var app appsv1alpha1.App
    if err := r.Get(ctx, req.NamespacedName, &app); err != nil {
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }

    // 根据App的字段定义Deployment
    deploy := &appsv1.Deployment{
        ObjectMeta: metav1.ObjectMeta{
            Name:      app.Name,
            Namespace: app.Namespace,
        },
        Spec: appsv1.DeploymentSpec{
            Replicas: app.Spec.Replicas,
            Selector: &metav1.LabelSelector{
                MatchLabels: map[string]string{"app": app.Name},
            },
            Template: corev1.PodTemplateSpec{
                ObjectMeta: metav1.ObjectMeta{
                    Labels: map[string]string{"app": app.Name},
                },
                Spec: corev1.PodSpec{
                    Containers: []corev1.Container{
                        {
                            Name:  app.Name,
                            Image: app.Spec.Image,
                        },
                    },
                },
            },
        },
    }

    // 设置Deploment的Owner是App,这样App被删除,Deployment也一并消失,当然如果Deployment状态更新也会触发App的Reconcile
    if err := ctrl.SetControllerReference(&app, deploy, r.Scheme); err != nil {
        return ctrl.Result{}, err
    }

    // 查看当前是否已经有Deployment资源,如果没找到就返回创建
    var existing appsv1.Deployment
    err := r.Get(ctx, types.NamespacedName{Name: deploy.Name, Namespace: deploy.Namespace}, &existing)
    if err != nil && apierrors.IsNotFound(err) {
        log.Info("Creating Deployment", "name", deploy.Name)
        return ctrl.Result{}, r.Create(ctx, deploy)
    } else if err != nil {
        return ctrl.Result{}, err
    }

    // 如果Replicas变了就更新
    if *existing.Spec.Replicas != *app.Spec.Replicas {
        existing.Spec.Replicas = app.Spec.Replicas
        return ctrl.Result{}, r.Update(ctx, &existing)
    }

    // Deployment状态更新,App Reconsile,同步更新App的状态,这个可以在Describe里看到
    app.Status.AvailableReplicas = existing.Status.AvailableReplicas
    if err := r.Status().Update(ctx, &app); err != nil {
        log.Error(err, "Failed to update App status")
        return ctrl.Result{}, err
    }

    return ctrl.Result{}, nil
}

还需要一些import

import (
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    types "k8s.io/apimachinery/pkg/types"
    appsv1 "k8s.io/api/apps/v1"
    corev1 "k8s.io/api/core/v1"
    apierrors "k8s.io/apimachinery/pkg/api/errors"
)

测试

make install

这一步需要你本地有k8s环境,它会将你的CRD部署到集群。

make run

这一步会将你的controller,也就是我们说的operator本地起起来。

config/samples/apps_v1alpha1_app.yaml添加我们CRD字段的值用来测试,这里我们可以用nginx来试试,当然也可以用个其他小的镜像,主要是为了下载的快。

apiVersion: apps.mydomain.com/v1alpha1
kind: App
metadata:
  name: example-app
spec:
  image: nginx:1.27.2
  replicas: 2

随后手动apply到k8s集群,通过get pods就可以查看我们的App产生的Deployment(nginx)了。

kubectl apply -f config/samples/apps_v1alpha1_app.yaml
kubectl get pod
NAME                          READY   STATUS    RESTARTS   AGE
app-sample-544c8d4f86-lw49d   1/1     Running   0          15m
app-sample-544c8d4f86-vx8t2   1/1     Running   0          15m

拓展

当然这一些都只是一个简单的operator demo,在实际的应用过程中,我们可能会创建不同的operator来实现不同的业务,譬如有Infrastructure的Operator,GitOps的Operator,权限管理的Operator。

CRD的背后往往也会更复杂一些,不只有一个孤零零的Depolyment,还需要有其他一些列的资源来配合,这样才能体现CRD的优势,一换多。这时一个最佳的实践就是根据类型不同的Controller处理不同的资源。

如:

internal/controllers/
    ├── deployment_controller.go  
    ├── service_account_controller.go     
    ├── service_controller.go    
    ├── gateway_controller.go 
    └── ..._controller.go  

写在最后

可见,通过kubebuild已经可以快速创建一个operator的模版,但是还是需要根据实际业务以及需求还定义符合需求的CRD,才能真正的提升我们的云原生治理能力。

更多推荐