使用操作员SDK创建Kubernetes运营商
#go #kubernetes #operatorsdk

如果您开发API或微服务,尤其是在中等环境中,则可能使用Kubernetes。

Kubernetes是一个由Google在2015年中期创建的项目,很快成为管理容器执行的标准。您可以在机器上托管它,也可以使用由AWSGoogleDigitalOcean等大型云玩家提供的解决方案。

在这篇文章中,我想谈论另一个功能:将其扩展以创建新功能的可能性。让我们从理解本文的基本概念开始。

资源和控制器

最基本的概念之一是K8S管理资源。根据官方documentation

资源是Kubernetes API中的端点,该端点存储了特定类型的API对象的集合;例如,内置的“ pods”资源包含一个POD对象的集合。

K8S使用另一个概念管理这些资源:控制器。当我们使用K8S功能时,我们需要在YAML文件中定义我们期望的状态。例如:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2 # tells deployment to run 2 pods that match the template
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80

spec 键内的信息对应于资源的所需状态。

K8S的作用是确保集群中包含的对象的当前状态等于所需的对象。在这种情况下,两个NGINX容器版本1.14.2在端口80上运行。它使用所谓的A Control Loop

进行操作。

operator-reconciliation-kube-only

它检查了资源的当前状态是否与所需状态有所不同,如果是,则执行链接到对象的控制器的和解函数。这样,我们可以定义这样的控制器:

一个控制器跟踪至少一种类型的Kubernetes资源。这些对象具有代表所需状态的规格字段。该资源的控制器负责使当前状态更接近所需的状态。

k8s具有一系列内置资源,例如 pod 部署 service ,以及跟踪每一个生命周期的控制器他们。但是除了它们外,我们还可以通过自定义资源定义(CRD)创建我们的资源。 CRD和控制器的组合是我们所说的operator,这是我们将在本文中探索的。

操作员SDK

为了说明我们可以使用操作员做什么,我将使用操作员SDK创建概念证明。根据official website ::

操作员SDK使构建Kubernetes-intagity应用程序变得容易,该过程可能需要深入,特定于应用程序的操作知识。该项目是Operator Framework的组成部分,Operator Framework是一种开源工具包,用于以实用,自动化和可扩展的方式管理本机Kubernetes应用程序。

可以使用GoAnsibleHelm创建操作员。在本文中,我将使用Go

第一步是在计算机上安装SDK CLI。我使用了啤酒,但其他选项在documentation中。

brew install operator-sdk

下一步是使用CLI使用命令来生成项目脚手架:

operator-sdk init --domain minetto.dev --repo github.com/eminetto/k8s-operator-talk
operator-sdk create api --version v1alpha1 --kind Application --resource --controller

第一个命令通过指示域,k8s将用来识别资源的信息以及用于GO软件包名称的存储库名称来初始化项目。第二个命令在 alpha1 版本和控制器骨架中创建新的应用程序资源。

在进入代码之前,必须了解概念证明的目的。以其本机形式,在K8S上运行应用程序需要开发人员了解部署,POD,服务等等概念。我的目标是将这种认知负载减少到仅两个资源:namespace,该应用程序将位于该应用程序中群集和应用程序,它将定义应用程序的所需状态。例如,团队只需要创建以下 yaml

apiVersion: v1
kind: Namespace
metadata:
  name: application-sample
---
apiVersion: minetto.dev/v1alpha1
kind: Application
metadata:
  name: application-sample
  namespace: application-sample
spec:
  image: nginx:latest
  replicas: 2
  port: 80

使用命令将其应用于集群:

kubectl apply -f application.yaml

,其余的将由我们的控制器创建。

第一步是配置我们的资源以具有与 spec 相关的字段。为此,您必须更改 api/v1alpha/application_types.go 文件,然后将字段添加到结构:

type ApplicationSpec struct {
    Image    string `json:"image,omitempty"`
    Replicas int32  `json:"replicas,omitempty"`
    Port     int32  `json:"port,omitempty"`
}

稍后,我们将使用此信息来生成在群集上安装CRD所需的文件。我们还将使用此结构来创建所需的资源。

下一步是为我们的控制器创建逻辑。 operator-sdk 使Controllers/application_controller.go文件和和解函数签名。每当K8S检测对象的当前状态与所需状态之间的差异时,控制循环调用此函数。在SDK生成的 main.go 文件中,我们在应用程序资源和控制器之间具有链接,现在不必担心它。操作员SDK的优点之一是,它使我们能够专注于控制器逻辑,并提取其工作所需的所有庞大细节。

