Kubernetes Operator 中打包 Job 脚本

使用 Go 将脚本嵌入到您的 Kubernetes Operator 中。
目前还没有读者喜欢这个。
Ships at sea on the web

当使用复杂的 Kubernetes Operator 时,您经常需要编排 Job 来执行工作负载任务。Job 实现示例通常提供直接在清单中编写的简单脚本。然而,在任何相当复杂的应用程序中,确定如何处理更复杂的脚本可能具有挑战性。

过去,我通过将脚本包含在应用程序镜像中来解决这个问题。这种方法效果不错,但确实有一个缺点。每当需要更改时,我都必须重建应用程序镜像以包含修订。这浪费了很多时间,特别是当我的应用程序镜像需要很长时间才能构建时。这也意味着我同时维护应用程序镜像和 Operator 镜像。如果我的 Operator 仓库不包含应用程序镜像,那么我将在仓库之间进行相关更改。最终,我增加了提交的数量,并使我的工作流程复杂化。每次更改都意味着我必须管理和同步仓库之间的提交和镜像引用。

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

嵌入 Job 脚本

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

这是我的简单示例脚本

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

现在开始处理 Operator 的逻辑。

Operator 逻辑

这是我的 Operator 协调过程中的过程

  1. 检索脚本的内容
  2. 将脚本的内容添加到 ConfigMap
  3. 通过以下方式在 Job 中运行 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
}

在我的 Operator 定义 Job 后,剩下的就是等待 Job 完成。查看我的 Operator 日志,我可以看到记录的流程中的每个步骤,直到协调完成

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"}

Go 适用于 Kubernetes

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

Bobby Gryzynger
我是一名在 Red Hat 工作的资深软件工程师,居住在纽约市地区。我在内容管理系统和云原生技术方面拥有丰富的经验。

评论已关闭。

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 获得许可。
© . All rights reserved.