如何在Kubernetes运营商中打包工作脚本

127 阅读4分钟

在使用复杂的Kubernetes运营商时,你经常要协调Job来执行工作负载任务。作业实现的例子通常提供直接写在清单中的微不足道的脚本。然而,在任何合理复杂的应用中,确定如何处理比琐碎的脚本可能是一个挑战。

在过去,我是通过将我的脚本包含在应用程序的图像中来解决这个问题的。这种方法足够好用,但它确实有一个缺点。任何时候需要改变,我都不得不重建应用程序的镜像,以包括修订内容。这就浪费了很多时间,特别是当我的应用图像需要大量时间来构建时。这也意味着我同时在维护一个应用程序镜像和一个操作员镜像。如果我的操作员仓库不包括应用程序的图像,那么我就会在不同的仓库进行相关的修改。最终,我成倍地增加了我的提交数量,使我的工作流程变得复杂。每一个变化都意味着我必须管理和同步提交和存储库之间的图像参考。

鉴于这些挑战,我想找到一种方法,将我的Job脚本保留在运营商的代码库中。这样一来,我就可以与我的操作员的调和逻辑同步修改我的脚本了。我的目标是设计一个工作流程,只有当我需要对我的脚本进行修改时才需要重建操作员的图像。幸运的是,我使用Go编程语言,它提供了非常有用的 go:embed功能。这允许开发者将文本文件与他们的应用程序的二进制文件打包。通过利用这一功能,我发现我可以在操作员的图像中维护我的作业脚本。

嵌入工作脚本

出于示范目的,我的任务脚本不包括任何实际的业务逻辑。然而,通过使用嵌入式脚本,而不是将脚本直接写入作业清单,这种方法使复杂的脚本既井然有序,又从作业定义本身中抽象出来。

这是我的简单示例脚本。

$ cat embeds/task.sh
#!/bin/sh
echo "Starting task script."
# Something complicated...
echo "Task complete."

现在要在操作员的逻辑上下功夫。

操作员逻辑

以下是我的操作员对账内的过程。

  1. 检索脚本的内容
  2. 将脚本的内容添加到一个配置图中
  3. 通过以下方式在工作中运行ConfigMap的脚本
    1. 定义一个指向ConfigMap的卷
    2. 使卷的内容可以执行
    3. 将卷轴挂载到Job上

下面是代码。

// STEP 1: retrieve the script content from the codebase.
//go:embed embeds/task.sh
var taskScript string

