In molti progetti di sviluppo software basati sul cloud si progetta e si mantiene un processo CI/CD per rilasciare le applicazioni negli ambienti cloud in modo efficiente, sicuro e produttivo.
Quando si parla di CI/CD, l'attenzione è quasi sempre rivolta al lato cloud/remoto, mentre raramente ci si sofferma sulla fase che precede immediatamente la CI/CD: lo sviluppo e il testing in locale, sul laptop dello sviluppatore.
In questo articolo spiegherò perché molti dei metodi più diffusi per lo sviluppo locale sono tutt'altro che ideali e mostrerò una demo di un approccio alternativo, cloud-native, capace di migliorare la produttività vostra (o dei vostri sviluppatori). 🌤
Foto di Christina @ wocintechchat.com su Unsplash
👨🏼💻 Sviluppo locale
Non esiste una best practice universale — tutto dipende dai requisiti e dalle esigenze dell'organizzazione e del progetto — ma il flusso CI/CD tipico assomiglia più o meno a questo:

Vediamo cosa succede subito prima dello step #1: lo sviluppo locale.
Nella maggior parte dei casi avviene sulla macchina dello sviluppatore: pianificazione, progettazione, scrittura del codice e dei test, esecuzione dei test e magari un POC — talvolta con verifiche manuali per accertarsi che tutto funzioni come previsto.
Molti progetti investono pesantemente in setup locali che riproducono l'ambiente cloud, allo scopo di verificare se la nuova feature o la fix introdotta funziona correttamente, senza causare regressioni su altre funzionalità o logiche esistenti.
Setup di questo tipo servono soprattutto a eseguire determinate categorie di test automatizzati (system test, test di integrazione o end-to-end). E sì, volendo, ci si possono anche fare i test manuali 😏.
Gli ambienti locali, inoltre, vengono spesso usati per il debug del codice. Alcuni sviluppatori sono fermi sostenitori di quello che io chiamo "debug-driven development", ma il debug può servire a molto altro: è utilissimo per indagare su un bug ostico o per capire meglio un flusso complesso.
Metodi più comuni per lo sviluppo locale
Mettiamo che stiate lavorando a un progetto con diversi microservizi rilasciati su Kubernetes. Ecco alcuni metodi diffusi nel settore in cui mi sono imbattuto:
- Metodo Docker-compose: scrivere e mantenere manifest docker-compose (di fatto un duplicato dei manifest YAML di K8s già esistenti) che avviano i servizi e le dipendenze di terze parti per il testing locale.
- Metodo K8s locale: avviare un cluster Kubernetes locale con minikube/k3s/kind/altro e rilasciare i propri servizi in locale per il testing, il tutto orchestrato da una serie di script.
- Metodo Test Suites: Makefile o script bash che lanciano una suite di test di integrazione per ogni servizio, con le dipendenze di terze parti avviate in locale come container Docker (dockertest, ad esempio, è una scelta diffusa per setup di questo tipo).
Sono tutte opzioni valide per eseguire e testare la propria applicazione in locale.
Svantaggi dei metodi più comuni di sviluppo locale
I metodi appena descritti, però, presentano anche alcuni limiti, tra cui:
Mancato supporto in locale da parte di terze parti
Alcune dipendenze cloud di terze parti non offrono modalità di esecuzione in locale. In certi casi è possibile emularle facendo "mocking/stubbing" delle API di terze parti, ma è un approccio che richiede molto lavoro di manutenzione e non rispecchia davvero la realtà. È, di fatto, una scorciatoia. Cosa succede, ad esempio, se una delle vostre query al DB è errata? Come fate a scoprirlo con il testing o il debug se non state "dialogando" con un'istanza DB reale?
Problemi di consumo elevato di risorse
Se la vostra applicazione richiede molte risorse, il laptop potrebbe non essere all'altezza. Ad esempio, lanciate uno stress test per riprodurre un memory leak. Oppure il bug si manifesta solo quando girano contemporaneamente 20 repliche dell'applicazione. Risultato: il laptop diventa lentissimo 😅. E la produttività ne risente.
Spegnimenti durante il debug
Se dovete testare o fare il debug di un bug ostico o raro, che si manifesta magari ogni qualche giorno, la macchina locale potrebbe spegnersi, andare in sleep o in ibernazione, costringendovi a ricominciare daccapo.
Nessuna single source of truth
Con il metodo Docker-compose finite per violare il principio della "single source of truth", mantenendo più rappresentazioni dello stesso ambiente.
Eseguire test di integrazione ≠ integrazione reale
Con il metodo Test Suites, eseguire test di integrazione è certamente una buona pratica con molti vantaggi (anche in locale), ma non equivale a integrare le proprie modifiche in un ambiente cloud reale. Esistono bug e problemi di integrazione/deployment che potrebbero sfuggire (problemi di connettività di rete verso altri servizi, errori di configurazione, crash all'avvio, ecc.).
Tutti questi limiti penalizzano l'esperienza di sviluppo e riducono la produttività. E spesso non rispecchiano la realtà (il classico "It works on my machine!"): per questo finiscono per offrire poco valore.
Esiste un modo migliore, più moderno, di affrontare la fase di sviluppo locale, migliorando produttività e usabilità? Sì, esiste eccome.
☁️ **_Ambienti di sviluppo cloud-native_**
Vediamo ora gli ambienti remoti, cloud-native.
Trasformando l'ambiente locale in un ambiente remoto, i problemi appena descritti svaniscono. E in più l'esperienza di sviluppo diventa moderna, affidabile e cloud-native.
Come funzionano gli ambienti di sviluppo cloud-native?
Si alloca un ambiente cloud separato per ciascuno sviluppatore, replica (non identica, ma abbastanza fedele) dei vostri ambienti di sviluppo/produzione.
Con un approccio basato su ambienti personali cloud-native, ogni sviluppatore del team ha la libertà di usare il proprio ambiente come ritiene più opportuno: per testare nuove feature e bug fix, per riprodurre bug, oppure per sperimentare e imparare. Tutto in un ambiente molto simile alla produzione. ✌️
Si possono poi adattare gli strumenti per rendere questi ambienti ancora più utili, ad esempio eseguendo test manuali o automatizzati, "riproducendo" il traffico di produzione verso gli ambienti personali, e così via.
Un'altra opportunità interessante è il debug remoto in tempo reale all'interno di questi ambienti, in totale isolamento e senza interferire con altri ambienti. Si possono sfruttare strumenti già pronti, pensati proprio per questo (ne vedremo un esempio nel prossimo capitolo).
I punti di forza per produttività, manutenibilità e affidabilità
I motivi per passare ad ambienti di sviluppo cloud-native sono molti, ma vi propongo gli otto principali.
Potrete:
- Riutilizzare i manifest YAML di K8s esistenti, senza dover mantenere configurazioni di deployment duplicate. (Restando DRY 🌵)
- Rilasciare in un ambiente cloud con la stessa procedura usata per dev/prod, ma con un livello di isolamento aggiuntivo.
- Smettere di dipendere dall'hardware locale. L'infrastruttura cloud può scalare facilmente verso l'alto se servono test esigenti in termini di risorse, e verso il basso nel weekend.
- Avviare test di lunga durata nel vostro ambiente senza il timore che il laptop si spenga o vada in ibernazione.
- Individuare rapidamente i problemi di integrazione, perché si usano le API reali dei servizi cloud, non dei mock (potete ad esempio comunicare con PubSub, Cloud SQL, Datastore e molti altri servizi cloud).
- Sfruttare l'infrastruttura integrata che il cloud offre per monitoring, alerting, profiling, tracing e aggregazione dei log, esattamente come fate in produzione.
- Risparmiare tempo, evitando di lanciare ogni volta script di setup elaborati. Il cloud è veloce. L'ambiente è pronto quando vi serve.
- Usare questi ambienti per imparare Kubernetes con la pratica (se siete principianti), senza impattare gli altri ambienti.
Infine — oltre agli sviluppatori — anche gli engineer DevOps/SRE possono trarre vantaggio da un ambiente personale, sperimentando modifiche alla configurazione dell'infrastruttura prima di toccare quelli condivisi con il resto del team.
🙋♂️ Una domanda al volo!
La mia bolletta cloud andrà alle stelle?
Il primo istinto è quello di provisionare una replica completamente separata e indipendente delle istanze cloud per ogni sviluppatore, isolata per progetti, dove ogni progetto contiene una replica della propria infrastruttura.
In linea di massima è una buona pratica, soprattutto sul fronte sicurezza, perché garantisce un buon isolamento. Tuttavia, il costo di questo approccio può diventare un problema, in particolare con la crescita dell'azienda o del team.
Trattandosi di ambienti di sviluppo, in molti casi isolati dalla produzione, si può accettare un compromesso fra costi e sicurezza.
Nota a margine: questo compromesso fra sicurezza e costi varia a seconda dell'organizzazione e dei requisiti — assicuratevi di comprenderne le implicazioni. Questa guida alla sicurezza GKE può esservi d'aiuto.
E come si raggiunge questo compromesso? Si possono sfruttare le funzionalità multi-tenant dei propri servizi cloud per contenere i costi condividendo le risorse fra ambienti.
Si parla spesso di "Soft Multi-Tenancy": il termine "Soft" indica che il rischio è limitato perché le risorse sono condivise solo da utenti appartenenti alla stessa organizzazione "fidata".
Per esempio, tutti gli sviluppatori di un team che lavora a un'applicazione possono condividere un cluster K8s, in cui ciascuno dispone dell'intero stack applicativo replicato in un proprio namespace K8s.
Un altro esempio: tutti gli sviluppatori possono condividere una singola istanza DB, dove ognuno ha utente DB, schema e tabelle dedicati.
È proprio l'approccio che mostreremo nel prossimo capitolo.
È un incubo da configurare e mantenere? 🤕🥸
Niente affatto! Si può (e si dovrebbe) automatizzare tutto e renderlo facilmente riproducibile applicando i principi di Infrastructure as Code e GitOps.
Una volta descritta tutta la configurazione come codice, creare o distruggere un ambiente personale è questione di pochi click. E con meno spazio per l'errore umano, perché il processo è automatizzato.
Perché tutto questo non rientra nello scope dell'ambiente di sviluppo? 🤔
Il classico ambiente di sviluppo non è isolato: è condiviso fra tutti gli sviluppatori. Voi fate il merge del vostro codice e contemporaneamente un collega fa il merge del suo: in pratica è un calderone. Quando il team cresce, fare debug ed esperimenti in un ambiente condiviso diventa impraticabile.
Inoltre, lo stadio dell'ambiente di sviluppo arriva un po' troppo tardi. Avete già aperto una PR, superato la code review e fatto il merge. E adesso scoprite che le vostre modifiche fanno crashare immediatamente il servizio. A questo punto è un blocker e richiede un revert/rollback/hotfix. Una grande perdita di tempo per voi e per i vostri colleghi.
Ambienti personali o ambienti effimeri? 👀
La risposta breve è: dipende da voi.
In sintesi, con gli ambienti personali si dedica un ambiente a una persona specifica per uno scopo di lungo periodo. Con gli ambienti effimeri, invece, si crea un ambiente per una modifica specifica (che viene distrutto una volta fatto il merge della modifica stessa).
Un mio collega
ha scritto un ottimo articolo sugli ambienti effimeri, che vi consiglio di leggere.
Si possono adottare entrambi gli approcci, anche se per le vostre esigenze potrebbe essere un eccesso.
Nella Parte 2 illustrerò un setup di questo tipo con un'architettura di esempio, basata su Terraform, ArgoCD e Telepresence. Vai alla Parte 2!