Les opérateurs sont comme ces collègues surperformants qui savent toujours quoi faire. Ils étendent les capacités de Kubernetes, vous permettant d'automatiser la gestion d'applications complexes. Pensez à eux comme à vos gardiens personnels d'applications, surveillant l'état, apportant des modifications si nécessaire et s'assurant que tout fonctionne bien.

Kubernetes Operator SDK : Votre nouveau meilleur ami

Vous vous dites peut-être : "Génial, encore un outil à apprendre." Mais attendez ! Le Kubernetes Operator SDK est comme le couteau suisse du développement d'opérateurs (mais bien plus cool et moins cliché). C'est une boîte à outils qui simplifie le processus de création, de test et de maintenance des opérateurs.

Avec l'Operator SDK, vous pouvez :

  • Structurer votre projet d'opérateur plus rapidement que vous ne pouvez dire "Exception d'exécution Java"
  • Générer du code standard (parce que qui a le temps pour ça ?)
  • Tester votre opérateur sans sacrifier un cluster aux dieux de la démo
  • Emballer et déployer votre opérateur facilement

Quand personnaliser votre application Java

Avouons-le, certaines applications Java sont comme cet ami qui insiste pour utiliser un téléphone à clapet en 2023 – elles sont spéciales et nécessitent une attention particulière. Vous pourriez avoir besoin d'un opérateur personnalisé lorsque :

  • La configuration de votre application est plus complexe que votre dernière relation
  • Le déploiement et les mises à jour nécessitent un doctorat en science des fusées
  • Vous avez besoin de stratégies de basculement qui rendraient un casino de Vegas jaloux
  • Gérer les dépendances ressemble à rassembler des chats

Commencer : Operator SDK et Java, un duo parfait dans le monde de Kubernetes

Bien, retroussons nos manches et mettons-nous au travail. Tout d'abord, nous devons configurer notre environnement de développement :

Générez l'API pour votre ressource personnalisée :


operator-sdk create api --group=app --version=v1alpha1 --kind=QuarkusApp

Créez un nouveau projet d'opérateur :


mkdir quarkus-operator
cd quarkus-operator
operator-sdk init --domain=example.com --repo=github.com/example/quarkus-operator

Installez l'Operator SDK (parce que la magie ne se fait pas sans outils) :


# Pour les utilisateurs de macOS (en supposant que vous ayez Homebrew)
brew install operator-sdk

# Pour les courageux utilisant Linux
curl -LO https://github.com/operator-framework/operator-sdk/releases/latest/download/operator-sdk_linux_amd64
chmod +x operator-sdk_linux_amd64
sudo mv operator-sdk_linux_amd64 /usr/local/bin/operator-sdk

Félicitations ! Vous venez de poser les bases de votre opérateur d'application Quarkus. C'est comme planter une graine, sauf que celle-ci se transforme en un système de gestion d'applications complet.

Créer votre opérateur personnalisé : la partie amusante

Maintenant que notre projet est configuré, il est temps d'ajouter un peu de magie. Nous allons créer une définition de ressource personnalisée (CRD) qui décrit les propriétés uniques de notre application Quarkus et un contrôleur pour gérer son cycle de vie.

Tout d'abord, définissons notre CRD. Ouvrez le fichier api/v1alpha1/quarkusapp_types.go et ajoutez quelques champs :


type QuarkusAppSpec struct {
	// INSÉRER DES CHAMPS DE SPÉCIFICATION SUPPLÉMENTAIRES
	Image string `json:"image"`
	Replicas int32 `json:"replicas"`
	ConfigMap string `json:"configMap,omitempty"`
}

type QuarkusAppStatus struct {
	// INSÉRER DES CHAMPS DE STATUT SUPPLÉMENTAIRES
	Nodes []string `json:"nodes"`
}

Maintenant, implémentons la logique du contrôleur. Ouvrez controllers/quarkusapp_controller.go et ajoutez du contenu à la fonction Reconcile :


