Lo abbiamo già discusso in articoli precedenti ma è importante sottolineare ancora una volta che, nell'epoca moderna, lo sviluppo del codice e soprattutto l'orchestrazione dei rilasci sono diventati notevolmente più complessi da dover gestire. Nello sviluppo del codice ci sono diversi linguaggi da imparare, sono sempre più difficili da trovare figure full stack che sanno lavorare sia su framework di backend che su quelli di frontend, poiché ne esistono centinaia e continuano a cambiare di giorno in giorno su entrambi i fronti. Lato deployment model, invece, l'avvento dei microservizi e di applicazioni sempre più complesse (vedi Netflix, Google etc.), la situazione non è tanto migliore: già a partire dal sistema di versionamento, di gestione delle compatibilità, infrastruttura, sicurezza e così via ci sarebbe da scrivere un intero libro.

Nel mondo DevOps, in particolare, ci si concentra principalmente a quelle che sono applicazioni cloud-native, ovvero particolari tipologie di servizi che nascono in cloud e cercano di trarne tutti i vantaggi. Lato tecnico, però, questo si traduce nell'avere un repository bloccato, in cui tutte le change sono versionate e approvate prima di essere rilasciate, in avere Infrastructure as Code (IaC) con Terraform o altri che permettono di costruire l'infrastruttura e, infine, tutto un sistema di continuous integration e continuous deployment per quanto riguarda la preparazione dei sorgenti e il loro rilascio in un determinato ambiente. La modalità di lavoro "classica" adottata per la maggior parte dei casi oggi, è basata su un modello chiamato "push-based deployments".

Il codice sorgente, inteso proprio come codice dell'applicazione, codice infrastrutturale, file di configurazione, deployment files di Kubernetes e così via, è contenuto all'interno del repository. Ad ogni modifica dello stesso codice, viene invocata una pipeline che verifica le change ed effettua determinati tipi di operazioni in base al contesto. Nel caso di un'applicazione containerizzata, la pipeline compilerà i sorgenti e produrrà un artefatto, per esempio un'immagine Docker, da pubblicare su un container registry ed eventualmente pubblicherà la configurazione della stessa applicazione in uno storage dedicato (esempio le secret su Azure Key Vault, piuttosto che configurazioni in base al cliente/ambiente di destinazione).

Al verificarsi di un cambiamento sul container registry, di una configurazione, oppure nel momento di un rilascio, partirà un'altra pipeline di continuous deployment che si occuperà di recuperare l'immagine Docker dal container registry, di applicare la configurazione corretta e, infine, di fare il deployment dentro un ambiente desiderato.

Questo modello di rilascio funziona perfettamente, ma funziona fino a quando l'ambiente o il prodotto che stiamo rilasciando è piuttosto piccolo e gestibile. Il problema è che le pipeline sono centrali nell'esecuzione di tutti i processi e, spesso, hanno più permessi del necessario proprio perché devono gestire tutto il sistema end-to-end. Inoltre, non sempre tutto il processo è automatizzato e, quindi, potrebbero esserci delle deviazioni in produzione rispetto a quello che è il desired state.

In ottica più moderna, per applicazioni cloud-native, altamente performanti, che hanno necessità di diversi deployment continui ed evitare il più possibile errori (sia di deployment che eventuali problemi di sicurezza dovuti alle pipeline), invece, si usa un approccio chiamato "pull-based deployment".

Il risultato finale che si ottiene con un meccanismo di pull-based deployment è identico al precedente. Di fatto, l'immagine di Docker finirà sempre per essere caricata dentro l'ambiente di Kubernetes desiderato e avrà la configurazione corretta, ma è tutto il sistema che cambia. La fase iniziale è piuttosto simile, nel senso che abbiamo sempre bisogno di un repository dell'applicazione e una pipeline che viene triggerata nel momento in cui si siano verificate modifiche nel codice sorgente, per produrre un artefatto da pubblicare nel container registry.

La parte interessante si sviluppa successivamente. Infatti, il repository che prima conteneva la configurazione dell'ambiente ora contiene quello che serve ad effettuare il rilascio (ad esempio deployment file di Kubernetes, script di installazione etc.) e viene aggiornato automaticamente dalla pipeline. In Kubernetes è stato caricato un operatore, ovvero una specie di servizio che gira in background e resta in ascolto di eventuali modifiche su qualche sistema per effettuare operazioni precise: in questo caso, l'operatore resta in ascolto su modifiche all'interno del repository e, qualora vengano effettuate (e lo saranno automaticamente dalla pipeline), l'operatore si occuperà automaticamente di effettuare il deployment, eliminando, di fatto, la gestione delle pipeline che non hanno più alcun contatto con l'ambiente esterno.

