La pipeline CI/CD nel 2023

La pipeline CI/CD nel 2023

 24 Dec 2022 -  Giulio Vian -  ~17 minuti

Ecco un lungo articolo in tempo per le vacanze natalizie. Oggi voglio darvi una panoramica su cosa prevedere in una moderna pipeline di Continuous Integration / Continuous Delivery (CI/CD). Com’è mio solito non mi focalizzerò su specifiche tecnologie quanto sul processo, limitando ad accenni di prodotti e soluzioni tecniche, quel tanto da esemplificare i concetti.

A mio modesto parere, il migliore approccio per disegnare un processo, od anche un’architettura, è di procedere a ritroso, dall’obiettivo finale con i suoi requisiti fondamentali, all’indietro fino alla pianificazione dell’evoluzione del prodotto. In pratica, senza chiarezza sugli obiettivi, risulta difficile costruire un percorso per realizzarli e con grande probabilità si arrangierà qualcosa che soddisfa l’ego di qualcuno ma non gli utilizzatori del prodotto né di coloro che lo progettano e l’implementano.

Ho scomposto in nove fasi generali la costruzione di un prodotto o servizio basato sul software. Voglio osservare che con “pipeline” intendo un oggetto logico che potrebbe non avere una corrispondenza univoca, bensì una concatenazione di pipeline fisiche, ad esempio distinte tra CI e CD.

Cominciamo dunque dall’ultima fase, con quanto serve a…

9. Dopo il rilascio in produzione

Cinque sono gli elementi da considerare per il sistema in esercizio:

  • il monitoraggio operativo (metriche di business) con metriche quali numero di utenti unici attivi, quelli anonimi, quanti completano una certa transazione ecc.;
  • il monitoraggio tecnico (metriche operazionali tecniche) con metriche come il consumo di risorse (CPU, memoria, storage, rete) fino a calcolare i costi di esercizio (affitto di server, licenze, ecc.) e metriche fini come numero di connessioni concorrenti, thread, durata delle transazioni, ecc.;
  • l’analisi dei log per identificare anomalie, in particolare attacchi, e tendenze nel comportamento vuoi del sistema, vuoi degli utilizzatori;
  • il sistema delle segnalazioni utente e il supporto tecnico;
  • la documentazione utente.

La composizione dei requisiti di business e dell’architettura tecnica stabiliscono le possibili implementazioni. Ad esempio, una applicazione desktop impiegherà telemetria, mentre un sito web può esser direttamente monitorato. La documentazione utente per una libreria, un SDK, sarà un sito con un filtro per la versione della libreria; per una applicazione mobile potrebbero essere delle animazioni, dei video, incorporati nella stessa.

Raccomandazione: indipendentemente dal genere di sistema o applicazione vi sarà sempre e comunque la necessità di coprire i cinque elementi di cui sopra con strumenti e dati correlabili.

Esempi: Prometheus, Grafana, Open Telemetry, Information technology service management (ITSM), JIRA, ServiceNow, PagerDuty.

A quanto detto sopra potremo dover aggiungere quegli elementi dell’esercizio operativo che potrebbero entrare nella pipeline. Un esempio potrebbe essere lo spazio di backup e l’agente di backup che vengono definiti e configurati nel codice di costruzione dell’infrastruttura.

8. Il rilascio in produzione

I principali obiettivi specifici di questa fase (obiettivi tattici) sono:

  • minimizzare l’inconvenienza per l’utente (interruzione di servizio, durata dell’aggiornamento, ecc.)
  • esser sicuri che quanto rilasciato non abbia problemi maggiori della versione che si va a sostituire (smoke tests),
  • informare le parti interessate (stakeholder) del rilascio.

Raccomandazione: adottare architetture che permettano rilasci con disservizio minimo e limitino i danni in caso di malfunzionamento (blast radio). Notare quali decisioni architetturali prese in precedenza (upstream) possono ostacolare il rilascio e lavorare per migliorare la situazione.