func (r *QuarkusAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	log := r.Log.WithValues("quarkusapp", req.NamespacedName)

	// Récupérer l'instance QuarkusApp
	quarkusApp := &appv1alpha1.QuarkusApp{}
	err := r.Get(ctx, req.NamespacedName, quarkusApp)
	if err != nil {
		if errors.IsNotFound(err) {
			// Objet de requête non trouvé, pourrait avoir été supprimé après la demande de réconciliation.
			// Retourner et ne pas réenfile
			log.Info("Ressource QuarkusApp non trouvée. Ignorer car l'objet doit être supprimé")
			return ctrl.Result{}, nil
		}
		// Erreur de lecture de l'objet - réenfile la demande.
		log.Error(err, "Échec de la récupération de QuarkusApp")
		return ctrl.Result{}, err
	}

	// Vérifier si le déploiement existe déjà, sinon créer un nouveau
	found := &appsv1.Deployment{}
	err = r.Get(ctx, types.NamespacedName{Name: quarkusApp.Name, Namespace: quarkusApp.Namespace}, found)
	if err != nil && errors.IsNotFound(err) {
		// Définir un nouveau déploiement
		dep := r.deploymentForQuarkusApp(quarkusApp)
		log.Info("Création d'un nouveau déploiement", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
		err = r.Create(ctx, dep)
		if err != nil {
			log.Error(err, "Échec de la création du nouveau déploiement", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
			return ctrl.Result{}, err
		}
		// Déploiement créé avec succès - retourner et réenfile
		return ctrl.Result{Requeue: true}, nil
	} else if err != nil {
		log.Error(err, "Échec de la récupération du déploiement")
		return ctrl.Result{}, err
	}

	// S'assurer que la taille du déploiement est la même que celle de la spécification
	size := quarkusApp.Spec.Replicas
	if *found.Spec.Replicas != size {
		found.Spec.Replicas = &size
		err = r.Update(ctx, found)
		if err != nil {
			log.Error(err, "Échec de la mise à jour du déploiement", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)
			return ctrl.Result{}, err
		}
		// Spécification mise à jour - retourner et réenfile
		return ctrl.Result{Requeue: true}, nil
	}

	// Mettre à jour le statut de QuarkusApp avec les noms des pods
	// Lister les pods pour le déploiement de cette QuarkusApp
	podList := &corev1.PodList{}
	listOpts := []client.ListOption{
		client.InNamespace(quarkusApp.Namespace),
		client.MatchingLabels(labelsForQuarkusApp(quarkusApp.Name)),
	}
	if err = r.List(ctx, podList, listOpts...); err != nil {
		log.Error(err, "Échec de la liste des pods", "QuarkusApp.Namespace", quarkusApp.Namespace, "QuarkusApp.Name", quarkusApp.Name)
		return ctrl.Result{}, err
	}
	podNames := getPodNames(podList.Items)

	// Mettre à jour status.Nodes si nécessaire
	if !reflect.DeepEqual(podNames, quarkusApp.Status.Nodes) {
		quarkusApp.Status.Nodes = podNames
		err := r.Status().Update(ctx, quarkusApp)
		if err != nil {
			log.Error(err, "Échec de la mise à jour du statut de QuarkusApp")
			return ctrl.Result{}, err
		}
	}

	return ctrl.Result{}, nil
}

Ce contrôleur créera un déploiement pour notre application Quarkus, s'assurera que le nombre de répliques correspond à la spécification et mettra à jour le statut avec la liste des noms de pods.

Rendre votre opérateur infaillible

Maintenant que nous avons un opérateur de base, ajoutons-lui des super-pouvoirs pour le rendre résilient et auto-réparateur. Nous allons implémenter une récupération automatique et un redimensionnement basé sur l'état de l'application.

Ajoutez ceci à votre contrôleur :


func (r *QuarkusAppReconciler) checkAndHeal(ctx context.Context, quarkusApp *appv1alpha1.QuarkusApp) error {
	// Vérifier la santé des pods
	podList := &corev1.PodList{}
	listOpts := []client.ListOption{
		client.InNamespace(quarkusApp.Namespace),
		client.MatchingLabels(labelsForQuarkusApp(quarkusApp.Name)),
	}
	if err := r.List(ctx, podList, listOpts...); err != nil {
		return err
	}

	unhealthyPods := 0
	for _, pod := range podList.Items {
		if pod.Status.Phase != corev1.PodRunning {
			unhealthyPods++
		}
	}

	// Si plus de 50 % des pods sont en mauvaise santé, déclencher un redémarrage progressif
	if float32(unhealthyPods)/float32(len(podList.Items)) > 0.5 {
		deployment := &appsv1.Deployment{}
		err := r.Get(ctx, types.NamespacedName{Name: quarkusApp.Name, Namespace: quarkusApp.Namespace}, deployment)
		if err != nil {
			return err
		}

		// Déclencher un redémarrage progressif en mettant à jour une annotation
		if deployment.Spec.Template.Annotations == nil {
			deployment.Spec.Template.Annotations = make(map[string]string)
		}
		deployment.Spec.Template.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339)

		err = r.Update(ctx, deployment)
		if err != nil {
			return err
		}
	}

	return nil
}

N'oubliez pas d'appeler cette fonction dans votre boucle Reconcile :


if err := r.checkAndHeal(ctx, quarkusApp); err != nil {
	log.Error(err, "Échec de la guérison de QuarkusApp")
	return ctrl.Result{}, err
}

Automatiser les mises à jour : Parce que qui a le temps pour le travail manuel ?

Ajoutons un peu de magie d'automatisation pour gérer les mises à jour. Nous allons créer une fonction qui vérifie les nouvelles versions de notre application Quarkus et déclenche une mise à jour si nécessaire :


func (r *QuarkusAppReconciler) checkAndUpdate(ctx context.Context, quarkusApp *appv1alpha1.QuarkusApp) error {
	// Dans un scénario réel, vous vérifieriez une source externe pour la dernière version
	// Pour cet exemple, nous utiliserons une annotation sur le CR pour simuler une nouvelle version
	newVersion, exists := quarkusApp.Annotations["newVersion"]
	if !exists {
		return nil // Aucune nouvelle version disponible
	}

	deployment := &appsv1.Deployment{}
	err := r.Get(ctx, types.NamespacedName{Name: quarkusApp.Name, Namespace: quarkusApp.Namespace}, deployment)
	if err != nil {
		return err
	}

	// Mettre à jour l'image vers la nouvelle version
	for i, container := range deployment.Spec.Template.Spec.Containers {
		if container.Name == quarkusApp.Name {
			deployment.Spec.Template.Spec.Containers[i].Image = newVersion
			break
		}
	}

	// Mettre à jour le déploiement
	err = r.Update(ctx, deployment)
	if err != nil {
		return err
	}

	// Supprimer l'annotation pour éviter les mises à jour continues
	delete(quarkusApp.Annotations, "newVersion")
	return r.Update(ctx, quarkusApp)
}

Encore une fois, appelez cette fonction dans votre boucle Reconcile :


if err := r.checkAndUpdate(ctx, quarkusApp); err != nil {
	log.Error(err, "Échec de la mise à jour de QuarkusApp")
	return ctrl.Result{}, err
}

Intégration avec des ressources externes : Parce qu'aucune application n'est une île

La plupart des applications Quarkus doivent interagir avec des ressources externes comme des bases de données ou des caches. Ajoutons un peu de logique pour gérer ces dépendances :


func (r *QuarkusAppReconciler) ensureDatabaseExists(ctx context.Context, quarkusApp *appv1alpha1.QuarkusApp) error {
	// Vérifier si une base de données est spécifiée dans le CR
	if quarkusApp.Spec.Database == "" {
		return nil // Aucune base de données nécessaire
	}

	// Vérifier si la base de données existe
	database := &v1alpha1.Database{}
	err := r.Get(ctx, types.NamespacedName{Name: quarkusApp.Spec.Database, Namespace: quarkusApp.Namespace}, database)
	if err != nil && errors.IsNotFound(err) {
		// La base de données n'existe pas, créons-la
		newDB := &v1alpha1.Database{
			ObjectMeta: metav1.ObjectMeta{
				Name:      quarkusApp.Spec.Database,
				Namespace: quarkusApp.Namespace,
			},
			Spec: v1alpha1.DatabaseSpec{
				Engine:  "postgres",
				Version: "12",
			},
		}
		err = r.Create(ctx, newDB)
		if err != nil {
			return err
		}
	} else if err != nil {
		return err
	}

	// La base de données existe, s'assurer que notre application a les bonnes informations de connexion
	secret := &corev1.Secret{}
	err = r.Get(ctx, types.NamespacedName{Name: database.Status.CredentialsSecret, Namespace: quarkusApp.Namespace}, secret)
	if err != nil {
		return err
	}

	// Mettre à jour les variables d'environnement de l'application Quarkus avec les informations de connexion à la base de données
	deployment := &appsv1.Deployment{}
	err = r.Get(ctx, types.NamespacedName{Name: quarkusApp.Name, Namespace: quarkusApp.Namespace}, deployment)
	if err != nil {
		return err
	}

	envVars := []corev1.EnvVar{
		{
			Name: "DB_URL",
			Value: fmt.Sprintf("jdbc:postgresql://%s:%d/%s",
				database.Status.Host,
				database.Status.Port,
				database.Status.Database),
		},
		{
			Name: "DB_USER",
			ValueFrom: &corev1.EnvVarSource{
				SecretKeyRef: &corev1.SecretKeySelector{
					LocalObjectReference: corev1.LocalObjectReference{
						Name: secret.Name,
					},
					Key: "username",
				},
			},
		},
		{
			Name: "DB_PASSWORD",
			ValueFrom: &corev1.EnvVarSource{
				SecretKeyRef: &corev1.SecretKeySelector{
					LocalObjectReference: corev1.LocalObjectReference{
						Name: secret.Name,
					},
					Key: "password",
				},
			},
		},
	}

	// Mettre à jour les variables d'environnement du déploiement
	for i, container := range deployment.Spec.Template.Spec.Containers {
		if container.Name == quarkusApp.Name {
			deployment.Spec.Template.Spec.Containers[i].Env = append(container.Env, envVars...)
			break
		}
	}

	return r.Update(ctx, deployment)
}

N'oubliez pas d'appeler cette fonction dans votre boucle Reconcile également !

Surveillance et journalisation : Parce que voler à l'aveugle n'est pas amusant

Pour garder un œil sur notre opérateur et notre application Quarkus, ajoutons des capacités de surveillance et de journalisation. Nous utiliserons Prometheus pour les métriques et nous intégrerons au système de journalisation de Kubernetes.

Tout d'abord, ajoutons quelques métriques à notre opérateur. Ajoutez ceci à votre contrôleur :


var (
	reconcileCount = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "quarkusapp_reconcile_total",
			Help: "Le nombre total de réconciliations par QuarkusApp",
		},
		[]string{"quarkusapp"},
	)
	reconcileErrors = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "quarkusapp_reconcile_errors_total",
			Help: "Le nombre total d'erreurs de réconciliation par QuarkusApp",
		},
		[]string{"quarkusapp"},
	)
)

