Only Build Your Binaries Once

Only Build Your Binaries Once

 06 Jan 2023 -  Giulio Vian -  ~7 minuti

Uno dei principi evidenziati da Continuous Delivery   , già molti anni fa, è Only Build Your Binaries Once (Cap.5 p.113), traducibile con costruisci i tuoi binari una sola volta.

Come accade a tutti i bravi principi, la sua formulazione è semplice ma le ramificazioni numerose. Quest’oggi cercherò di sviluppare le principali conseguenze e chiarire i motivi che giustificano il principio di generare i binari una volta sola.

Prima di addentrarci bisogna chiarire che il termine “binari”, nel contesto degli autori originali ma anche in questo articolo, comprende ogni genere di artefatti prodotti dalla continuous integration (ossia la build) ed in particolare i pacchetti di installazione o distribuzione.

Violazioni

Iniziamo chiedendoci cosa intendano gli autori con una sola volta. Il modo più facile di comprendere questa parte, consideriamo i casi contrari, dove i binari son costruiti più di una volta perché sono specifici per una particolare destinazione.

Un primo caso di violazione del principio che ho osservato è in organizzazioni che rilasciano versioni appartenenti a rami sorgenti diversi. Questi gruppi gestiscono i sorgenti con rami di lunga durata e ciascun ramo corrisponde ad ambienti di rilascio (es. sviluppo, integrazione, accettazione, produzione). Per rilasciare su un ambiente viene presa la versione del ramo corrispondente, compilata e finalmente rilasciata nell’ambiente destinatario. La promozione di una modifica si compie con una operazione di merge da un ramo “inferiore” ad uno superiore.

Un altro esempio di violazione sono i framework JavaScript client come React che annegano nel codice prodotto valori specifici per l’ambiente destinatario. Precisamente non il framework medesimo ma gli strumenti al contorno, in particolare il tool che produce il pacchetto da distribuire. Se si prova a modificare questi valori nel codice, si romperà il controllo sull’hash dei file corrispondenti e l’applicazione diventa inusabile. Un ulteriore problema di questi framework JavaScript è l’indicare la versione del framework, ad esempio React ha react.production.min.js e react.development.js e le pagine HTML fanno esplicito riferimento all’una o all’altra versione.

Invalidazione dei test

Le soluzioni descritte violano chiaramente il principio di generare il pacchetto una sola volta. Il primo e principale problema che creano è di invalidare i test. Se testo una versione in un ambiente, ma la versione che provo nell’ambiente successivo è differente, ad es. perché costruita con sorgenti diversi, quale garanzia ho che i test effettuati nell’ambiente precedente siano ancora validi e significativi?

Qualcuno obietterà che certe modifiche non invalidano i test, ma bisogna esserne certi al 100%, meglio se dimostrabile matematicamente. Qualcun altro potrebbe eccepire che, facendo particolare attenzione è possibile dimostrare che il passaggio delle modifiche mediante merge sia il medesimo in tutti i passaggi. A questo obietto che, sì, i sorgenti sono identici, ma non è possibile garantire che gli strumenti usati per generare i binari, e perciò il pacchetto di installazione, siano identici e producano il medesimo risultato.

Osservo invece che l’interrogativo non dipende dal numero di ambienti e dai livelli di promozione, ma solo dal portare in produzione binari diversi da quelli testati. Devo ricordare che conta sempre e solo la produzione? Se porto in produzione qualcosa che non è stato realmente testato perché era una versione diversa, sto assumendo un serio rischio che qualcosa non funzioni: è un salto nel buio.

Merge? No grazie

Le strategie basate sulla promozione dei sorgenti comportano troppo frequentemente dei merge-set elefantiaci: tantissime modifiche su moltissimi file con dovizia di conflitti. Queste situazioni comportano un insano dispendio di energie per risolvere tutti gli inconvenienti, con una elevata probabilità d’errore.

L’esperienza ci ha dimostrato che, quanto più circoscritta una modifica, maggiori sono le possibilità di successo del merge, al punto che viene completato automaticamente dagli strumenti di version control.

Si riaffaccia il principio Lean degli small batches, del minimizzare la quantità di modifiche contenute in un rilascio. Qui siamo all’inizio della catena, se non siamo in grado di limitarci a questo stadio, sarà ancora più difficile nelle fasi successive.

Altre motivazioni

L’invalidazione dei test è il principale motivo per non usare versioni diverse da quella che andrà in produzione, ma ci sono altre ragioni per stare lontani da versioni legate ad un ambiente. Queste altre motivazioni son ulteriori elementi di rischio.

