Waarom Blue-Green Deployments?

Voordat we in de details duiken, laten we snel herhalen waarom blue-green deployments zo geweldig zijn:

  • Deployments zonder downtime
  • Eenvoudige terugdraaien als er iets misgaat
  • Mogelijkheid om te testen in een productie-achtige omgeving
  • Verminderd risico en stress voor je operations team

Stel je nu voor dat je dit allemaal doet met de kracht van Kubernetes Operators. Enthousiast? Dat zou je moeten zijn!

De Basis Leggen: Onze Aangepaste Controller

Onze missie, als we ervoor kiezen om deze te accepteren (en dat doen we), is om een aangepaste controller te maken die blue-green deployments beheert. Deze controller zal veranderingen in onze aangepaste resource in de gaten houden en het deploymentproces orkestreren.

Laten we eerst onze aangepaste resource definiëren:

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

Niets te ingewikkeld hier, gewoon een standaard Kubernetes deployment met een twist - het is ons aangepaste resourcetype!

De Kern van de Zaak: Controller Logica

Laten we nu in de controller logica duiken. We gebruiken Go omdat, nou ja, het is prachtig (sorry, kon het niet laten).


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)

	// Haal de BlueGreenDeployment instantie op
	blueGreen := &mycompanyv1.BlueGreenDeployment{}
	err := r.Get(ctx, req.NamespacedName, blueGreen)
	if err != nil {
		if errors.IsNotFound(err) {
			// Object niet gevonden, return. Aangemaakte objecten worden automatisch opgeruimd.
			return reconcile.Result{}, nil
		}
		// Fout bij het lezen van het object - opnieuw in de wachtrij plaatsen.
		return reconcile.Result{}, err
	}

	// Controleer of de deployment al bestaat, zo niet, maak een nieuwe aan
	found := &appsv1.Deployment{}
	err = r.Get(ctx, types.NamespacedName{Name: blueGreen.Name + "-blue", Namespace: blueGreen.Namespace}, found)
	if err != nil && errors.IsNotFound(err) {
		// Definieer een nieuwe deployment
		dep := r.deploymentForBlueGreen(blueGreen, "-blue")
		log.Info("Een nieuwe Deployment maken", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
		err = r.Create(ctx, dep)
		if err != nil {
			log.Error(err, "Mislukt om nieuwe Deployment te maken", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
			return reconcile.Result{}, err
		}
		// Deployment succesvol aangemaakt - return en opnieuw in de wachtrij plaatsen
		return reconcile.Result{Requeue: true}, nil
	} else if err != nil {
		log.Error(err, "Mislukt om Deployment op te halen")
		return reconcile.Result{}, err
	}

	// Zorg ervoor dat de deployment grootte overeenkomt met de spec
	size := blueGreen.Spec.Size
	if *found.Spec.Replicas != size {
		found.Spec.Replicas = &size
		err = r.Update(ctx, found)
		if err != nil {
			log.Error(err, "Mislukt om Deployment bij te werken", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)
			return reconcile.Result{}, err
		}
		// Spec bijgewerkt - return en opnieuw in de wachtrij plaatsen
		return reconcile.Result{Requeue: true}, nil
	}

	// Werk de BlueGreenDeployment status bij met de pod namen
	// Lijst de pods voor deze deployment
	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, "Mislukt om pods op te sommen", "BlueGreenDeployment.Namespace", blueGreen.Namespace, "BlueGreenDeployment.Name", blueGreen.Name)
		return reconcile.Result{}, err
	}
	podNames := getPodNames(podList.Items)

	// Update status.Nodes indien nodig
	if !reflect.DeepEqual(podNames, blueGreen.Status.Nodes) {
		blueGreen.Status.Nodes = podNames
		err := r.Status().Update(ctx, blueGreen)
		if err != nil {
			log.Error(err, "Mislukt om BlueGreenDeployment status bij te werken")
			return reconcile.Result{}, err
		}
	}

	return reconcile.Result{}, nil
}

// deploymentForBlueGreen retourneert een bluegreen Deployment object
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",
						}},
					}},
				},
			},
		},
	}
	// Stel BlueGreenDeployment instantie in als de eigenaar en controller
	controllerutil.SetControllerReference(m, dep, r.Scheme)
	return dep
}

// labelsForBlueGreen retourneert de labels voor het selecteren van de resources
// die behoren tot de gegeven bluegreen CR naam.
func labelsForBlueGreen(name string) map[string]string {
	return map[string]string{"app": "bluegreen", "bluegreen_cr": name}
}

// getPodNames retourneert de pod namen van de array van pods die zijn doorgegeven
func getPodNames(pods []corev1.Pod) []string {
	var podNames []string
	for _, pod := range pods {
		podNames = append(podNames, pod.Name)
	}
	return podNames
}

Dat is een hoop code, maar laten we het opsplitsen:

  1. We definiëren een BlueGreenReconciler struct die de Reconcile methode implementeert.
  2. In de Reconcile methode halen we onze aangepaste resource op en controleren we of er een deployment bestaat.
  3. Als de deployment niet bestaat, maken we een nieuwe aan met deploymentForBlueGreen.
  4. We zorgen ervoor dat de deployment grootte overeenkomt met onze spec en werken indien nodig bij.
  5. Ten slotte werken we de status van onze aangepaste resource bij met de pod namen.

Het Geheime Ingrediënt: Blue-Green Magie