func init() {
	metrics.Registry.MustRegister(reconcileCount, reconcileErrors)
}

Maintenant, mettez à jour votre fonction Reconcile pour utiliser ces métriques :


func (r *QuarkusAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	log := r.Log.WithValues("quarkusapp", req.NamespacedName)

	// Incrémenter le compteur de réconciliation
	reconcileCount.WithLabelValues(req.NamespacedName.String()).Inc()

	// ... reste de votre logique de réconciliation ...

	if err != nil {
		// Incrémenter le compteur d'erreurs
		reconcileErrors.WithLabelValues(req.NamespacedName.String()).Inc()
		log.Error(err, "Échec de la réconciliation")
		return ctrl.Result{}, err
	}

	return ctrl.Result{}, nil
}

Pour la journalisation, nous utilisons déjà le logger de controller-runtime. Ajoutons quelques journaux plus détaillés :


log.Info("Début de la réconciliation", "QuarkusApp", quarkusApp.Name)

// ... après vérification et guérison ...
log.Info("Vérification de la santé terminée", "UnhealthyPods", unhealthyPods)

// ... après mise à jour ...
log.Info("Vérification de la mise à jour terminée", "NewVersion", newVersion)

// ... après s'être assuré que la base de données existe ...
log.Info("Vérification de la base de données terminée", "Database", quarkusApp.Spec.Database)