Anzitutto la possibilità di rilasciare la versione sbagliata nell’ambiente incorretto: nel caso più fortunato il sistema non parte, altrimenti potremmo incappare in bug sottili ma devastanti. Ulteriori rischi sono sul fronte della sicurezza e della privacy. Rilasciare in produzione la versione di debug offre agli attaccanti un terreno più facile, ad esempio nel formato dei call frames. Quanto alla privacy se in debug tutta la memoria è accessibile senza filtri, aumenta il rischio di esfiltrare dati sensibili (Personally identifiable information o PII).

Ogni piattaforma o linguaggio offre due modalità di compilazione o traduzione, generalmente etichettate come debug e release. Il codice in modalità debug è meno efficiente di quello ottimizzato per la produzione. È essenziale che si validi in pre-produzione quest’ultima versione e non quella di debug: il comportamento cambia in modo sottile e difficilmente prevedibile. Ad esempio una operazione può scadere (timeout) in debug con una certa frequenza, mentre in release la frequenza cala drasticamente.

Deploy

Fino a questo punto ci siam focalizzati sulla porzione maggiore del pacchetto di rilascio, ossia i binari dell’applicazione (o componente). Nel pacchetto, o equivalentemente nella pipeline, son presenti gli script di rilascio e installazione. Anche questi devono sottostare al medesimo principio: uso sempre gli stessi script in ogni ambiente, garantendo per quanto possibile la consistenza del processo di deploy.

Qualcuno obietterà l’impossibilità della cosa: un ambiente di test non ha l’infrastruttura distribuita di alta affidabilità (HA) e disaster recovery (DR) che ho in produzione, per cui, al minimo, gli script avranno delle sezioni condizionate al tipo d’ambiente. A questi rispondo che le condizioni vanno evitate per quanto possibile: la stragrande maggioranza delle condizioni possono esser validamente sostituite da cicli (loop) su elenchi. Aggiungo infine che almeno un ambiente di validazione deve rispecchiare la topologia produzione in termini di HA e DR, ridotto in scala così da risparmiare sui costi. Tale ambiente permette di verificare la correttezza degli script senza compromessi e giungere con maggior serenità in produzione.

Collaterali

Sperando di esser stato chiaro ed esaustivo sulle ragioni a supporto del principio di costruire il pacchetto di distribuzione una volta sola, passiamo ad esaminare alcuni effetti della sua applicazione.

Un beneficio si presenta nel risparmiare risorse computazionali dedicate alle pipeline bilanciate da un aumento dei consumi di storage (disco), il che risulta evidente nello scenario di un ramo sorgente per ambiente. Non cadiamo però nell’errore di pensare che adottando invece la tecnica delle reproducible build   saremo sempre certi di poter riprodurre gli stessi identici eseguibili. Se dobbiamo andare indietro nel tempo, potremmo trovarci nell’impossibilità di usare la stessa pipeline, con gli stessi compilatori e librerie usati in origine. Conservare i pacchetti prodotti dalla build è molto più facile e soddisfa possibili richieste dall’audit e dalla sicurezza.

La build è il momento di produrre una quantità di (meta-)dati sul pacchetto di distribuzione ed il suo contenuto. In particolare si produrrà una distinta base software, in inglese Software Bill of Materials (SBOM), che elenchi tutti i file che verranno distribuiti, le loro dipendenze e soprattutto l’hash crittografico dei file. Salvata in luogo sicuro permette di controllare se i file in produzione siano stati alterati da un attacco.

Eccezioni

Si dice che l’eccezione confermi la regola, così al principio di costruire i binari una volta per tutte c'è un’eccezione ed è la firma dei binari o del pacchetto. È il caso delle applicazioni mobile, Android APK o Apple IPA, che devono esser firmate per essere pubblicate su uno Store.

Di norma la chiave crittografica per la firma ufficiale è conservata in un luogo sicuro, magari protetto da un hardware security module (HSM), disponibile ad una pipeline privilegiata da una specifica autorizzazione e sovente un’approvazione manuale. Ciò vuol dire che una normale build non può usare questa chiave.

L’approccio che raccomando è di usare il medesimo script per la build la quale produce dei binari non firmati e due stadi successivi, uno non-privilegiato che firma usando una chiave interna al gruppo di sviluppo e test, e uno stadio successivo che applica la chiave di firma ufficiale sempre ai binari neutri. Si eviti che la firma di sviluppo appaia mai in pubblico, ad es. su Store ufficiali. Nel caso venga sottratta, preveniamo che gli utenti siano ingannati con versioni farlocche del software firmate da una chiave valida ma non ufficiale.

Voi che ne pensate? Fatemelo sapere nei commenti.

comments powered by Disqus