和解函数代码和辅助设备在下面。我试图记录最重要的摘录:

func (r *ApplicationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    l := log.FromContext(ctx)
    var app minettodevv1alpha1.Application
    //recupera os detalhes do objeto sendo gerenciado
    if err := r.Get(ctx, req.NamespacedName, &app); err != nil {
        if apierrors.IsNotFound(err) {
            return ctrl.Result{}, nil
        }
        l.Error(err, "unable to fetch Application")
        return ctrl.Result{}, err
    }
    /*
    The finalizer is essential because it tells K8s we need control over object deletion. 
    After all, how we will create other resources must be excluded together.
    Without the finalizer, there is no time for the K8s garbage collector to delete, 
    and we risk having useless resources in the cluster.
    */
    if !controllerutil.ContainsFinalizer(&app, finalizer) {
        l.Info("Adding Finalizer")
        controllerutil.AddFinalizer(&app, finalizer)
        return ctrl.Result{}, r.Update(ctx, &app)
    }

    if !app.DeletionTimestamp.IsZero() {
        l.Info("Application is being deleted")
        return r.reconcileDelete(ctx, &app)
    }
    l.Info("Application is being created")
    return r.reconcileCreate(ctx, &app)
}

func (r *ApplicationReconciler) reconcileCreate(ctx context.Context, app *minettodevv1alpha1.Application) (ctrl.Result, error) {
    l := log.FromContext(ctx)
    l.Info("Creating deployment")
    err := r.createOrUpdateDeployment(ctx, app)
    if err != nil {
        return ctrl.Result{}, err
    }
    l.Info("Creating service")
    err = r.createService(ctx, app)
    if err != nil {
        return ctrl.Result{}, err
    }
    return ctrl.Result{}, nil
}

func (r *ApplicationReconciler) createOrUpdateDeployment(ctx context.Context, app *minettodevv1alpha1.Application) error {
    var depl appsv1.Deployment
    deplName := types.NamespacedName{Name: app.ObjectMeta.Name + "-deployment", Namespace: app.ObjectMeta.Name}
    if err := r.Get(ctx, deplName, &depl); err != nil {
        if !apierrors.IsNotFound(err) {
            return fmt.Errorf("unable to fetch Deployment: %v", err)
        }
        /*If there is no Deployment, we will create it.
        An essential section in the definition is OwnerReferences, as it indicates to k8s that 
        an Application is creating this resource. 
        This is how k8s knows that when we remove an Application, it must also remove 
        all the resources it created.
        Another important detail is that we use data from our Application to create the Deployment, 
        such as image information, port, and replicas.
        */
        if apierrors.IsNotFound(err) {
            depl = appsv1.Deployment{
                ObjectMeta: metav1.ObjectMeta{
                    Name:        app.ObjectMeta.Name + "-deployment",
                    Namespace:   app.ObjectMeta.Name,
                    Labels:      map[string]string{"label": app.ObjectMeta.Name, "app": app.ObjectMeta.Name},
                    Annotations: map[string]string{"imageregistry": "https://hub.docker.com/"},
                    OwnerReferences: []metav1.OwnerReference{
                        {
                            APIVersion: app.APIVersion,
                            Kind:       app.Kind,
                            Name:       app.Name,
                            UID:        app.UID,
                        },
                    },
                },
                Spec: appsv1.DeploymentSpec{
                    Replicas: &app.Spec.Replicas,
                    Selector: &metav1.LabelSelector{
                        MatchLabels: map[string]string{"label": app.ObjectMeta.Name},
                    },
                    Template: v1.PodTemplateSpec{
                        ObjectMeta: metav1.ObjectMeta{
                            Labels: map[string]string{"label": app.ObjectMeta.Name, "app": app.ObjectMeta.Name},
                        },
                        Spec: v1.PodSpec{
                            Containers: []v1.Container{
                                {
                                    Name:  app.ObjectMeta.Name + "-container",
                                    Image: app.Spec.Image,
                                    Ports: []v1.ContainerPort{
                                        {
                                            ContainerPort: app.Spec.Port,
                                        },
                                    },
                                },
                            },
                        },
                    },
                },
            }
            err = r.Create(ctx, &depl)
            if err != nil {
                return fmt.Errorf("unable to create Deployment: %v", err)
            }
            return nil
        }
    }
    /*The controller also needs to manage the update because if the dev changes any information 
    in an existing Application, this must impact other resources.*/
    depl.Spec.Replicas = &app.Spec.Replicas
    depl.Spec.Template.Spec.Containers[0].Image = app.Spec.Image
    depl.Spec.Template.Spec.Containers[0].Ports[0].ContainerPort = app.Spec.Port
    err := r.Update(ctx, &depl)
    if err != nil {
        return fmt.Errorf("unable to update Deployment: %v", err)
    }
    return nil
}