Quanto maggiore è il grado di persistenza di una componente, tanto più limitate devono essere le modifiche, e maggiore il grado di controllo e revisione. Al vertice son le basi dati, relazionali, no-SQL o altro: qui il grado di persistenza è massimo e le modifiche possono essere distruttive e bloccare completamente il servizio. Appena inferiori appaiono le modifiche all’infrastruttura, supponendo che storage e basi dati siano elementi protetti. I processi in esecuzione sono quasi sempre un elemento volatile, indipendentemente che siano container, macchine virtuali o processi batch. Quest’ultimi possono avere un certo grado di persistenza da tenere conto nella progettazione di un rilascio.

È essenziale che ci siano dei test sia pre- che post- rilascio. I test precedenti verificano lo stato del sistema obiettivo prima di alterarlo, ad esempio la versione del sistema operativo o di Kubernetes o della base dati. Dopo ogni cambiamento di stato del sistema obiettivo ci deve essere un test per confermare che la situazione sia coerente con quella attesa dalla procedura di deploy, fino al test finale che verificherà la salute del sistema obiettivo e che le funzioni principali non siano state compromesse.

Ogni passo di deploy deve avere una contro-azione da applicare in caso di fallimento così da ripristinare l’operatività. Sia l’avvio che il completamento del rilascio devono produrre registrazioni nei sistemi di Service Management and Operations. Tramite ITSM o direttamente gli utenti vengono informati e ricevono l’aggiornamento della documentazione con le note di rilascio. Questo è vieppiù indispensabile se siamo soggetti a regolamentazione.

Infine, il passaggio in produzione deve essere dettagliatamente tracciato e può necessitare di passaggi approvativi. Oltre ad informare lo strumento ITSM, anche gli strumenti di monitoraggio devono essere notificati di eventi importanti quali: date di rilasci importanti, inizio del deploy, fasi di pre-riscaldamento, completamento del deploy, ecc.

Architetture: Blue-green, canary deployments, API gateway, reverse proxies. Esempi: GitHub Actions, Azure DevOps Pipelines, GitLab, Octopus Deploy, Atlassian Bitbucket/Bamboo, ArgoCD, Flux.

Da approfondire: architetture a micro-servizi come sono comunemente intese vanno cautamente ponderate. Se da un lato riducono l’impatto di un rilascio, dall’altro complicano la risoluzione dei problemi (troubleshooting) così come per tutti i sistemi distribuiti.

7. I rilasci prima della produzione

Lo scopo dei rilasci pre-produzione è sempre (e solo) di validare e verificare: non hanno alcun valore intrinseco, servono solo a prepararci ad andare in produzione perché solo i rilasci in produzione danno del valore agli utenti. Per il team responsabile del rilascio ogni rilascio prima della produzione è un’occasione per validare il processo automatizzato e conoscerne le tempistiche. Per i team che dipendono dai nostri rilasci è il momento per rieseguire i propri test di integrazione e verificare che le API siano compatibili.

Raccomandazione: Il processo di rilascio deve essere automatizzato quanto più possibile, preferibilmente con un approccio dichiarativo, lasciando pressoché nulla all’esecuzione manuale. In particolare:

  • le risorse infrastrutturali necessarie al sistema (Infrastructure-as-Code),
  • le configurazioni richieste per le risorse (Configuration-as-Code),
  • modifiche di storage e basi dati come script SQL,
  • modifiche a componenti applicative come binari e il loro lancio.

La pipeline di rilascio deve includere test automatizzati di vario genere tra cui:

  • test di integrazione;
  • test a livello di API;
  • validazione della User Interface;
  • una varietà di test funzionali (ad es. generativi);
  • test non-funzionali tra cui:
    • Dynamic application security testing (DAST),
    • Interactive application security testing (IAST),
    • Performance, scalability and reliability testing,
    • chaos testing,
    • Internationalization (I18N),
    • accessibilità (A11Y);
  • sonde e statistiche per ottimizzare future esecuzioni di test (test impact analysis) al fine di minimizzare i tempi di rilascio.