func (r *MyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
        ctxlog := ctrllog.FromContext(ctx)
        myresource := &myresourcev1alpha.MyResource{}
        r.Get(ctx, req.NamespacedName, d)

        // STEP 2: create the ConfigMap with the script's content.
        configmap := &corev1.ConfigMap{}
        err := r.Get(ctx, types.NamespacedName{Name: "my-configmap", Namespace: myresource.Namespace}, configmap)
        if err != nil && apierrors.IsNotFound(err) {

                ctxlog.Info("Creating new ConfigMap")
                configmap := &corev1.ConfigMap{
                        ObjectMeta: metav1.ObjectMeta{
                                Name:      "my-configmap",
                                Namespace: myresource.Namespace,
                        },
                        Data: map[string]string{
                                "task.sh": taskScript,
                        },
                }

                err = ctrl.SetControllerReference(myresource, configmap, r.Scheme)
                if err != nil {
                        return ctrl.Result{}, err
                }
                err = r.Create(ctx, configmap)
                if err != nil {
                        ctxlog.Error(err, "Failed to create ConfigMap")
                        return ctrl.Result{}, err
                }
                return ctrl.Result{Requeue: true}, nil
        }

        // STEP 3: create the Job with the ConfigMap attached as a volume.
        job := &batchv1.Job{}
        err = r.Get(ctx, types.NamespacedName{Name: "my-job", Namespace: myresource.Namespace}, job)
        if err != nil && apierrors.IsNotFound(err) {

                ctxlog.Info("Creating new Job")
                configmapMode := int32(0554)
                job := &batchv1.Job{
                        ObjectMeta: metav1.ObjectMeta{
                                Name:      "my-job",
                                Namespace: myresource.Namespace,
                        },
                        Spec: batchv1.JobSpec{
                                Template: corev1.PodTemplateSpec{
                                        Spec: corev1.PodSpec{
                                                RestartPolicy: corev1.RestartPolicyNever,
                                                // STEP 3a: define the ConfigMap as a volume.
                                                Volumes: []corev1.Volume{{
                                                        Name: "task-script-volume",
                                                        VolumeSource: corev1.VolumeSource{
                                                                ConfigMap: &corev1.ConfigMapVolumeSource{
                                                                        LocalObjectReference: corev1.LocalObjectReference{
                                                                                Name: "my-configmap",
                                                                        },
                                                                        DefaultMode: &configmapMode,
                                                                },
                                                        },
                                                }},
                                                Containers: []corev1.Container{
                                                        {
                                                                Name:  "task",
                                                                Image: "busybox",
                                                                Resources: corev1.ResourceRequirements{
                                                                        Requests: corev1.ResourceList{
                                                                                corev1.ResourceCPU:    *resource.NewMilliQuantity(int64(50), resource.DecimalSI),
                                                                                corev1.ResourceMemory: *resource.NewScaledQuantity(int64(250), resource.Mega),
                                                                        },
                                                                        Limits: corev1.ResourceList{
                                                                                corev1.ResourceCPU:    *resource.NewMilliQuantity(int64(100), resource.DecimalSI),
                                                                                corev1.ResourceMemory: *resource.NewScaledQuantity(int64(500), resource.Mega),
                                                                        },
                                                                },
                                                                // STEP 3b: mount the ConfigMap volume.
                                                                VolumeMounts: []corev1.VolumeMount{{
                                                                        Name:      "task-script-volume",
                                                                        MountPath: "/scripts",
                                                                        ReadOnly:  true,
                                                                }},
                                                                // STEP 3c: run the volume-mounted script.
                                                                Command: []string{"/scripts/task.sh"},
                                                        },
                                                },
                                        },
                                },
                        },
                }

                err = ctrl.SetControllerReference(myresource, job, r.Scheme)
                if err != nil {
                        return ctrl.Result{}, err
                }
                err = r.Create(ctx, job)
                if err != nil {
                        ctxlog.Error(err, "Failed to create Job")
                        return ctrl.Result{}, err
                }
                return ctrl.Result{Requeue: true}, nil
        }

        // Requeue if the job is not complete.
        if *job.Spec.Completions == 0 {
                ctxlog.Info("Requeuing to wait for Job to complete")
                return ctrl.Result{RequeueAfter: time.Second * 15}, nil
        }

        ctxlog.Info("All done")
        return ctrl.Result{}, nil
}

在我的操作员定义了作业后,剩下的就是等待作业的完成。看看我的操作员日志,我可以看到过程中的每个步骤都被记录下来,直到对账完成。

2022-08-07T18:25:11.739Z  INFO  controller.myresource   Creating new ConfigMap  {"reconciler group": "myoperator.myorg.com", "reconciler kind": "MyResource", "name": "myresource-example", "namespace": "default"}
2022-08-07T18:25:11.765Z  INFO  controller.myresource   Creating new Job        {"reconciler group": "myoperator.myorg.com", "reconciler kind": "MyResource", "name": "myresource-example", "namespace": "default"}
2022-08-07T18:25:11.780Z  INFO  controller.myresource   All done        {"reconciler group": "myoperator.myorg.com", "reconciler kind": "MyResource", "name": "myresource-example", "namespace": "default"}

选择Kubernetes

当涉及到在运营商管理的工作负载和应用程序中管理脚本时,go:embed ,为简化开发工作流程和抽象业务逻辑提供了一个有用的机制。随着你的操作者及其脚本变得越来越复杂,这种抽象和关注点的分离对于操作者的可维护性和清晰性变得越来越重要。