CI/CD per microservizi

I cicli di rilascio più veloci sono un vantaggio principale delle architetture di microservizi. Senza un processo di integrazione continua e recapito continuo (CI/CD) affidabile, si perde l'agilità dei microservizi. Questo articolo descrive le problematiche comuni di CI/CD nelle architetture di microservizi e consiglia approcci per creare, convalidare, proteggere e distribuire i servizi in modo indipendente.

Definizione di CI/CD

CI/CD si riferisce a diversi processi correlati: integrazione continua, recapito continuo e distribuzione continua.

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

  • Consegna continua (CD): Le modifiche al codice che superano il processo di CI vengono pubblicate automaticamente in un ambiente simile a quello di produzione. La distribuzione nell'ambiente di produzione live potrebbe richiedere l'approvazione manuale, ma in caso contrario è automatizzata. L'obiettivo è che il codice sia sempre pronto per la distribuzione nell'ambiente di produzione.

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

Considerare gli obiettivi seguenti di un processo 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 di distribuire una nuova versione di un servizio nell'ambiente di produzione, viene distribuita in ambienti di sviluppo/test e controllo di qualità per la convalida. Le barriere di qualità vengono applicate in ogni fase.

  • Una nuova versione di un servizio può essere distribuita side-by-side con la versione precedente.

  • Vengono applicati sufficienti criteri di controllo di accesso. Le pipeline eseguono l'autenticazione per Azure con credenziali federate e di breve durata anziché segreti di lunga durata.

  • Per i carichi di lavoro in contenitori, è possibile considerare attendibili le immagini del contenitore distribuite nell'ambiente di produzione. Tale attendibilità viene stabilita tramite immagini firmate, attestazioni SBOM (Software Bill of Materials) e analisi delle vulnerabilità applicate nella pipeline.

Perché una pipeline CI/CD affidabile è importante

In un'applicazione monolitica tradizionale, una singola pipeline di compilazione produce l'eseguibile dell'applicazione. Tutto il lavoro di sviluppo confluisce in questa pipeline. Se il team rileva un bug ad alta priorità, la correzione deve essere integrata, testata e pubblicata, che può ritardare il rilascio delle nuove funzionalità. È possibile ridurre questi problemi usando moduli ben fattoriti e rami di funzionalità per limitare l'effetto delle modifiche al codice. Tuttavia, man mano che l'applicazione diventa più complessa e vengono aggiunte nuove funzionalità, il processo di rilascio di un monolite tende a diventare più complicato e con maggiori probabilità di fallire.

Secondo la filosofia dei microservizi, non dovrebbe mai esserci un lungo ciclo di rilascio unico a cui ogni team deve mettersi in coda. Il team che compila il servizio A può rilasciare un aggiornamento quando sceglie e non deve attendere che le modifiche nel servizio B vengano unite, testate e distribuite.

Diagramma che confronta CI/CD per architetture monolitiche e microservizi.

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

