Operators zijn als die overijverige collega's die altijd weten wat ze moeten doen. Ze breiden de mogelijkheden van Kubernetes uit, waardoor je het beheer van complexe applicaties kunt automatiseren. Zie ze als je persoonlijke app-oppassers, die de status in de gaten houden, wijzigingen aanbrengen wanneer dat nodig is en ervoor zorgen dat alles soepel verloopt.

Kubernetes Operator SDK: Je Nieuwe Beste Vriend

Nu denk je misschien: "Geweldig, weer een tool om te leren." Maar wacht even! De Kubernetes Operator SDK is als het Zwitserse zakmes van operatorontwikkeling (maar dan veel cooler en minder cliché). Het is een toolkit die het proces van het maken, testen en onderhouden van operators vereenvoudigt.

Met Operator SDK kun je:

  • Je operatorproject sneller opzetten dan je "Java Runtime Exception" kunt zeggen
  • Standaardcode genereren (want wie heeft daar tijd voor?)
  • Je operator testen zonder een cluster op te offeren aan de demogoden
  • Je operator eenvoudig verpakken en implementeren

Wanneer Je Eigen Aanpassingen Moet Maken voor Je Java-app

Laten we eerlijk zijn, sommige Java-apps zijn als die ene vriend die in 2023 nog steeds een klaptelefoon gebruikt – ze zijn speciaal en hebben extra aandacht nodig. Je hebt misschien een aangepaste operator nodig wanneer:

  • De configuratie van je app complexer is dan je laatste relatie
  • Implementatie en updates een PhD in raketwetenschap vereisen
  • Je failoverstrategieën nodig hebt die een casino in Vegas jaloers zouden maken
  • Het beheren van afhankelijkheden voelt als het hoeden van katten

Aan de Slag: Operator SDK en Java, een Match Made in Kubernetes Heaven

Oké, laten we de mouwen opstropen en aan de slag gaan. Allereerst moeten we onze ontwikkelomgeving opzetten:

Genereer de API voor je Custom Resource:


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

Maak een nieuw operatorproject aan:


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

Installeer Operator SDK (want magie gebeurt niet zonder tools):


# Voor macOS-gebruikers (ervan uitgaande dat je Homebrew hebt)
brew install operator-sdk

# Voor de dappere zielen die Linux gebruiken
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
    

Gefeliciteerd! Je hebt zojuist de basis gelegd voor je Quarkus-appoperator. Het is als het planten van een zaadje, behalve dat dit uitgroeit tot een volwaardig app-beheersysteem.

Je Eigen Operator Maken: Het Leuke Deel

Nu we ons project hebben opgezet, is het tijd om wat echte magie toe te voegen. We maken een Custom Resource Definition (CRD) die de unieke eigenschappen van onze Quarkus-app beschrijft en een controller om de levenscyclus ervan te beheren.

Laten we eerst onze CRD definiëren. Open het bestand api/v1alpha1/quarkusapp_types.go en voeg enkele velden toe:


type QuarkusAppSpec struct {
	// VOEG EXTRA SPEC VELDEN TOE
	Image string `json:"image"`
	Replicas int32 `json:"replicas"`
	ConfigMap string `json:"configMap,omitempty"`
}

type QuarkusAppStatus struct {
	// VOEG EXTRA STATUSVELD TOE
	Nodes []string `json:"nodes"`
}