Ovviamente la presenza di test automatizzati non esclude quelli manuali. La pipeline di rilascio e relativi script devono essere gli stessi indipendentemente dall’ambiente obiettivo, ossia identici per tutti gli ambienti salvo i valori dei parametri. Obiettivi essenziale dei rilascio pre-produzione sono:

  • validare il processo di rilascio automatico,
  • le tempistiche del processo stesso per determinare finestre di disservizio.

Esempi: Terraform, Ansible, Docker, scripting languages (bash, PowerShell, Python), database configuration (Flyway, Liquibase, RedGate, dbMaestro).

6. Dal codice al pacchetto di rilascio

Lo scopo di questa fase è di aggregare in un unico punto tutto quanto sia necessario per il rilascio indipendentemente dall’ambiente in cui si rilascerà. Le attività di questa fase sono:

  • la raccolta di tutti i sorgenti, script, tool e documentazione da includere nel pacchetto di rilascio,
  • generazione, compilazione e transpilazione nei formati necessari al deploy,
  • analisi di tutti i sorgenti che, direttamente o indirettamente, saranno impiegati nel deploy,
  • esecuzione di test (vedi sopra),
  • raccolta di tutti i metadati connessi a questo rilascio (log di compilazione, risultati delle analisi, dei test, ecc.),
  • pubblicazione del pacchetto di rilascio.

Spessissimo questo punto di aggregazione è un singolo file (tar, zip, tgz, ecc.) reperibile in un luogo preciso. In altri casi è una collezione organica di file, un unico oggetto logico scomposto per convenienza. Un paio di esempi possono chiarire. Prendiamo Node.JS che offre un diverso pacchetto di installazione con binari per x64, x32, arm32, arm64, ecc. Si tratta dello stesso rilascio in versioni diverse, quindi il rilascio non è un singolo file, ma molti, uno per ciascuna piattaforma. Un altro esempio è un sito statico che viene aggiornato mediante rsync o robocopy.

Parliamo ora del pacchetto stesso. Esso deve essere lo stesso per tutti gli ambienti, seguendo il principio Only Build Your Binaries Once reso popolare da Continuous Delivery   (p.113 e sgg.). C’è un caso in cui il pacchetto può essere diverso per la produzione? Sì, ma è anche l’unico che conosca. È il caso in cui il pacchetto è firmato crittograficamente o contiene degli elementi cifrati. Il caso più comune sono le applicazioni mobili che devono essere pubblicate in un Store. È comprensibile che la chiave di firma per la produzione sia diversa da quella di non-produzione, con la chiave di produzione tenuta ben al sicuro. I dati in chiaro, ovvero i binari prima della firma, devono essere sempre gli stessi. La tecnica più semplice è usare due stadi, nel primo la CI produce un pacchetto non firmato, nel secondo il pacchetto viene firmato e storicizzato prima del deploy sull’ambiente di destinazione. In nessun altro caso si possono impiegare pacchetti diversi per ambienti diversi.

Cosa non fare: recuperare i file da rilasciare da più posti, lasciare fuori dal pacchetto gli script di rilascio o gli aggiornamenti dei database.

Cosa fare con componenti che presumono l’uso di pacchetti diversi per la produzione e altri per lo sviluppo? Ad esempio React si presenta come react.production.min.js o react.development.js. La regola generale è che le versioni di sviluppo (debug)… devono restare confinate allo sviluppo, ergo il pacchetto di rilascio ha solo versioni di produzione. compilate in release con relative ottimizzazioni. Le versioni di sviluppo/debug sono usate solo sulle macchine degli sviluppatori e mai in ambienti di test. Ma se ho bisogno di andare in debug in un ambiente di test? L’approccio corretto è di includere nel pacchetto di rilascio le informazioni di debug (symbols). L’installazione dei simboli nell’ambiente target sarà convenientemente facoltativa (non volendo facilitare un attacco grazie alla disponibilità di questi dati), e devono comunque essere prodotti contestualmente al resto del pacchetto.

