Warum Blue-Green-Deployments?

Bevor wir ins Detail gehen, fassen wir kurz zusammen, warum Blue-Green-Deployments so beliebt sind:

  • Deployments ohne Ausfallzeiten
  • Einfache Rückabwicklung, falls etwas schiefgeht
  • Möglichkeit, in einer produktionsähnlichen Umgebung zu testen
  • Reduziertes Risiko und weniger Stress für Ihr Ops-Team

Stellen Sie sich nun vor, all dies mit der Kraft von Kubernetes Operators zu tun. Begeistert? Sollten Sie sein!

Die Bühne bereiten: Unser benutzerdefinierter Controller

Unsere Mission, sollten wir uns dafür entscheiden (und das tun wir), ist es, einen benutzerdefinierten Controller zu erstellen, der Blue-Green-Deployments verwaltet. Dieser Controller wird Änderungen an unserer benutzerdefinierten Ressource überwachen und den Deployment-Prozess orchestrieren.

Als Erstes definieren wir unsere benutzerdefinierte Ressource:

apiVersion: mycompany.com/v1
kind: BlueGreenDeployment
metadata:
  name: my-awesome-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-awesome-app
  template:
    metadata:
      labels:
        app: my-awesome-app
    spec:
      containers:
      - name: my-awesome-app
        image: myregistry.com/my-awesome-app:v1
        ports:
        - containerPort: 8080

Nichts allzu Kompliziertes hier, nur ein Standard-Kubernetes-Deployment mit einem Twist - es ist unser benutzerdefinierter Ressourcentyp!

Der Kern der Sache: Controller-Logik

Jetzt tauchen wir in die Controller-Logik ein. Wir verwenden Go, weil es einfach großartig ist (Entschuldigung, konnte nicht widerstehen).


package controller

import (
	"context"
	"fmt"
	"time"

	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller"
	"sigs.k8s.io/controller-runtime/pkg/handler"
	"sigs.k8s.io/controller-runtime/pkg/manager"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"
	"sigs.k8s.io/controller-runtime/pkg/source"

	mycompanyv1 "github.com/mycompany/api/v1"
)

type BlueGreenReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

func (r *BlueGreenReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
	log := r.Log.WithValues("bluegreen", req.NamespacedName)

	// Die BlueGreenDeployment-Instanz abrufen
	blueGreen := &mycompanyv1.BlueGreenDeployment{}
	err := r.Get(ctx, req.NamespacedName, blueGreen)
	if err != nil {
		if errors.IsNotFound(err) {
			// Objekt nicht gefunden, zurückkehren. Erstellte Objekte werden automatisch gelöscht.
			return reconcile.Result{}, nil
		}
		// Fehler beim Lesen des Objekts - Anfrage erneut in die Warteschlange stellen.
		return reconcile.Result{}, err
	}

	// Überprüfen, ob das Deployment bereits existiert, falls nicht, ein neues erstellen
	found := &appsv1.Deployment{}
	err = r.Get(ctx, types.NamespacedName{Name: blueGreen.Name + "-blue", Namespace: blueGreen.Namespace}, found)
	if err != nil && errors.IsNotFound(err) {
		// Ein neues Deployment definieren
		dep := r.deploymentForBlueGreen(blueGreen, "-blue")
		log.Info("Erstelle ein neues Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
		err = r.Create(ctx, dep)
		if err != nil {
			log.Error(err, "Fehler beim Erstellen eines neuen Deployments", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
			return reconcile.Result{}, err
		}
		// Deployment erfolgreich erstellt - zurückkehren und erneut in die Warteschlange stellen
		return reconcile.Result{Requeue: true}, nil
	} else if err != nil {
		log.Error(err, "Fehler beim Abrufen des Deployments")
		return reconcile.Result{}, err
	}

	// Sicherstellen, dass die Deployment-Größe mit der Spezifikation übereinstimmt
	size := blueGreen.Spec.Size
	if *found.Spec.Replicas != size {
		found.Spec.Replicas = &size
		err = r.Update(ctx, found)
		if err != nil {
			log.Error(err, "Fehler beim Aktualisieren des Deployments", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)
			return reconcile.Result{}, err
		}
		// Spezifikation aktualisiert - zurückkehren und erneut in die Warteschlange stellen
		return reconcile.Result{Requeue: true}, nil
	}

	// Den Status des BlueGreenDeployments mit den Pod-Namen aktualisieren
	// Die Pods für dieses Deployment auflisten
	podList := &corev1.PodList{}
	listOpts := []client.ListOption{
		client.InNamespace(blueGreen.Namespace),
		client.MatchingLabels(labelsForBlueGreen(blueGreen.Name)),
	}
	if err = r.List(ctx, podList, listOpts...); err != nil {
		log.Error(err, "Fehler beim Auflisten der Pods", "BlueGreenDeployment.Namespace", blueGreen.Namespace, "BlueGreenDeployment.Name", blueGreen.Name)
		return reconcile.Result{}, err
	}
	podNames := getPodNames(podList.Items)

	// Status.Nodes bei Bedarf aktualisieren
	if !reflect.DeepEqual(podNames, blueGreen.Status.Nodes) {
		blueGreen.Status.Nodes = podNames
		err := r.Status().Update(ctx, blueGreen)
		if err != nil {
			log.Error(err, "Fehler beim Aktualisieren des BlueGreenDeployment-Status")
			return reconcile.Result{}, err
		}
	}

	return reconcile.Result{}, nil
}

// deploymentForBlueGreen gibt ein BlueGreen-Deployment-Objekt zurück
func (r *BlueGreenReconciler) deploymentForBlueGreen(m *mycompanyv1.BlueGreenDeployment, suffix string) *appsv1.Deployment {
	ls := labelsForBlueGreen(m.Name)
	replicas := m.Spec.Size

	dep := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      m.Name + suffix,
			Namespace: m.Namespace,
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: &replicas,
			Selector: &metav1.LabelSelector{
				MatchLabels: ls,
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: ls,
				},
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{{
						Image: m.Spec.Image,
						Name:  "bluegreen",
						Ports: []corev1.ContainerPort{{
							ContainerPort: 8080,
							Name:          "bluegreen",
						}},
					}},
				},
			},
		},
	}
	// Setzt die BlueGreenDeployment-Instanz als Eigentümer und Controller
	controllerutil.SetControllerReference(m, dep, r.Scheme)
	return dep
}