Nu implementeren we de controllerlogica. Open controllers/quarkusapp_controller.go en voeg wat inhoud toe aan de Reconcile functie:


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

	// Haal de QuarkusApp-instantie op
	quarkusApp := &appv1alpha1.QuarkusApp{}
	err := r.Get(ctx, req.NamespacedName, quarkusApp)
	if err != nil {
		if errors.IsNotFound(err) {
			// Verzoekobject niet gevonden, kan na reconciliatieverzoek zijn verwijderd.
			// Retourneer en niet opnieuw in de wachtrij plaatsen
			log.Info("QuarkusApp-resource niet gevonden. Negeer omdat object moet worden verwijderd")
			return ctrl.Result{}, nil
		}
		// Fout bij het lezen van het object - plaats het verzoek opnieuw in de wachtrij.
		log.Error(err, "Kon QuarkusApp niet ophalen")
		return ctrl.Result{}, err
	}

	// Controleer of de implementatie al bestaat, zo niet, maak een nieuwe aan
	found := &appsv1.Deployment{}
	err = r.Get(ctx, types.NamespacedName{Name: quarkusApp.Name, Namespace: quarkusApp.Namespace}, found)
	if err != nil && errors.IsNotFound(err) {
		// Definieer een nieuwe implementatie
		dep := r.deploymentForQuarkusApp(quarkusApp)
		log.Info("Een nieuwe implementatie maken", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
		err = r.Create(ctx, dep)
		if err != nil {
			log.Error(err, "Kon nieuwe implementatie niet maken", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
			return ctrl.Result{}, err
		}
		// Implementatie succesvol gemaakt - retourneer en opnieuw in de wachtrij plaatsen
		return ctrl.Result{Requeue: true}, nil
	} else if err != nil {
		log.Error(err, "Kon implementatie niet ophalen")
		return ctrl.Result{}, err
	}

	// Zorg ervoor dat de implementatiegrootte overeenkomt met de specificatie
	size := quarkusApp.Spec.Replicas
	if *found.Spec.Replicas != size {
		found.Spec.Replicas = &size
		err = r.Update(ctx, found)
		if err != nil {
			log.Error(err, "Kon implementatie niet bijwerken", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)
			return ctrl.Result{}, err
		}
		// Specificatie bijgewerkt - retourneer en opnieuw in de wachtrij plaatsen
		return ctrl.Result{Requeue: true}, nil
	}

	// Werk de status van de QuarkusApp bij met de podnamen
	// Lijst de pods op voor de implementatie van deze 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, "Kon pods niet opsommen", "QuarkusApp.Namespace", quarkusApp.Namespace, "QuarkusApp.Name", quarkusApp.Name)
		return ctrl.Result{}, err
	}
	podNames := getPodNames(podList.Items)

	// Werk status.Nodes bij indien nodig
	if !reflect.DeepEqual(podNames, quarkusApp.Status.Nodes) {
		quarkusApp.Status.Nodes = podNames
		err := r.Status().Update(ctx, quarkusApp)
		if err != nil {
			log.Error(err, "Kon QuarkusApp-status niet bijwerken")
			return ctrl.Result{}, err
		}
	}

	return ctrl.Result{}, nil
}

Deze controller maakt een implementatie voor onze Quarkus-app, zorgt ervoor dat het aantal replica's overeenkomt met de specificatie en werkt de status bij met de lijst van podnamen.

Je Operator Onverwoestbaar Maken

Nu we een basisoperator hebben, laten we wat superkrachten toevoegen om hem veerkrachtig en zelfherstellend te maken. We implementeren automatische herstel- en schaalstrategieën op basis van de status van de applicatie.

Voeg dit toe aan je controller:


func (r *QuarkusAppReconciler) checkAndHeal(ctx context.Context, quarkusApp *appv1alpha1.QuarkusApp) error {
	// Controleer de gezondheid van de 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++
		}
	}

	// Als meer dan 50% van de pods ongezond is, start een rollende herstart
	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
		}

		// Start een rollende herstart door een annotatie bij te werken
		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
}

Vergeet niet deze functie aan te roepen in je Reconcile loop:


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

Updates Automatiseren: Want Wie Heeft Tijd voor Handarbeid?

Laten we wat automatiseringsmagie toevoegen om updates af te handelen. We maken een functie die controleert op nieuwe versies van onze Quarkus-app en een update start wanneer dat nodig is:


func (r *QuarkusAppReconciler) checkAndUpdate(ctx context.Context, quarkusApp *appv1alpha1.QuarkusApp) error {
	// In een echte wereld zou je een externe bron controleren voor de nieuwste versie
	// Voor dit voorbeeld gebruiken we een annotatie op de CR om een nieuwe versie te simuleren
	newVersion, exists := quarkusApp.Annotations["newVersion"]
	if !exists {
		return nil // Geen nieuwe versie beschikbaar
	}

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

	// Werk de afbeelding bij naar de nieuwe versie
	for i, container := range deployment.Spec.Template.Spec.Containers {
		if container.Name == quarkusApp.Name {
			deployment.Spec.Template.Spec.Containers[i].Image = newVersion
			break
		}
	}

	// Werk de implementatie bij
	err = r.Update(ctx, deployment)
	if err != nil {
		return err
	}

	// Verwijder de annotatie om continue updates te voorkomen
	delete(quarkusApp.Annotations, "newVersion")
	return r.Update(ctx, quarkusApp)
}

Roep deze functie opnieuw aan in je Reconcile loop:


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

Integreren met Externe Bronnen: Want Geen App is een Eiland

De meeste Quarkus-apps moeten communiceren met externe bronnen zoals databases of caches. Laten we wat logica toevoegen om deze afhankelijkheden te beheren:


