Gli operatori sono come quei colleghi super efficienti che sanno sempre cosa fare. Estendono le capacità di Kubernetes, permettendoti di automatizzare la gestione di applicazioni complesse. Pensali come i tuoi babysitter personali per le app, che tengono d'occhio lo stato, apportano modifiche quando necessario e assicurano che tutto funzioni senza intoppi.

Kubernetes Operator SDK: Il tuo nuovo migliore amico

Ora potresti pensare: "Fantastico, un altro strumento da imparare." Ma aspetta un attimo! Il Kubernetes Operator SDK è come il coltellino svizzero dello sviluppo di operatori (ma molto più cool e meno banale). È un toolkit che semplifica il processo di creazione, test e manutenzione degli operatori.

Con Operator SDK, puoi:

  • Impostare il tuo progetto operatore più velocemente di quanto tu possa dire "Eccezione di runtime Java"
  • Generare codice boilerplate (perché chi ha tempo per quello?)
  • Testare il tuo operatore senza sacrificare un cluster agli dei delle demo
  • Imballare e distribuire il tuo operatore con facilità

Quando personalizzare la tua app Java

Ammettiamolo, alcune app Java sono come quell'amico che insiste nell'usare un telefono a conchiglia nel 2023: sono speciali e richiedono un'attenzione extra. Potresti aver bisogno di un operatore personalizzato quando:

  • La configurazione della tua app è più complessa della tua ultima relazione
  • Il deployment e gli aggiornamenti richiedono un dottorato in ingegneria aerospaziale
  • Hai bisogno di strategie di failover che farebbero invidia a un casinò di Las Vegas
  • Gestire le dipendenze sembra come radunare gatti

Iniziare: Operator SDK e Java, un'accoppiata perfetta in Kubernetes

Bene, rimbocchiamoci le maniche e mettiamoci al lavoro. Prima di tutto, dobbiamo configurare il nostro ambiente di sviluppo:

Genera l'API per la tua risorsa personalizzata:


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

Crea un nuovo progetto operatore:


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

Installa Operator SDK (perché la magia non accade senza strumenti):


# Per gli utenti macOS (supponendo che tu abbia Homebrew)
brew install operator-sdk

# Per i coraggiosi che usano 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

Congratulazioni! Hai appena gettato le basi per il tuo operatore dell'app Quarkus. È come piantare un seme, solo che questo cresce in un sistema di gestione delle app a tutti gli effetti.

Creare il tuo operatore personalizzato: la parte divertente

Ora che abbiamo impostato il nostro progetto, è il momento di aggiungere un po' di vera magia. Creeremo una Custom Resource Definition (CRD) che descrive le proprietà uniche della nostra app Quarkus e un controller per gestirne il ciclo di vita.

Per prima cosa, definiamo la nostra CRD. Apri il file api/v1alpha1/quarkusapp_types.go e aggiungi alcuni campi:


type QuarkusAppSpec struct {
	// INSERISCI CAMPI SPECIFICI AGGIUNTIVI
	Image string `json:"image"`
	Replicas int32 `json:"replicas"`
	ConfigMap string `json:"configMap,omitempty"`
}

type QuarkusAppStatus struct {
	// INSERISCI CAMPI DI STATO AGGIUNTIVI
	Nodes []string `json:"nodes"`
}

