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:
- Wir definieren eine
BlueGreenReconciler
-Struktur, die die Reconcile-Methode implementiert. - In der Reconcile-Methode rufen wir unsere benutzerdefinierte Ressource ab und prüfen, ob ein Deployment existiert.
- Wenn das Deployment nicht existiert, erstellen wir ein neues mit
deploymentForBlueGreen
. - Wir stellen sicher, dass die Deployment-Größe mit unserer Spezifikation übereinstimmt und aktualisieren sie bei Bedarf.
- 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:
- Bauen Sie Ihr Operator-Image und pushen Sie es in ein Container-Registry.
- Erstellen Sie die notwendigen RBAC-Rollen und -Bindungen für Ihren Operator.
- Deployen Sie Ihren Operator in Ihrem Kubernetes-Cluster.
- 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!