Nu komt de magie van blue-green deployments. We moeten logica toevoegen om zowel blue als green deployments te maken en daartussen te schakelen. Laten we onze controller verbeteren:


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

	// Maak of werk blue deployment bij
	blueDeployment := r.deploymentForBlueGreen(blueGreen, "-blue")
	if err := r.createOrUpdateDeployment(ctx, blueDeployment); err != nil {
		return reconcile.Result{}, err
	}

	// Maak of werk green deployment bij
	greenDeployment := r.deploymentForBlueGreen(blueGreen, "-green")
	if err := r.createOrUpdateDeployment(ctx, greenDeployment); err != nil {
		return reconcile.Result{}, err
	}

	// Controleer of het tijd is om te schakelen
	if shouldSwitch(blueGreen) {
		if err := r.switchTraffic(ctx, blueGreen); err != nil {
			return reconcile.Result{}, err
		}
	}

	// ... (rest van de code)
}

func (r *BlueGreenReconciler) createOrUpdateDeployment(ctx context.Context, dep *appsv1.Deployment) error {
	// Controleer of de deployment al bestaat
	found := &appsv1.Deployment{}
	err := r.Get(ctx, types.NamespacedName{Name: dep.Name, Namespace: dep.Namespace}, found)
	if err != nil && errors.IsNotFound(err) {
		// Maak de deployment
		err = r.Create(ctx, dep)
		if err != nil {
			return err
		}
	} else if err != nil {
		return err
	} else {
		// Werk de deployment bij
		found.Spec = dep.Spec
		err = r.Update(ctx, found)
		if err != nil {
			return err
		}
	}
	return nil
}

func shouldSwitch(bg *mycompanyv1.BlueGreenDeployment) bool {
	// Implementeer je logica om te bepalen of het tijd is om te schakelen
	// Dit kan gebaseerd zijn op een timer, handmatige trigger of andere criteria
	return false
}

func (r *BlueGreenReconciler) switchTraffic(ctx context.Context, bg *mycompanyv1.BlueGreenDeployment) error {
	// Implementeer de logica om verkeer tussen blue en green te schakelen
	// Dit kan inhouden dat je een service of ingress resource bijwerkt
	return nil
}

Deze verbeterde versie maakt zowel blue als green deployments en bevat placeholder functies om te bepalen wanneer en hoe het verkeer te schakelen.

Alles Samenbrengen

Nu we onze controller logica hebben, moeten we de operator opzetten. Hier is een basis main.go bestand om ons op weg te helpen:


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", "Het adres waar het metric endpoint aan gebonden is.")
	flag.BoolVar(&enableLeaderElection, "enable-leader-election", false,
		"Schakel leiderselectie in voor controller manager. Dit zorgt ervoor dat er slechts één actieve controller manager is.")
	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, "niet in staat om manager te starten")
		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, "niet in staat om controller te maken", "controller", "BlueGreen")
		os.Exit(1)
	}

	setupLog.Info("manager starten")
	if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
		setupLog.Error(err, "probleem bij het uitvoeren van manager")
		os.Exit(1)
	}
}

Deployment en Testen

Nu we onze operator klaar hebben, is het tijd om deze te deployen en te testen. Hier is een snelle checklist:

  1. Bouw je operator image en push deze naar een container registry.
  2. Maak de benodigde RBAC-rollen en bindingen voor je operator.
  3. Deploy je operator naar je Kubernetes cluster.
  4. Maak een BlueGreenDeployment aangepaste resource en zie de magie gebeuren!

Hier is een voorbeeld van hoe je een BlueGreenDeployment maakt:


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

Valkuilen en Aandachtspunten

Voordat je dit in productie implementeert, houd deze punten in gedachten:

  • Resourcebeheer: Het gelijktijdig uitvoeren van twee deployments kan je resourcegebruik verdubbelen. Plan dienovereenkomstig!
  • Database migraties: Wees voorzichtig met databaseschema's die niet achterwaarts compatibel zijn.
  • Sticky sessions: Als je app afhankelijk is van sticky sessions, moet je hier voorzichtig mee omgaan tijdens de switch.
  • Testen: Test je operator grondig in een niet-productieomgeving eerst. Geloof me, je zult jezelf later dankbaar zijn.

Afronden

En daar heb je het! Een aangepaste Kubernetes Operator die blue-green deployments als een pro afhandelt. We hebben veel behandeld, van aangepaste resources tot controller logica en zelfs enkele deployment tips.

Onthoud, dit is nog maar het begin. Je kunt deze operator uitbreiden om complexere scenario's aan te kunnen, monitoring en waarschuwingen toe te voegen, of zelfs integreren met je CI/CD-pijplijn.

"Met grote macht komt grote verantwoordelijkheid" - Oom Ben (en elke DevOps engineer ooit)

Ga nu op pad en deploy met vertrouwen! En als je problemen tegenkomt, nou ja... daar zijn rollbacks voor, toch?

Stof tot Nadenken

Als je dit in je eigen projecten implementeert, overweeg dan het volgende:

  • Hoe zou je deze operator kunnen uitbreiden om canary deployments aan te kunnen?
  • Welke metrics zouden nuttig zijn om te verzamelen tijdens het deploymentproces?
  • Hoe zou je dit kunnen integreren met externe tools zoals Prometheus of Grafana?

Veel codeerplezier, en mogen je deployments altijd groen (of blauw, afhankelijk van je voorkeur) zijn!