Ora, implementiamo la logica del controller. Apri controllers/quarkusapp_controller.go e aggiungi del contenuto alla funzione Reconcile:


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

	// Recupera l'istanza di QuarkusApp
	quarkusApp := &appv1alpha1.QuarkusApp{}
	err := r.Get(ctx, req.NamespacedName, quarkusApp)
	if err != nil {
		if errors.IsNotFound(err) {
			// Oggetto richiesta non trovato, potrebbe essere stato eliminato dopo la richiesta di riconciliazione.
			// Ritorna e non rimettere in coda
			log.Info("Risorsa QuarkusApp non trovata. Ignorando poiché l'oggetto deve essere eliminato")
			return ctrl.Result{}, nil
		}
		// Errore nella lettura dell'oggetto - rimetti in coda la richiesta.
		log.Error(err, "Impossibile ottenere QuarkusApp")
		return ctrl.Result{}, err
	}

	// Controlla se il deployment esiste già, altrimenti creane uno nuovo
	found := &appsv1.Deployment{}
	err = r.Get(ctx, types.NamespacedName{Name: quarkusApp.Name, Namespace: quarkusApp.Namespace}, found)
	if err != nil && errors.IsNotFound(err) {
		// Definisci un nuovo deployment
		dep := r.deploymentForQuarkusApp(quarkusApp)
		log.Info("Creazione di un nuovo Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
		err = r.Create(ctx, dep)
		if err != nil {
			log.Error(err, "Impossibile creare un nuovo Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
			return ctrl.Result{}, err
		}
		// Deployment creato con successo - ritorna e rimetti in coda
		return ctrl.Result{Requeue: true}, nil
	} else if err != nil {
		log.Error(err, "Impossibile ottenere il Deployment")
		return ctrl.Result{}, err
	}

	// Assicurati che la dimensione del deployment sia la stessa dello spec
	size := quarkusApp.Spec.Replicas
	if *found.Spec.Replicas != size {
		found.Spec.Replicas = &size
		err = r.Update(ctx, found)
		if err != nil {
			log.Error(err, "Impossibile aggiornare il Deployment", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)
			return ctrl.Result{}, err
		}
		// Spec aggiornato - ritorna e rimetti in coda
		return ctrl.Result{Requeue: true}, nil
	}

	// Aggiorna lo stato di QuarkusApp con i nomi dei pod
	// Elenca i pod per il deployment di questa 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, "Impossibile elencare i pod", "QuarkusApp.Namespace", quarkusApp.Namespace, "QuarkusApp.Name", quarkusApp.Name)
		return ctrl.Result{}, err
	}
	podNames := getPodNames(podList.Items)

	// Aggiorna status.Nodes se necessario
	if !reflect.DeepEqual(podNames, quarkusApp.Status.Nodes) {
		quarkusApp.Status.Nodes = podNames
		err := r.Status().Update(ctx, quarkusApp)
		if err != nil {
			log.Error(err, "Impossibile aggiornare lo stato di QuarkusApp")
			return ctrl.Result{}, err
		}
	}

	return ctrl.Result{}, nil
}

Questo controller creerà un deployment per la nostra app Quarkus, assicurerà che il numero di repliche corrisponda allo spec e aggiornerà lo stato con l'elenco dei nomi dei pod.

Rendere il tuo operatore a prova di proiettile

Ora che abbiamo un operatore di base, aggiungiamo alcuni superpoteri per renderlo resiliente e auto-riparante. Implementeremo il recupero automatico e la scalabilità basata sullo stato dell'applicazione.

Aggiungi questo al tuo controller:


func (r *QuarkusAppReconciler) checkAndHeal(ctx context.Context, quarkusApp *appv1alpha1.QuarkusApp) error {
	// Controlla la salute dei pod
	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++
		}
	}

	// Se più del 50% dei pod è malsano, avvia un riavvio a rotazione
	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
		}

		// Avvia un riavvio a rotazione aggiornando un'annotazione
		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
}

Non dimenticare di chiamare questa funzione nel tuo ciclo Reconcile:


if err := r.checkAndHeal(ctx, quarkusApp); err != nil {
	log.Error(err, "Impossibile guarire QuarkusApp")
	return ctrl.Result{}, err
}

Automatizzare gli aggiornamenti: perché chi ha tempo per il lavoro manuale?

Aggiungiamo un po' di magia automatica per gestire gli aggiornamenti. Creeremo una funzione che controlla le nuove versioni della nostra app Quarkus e avvia un aggiornamento quando necessario:


func (r *QuarkusAppReconciler) checkAndUpdate(ctx context.Context, quarkusApp *appv1alpha1.QuarkusApp) error {
	// In uno scenario reale, controlleresti una fonte esterna per l'ultima versione
	// Per questo esempio, useremo un'annotazione sul CR per simulare una nuova versione
	newVersion, exists := quarkusApp.Annotations["newVersion"]
	if !exists {
		return nil // Nessuna nuova versione disponibile
	}

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

	// Aggiorna l'immagine alla nuova versione
	for i, container := range deployment.Spec.Template.Spec.Containers {
		if container.Name == quarkusApp.Name {
			deployment.Spec.Template.Spec.Containers[i].Image = newVersion
			break
		}
	}

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

	// Rimuovi l'annotazione per prevenire aggiornamenti continui
	delete(quarkusApp.Annotations, "newVersion")
	return r.Update(ctx, quarkusApp)
}

Ancora una volta, chiama questa funzione nel tuo ciclo Reconcile:


if err := r.checkAndUpdate(ctx, quarkusApp); err != nil {
	log.Error(err, "Impossibile aggiornare QuarkusApp")
	return ctrl.Result{}, err
}

Integrazione con risorse esterne: perché nessuna app è un'isola

La maggior parte delle app Quarkus deve interagire con risorse esterne come database o cache. Aggiungiamo un po' di logica per gestire queste dipendenze:


func (r *QuarkusAppReconciler) ensureDatabaseExists(ctx context.Context, quarkusApp *appv1alpha1.QuarkusApp) error {
	// Controlla se un database è specificato nel CR
	if quarkusApp.Spec.Database == "" {
		return nil // Nessun database necessario
	}

	// Controlla se il database esiste
	database := &v1alpha1.Database{}
	err := r.Get(ctx, types.NamespacedName{Name: quarkusApp.Spec.Database, Namespace: quarkusApp.Namespace}, database)
	if err != nil && errors.IsNotFound(err) {
		// Il database non esiste, creiamolo
		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
	}

	// Il database esiste, assicurati che la nostra app abbia le informazioni di connessione corrette
	secret := &corev1.Secret{}
	err = r.Get(ctx, types.NamespacedName{Name: database.Status.CredentialsSecret, Namespace: quarkusApp.Namespace}, secret)
	if err != nil {
		return err
	}

	// Aggiorna le variabili d'ambiente dell'app Quarkus con le informazioni di connessione al database
	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",
				},
			},
		},
	}

	// Aggiorna le variabili d'ambiente del deployment
	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)
}

Non dimenticare di chiamare questa funzione anche nel tuo ciclo Reconcile!

Monitoraggio e logging: perché volare alla cieca non è divertente

Per tenere d'occhio il nostro operatore e l'app Quarkus, aggiungiamo alcune capacità di monitoraggio e logging. Useremo Prometheus per le metriche e ci integreremo con il sistema di logging di Kubernetes.

Per prima cosa, aggiungiamo alcune metriche al nostro operatore. Aggiungi questo al tuo controller:


var (
	reconcileCount = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "quarkusapp_reconcile_total",
			Help: "Il numero totale di riconciliazioni per QuarkusApp",
		},
		[]string{"quarkusapp"},
	)
	reconcileErrors = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "quarkusapp_reconcile_errors_total",
			Help: "Il numero totale di errori di riconciliazione per QuarkusApp",
		},
		[]string{"quarkusapp"},
	)
)

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

Ora, aggiorna la tua funzione Reconcile per utilizzare queste metriche:


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

	// Incrementa il conteggio delle riconciliazioni
	reconcileCount.WithLabelValues(req.NamespacedName.String()).Inc()

	// ... resto della tua logica di riconciliazione ...

	if err != nil {
		// Incrementa il conteggio degli errori
		reconcileErrors.WithLabelValues(req.NamespacedName.String()).Inc()
		log.Error(err, "Riconciliazione fallita")
		return ctrl.Result{}, err
	}

	return ctrl.Result{}, nil
}

Per il logging, stiamo già utilizzando il logger di controller-runtime. Aggiungiamo un po' di logging più dettagliato:


log.Info("Inizio della riconciliazione", "QuarkusApp", quarkusApp.Name)

// ... dopo il controllo e la guarigione ...
log.Info("Controllo della salute completato", "UnhealthyPods", unhealthyPods)

// ... dopo l'aggiornamento ...
log.Info("Controllo dell'aggiornamento completato", "NewVersion", newVersion)

// ... dopo aver assicurato l'esistenza del database ...
log.Info("Controllo del database completato", "Database", quarkusApp.Spec.Database)

log.Info("Riconciliazione completata con successo", "QuarkusApp", quarkusApp.Name)

Conclusione: ora sei un mago degli operatori Kubernetes!

Congratulazioni! Hai appena creato un operatore Kubernetes personalizzato per la tua eccentrica applicazione Quarkus. Ricapitoliamo ciò che abbiamo realizzato:

  • Impostato un progetto utilizzando il Kubernetes Operator SDK
  • Creato una Custom Resource Definition per la nostra app Quarkus
  • Implementato un controller per gestire il ciclo di vita dell'app
  • Aggiunto capacità di auto-guarigione e aggiornamento automatico
  • Integrato con risorse esterne come database
  • Impostato il monitoraggio e il logging per il nostro operatore

Ricorda, con grande potere viene grande responsabilità. Il tuo operatore personalizzato è ora incaricato di gestire la tua applicazione Quarkus, quindi assicurati di testarlo a fondo prima di rilasciarlo sul tuo cluster di produzione.

Man mano che continui il tuo viaggio nel mondo degli operatori Kubernetes, continua a esplorare e sperimentare. Le possibilità sono infinite, e chissà? Potresti creare la prossima grande novità nella gestione delle applicazioni cloud-native.

Ora vai avanti e opera con fiducia, magnifico mago di Kubernetes!

"Nel mondo di Kubernetes, l'operatore è la bacchetta, e tu, amico mio, sei il mago." - Probabilmente Silente se fosse un ingegnere DevOps

Buona programmazione, e che i tuoi pod siano sempre sani e i tuoi cluster sempre scalabili!