// labelsForBlueGreen gibt die Labels zurück, um die Ressourcen auszuwählen,
// die zum angegebenen BlueGreen-CR-Namen gehören.
func labelsForBlueGreen(name string) map[string]string {
	return map[string]string{"app": "bluegreen", "bluegreen_cr": name}
}

// getPodNames gibt die Pod-Namen des übergebenen Pod-Arrays zurück
func getPodNames(pods []corev1.Pod) []string {
	var podNames []string
	for _, pod := range pods {
		podNames = append(podNames, pod.Name)
	}
	return podNames
}

Uff! Das ist eine Menge Code, aber lassen Sie uns das aufschlüsseln:

  1. Wir definieren eine BlueGreenReconciler-Struktur, die die Reconcile-Methode implementiert.
  2. In der Reconcile-Methode rufen wir unsere benutzerdefinierte Ressource ab und prüfen, ob ein Deployment existiert.
  3. Wenn das Deployment nicht existiert, erstellen wir ein neues mit deploymentForBlueGreen.
  4. Wir stellen sicher, dass die Deployment-Größe mit unserer Spezifikation übereinstimmt und aktualisieren sie bei Bedarf.
  5. Schließlich aktualisieren wir den Status unserer benutzerdefinierten Ressource mit den Pod-Namen.

Das Geheimnis: Blue-Green-Magie

Hier passiert die Blue-Green-Deployment-Magie. Wir müssen Logik hinzufügen, um sowohl blaue als auch grüne Deployments zu erstellen und zwischen ihnen zu wechseln. Lassen Sie uns unseren Controller erweitern:


func (r *BlueGreenReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
	// ... (vorheriger Code)

	// Blaues Deployment erstellen oder aktualisieren
	blueDeployment := r.deploymentForBlueGreen(blueGreen, "-blue")
	if err := r.createOrUpdateDeployment(ctx, blueDeployment); err != nil {
		return reconcile.Result{}, err
	}

	// Grünes Deployment erstellen oder aktualisieren
	greenDeployment := r.deploymentForBlueGreen(blueGreen, "-green")
	if err := r.createOrUpdateDeployment(ctx, greenDeployment); err != nil {
		return reconcile.Result{}, err
	}

	// Überprüfen, ob es Zeit ist zu wechseln
	if shouldSwitch(blueGreen) {
		if err := r.switchTraffic(ctx, blueGreen); err != nil {
			return reconcile.Result{}, err
		}
	}

	// ... (restlicher Code)
}

func (r *BlueGreenReconciler) createOrUpdateDeployment(ctx context.Context, dep *appsv1.Deployment) error {
	// Überprüfen, ob das Deployment bereits existiert
	found := &appsv1.Deployment{}
	err := r.Get(ctx, types.NamespacedName{Name: dep.Name, Namespace: dep.Namespace}, found)
	if err != nil && errors.IsNotFound(err) {
		// Das Deployment erstellen
		err = r.Create(ctx, dep)
		if err != nil {
			return err
		}
	} else if err != nil {
		return err
	} else {
		// Das Deployment aktualisieren
		found.Spec = dep.Spec
		err = r.Update(ctx, found)
		if err != nil {
			return err
		}
	}
	return nil
}

func shouldSwitch(bg *mycompanyv1.BlueGreenDeployment) bool {
	// Implementieren Sie Ihre Logik, um zu bestimmen, ob es Zeit ist zu wechseln
	// Dies könnte auf einem Timer, einem manuellen Auslöser oder anderen Kriterien basieren
	return false
}

func (r *BlueGreenReconciler) switchTraffic(ctx context.Context, bg *mycompanyv1.BlueGreenDeployment) error {
	// Implementieren Sie die Logik, um den Traffic zwischen Blau und Grün zu wechseln
	// Dies könnte das Aktualisieren einer Service- oder Ingress-Ressource beinhalten
	return nil
}