L'operatore che abbiamo menzionato deve necessariamente vivere all'interno del cluster di riferimento per funzionare e lo scopo che gli abbiamo dato è solo per spiegare come cambia il sistema. E' infatti possibile costruire un operatore che, ad esempio, dato un cambio su un repository invia un messaggio su Microsoft Teams per notificare che un deployment è in corso, oppure un operatore che invii una mail in caso in cui il cluster non sia in buono stato (esempio la morte di un nodo).

Il processo inizia a diventare ancor più interessante quanto vediamo questa cosa da una prospettiva più ampia e dobbiamo applicare il tutto in scala. Supponendo di avere 100 microservizi, ci ritroveremo, nel caso classico, ad avere 200 repository (100 di applicazioni, 100 di configurazioni) e 200 pipeline (100 di CI, 100 di CD). Nel caso del sistema pull-based, invece, avremo bisogno solo di 101 repository (100 di applicazioni, 1 di configurazione) e 100 pipeline per effettuare la validazione/creazione dell'immagine Docker, mentre per il deployment sarà l'operatore di Kubernetes a pensarci. E' chiaramente anche un vantaggio in termini operativi e di costi, poiché ci richiederà molto meno tempo di sviluppo e un approccio comune a tutti i team che lavorano allo stesso sistema (infatti, anche le pipeline potrebbero essere semplificate tramite l'uso di template).

Questa tipologia di deployment mette il repository al centro di tutto, poiché non solo contiene il codice sorgente, ma contiene anche tutte le istruzioni per fare il deployment. Di fatto, questa modalità prendere anche il nome di GitOps, perché, appunto tutto il sistema è basato su git che ha tutta una history e un sistema di tagging per permettere e validare le change.

Tra i principi di GitOps troviamo:

  • l'intero sistema è descritto in maniera dichiarativa: dall'infrastruttura ai file di installazione, tutto è descritto come codice all'interno del repository, in modo tale che tutto sia ricreabile esattamente nello stesso stato in caso di disaster recovery;
  • lo stato desiderato è sempre descritto tramite git: questo ci consente non solo di applicare facilmente modifiche e nuovi rilasci, ma con un semplice "git revert" è possibile fare un intero rollback;
  • tutte le modifiche vengono applicate automaticamente: vero, ma senza l'uso di pipeline che avrebbero accesso al cluster e ad una infrastruttura che è potenzialmente esposta e soggetta a vulnerabilità, così da mantenere controllo e sicurezza con separation of concerns;
  • deve essere presente un agent per identificare deviazioni nel sistema rispetto allo stato desiderato: è proprio il ruolo dell'operatore di Kubernetes rimanere in ascolto di variazioni, poiché indicano che deve reagire e poter applicare le modifiche necessarie (ad esempio fare il rollout di una nuova versione) nel caso in cui lo stato desiderato sia cambiato, ma deve anche essere in grado di ricostruire il sistema qualora Kubernetes abbia dei problemi nel cluster (per esempio la perdita di un nodo che causa la indisponibilità di un servizio).

Tutto questo significa che, soprattutto per progetti di larga scala, è possibile avere alta affidabilità per i rilasci, maggiore sicurezza considerando che il sistema non è più esposto, un sistema unico e centralizzato per fare i deployment e uno stato sempre affidabile che corrisponde al 100% con quello che ci aspettiamo di vedere in produzione, tant'è che può essere sempre e comunque riproducibile (rendendo ancor più vero il concetto di Immutable Infrastructure).

All'interno di questo articolo cerchiamo di vedere come poter mettere in pratica tutto il flusso facendo qualche considerazione sui vari aspetti del processo e partendo dalle basi costruendo assieme un microservizio.

4 pagine in totale: 1 2 3 4
Contenuti dell'articolo

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Approfondimenti

Top Ten Articoli

Articoli via e-mail

Iscriviti alla nostra newsletter nuoviarticoli per ricevere via e-mail le notifiche!

In primo piano

I più letti di oggi

In evidenza

Misc