Perché i Deployment Blue-Green?
Prima di addentrarci nei dettagli, facciamo un rapido riepilogo del perché i deployment blue-green sono così apprezzati:
- Deployment senza downtime
- Facile rollback se qualcosa va storto
- Possibilità di testare in un ambiente simile alla produzione
- Rischio e stress ridotti per il tuo team operativo
Ora, immagina di fare tutto questo con la potenza degli Operator di Kubernetes. Sei entusiasta? Dovresti esserlo!
Preparare il Terreno: Il Nostro Controller Personalizzato
La nostra missione, se decidiamo di accettarla (e lo facciamo), è creare un controller personalizzato che gestisca i deployment blue-green. Questo controller monitorerà i cambiamenti della nostra risorsa personalizzata e orchestrerà il processo di deployment.
Prima di tutto, definiamo la nostra risorsa personalizzata:
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
Niente di troppo complicato qui, solo un deployment standard di Kubernetes con una particolarità: è il nostro tipo di risorsa personalizzata!
Il Cuore della Questione: La Logica del Controller
Ora, immergiamoci nella logica del controller. Useremo Go perché, beh, è fantastico (scusa, non ho resistito).
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)
// Recupera l'istanza di BlueGreenDeployment
blueGreen := &mycompanyv1.BlueGreenDeployment{}
err := r.Get(ctx, req.NamespacedName, blueGreen)
if err != nil {
if errors.IsNotFound(err) {
// Oggetto non trovato, ritorna. Gli oggetti creati vengono automaticamente raccolti come spazzatura.
return reconcile.Result{}, nil
}
// Errore nella lettura dell'oggetto - rimetti in coda la richiesta.
return reconcile.Result{}, err
}
// Controlla se il deployment esiste già, altrimenti creane uno nuovo
found := &appsv1.Deployment{}
err = r.Get(ctx, types.NamespacedName{Name: blueGreen.Name + "-blue", Namespace: blueGreen.Namespace}, found)
if err != nil && errors.IsNotFound(err) {
// Definisci un nuovo deployment
dep := r.deploymentForBlueGreen(blueGreen, "-blue")
log.Info("Creazione di un nuovo Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
err = r.Create(ctx, dep)
if err != nil {
log.Error(err, "Impossibile creare un nuovo Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
return reconcile.Result{}, err
}
// Deployment creato con successo - ritorna e rimetti in coda
return reconcile.Result{Requeue: true}, nil
} else if err != nil {
log.Error(err, "Impossibile ottenere il Deployment")
return reconcile.Result{}, err
}
// Assicurati che la dimensione del deployment sia la stessa della specifica
size := blueGreen.Spec.Size
if *found.Spec.Replicas != size {
found.Spec.Replicas = &size
err = r.Update(ctx, found)
if err != nil {
log.Error(err, "Impossibile aggiornare il Deployment", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)
return reconcile.Result{}, err
}
// Specifica aggiornata - ritorna e rimetti in coda
return reconcile.Result{Requeue: true}, nil
}
// Aggiorna lo stato di BlueGreenDeployment con i nomi dei pod
// Elenca i pod per questo 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, "Impossibile elencare i pod", "BlueGreenDeployment.Namespace", blueGreen.Namespace, "BlueGreenDeployment.Name", blueGreen.Name)
return reconcile.Result{}, err
}
podNames := getPodNames(podList.Items)
// Aggiorna status.Nodes se necessario
if !reflect.DeepEqual(podNames, blueGreen.Status.Nodes) {
blueGreen.Status.Nodes = podNames
err := r.Status().Update(ctx, blueGreen)
if err != nil {
log.Error(err, "Impossibile aggiornare lo stato di BlueGreenDeployment")
return reconcile.Result{}, err
}
}
return reconcile.Result{}, nil
}
// deploymentForBlueGreen restituisce un oggetto Deployment bluegreen
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",
}},
}},
},
},
},
}
// Imposta l'istanza di BlueGreenDeployment come proprietario e controller
controllerutil.SetControllerReference(m, dep, r.Scheme)
return dep
}
// labelsForBlueGreen restituisce le etichette per selezionare le risorse
// appartenenti al nome del CR bluegreen dato.
func labelsForBlueGreen(name string) map[string]string {
return map[string]string{"app": "bluegreen", "bluegreen_cr": name}
}
// getPodNames restituisce i nomi dei pod dell'array di pod passato
func getPodNames(pods []corev1.Pod) []string {
var podNames []string
for _, pod := range pods {
podNames = append(podNames, pod.Name)
}
return podNames
}
Uff! È un bel po' di codice, ma analizziamolo:
- Definiamo una struttura
BlueGreenReconciler
che implementa il metodo Reconcile. - Nel metodo Reconcile, recuperiamo la nostra risorsa personalizzata e controlliamo se esiste un deployment.
- Se il deployment non esiste, ne creiamo uno nuovo usando
deploymentForBlueGreen
. - Assicuriamo che la dimensione del deployment corrisponda alla nostra specifica e aggiorniamo se necessario.
- Infine, aggiorniamo lo stato della nostra risorsa personalizzata con i nomi dei pod.
Il Segreto: La Magia Blue-Green
Ora, ecco dove avviene la magia del deployment blue-green. Dobbiamo aggiungere la logica per creare sia i deployment blue che green e passare da uno all'altro. Miglioriamo il nostro controller:
func (r *BlueGreenReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
// ... (codice precedente)
// Crea o aggiorna il deployment blue
blueDeployment := r.deploymentForBlueGreen(blueGreen, "-blue")
if err := r.createOrUpdateDeployment(ctx, blueDeployment); err != nil {
return reconcile.Result{}, err
}
// Crea o aggiorna il deployment green
greenDeployment := r.deploymentForBlueGreen(blueGreen, "-green")
if err := r.createOrUpdateDeployment(ctx, greenDeployment); err != nil {
return reconcile.Result{}, err
}
// Controlla se è il momento di passare
if shouldSwitch(blueGreen) {
if err := r.switchTraffic(ctx, blueGreen); err != nil {
return reconcile.Result{}, err
}
}
// ... (resto del codice)
}
func (r *BlueGreenReconciler) createOrUpdateDeployment(ctx context.Context, dep *appsv1.Deployment) error {
// Controlla se il deployment esiste già
found := &appsv1.Deployment{}
err := r.Get(ctx, types.NamespacedName{Name: dep.Name, Namespace: dep.Namespace}, found)
if err != nil && errors.IsNotFound(err) {
// Crea il deployment
err = r.Create(ctx, dep)
if err != nil {
return err
}
} else if err != nil {
return err
} else {
// Aggiorna il deployment
found.Spec = dep.Spec
err = r.Update(ctx, found)
if err != nil {
return err
}
}
return nil
}
func shouldSwitch(bg *mycompanyv1.BlueGreenDeployment) bool {
// Implementa la tua logica per determinare se è il momento di passare
// Questo potrebbe basarsi su un timer, un trigger manuale o altri criteri
return false
}
func (r *BlueGreenReconciler) switchTraffic(ctx context.Context, bg *mycompanyv1.BlueGreenDeployment) error {
// Implementa la logica per passare il traffico tra blue e green
// Questo potrebbe comportare l'aggiornamento di una risorsa di servizio o ingress
return nil
}
Questa versione migliorata crea sia i deployment blue che green e include funzioni segnaposto per determinare quando passare e come passare il traffico.
Mettere Tutto Insieme
Ora che abbiamo la logica del nostro controller, dobbiamo configurare l'operator. Ecco un file main.go
di base per iniziare:
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", "L'indirizzo a cui si lega l'endpoint delle metriche.")
flag.BoolVar(&enableLeaderElection, "enable-leader-election", false,
"Abilita l'elezione del leader per il gestore del controller. Abilitando questo, ci sarà solo un gestore del controller attivo.")
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, "impossibile avviare il gestore")
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, "impossibile creare il controller", "controller", "BlueGreen")
os.Exit(1)
}
setupLog.Info("avvio del gestore")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "problema nell'esecuzione del gestore")
os.Exit(1)
}
}
Deployment e Test
Ora che abbiamo il nostro operator pronto, è il momento di distribuirlo e testarlo. Ecco una lista di controllo rapida:
- Costruisci l'immagine del tuo operator e caricala su un registro di container.
- Crea i ruoli RBAC necessari e i binding per il tuo operator.
- Distribuisci il tuo operator nel tuo cluster Kubernetes.
- Crea una risorsa personalizzata BlueGreenDeployment e guarda la magia accadere!
Ecco un esempio di come creare un BlueGreenDeployment:
apiVersion: mycompany.com/v1
kind: BlueGreenDeployment
metadata:
name: my-cool-app
spec:
replicas: 3
image: mycoolapp:v1
Trappole e Insidie
Prima di correre a implementare questo in produzione, tieni a mente questi punti:
- Gestione delle risorse: Eseguire due deployment contemporaneamente può raddoppiare l'uso delle risorse. Pianifica di conseguenza!
- Migrazioni del database: Fai attenzione agli schemi del database che non sono retrocompatibili.
- Sessioni sticky: Se la tua app si basa su sessioni sticky, dovrai gestirle con attenzione durante il passaggio.
- Test: Testa accuratamente il tuo operator in un ambiente non di produzione prima. Fidati, ti ringrazierai più tardi.
Conclusione
Ecco fatto! Un Operator Kubernetes personalizzato che gestisce i deployment blue-green come un campione. Abbiamo coperto molti argomenti, dalle risorse personalizzate alla logica del controller e persino alcuni suggerimenti per il deployment.
Ricorda, questo è solo l'inizio. Puoi estendere questo operator per gestire scenari più complessi, aggiungere monitoraggio e allerta, o persino integrarlo con la tua pipeline CI/CD.
"Con grande potere derivano grandi responsabilità" - Zio Ben (e ogni ingegnere DevOps di sempre)
Ora vai avanti e distribuisci con fiducia! E se incontri problemi, beh... è per questo che esistono i rollback, giusto?
Spunti di Riflessione
Mentre implementi questo nei tuoi progetti, considera quanto segue:
- Come potresti estendere questo operator per gestire i deployment canary?
- Quali metriche sarebbero utili da raccogliere durante il processo di deployment?
- Come potresti integrare questo con strumenti esterni come Prometheus o Grafana?
Buona programmazione, e che i tuoi deployment siano sempre verdi (o blu, a seconda delle tue preferenze)!