Diese erweiterte Version erstellt sowohl blaue als auch grüne Deployments und enthält Platzhalterfunktionen, um zu bestimmen, wann und wie der Traffic gewechselt werden soll.

Alles zusammenfügen

Jetzt, da wir unsere Controller-Logik haben, müssen wir den Operator einrichten. Hier ist eine einfache main.go-Datei, um uns zu starten:


package main

import (
	"flag"
	"os"

	"k8s.io/apimachinery/pkg/runtime"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
	_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"

	mycompanyv1 "github.com/mycompany/api/v1"
	"github.com/mycompany/controllers"
)

var (
	scheme   = runtime.NewScheme()
	setupLog = ctrl.Log.WithName("setup")
)

func init() {
	utilruntime.Must(clientgoscheme.AddToScheme(scheme))
	utilruntime.Must(mycompanyv1.AddToScheme(scheme))
}

func main() {
	var metricsAddr string
	var enableLeaderElection bool
	flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "Die Adresse, an die der Metrik-Endpunkt gebunden wird.")
	flag.BoolVar(&enableLeaderElection, "enable-leader-election", false,
		"Führungswahl für den Controller-Manager aktivieren. Dies stellt sicher, dass es nur einen aktiven Controller-Manager gibt.")
	flag.Parse()

	ctrl.SetLogger(zap.New(zap.UseDevMode(true)))

	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
		Scheme:             scheme,
		MetricsBindAddress: metricsAddr,
		LeaderElection:     enableLeaderElection,
		Port:               9443,
	})
	if err != nil {
		setupLog.Error(err, "Manager konnte nicht gestartet werden")
		os.Exit(1)
	}

	if err = (&controllers.BlueGreenReconciler{
		Client: mgr.GetClient(),
		Log:    ctrl.Log.WithName("controllers").WithName("BlueGreen"),
		Scheme: mgr.GetScheme(),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "Controller konnte nicht erstellt werden", "controller", "BlueGreen")
		os.Exit(1)
	}

	setupLog.Info("Manager wird gestartet")
	if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
		setupLog.Error(err, "Problem beim Ausführen des Managers")
		os.Exit(1)
	}
}

Deployment und Testen

Jetzt, da unser Operator bereit ist, ist es Zeit, ihn zu deployen und zu testen. Hier ist eine kurze Checkliste:

  1. Bauen Sie Ihr Operator-Image und pushen Sie es in ein Container-Registry.
  2. Erstellen Sie die notwendigen RBAC-Rollen und -Bindungen für Ihren Operator.
  3. Deployen Sie Ihren Operator in Ihrem Kubernetes-Cluster.
  4. Erstellen Sie eine BlueGreenDeployment-Ressource und beobachten Sie die Magie!

Hier ist ein Beispiel, wie man ein BlueGreenDeployment erstellt:


apiVersion: mycompany.com/v1
kind: BlueGreenDeployment
metadata:
  name: my-cool-app
spec:
  replicas: 3
  image: mycoolapp:v1

Fallstricke und Stolpersteine

Bevor Sie dies in der Produktion implementieren, beachten Sie folgende Punkte:

  • Ressourcenmanagement: Das gleichzeitige Ausführen von zwei Deployments kann Ihren Ressourcenverbrauch verdoppeln. Planen Sie entsprechend!
  • Datenbankmigrationen: Seien Sie vorsichtig mit Datenbankschemata, die nicht abwärtskompatibel sind.
  • Sticky Sessions: Wenn Ihre App auf Sticky Sessions angewiesen ist, müssen Sie dies beim Wechsel sorgfältig handhaben.
  • Testen: Testen Sie Ihren Operator gründlich in einer Nicht-Produktionsumgebung. Sie werden sich später dafür danken.

Zusammenfassung

Und da haben Sie es! Ein benutzerdefinierter Kubernetes-Operator, der Blue-Green-Deployments wie ein Profi handhabt. Wir haben viel abgedeckt, von benutzerdefinierten Ressourcen über Controller-Logik bis hin zu einigen Deployment-Tipps.

Denken Sie daran, dies ist nur der Anfang. Sie können diesen Operator erweitern, um komplexere Szenarien zu handhaben, Monitoring und Alarmierung hinzuzufügen oder ihn sogar in Ihre CI/CD-Pipeline zu integrieren.

"Mit großer Macht kommt große Verantwortung" - Onkel Ben (und jeder DevOps-Ingenieur jemals)

Gehen Sie nun voran und deployen Sie mit Zuversicht! Und wenn Sie auf Probleme stoßen, nun ja... dafür sind Rollbacks da, oder?

Denkanstöße

Während Sie dies in Ihren eigenen Projekten implementieren, überlegen Sie Folgendes:

  • Wie könnten Sie diesen Operator erweitern, um Canary-Deployments zu handhaben?
  • Welche Metriken wären während des Deployment-Prozesses nützlich zu sammeln?
  • Wie könnten Sie dies mit externen Tools wie Prometheus oder Grafana integrieren?

Viel Spaß beim Programmieren, und mögen Ihre Deployments immer grün (oder blau, je nach Vorliebe) sein!