Raccomandazione: la costruzione del pacchetto deve comprendere la massima quantità di controlli statici possibili con regole precise sulla tolleranza ammessa:

  • il compilatore non deve generare warnings,
  • gli analizzatori sintattici o linters, come Sonar, non generano avvisi,
  • le metriche di qualità non superano soglie d’allarme,
  • nessun problema di sicurezza rilevato dallo strumento Static application security testing (SAST),
  • le librerie usate non presentano problemi di sicurezza o di licenza secondo lo strumento di Software Composition Analysis (SCA),
  • tutti i test automatizzati (unit e la parte di test di integrazione applicabili senza installare) passano,
  • le altre metriche dei test rientrano nella tolleranza ammessa (percentuale di copertura dei test, ripetizione di test falliti, durata dei test, ecc.).

La lista è indicativa e possiamo ragionevolmente prevedere evoluzioni e integrazioni tra gli strumenti, per cui la distinzione tra i vari tipi di analisi diventerà più teorica che pratica. Ovviamente si eviterà di applicare inutili analisi al codice che non è destinato al rilascio (ad es. i progetti di unit test): è uno spreco di risorse e fonte di disturbo.

Il pacchetto deve avere una chiara identificazione e rintracciabilità (versione dei sorgenti, identificativo della build) intrinseca, cioè parte del pacchetto stesso. Molti formati, Maven JARs, NuGet, npm, permettono di memorizzare tali informazioni, lasciando la responsabilità di compilarle allo sviluppatore.

Ci sono altre cose generate in questa fase oltre al pacchetto di rilascio? ‘A voglia, direbbe qualcuno. Io raccomando:

  • i simboli di debug,
  • le note di rilascio (release notes),
  • dati sui test e la qualità del codice,
  • Software Bill of Materials (SBOM).

5. Integrazione del codice

Passiamo ora a considerare come integrare il rilascio del codice nella pipeline.

Per la mia esperienza, ai fini della Continuous Delivery è relativamente poco importante quale modello si scelga per la strategia di branching: trunk-based, release-branches, git-flow. La scelta è legata a molteplici fattori (dimensione del team, frequenza di aggiornamento, pratiche di programmazione condivisa come il pair programming, ecc.); l’importante è che sia definita nei particolari e concorde nei team che collaborano alla codebase.

A supporto di questa fase le moderne piattaforme di sviluppo offrono delle preziose funzionalità sotto il nome di Pull request (PR). Una PR si compone di elementi diversi:

  • un aspetto comunicativo perché informa il resto del team di un cambiamento del codice;
  • l’approvazione da parte di revisori, solitamente diversi dall’autore della modifica;
  • è un momento di verifica formale in connessione all’audit e alle regolamentazioni;
  • l’automazione dei controlli di qualità tramite specifiche pipelines e notificazione ad altri strumenti come Sonar;
  • standardizzazione nel formato dei commit message, associazione con work-item/ticket/issue, tipologia di merge ammessa e altro.

Questi elementi sono componibili per modellarsi sul processo scelto dal team. Osservo che alcuni cultori di Agile si fossilizzano sugli aspetti negativi di usare le PR come strumenti di collaborazione e spesso trascurano il suo valore per gli aspetti di qualità. In questo gli strumenti moderni sono assai potenti e flessibili.

Il meccanismo primitivo di Pre-commit hook permette di realizzare parte dei suddetti ma decisamente in modo meno agevole.

Raccomandazione: moderne piattaforme come Azure DevOps, Bitbucket, GitHub, GitLab offrono molte utili funzionalità sotto l’ombrello Pull-Request. Studiate come avvantaggiarvene senza appesantire il vostro processo.

4. La qualità del codice

Quali sono le caratteristiche fondamentali che deve possedere il nostro codice dal punto di vista della pipeline di rilascio? A mio avviso sono tre:

  • la semplicità di rilascio e deploy,
  • la possibilità di una semplice e sicura configurazione separata dal pacchetto di distribuzione,
  • infine deve essere realizzato in modo che sia osservabile una volta in esercizio.

