Empaquetage des scripts de Job dans les opérateurs Kubernetes


  • Français


  • Lorsque vous utilisez un opérateur Kubernetes complexe, vous devez souvent orchestrer des Jobs pour effectuer des tâches de charge de travail. Exemples d’implémentations de Job fournissent généralement des scripts triviaux écrits directement dans le manifeste. Cependant, dans toute application raisonnablement complexe, il peut être difficile de déterminer comment gérer des scripts plus que triviaux.

    Dans le passé, j’ai abordé ce problème en incluant mes scripts dans une image d’application. Cette approche fonctionne assez bien, mais elle a un inconvénient. Chaque fois que des modifications sont nécessaires, je suis obligé de reconstruire l’image de l’application pour inclure les révisions. C’est beaucoup de temps perdu, surtout lorsque l’image de mon application prend beaucoup de temps à se construire. Cela signifie également que je maintiens à la fois une image d’application et une image d’opérateur. Si mon référentiel d’opérateur n’inclut pas l’image de l’application, j’apporte des modifications connexes dans les référentiels. En fin de compte, je multiplie le nombre de commits que je fais et complique mon workflow. Chaque changement signifie que je dois gérer et synchroniser les commits et les références d’image entre les référentiels.

    Compte tenu de ces défis, je voulais trouver un moyen de conserver mes scripts de travail dans la base de code de mon opérateur. De cette façon, j’ai pu réviser mes scripts en tandem avec la logique de réconciliation de mon opérateur. Mon objectif était de concevoir un flux de travail qui ne me demanderait de reconstruire l’image de l’opérateur que lorsque j’aurais besoin de faire des révisions à mes scripts. Heureusement, j’utilise le langage de programmation Go, qui fournit l’immensément utile go:embed caractéristique. Cela permet aux développeurs de conditionner des fichiers texte avec le binaire de leur application. En tirant parti de cette fonctionnalité, j’ai découvert que je pouvais conserver mes scripts de travail à l’image de mon opérateur.

    Incorporer le script de tâche

    À des fins de démonstration, mon script de tâche n’inclut aucune logique métier réelle. Cependant, en utilisant un script intégré plutôt qu’en écrivant le script directement dans le manifeste du travail, cette approche conserve les scripts complexes à la fois bien organisés et abstraits de la définition du travail elle-même.

    Voici mon exemple de script simple :

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

    Passons maintenant à la logique de l’opérateur.

    Logique de l’opérateur

    Voici le processus dans le rapprochement de mon opérateur :

    1. Récupérer le contenu du script
    2. Ajouter le contenu du script à un ConfigMap
    3. Exécutez le script de ConfigMap dans le Job en
      1. Définir un volume faisant référence à la ConfigMap
      2. Rendre le contenu du volume exécutable
      3. Monter le volume sur le Job

    Voici le code :

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

    Une fois que mon opérateur a défini le travail, il ne reste plus qu’à attendre que le travail se termine. En regardant les journaux de mon opérateur, je peux voir chaque étape du processus enregistrée jusqu’à ce que le rapprochement soit terminé :

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

    Optez pour Kubernetes

    Lorsqu’il s’agit de gérer des scripts dans des charges de travail et des applications gérées par l’opérateur, go:embed fournit un mécanisme utile pour simplifier le workflow de développement et résumer la logique métier. Au fur et à mesure que votre opérateur et ses scripts deviennent plus complexes, ce type d’abstraction et de séparation des préoccupations devient de plus en plus important pour la maintenabilité et la clarté de votre opérateur.

    Source

    Houssen Moshinaly

    Pour contacter personnellement le taulier :

    Laisser un commentaire

    Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

    Copy code