CI/CD per architetture di microservizi

Azure

I cicli di rilascio più veloci rappresentano uno dei principali vantaggi delle architetture di microservizi. Ma senza un buon processo CI/CD, non si otterrà l'agilità promessa dai microservizi. Questo articolo descrive le sfide e consiglia alcuni approcci al problema.

Che cos'è CI/CD?

Quando si parla di CI/CD, si parla in realtà di diversi processi correlati: integrazione continua, recapito continuo e distribuzione continua.

  • Integrazione continua. Le modifiche al codice vengono spesso unite nel ramo main. I processi di compilazione e test automatizzati assicurano che il codice nel ramo principale sia sempre di qualità di produzione.

  • Recapito continuo. Tutte le modifiche al codice che passano il processo di integrazione continua vengono pubblicate automaticamente in un ambiente simile a quello di produzione. La distribuzione nell'ambiente di produzione live può richiedere l'approvazione manuale, ma per il resto è automatizzata. L'obiettivo è fare in modo che il codice sia sempre pronto per la distribuzione in produzione.

  • Distribuzione continua. Le modifiche al codice che superano i due passaggi precedenti vengono distribuite automaticamente nell'ambiente di produzione.

Di seguito alcuni obiettivi relativi a un processo di CI/CD affidabile per un'architettura di microservizi:

  • Ogni team può creare e distribuire i servizi di sua proprietà in modo indipendente, senza influenzare o interrompere gli altri team.

  • Prima che una nuova versione di un servizio venga distribuita all'ambiente di produzione, viene distribuita agli ambienti di sviluppo/test/controllo qualità per la convalida. I controlli di qualità vengono applicati in ogni fase.

  • Una nuova versione di un servizio può essere distribuita affiancata alla versione precedente.

  • Vengono applicati sufficienti criteri di controllo di accesso.

  • Per i carichi di lavoro in contenitori, è possibile considerare attendibili le immagini del contenitore distribuite nell'ambiente di produzione.

Perché una pipeline CI/CD affidabile è importante

In un'applicazione monolitica tradizionale è presente una singola pipeline di compilazione il cui output è l'eseguibile dell'applicazione. Tutte le attività di sviluppo confluiscono in questa pipeline. Se viene trovato un bug ad alta priorità, è necessario integrare, testare e pubblicare una correzione e questo può ritardare il rilascio di nuove funzionalità. È possibile attenuare questi problemi grazie alla presenza di moduli ben fattoriti e all'uso dei rami di funzionalità per ridurre al minimo l'impatto delle modifiche al codice. Con l'aumento della complessità dell'applicazione e l'aggiunta di altre funzionalità, tuttavia, il processo di rilascio di un'applicazione monolitica tende a diventare più delicato e soggetto a errori.

Seguendo la filosofia dei microservizi, non è mai previsto un lungo processo di rilascio che richiede l'allineamento di ogni team. Il team che compila il servizio "A" può rilasciare un aggiornamento in qualsiasi momento, senza attendere che vengano unite, testate e distribuite modifiche nel servizio "B".

Diagramma di un'applicazione monolitica di CI/CD

Per ottenere una velocità di rilascio elevata, la pipeline di versione deve essere automatizzata e altamente affidabile per ridurre al minimo i rischi. Se si rilascia all'ambiente di produzione una o più volte al giorno, le regressioni o le interruzioni del servizio devono essere rare. Al tempo stesso, in caso di distribuzione di un aggiornamento non valido è necessario poter eseguire in modo rapido e affidabile il rollback o il rollforward a una versione precedente di un servizio.