Non è possibile addentrarci ora nella considerazione se vi siano architetture più semplici in termini di rilascio, lo farò in un futuro articolo. Anticipo solo che è possibile (facile) complicare inutilmente qualsiasi architettura e rendere difficile il deploy di una applicazione. In questa sede cercherò di chiarire cosa intendo con “semplice”. Il disegno dell’applicazione o sistema è semplice riguardo il rilascio quando è possibile modificare un singolo modulo senza rilasciare/ricompilare l’intera applicazione/sistema. Inoltre, è semplice solo se prevede un numero limitato di processi di deploy (al massimo quattro o cinque, generalmente uno per ruolo del nodo), cioè degli script che installano la rispettiva porzione del sistema.

Che una applicazione sia parametrica e configurabile è ovvio, giusto no? Purtroppo mi capita sotto il naso codice zeppo di valori “schiantati” nel codice: stringhe di connessione a basi dati, URL, identificativi di certificati, fino a passwords e API keys. Parte della responsabilità risiede in pessimi esempi nella documentazione o nei corsi base di programmazione che non illustrano approcci adatti alla produzione. Quali elementi vanno nella configurazione? Fondamentalmente due: identificatori di oggetti esterni (come nomi di macchine o URL) e parametri di comportamento (es. frequenza di polling o timeout di connessione,). Il formato della configurazione deve puntare alla semplicità e caratterizzarsi per intuitività e auto-descrittività per quanto possibile. Disgraziatamente molti dei formati in uso falliscono l’obiettivo. La configurazione deve anche esser facile da modificare in modo automatico, con un linguaggio di scripting.

La qualità dell’osservabilità di un sistema in esecuzione è scolpita nel codice. Per quanto le moderne piattaforme si sforzino di offrire ricche metriche di base (pensiamo a JMX e OpenTelemetry), vi sarà sempre bisogno di esporre metriche e log specifici per il nostro sistema, con attenzione alla qualità delle informazioni esposte (vedi sopra). Nell’esporre informazioni interne al sistema, si filtreranno dati sensibili o perché segreti o perché personali (PII).

Raccomandazione: la configurazione deve essere pensata per chi la utilizzerà: altri sviluppatori, un amministratore in produzione, lo script di deploy. Curate di esplicitare le unità di misura dei parametri e inserire adeguati commenti in modo che sia facile modificare un valore. Similmente per gli aspetti di osservabilità: i messaggi di log devono essere comprensibili per chiunque non sia il programmatore. Più trascurerete questi aspetti, più faticosi i rilasci e difficile la risoluzione dei problemi che, inevitabilmente, emergeranno nell’esercizio reale di produzione.

3. L’ambiente di sviluppo

Ma il codice da dove viene?

Oggi arriva dalla mente di un programmatore, in futuro, pure. La pura scrittura del codice non richiede una macchina dedicata, sono le fasi di test e debug che necessitano di risorse specifiche. Debugger, basi dati, hardware, connessioni a sistemi esterni: la casistica è amplissima.

È evidente la stretta connessione tra ambiente di sviluppo, di build e di test. Il mercato attuale offre parziale soluzione al problema, ad esempio con i Dev containers   che però non posso usare in una pipeline per lanciare un ambiente dove eseguire i test, men che meno per definire l’ambiente di produzione. Al momento non ci sono soluzioni semplici e pulite se appena appena si superano i confini del puro web. Configurare un ambiente per un nuovo membro del team richiederà un alchemico miscuglio di script e file di configurazione, scarsamente riutilizzabile. Pazienza, ritengo il gioco valga sempre la candela ( Automatizzare? Sempre!   ).

Raccomandazione: automatizzate la costruzione dell’ambiente per lo sviluppatore, non solo per gli ambienti di rilascio.

2. Prima del codice

Siamo arrivati allo snodo cruciale, la congiunzione tra il programmatore e il resto del mondo: analisti, gestori di prodotto, di progetto, clienti, manager.

