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:
- We definiëren een
BlueGreenReconciler
struct die de Reconcile methode implementeert. - In de Reconcile methode halen we onze aangepaste resource op en controleren we of er een deployment bestaat.
- Als de deployment niet bestaat, maken we een nieuwe aan met
deploymentForBlueGreen
. - We zorgen ervoor dat de deployment grootte overeenkomt met onze spec en werken indien nodig bij.
- 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:
- Bouw je operator image en push deze naar een container registry.
- Maak de benodigde RBAC-rollen en bindingen voor je operator.
- Deploy je operator naar je Kubernetes cluster.
- 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!