Problematiche

  • Molte basi di codice indipendenti di piccole dimensioni. Ogni team è responsabile della compilazione del proprio servizio, con una propria pipeline di compilazione. In alcune organizzazioni, i team possono usare repository di codice separati. I repository separati possono causare una situazione in cui la conoscenza di come creare il sistema viene distribuita tra i team e nessuno nell'organizzazione sa come distribuire l'intera applicazione. Che cosa accade ad esempio in uno scenario di ripristino di emergenza, se è necessario eseguire rapidamente la distribuzione in un nuovo cluster?

    Mitigazione: avere una pipeline unificata e automatizzata per compilare e distribuire servizi, in modo che questa conoscenza non sia "nascosta" all'interno di ogni team.

  • Più linguaggi e framework. Se ogni team usa una propria combinazione di tecnologie, può essere difficile creare un unico processo di compilazione che possa essere usato nell'intera organizzazione. Il processo di compilazione deve essere sufficientemente flessibile da poter essere adattato da ogni team al linguaggio o al framework scelto.

    Mitigazione: in contenitori il processo di compilazione per ogni servizio. In questo modo, il sistema di compilazione deve solo essere in grado di eseguire i contenitori.

  • Integrazione e test di carico. Se i team rilasciano aggiornamenti al proprio ritmo, può essere difficile progettare test end-to-end affidabili, soprattutto quando i servizi includono dipendenze da altri servizi. Inoltre, l'esecuzione di un cluster di produzione completo può essere costosa, quindi è improbabile che ogni team esegua il proprio cluster completo su larga scala di produzione, solo per i test.

  • Gestione delle versioni. Ogni team deve essere in grado di distribuire un aggiornamento nell'ambiente di produzione. Ciò non significa assegnare a ogni membro del team le autorizzazioni necessarie a tale scopo. Un ruolo di responsabile del rilascio centralizzato, tuttavia, può ridurre la velocità delle distribuzioni.

    Mitigazione: più il processo CI/CD è automatizzato e affidabile, minore è la necessità di un'autorità centrale. È tuttavia possibile usare criteri diversi per il rilascio degli aggiornamenti di funzionalità principali e delle correzioni di bug secondarie. Il fatto di essere decentralizzato non significa una governance zero.

  • Aggiornamenti del servizio. L'aggiornamento di un servizio a una nuova versione non dovrà comportare interruzioni per gli altri servizi che dipendono da esso.

    Mitigazione: usare tecniche di distribuzione come la versione blu-verde o canary per le modifiche non di rilievo. Per le modifiche che causano un'interruzione dell'API, distribuire la nuova versione affiancata alla versione precedente. In questo modo, i servizi che usano l'API precedente possono essere aggiornati e testati per la nuova API. Vedere Aggiornamento dei servizi, di seguito.

Monorepo e multi-repository

Prima di creare un flusso di lavoro CI/CD, è necessario sapere come verrà strutturata e gestita la base di codice.

  • I team lavorano in repository separati o in un monorepo (singolo repository)?
  • Qual è la strategia per la creazione dei rami?
  • Chi può eseguire il push del codice nell'ambiente di produzione? Esiste un ruolo di responsabile del rilascio?

L'approccio con singolo repository sta diventando popolare, ma esistono vantaggi e svantaggi per entrambi.

  Singolo repository Più repository
Vantaggi Condivisione del codice
Più facile standardizzare codice e strumenti
Più semplice effettuare il refactoring del codice
Individuabilità - singola visualizzazione del codice
Proprietà chiara per ogni team
Potenzialmente meno conflitti di merge
Favorire la separazione dei microservizi
Problematiche Le modifiche al codice condiviso possono influire su più microservizi
Maggiore rischio di conflitti di merge
Gli strumenti devono supportare una base di codice di grandi dimensioni
Controllo di accesso
Processo di distribuzione più complesso
Più difficile la condivisione del codice
Più difficile applicare standard di codifica
Gestione delle dipendenze
Base di codice diffusa, scarsa individuabilità
Mancanza di un'infrastruttura condivisa

Aggiornamento dei servizi

Esistono varie strategie per aggiornare un servizio già in produzione. Di seguito vengono illustrare tre opzioni comuni: aggiornamento in sequenza, distribuzione di tipo blu-verde e versione canary.

Aggiornamenti in sequenza

In un aggiornamento in sequenza, si distribuiscono nuove istanze di un servizio e queste iniziano subito a ricevere richieste. Quando le nuove istanze diventano disponibili, quelle precedenti vengono rimosse.

Esempio. In Kubernetes gli aggiornamenti in sequenza sono il comportamento predefinito quando si aggiorna la specifica del pod per una distribuzione. Il controller di distribuzione crea un nuovo ReplicaSet per i pod aggiornati. Aumenta quindi le prestazioni del nuovo ReplicaSet riducendo al tempo stesso le prestazioni di quello precedente per gestire il numero di repliche desiderato. I pod precedenti non vengono eliminati finché non sono pronti quelli nuovi. Kubernetes mantiene una cronologia dell'aggiornamento, quindi è possibile eseguire il rollback di un aggiornamento, se necessario.