Le tecniche di gestione del lavoro (backlog) che troviamo nei team sono le più diverse, dai post-it a Jira. Qui ci interessa comprendere quali informazioni vogliamo catturare con questi strumenti. Il criterio è: se una informazione ci servirà per rilasciare, allora la dobbiamo cogliere sul nascere e trasmettere lungo la pipeline. Le informazioni devono essere organizzate in modo che sia facile sapere quali funzionalità, ad alto livello, sono presenti in un determinato pacchetto di rilascio (ad es. con la generazione automatica delle release notes). Una piattaforma di sviluppo moderna ci permetterà di collegare una storia (user story) ad un ramo o ad un commit e così tracciare l’evoluzione della storia attraverso la pipeline. Un’altra modalità con cui le piattaforma creano i collegamenti è mediante una speciale annotazione nel commento di un commit. Questa serve anche nel caso non ci sia uno strumento informatico di gestione del backlog. Un passo ulteriore consiste nell’arricchire ulteriormente queste informazioni usando ad esempio un formato dei messaggi di commit come Conventional Commits   . Se non creiamo queste associazioni all’inizio, addirittura prima ancora di scrivere una riga di codice, sarà estremamente complicato ricostruirle a posteriori. La parola chiave qui è tracciabilità.

Le informazioni che possiamo ficcare in un post-it, reale o virtuale, sono limitate, anzi, debbono esserlo! Dove annotiamo tutto il resto, che siano informazioni interne ai team o esterne, fruibili dagli utenti finali? Ci serve un sistema che ci permetta di gestire documentazione, organizzata gerarchicamente, catalogata, in grado di tracciare le modifiche. Un Wiki è un ottimo punto di partenza, e difatti le moderne piattaforme lo includono (GitHub Pages, Azure Wiki, GitLab Wiki). Esigenze più complesse possono essere soddisfatte con una pipeline di pubblicazione: l’automazione di CI/CD genera siti web di immediata e specifica fruizione documentale al cambio di un file in un repository. Tutta questa meraviglia tecnica non è però in grado di compensare l’assenza della figura di bibliotecario, o di un technical writer, che curi l’organizzazione, la forma espressiva, e soprattutto la coerenza dei contenuti.

Raccomandazione: non aspettate a definire gli standard per la documentazione e la tracciabilità: gli strumenti per farlo sono economici, numerosi e facili da usare.

1. Parte da lontano

Tenendo sempre presente che DevOps è the result of applying Lean principles to the technology value stream ( The DevOps Handbook   p. 3), ossia il risultato che si ottiene applicando i principi Lean alla catena del valore tecnologico, non possiamo che considerare come punto di partenza il prodotto (o servizio).

All’inizio della pipeline ci sono gli strumenti usati dal Product Owner (PO) per costruire il Product Backlog. Il PO ragiona in termini di trimestri, di annualità e oltre. Un team ragiona in termini di settimane o mesi. Dove il PO descrive le funzionalità con un basso grado di accuratezza, il team ha bisogno di precisione e chiarezza. Il backlog di prodotto deve tenere conto di fattori come stagionalità, cambiamenti del mercato, budget e quant’altro. Non si tratta di uno strumento pensato per organizzare il lavoro, ma per definire priorità aziendali.

In molti progetti dobbiamo considerare i contenuti che arrivano da altri team come i designer o i progettisti hardware. Se non si vuole ricadere in un processo a cascata (waterfall) si dovranno definire standard e strumenti condivisi tra i gruppi in modo che qualsiasi artefatto abbia l’indicazione della versione, l’informazione sia pubblicata in un luogo ben noto e facilmente accessibile, e infine ci sia piena tracciabilità tra i molti artefatti.

Raccomandazione: Evitate la trappola di usare lo stesso strumento per il PO e per gli sviluppatori. È invece opportuno un qualche genere di integrazione automatica in modo che alcuni oggetti nello strumento del PO siano proiettati nel backlog degli sviluppatori con un aggiornamento bidirezionale. A meno di segreti industriali, lo strumento del PO deve essere visibile a tutti i gruppi a valle, fino al supporto utente, così da permettere una visione collettiva delle scadenze importanti e dei cambiamenti in arrivo.

Esempi: Aha!, Miro, Asana, Productboard.

Conclusioni

Alla fine di questa lunga carrellata voglio riassumere i principi guida da usare nel progettare o migliorare le pipeline di CI/CD ovvero il processo del nostro ciclo di vita del software. A mio avviso sono tre:

  • introspezione del processo,
  • tracciabilità,
  • automazione.

Voi che ne pensate?

comments powered by Disqus