func (r *ApplicationReconciler) createService(ctx context.Context, app *minettodevv1alpha1.Application) error {
    srv := v1.Service{
        ObjectMeta: metav1.ObjectMeta{
            Name:      app.ObjectMeta.Name + "-service",
            Namespace: app.ObjectMeta.Name,
            Labels:    map[string]string{"app": app.ObjectMeta.Name},
            OwnerReferences: []metav1.OwnerReference{
                {
                    APIVersion: app.APIVersion,
                    Kind:       app.Kind,
                    Name:       app.Name,
                    UID:        app.UID,
                },
            },
        },
        Spec: v1.ServiceSpec{
            Type:                  v1.ServiceTypeNodePort,
            ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeLocal,
            Selector:              map[string]string{"app": app.ObjectMeta.Name},
            Ports: []v1.ServicePort{
                {
                    Name:       "http",
                    Port:       app.Spec.Port,
                    Protocol:   v1.ProtocolTCP,
                    TargetPort: intstr.FromInt(int(app.Spec.Port)),
                },
            },
        },
        Status: v1.ServiceStatus{},
    }
    _, err := controllerutil.CreateOrUpdate(ctx, r.Client, &srv, func() error {
        return nil
    })
    if err != nil {
        return fmt.Errorf("unable to create Service: %v", err)
    }
    return nil
}

func (r *ApplicationReconciler) reconcileDelete(ctx context.Context, app *minettodevv1alpha1.Application) (ctrl.Result, error) {
    l := log.FromContext(ctx)

    l.Info("removing application")

    controllerutil.RemoveFinalizer(app, finalizer)
    err := r.Update(ctx, app)
    if err != nil {
        return ctrl.Result{}, fmt.Errorf("Error removing finalizer %v", err)
    }
    return ctrl.Result{}, nil
}

要部署我们的自定义资源及其控制器,SDK在其 makefile 中提供命令:

make manifests
make docker-build docker-push IMG=registry.hub.docker.com/eminetto/k8s-operator-talk:latest
make deploy IMG=registry.hub.docker.com/eminetto/k8s-operator-talk:latest

第一个命令生成创建CRD所需的所有文件。第二个生成一个docker容器并将其推到指示的存储库。最后一个命令将生成的容器安装在群集上。提示:您可以使用倾斜度在开发环境中自动化控制器的生成和安装。该项目的repository具有完成所有这些工作的倾斜fi。要了解有关倾斜的更多信息,请查看有关该工具的my post

现在,将 yaml application 定义应用于群集,并且控制器将生成 exployment 服务 /em>应用程序运行所必需的。

我们可以检查控制器是否使用以下命令创建资源。

kubectl -n application-sample get applications
NAME                 AGE
application-sample   18s
kubectl -n application-sample get deployments
NAME                            READY   UP-TO-DATE   AVAILABLE   AGE
application-sample-deployment   2/2     2            2           41s
kubectl -n application-sample get pods
NAME                                             READY   STATUS    RESTARTS   AGE
application-sample-deployment-65b96554f8-8vv64   1/1     Running   0          56s
application-sample-deployment-65b96554f8-v54gp   1/1     Running   0          56s
kubectl -n application-sample get services
NAME                         TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
application-sample-service   NodePort   10.43.63.164   <none>        80:32591/TCP   66s

这篇文章最终很长,因此还有其他主题我将留下以后的文本,例如测试部分。但是我希望我能够引起对这个问题的兴趣。我对此感到非常兴奋,并相信它具有帮助创建自动化的难以置信的潜力,这使开发和运营团队的生活变得更加轻松。

最初于2023年9月8日在https://eltonminetto.dev出版