log.Info("Réconciliation terminée avec succès", "QuarkusApp", quarkusApp.Name)

Conclusion : Vous êtes maintenant un magicien des opérateurs Kubernetes !

Félicitations ! Vous venez de créer un opérateur Kubernetes personnalisé pour votre application Quarkus originale. Récapitulons ce que nous avons accompli :

  • Configuration d'un projet à l'aide de Kubernetes Operator SDK
  • Création d'une définition de ressource personnalisée pour notre application Quarkus
  • Implémentation d'un contrôleur pour gérer le cycle de vie de l'application
  • Ajout de capacités d'auto-guérison et de mise à jour automatique
  • Intégration avec des ressources externes comme des bases de données
  • Mise en place de la surveillance et de la journalisation pour notre opérateur

Rappelez-vous, avec un grand pouvoir vient une grande responsabilité. Votre opérateur personnalisé est maintenant en charge de la gestion de votre application Quarkus, alors assurez-vous de le tester soigneusement avant de le déployer sur votre cluster de production.

Alors que vous continuez votre voyage dans le monde des opérateurs Kubernetes, continuez à explorer et à expérimenter. Les possibilités sont infinies, et qui sait ? Vous pourriez bien créer la prochaine grande innovation dans la gestion des applications cloud-native.

Maintenant, allez de l'avant et opérez avec confiance, vous magnifique magicien de Kubernetes !

"Dans le monde de Kubernetes, l'opérateur est la baguette, et vous, mon ami, êtes le magicien." - Probablement Dumbledore s'il était ingénieur DevOps

Bon codage, et que vos pods soient toujours en bonne santé et vos clusters toujours évolutifs !