Challenges

  • Molte codebase indipendenti di piccole dimensioni: Ogni team è responsabile della creazione di un proprio servizio, con la propria pipeline di compilazione. In alcune organizzazioni, i team potrebbero usare repository di codice separati. I repository separati possono disperdere le conoscenze su come creare il sistema tra i team. Di conseguenza, nessuno dell'organizzazione sa come distribuire l'intera applicazione.

    Mitigazione: Avere una pipeline unificata e automatizzata o almeno un'infrastruttura pipeline comune per creare e distribuire servizi in modo che questa conoscenza non sia nascosta all'interno di ogni team. I modelli di pipeline riutilizzabili, ad esempio GitHub Actions flussi di lavoro riutilizzabili o Azure Pipelines, consentono di standardizzare la compilazione, il test, l'analisi e la distribuzione dei passaggi in ogni servizio.

  • Più linguaggi e framework: Ogni team usa una propria combinazione di tecnologie, quindi può essere difficile creare un singolo processo di compilazione che funzioni nel carico di lavoro. Il processo di compilazione deve essere sufficientemente flessibile che ogni team possa adattarlo per il linguaggio o il framework scelto.

    Mitigazione: Containerizzare il processo di compilazione per ogni servizio in modo che il sistema di compilazione debba eseguire solo i contenitori. Le piattaforme come GitHub Actions, Azure Pipelines e Registro Azure Container attività possono compilare e pubblicare immagini del contenitore in modo coerente indipendentemente dal linguaggio di origine.

  • Test di integrazione e carico: Gli aggiornamenti delle versioni di Teams sono al loro ritmo, quindi può essere difficile progettare test end-to-end affidabili, soprattutto quando i servizi hanno dipendenze da altri servizi. L'esecuzione di un cluster di produzione completo può essere costosa, pertanto è improbabile che ogni team esegua il proprio cluster completo su scala di produzione solo per i test.

    Mitigazione: Usare ambienti di anteprima temporanei, ad esempio namespace dedicati a ogni pull request in Kubernetes o ambienti di App contenitore di Azure creati su richiesta. Usa i test contrattuali per far emergere tempestivamente i problemi di integrazione senza richiedere una replica completa dell’ambiente di produzione.

  • Gestione delle versioni: Ogni team deve essere in grado di distribuire un aggiornamento nell'ambiente di produzione. Questo requisito non significa che ogni membro del team disponga delle autorizzazioni per la distribuzione. Un ruolo di gestione delle versioni centralizzato può ridurre la velocità di distribuzione.

    Mitigazione: Più il processo CI/CD è automatizzato e affidabile, meno è necessario un'autorità centrale. È possibile che siano ancora presenti criteri diversi per il rilascio degli aggiornamenti delle funzionalità principali rispetto alle correzioni di bug secondarie. Un approccio decentralizzato non significa governance zero. Imporre le approvazioni usando ambienti e approvazioni di Azure Pipelines o ambienti di distribuzione e revisori obbligatori di GitHub Actions e codificare i criteri del cluster usando Criteri di Azure per Servizio Azure Kubernetes (AKS) o OPA Gatekeeper.

  • Aggiornamenti del servizio: Quando si aggiorna un servizio a una nuova versione, l'aggiornamento non dovrebbe causare l'esito negativo di altri servizi che dipendono da esso.

    Mitigazione: Usare tecniche di distribuzione come le versioni blu-verde o canary per le modifiche che non causano interruzioni. Per le modifiche che interrompono l'API, distribuire la nuova versione insieme alla versione precedente. Con questo approccio, i servizi che usano l'API precedente possono essere aggiornati e testati per la nuova API. Per altre informazioni, vedere Aggiornare i servizi.

  • Gestione delle identità e dei segreti della pipeline: I segreti dei service principal a lunga scadenza archiviati nelle pipeline sono una fonte frequente di compromissioni e di oneri operativi. I segreti delle entità servizio scadono, possono essere esposti e richiedono la rotazione in molte pipeline indipendenti di microservizi.

    Mitigazione: Autenticare le pipeline in Azure con la federazione delle identità dei carichi di lavoro, che usa OpenID Connect (OIDC), così che non venga archiviato alcun segreto del client nella pipeline. Per altre informazioni, vedi Identità del carico di lavoro per Azure Pipelines e Configurare OpenID Connect in Azure per GitHub Actions. Archiviare eventuali segreti rimanenti in Azure Key Vault e farvi riferimento in fase di esecuzione.

  • Sicurezza della catena di approvvigionamento: Tutto ciò che distribuisci in produzione deve poter essere ricondotto al codice e alle dipendenze da cui è stato generato. I microservizi aumentano il numero di immagini, registri e pipeline, aumentando così la superficie di attacco della supply chain.

    Mitigazione: Firma le immagini del contenitore con Notation e Key Vault e verifica le firme al momento dell'ammissione usando Integrità delle immagini di AKS o Ratify. Generare un SBOM come artefatto di compilazione. Analizza il codice, le dipendenze e le pipeline con sicurezza DevOps per Microsoft Defender per il cloud e GitHub Advanced Security. Analizza le immagini di runtime con Microsoft Defender for Containers. Richiedi che tutte le scansioni siano superate prima che un rilascio possa procedere.

Monorepo vs. multirepo

Prima di creare un flusso di lavoro CI/CD, è necessario conoscere il modo in cui la codebase è strutturata e gestita, tra cui:

  • Sia che i team lavorino su repository separati o in un monorepo.
  • Strategia di diramazione.
  • Chi può eseguire il push del codice nell'ambiente di produzione e se esiste un gestore di versione.

I team usano ampiamente entrambi gli approcci nell'ambiente di produzione. La scelta dipende dalla topologia del team, dalla maturità degli strumenti e dalla quantità di codice condiviso tra i servizi.

  Monorepo Più repo
Vantaggi - Condivisione del codice

- Più facile standardizzare codice e strumenti

- Più facile effettuare il refactoring del codice

- Individuabilità (una singola visualizzazione del codice)
- Responsabilità chiare per ogni team

- Potenzialmente meno conflitti di unione

- Consente di applicare il disaccoppiamento dei microservizi
sfide - Le modifiche apportate al codice condiviso possono influire su più microservizi

- Maggiore potenziale per i conflitti di merge

- Gli strumenti devono adattarsi a una base di codice di grandi dimensioni

- Controllo di accesso

- Processo di distribuzione più complesso
- Più difficile condividere il codice

- Più difficile applicare gli standard di codifica

- Gestione delle dipendenze

- Base di codice diffusa, scarsa individuabilità

- Mancanza di infrastruttura condivisa

Indipendentemente dal modello scelto, usa trigger limitati al percorso nelle pipeline, ad esempio filtri del percorso in GitHub Actions o percorsi di trigger in Azure Pipelines. I trigger basati sul percorso contribuiscono a garantire che, a ogni commit, vengano ricompilati e ridistribuiti solo i microservizi interessati.

Aggiornare i servizi