func (r *QuarkusAppReconciler) ensureDatabaseExists(ctx context.Context, quarkusApp *appv1alpha1.QuarkusApp) error {
	// Controleer of een database is gespecificeerd in de CR
	if quarkusApp.Spec.Database == "" {
		return nil // Geen database nodig
	}

	// Controleer of de database bestaat
	database := &v1alpha1.Database{}
	err := r.Get(ctx, types.NamespacedName{Name: quarkusApp.Spec.Database, Namespace: quarkusApp.Namespace}, database)
	if err != nil && errors.IsNotFound(err) {
		// Database bestaat niet, laten we deze maken
		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
	}

	// Database bestaat, zorg ervoor dat onze app de juiste verbindingsinformatie heeft
	secret := &corev1.Secret{}
	err = r.Get(ctx, types.NamespacedName{Name: database.Status.CredentialsSecret, Namespace: quarkusApp.Namespace}, secret)
	if err != nil {
		return err
	}

	// Werk de omgevingsvariabelen van de Quarkus-app bij met de databaseverbindingsinformatie
	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",
				},
			},
		},
	}

	// Werk de omgevingsvariabelen van de implementatie bij
	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)
}

Vergeet niet deze functie ook in je Reconcile loop aan te roepen!

Monitoring en Logging: Want Blind Vliegen is Geen Pretje

Om onze operator en Quarkus-app in de gaten te houden, laten we wat monitoring- en logmogelijkheden toevoegen. We gebruiken Prometheus voor metrics en integreren met het Kubernetes-loggingsysteem.

Laten we eerst wat metrics aan onze operator toevoegen. Voeg dit toe aan je controller:


var (
	reconcileCount = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "quarkusapp_reconcile_total",
			Help: "Het totale aantal reconciliaties per QuarkusApp",
		},
		[]string{"quarkusapp"},
	)
	reconcileErrors = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "quarkusapp_reconcile_errors_total",
			Help: "Het totale aantal reconciliatiefouten per QuarkusApp",
		},
		[]string{"quarkusapp"},
	)
)

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

Werk nu je Reconcile functie bij om deze metrics te gebruiken:


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

	// Verhoog het reconciliatieaantal
	reconcileCount.WithLabelValues(req.NamespacedName.String()).Inc()

	// ... rest van je reconciliatielogica ...

	if err != nil {
		// Verhoog het foutaantal
		reconcileErrors.WithLabelValues(req.NamespacedName.String()).Inc()
		log.Error(err, "Reconciliatie mislukt")
		return ctrl.Result{}, err
	}

	return ctrl.Result{}, nil
}

Voor logging gebruiken we al de logger van controller-runtime. Laten we wat meer gedetailleerde logging toevoegen:


log.Info("Reconciliatie gestart", "QuarkusApp", quarkusApp.Name)

// ... na controle en genezing ...
log.Info("Gezondheidscontrole voltooid", "OngezondePods", unhealthyPods)

// ... na bijwerken ...
log.Info("Updatecontrole voltooid", "NieuweVersie", newVersion)

// ... na ervoor zorgen dat database bestaat ...
log.Info("Databasecontrole voltooid", "Database", quarkusApp.Spec.Database)

log.Info("Reconciliatie succesvol voltooid", "QuarkusApp", quarkusApp.Name)

Afronding: Je Bent Nu een Kubernetes Operator Tovenaar!

Gefeliciteerd! Je hebt zojuist een aangepaste Kubernetes-operator gemaakt voor je eigenzinnige Quarkus-applicatie. Laten we samenvatten wat we hebben bereikt:

  • Een project opgezet met de Kubernetes Operator SDK
  • Een Custom Resource Definition gemaakt voor onze Quarkus-app
  • Een controller geïmplementeerd om de levenscyclus van de app te beheren
  • Zelfherstellende en automatische updatefuncties toegevoegd
  • Geïntegreerd met externe bronnen zoals databases
  • Monitoring en logging ingesteld voor onze operator

Onthoud, met grote kracht komt grote verantwoordelijkheid. Je aangepaste operator is nu verantwoordelijk voor het beheren van je Quarkus-applicatie, dus zorg ervoor dat je deze grondig test voordat je hem op je productiecluster loslaat.

Terwijl je je reis in de wereld van Kubernetes-operators voortzet, blijf verkennen en experimenteren. De mogelijkheden zijn eindeloos, en wie weet? Misschien creëer je wel het volgende grote ding in cloud-native applicatiebeheer.

Ga nu met vertrouwen opereren, jij magnifieke Kubernetes-tovenaar!

"In de wereld van Kubernetes is de operator de toverstaf, en jij, mijn vriend, bent de tovenaar." - Waarschijnlijk Dumbledore als hij een DevOps-ingenieur was

Veel programmeerplezier, en moge je pods altijd gezond zijn en je clusters voor altijd schaalbaar!