Esempio. Azure Service Fabric usa la strategia di aggiornamento in sequenza per impostazione predefinita. Questa strategia è più adatta per la distribuzione di una versione di un servizio con nuove funzionalità senza modificare le API esistenti. Service Fabric avvia una distribuzione di aggiornamento aggiornando il tipo di applicazione a un subset dei nodi o di un dominio di aggiornamento. Esegue quindi il roll forward al dominio di aggiornamento successivo fino a quando non vengono aggiornati tutti i domini. Se un dominio di aggiornamento non riesce ad eseguire l'aggiornamento, il tipo di applicazione esegue il rollback alla versione precedente in tutti i domini. Tenere presente che un tipo di applicazione con più servizi (e se tutti i servizi vengono aggiornati come parte di una distribuzione di aggiornamento) è soggetto a errori. Se un servizio non viene aggiornato, viene eseguito il rollback dell'intera applicazione alla versione precedente e gli altri servizi non vengono aggiornati.

Una problematica degli aggiornamenti in sequenza è rappresentata dal fatto che durante il processo di aggiornamento è in esecuzione e riceve traffico una combinazione di versioni nuove e precedenti. Durante tale periodo, qualsiasi richiesta potrebbe essere indirizzata a una delle due delle versioni.

Per le modifiche che causano un'interruzione dell'API, è consigliabile supportare entrambe le versioni affiancate, fino a quando tutti i client della versione precedente non vengono aggiornati. Vedere Controllo delle versioni delle API.

Distribuzione di tipo blu-verde

In una distribuzione di tipo blu-verde, si distribuisce la nuova versione insieme alla precedente. Dopo aver convalidato la nuova versione, si trasferisce tutto il traffico contemporaneamente dalla versione precedente alla nuova. Dopo il trasferimento, si monitora l'applicazione per rilevare eventuali problemi. Se si verificano errori, è possibile tornare alla versione precedente. Supponendo che non sussistano problemi, è possibile eliminare la versione precedente.

Con un'applicazione monolitica o a più livelli più tradizionale, la distribuzione di tipo blu-verde comporta in genere il provisioning di due ambienti identici. Si distribuirà la nuova versione in un ambiente di gestione temporanea, quindi reindirizzare il traffico client all'ambiente di gestione temporanea, ad esempio scambiando indirizzi VIP. In un'architettura di microservizi gli aggiornamenti vengono eseguiti a livello di microservizio, quindi in genere si distribuisce l'aggiornamento nello stesso ambiente e si usa un meccanismo di individuazione dei servizi per lo scambio.

Esempio. In Kubernetes, non è necessario effettuare il provisioning di un cluster separato per eseguire distribuzioni di tipo blu-verde. È invece possibile sfruttare i selettori. Creare una nuova risorsa di distribuzione con una nuova specifica di pod e un set diverso di etichette. Creare questa distribuzione senza eliminare la precedente o modificare il servizio che vi fa riferimento. Quando i nuovi pod sono in esecuzione, è possibile aggiornare il selettore del servizio in base alla nuova distribuzione.

Uno svantaggio della distribuzione blu-verde è che durante l'aggiornamento, vengono eseguiti due volte il numero di pod per il servizio (corrente e successivo). Se i pod richiedono una grande quantità di risorse di memoria o CPU, potrebbe essere necessario aumentare temporaneamente il numero di istanze del cluster per gestire l'utilizzo delle risorse.

Versione canary

Con una versione canary, si implementa una versione aggiornata in un numero ridotto di client. Si monitora quindi il comportamento del nuovo servizio prima di procedere all'implementazione in tutti i client. In questo modo è possibile eseguire un'implementazione lenta in modo controllato, osservare i dati reali e individuare i problemi prima che abbiano un impatto su tutti i clienti.

Una versione canary è più complessa da gestire rispetto a un aggiornamento in sequenza o di tipo blu-verde, perché è necessario indirizzare dinamicamente le richieste a versioni diverse del servizio.

Esempio. In Kubernetes è possibile configurare un servizio per estendersi su due set di repliche (uno per ogni versione) e modificare manualmente i conteggi delle repliche. Questo approccio presenta tuttavia una granularità piuttosto grossolana a causa del modo in cui Kubernetes bilancia il carico tra i pod. Ad esempio, se si dispone di un totale di 10 repliche, è possibile spostare il traffico solo in incrementi del 10%. Se si usa una rete mesh di servizi, è possibile usare le relative regole di routing per implementare una strategia di versione canary più sofisticata.

Passaggi successivi