Esistono diverse strategie per l'aggiornamento di un servizio già in produzione, tra cui l'aggiornamento in sequenza, la distribuzione blu-verde e la versione canary. Questi modelli vengono spesso coordinati tramite un flusso di lavoro GitOps. Per ulteriori informazioni, vedi GitOps e progressive delivery.

Aggiornamenti in sequenza

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

Esempio in Kubernetes: In Kubernetes gli aggiornamenti in sequenza sono il comportamento predefinito quando si aggiorna la specifica del pod per una distribuzione. Il controller di deployment crea un nuovo ReplicaSet per i pod aggiornati. Quindi aumenta il nuovo ReplicaSet mentre riduce il ReplicaSet precedente per mantenere il numero di repliche desiderato. Non elimina i pod precedenti finché i nuovi pod non sono pronti. Kubernetes mantiene una cronologia dell'aggiornamento, quindi è possibile eseguire il rollback di un aggiornamento, se necessario.

Esempio in Container Apps: Container Apps usa le revisioni per gestire gli aggiornamenti progressivi. Quando si distribuisce una nuova revisione, Le app contenitore possono spostare gradualmente il traffico dalla revisione precedente alla nuova revisione usando regole di suddivisione del traffico. Se la nuova revisione rileva problemi, è possibile eseguire il rollback reindirizzando il traffico alla revisione precedente. È possibile configurare più revisioni attive contemporaneamente e controllare la percentuale di traffico ricevuta da ogni revisione.

Una sfida per gli aggiornamenti in sequenza è che durante il processo di aggiornamento, una combinazione di versioni precedenti e nuove vengono eseguite e ricevono traffico. Durante questo periodo, il sistema può instradare qualsiasi richiesta a una delle due versioni.

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

Distribuzione blu-verde

In una distribuzione blu-verde si distribuisce la nuova versione insieme alla versione precedente. Dopo aver convalidato la nuova versione, si passa tutto il traffico contemporaneamente dalla versione precedente alla nuova versione. Dopo l'opzione, si monitora l'applicazione per eventuali problemi. In caso di problemi, è possibile reindirizzare il traffico alla versione precedente. In caso di problemi, è possibile eliminare la versione precedente.

Con un'applicazione monolitica o a più livelli più tradizionale, la distribuzione blu-verde significa in genere creare due ambienti identici. Distribuire la nuova versione in un ambiente di staging e quindi reindirizzare il traffico client a tale ambiente, ad esempio scambiando un indirizzo IP virtuale. 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 cambiare il traffico.

Esempio in Kubernetes: In Kubernetes non è necessario creare un cluster separato per eseguire distribuzioni 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. Crea questa distribuzione, ma non eliminare la distribuzione precedente né modificare il servizio che punta ad essa. Dopo l'esecuzione dei nuovi pod, è possibile aggiornare il selettore del servizio in modo che corrisponda alla nuova distribuzione.

Uno svantaggio della distribuzione blu-verde è che durante l'aggiornamento, si eseguono due volte più pod per il servizio (corrente e successiva). Se i pod utilizzano una quantità significativa di risorse CPU o memoria, potrebbe essere necessario scalare temporaneamente il cluster per soddisfare il maggiore fabbisogno di risorse.

Versione Canary

In una versione canary si distribuisce una versione aggiornata in un piccolo subset di client e quindi si monitora il comportamento del nuovo servizio prima di distribuirlo a tutti i client. Con questo approccio, è possibile implementare gradualmente in modo controllato, monitorare i dati reali e identificare i problemi prima che influiscano su tutti i clienti.

Una versione canary è più complessa da gestire rispetto a un aggiornamento blu-verde o in sequenza perché è necessario instradare le richieste in modo dinamico a versioni diverse del servizio.

Esempio in Kubernetes: In Kubernetes è possibile configurare un servizio per estendersi su due set di repliche (uno per ogni versione) e modificare manualmente i conteggi delle repliche. Tuttavia, questo approccio è con granularità grossolana a causa del modo in cui Kubernetes bilancia il carico tra i pod. Ad esempio, se hai un totale di 10 repliche, puoi spostare il traffico solo in incrementi del 10%. Se utilizzi un service mesh, puoi usare le relative regole di instradamento per implementare una strategia di rilascio canary più avanzata.

Esempio in App contenitore: In App contenitore è possibile usare la suddivisione del traffico per inviare una percentuale definita di traffico a una nuova revisione (ad esempio 10% a v2 mentre 90% rimane su v1) e spostare i pesi man mano che aumenta la confidenza, senza che sia necessaria alcuna mesh di servizi esterna.

Rilascio progressivo e GitOps

Per i team che operano molti microservizi in Kubernetes, un modello basato sul pull di GitOps integra gli esempi precedenti basati su push. Lo stato del cluster desiderato si trova in Git e un operatore in-cluster riconcilia il cluster con tale stato. CI compila, testa, analizza, firma e pubblica l'immagine. CD allinea il cluster al manifest. Questa separazione offre audit trail e un ripristino di emergenza più semplice. Elimina anche la necessità che il runner CI disponga di credenziali di accesso diretto al cluster.

Passaggi successivi