Come Glaspoort distribuisce le modifiche al database con lo stesso rigore del codice applicativo, derivando ogni ambiente dalla produzione, creando database effimeri per ogni PR e trattando le migrazioni come l'unica fonte di verità.
di Hadi Farhat, Gideon Spierings, Ricardo de Vries e Raymon Veldman
Glaspoort progetta e gestisce infrastrutture in fibra ottica nei Paesi Bassi. Tutto ruota intorno all'aumento del numero di connessioni in fibra e, per molto tempo, il team dei dati ha trascorso le sue giornate a creare report di BI a supporto di questo obiettivo, mentre la domanda alla base di ciascun report era già superata nel momento in cui il report veniva completato. Il risultato è stato una proliferazione di report una tantum e utenti che non sapevano a chi rivolgersi per le domande successive.
Quindi abbiamo interrotto questa dipendenza. Invece di distribuire l'ennesimo report, abbiamo creato un'applicazione front-end personalizzata in cui i project manager vedono direttamente dove si trovano le opportunità per i loro progetti. La novità si nasconde sotto il cofano: utilizziamo i prodotti Databricks direttamente come blocchi costruttivi dell'app. Genie, per chattare con i dati e avviare analisi rapide. AI/BI Dashboards, per insight e analisi self-service. Flussi di lavoro automatizzati con Agent Bricks, che avvisano i project manager nel momento in cui emerge qualcosa di rilevante nei loro progetti, e Lakebase, il database OLTP di Databricks, per i dati transazionali dell'applicazione.
Questa combinazione unisce due mondi che fino a poco tempo fa vivevano separati: l'ambiente analitico e un front-end operativo, dove i dati analitici incontrano i dati transazionali. Ora il team dei dati dedica il proprio tempo agli spazi Genie e ai metadati invece che ai report una tantum. Ma nulla di tutto questo rimane efficiente senza una solida base sottostante: CI/CD, test sulla qualità dei dati, Infrastructure as Code e data governance. Un elemento di questa base ha richiesto la progettazione più attenta, ed è proprio l'elemento di cui parla il resto di questa storia: come distribuiamo le modifiche al database dietro l'app.
Dietro questa applicazione c'è un database Databricks Lakebase. Si tratta di un Postgres OLTP serverless, che viene eseguito accanto al lakehouse anziché essere agganciato ad esso. Il flusso di dati è semplice da descrivere e, come abbiamo scoperto, più interessante da gestire di quanto sembri:

Quest'ultimo punto è dove risiede la domanda interessante. Nel momento in cui decidi che un database OLTP merita lo stesso rigore del codice dell'applicazione, devi rispondere a una domanda difficile:
Come possiamo consentire a ogni PR di eseguire test su un database simile a quello di produzione, senza che le persone si ostacolino a vicenda e senza perdere i dati aggiornati che rendono il test significativo fin dall'inizio?
Le modalità di errore in questo caso sono familiari a chiunque abbia condiviso un database all'interno di un team. Le PR che superano i test in isolamento e si bloccano quando vengono unite. Ambienti di sviluppo e accettazione che si sono silenziosamente allontanati da come si presenta effettivamente la produzione. E il giorno del refresh che nessuno desidera, in cui recuperare dati puliti significa smantellare gli ambienti, ricollegare ogni stringa di connessione e riapplicare manualmente ogni grant di Postgres, perché i grant risiedono sul branch appena eliminato.
Questo post spiega come abbiamo evitato la maggior parte di questi problemi e l'unica decisione su cui stiamo ancora discutendo attivamente.
Se non hai ancora utilizzato i branch di Lakebase, ecco l'unico modello mentale di cui hai bisogno per questo post.
Un branch Lakebase è un branch Postgres copy-on-write derivato da un parent. La creazione di un branch non copia i dati; esegue il fork dello stato in modo economico e istantaneo, e il branch si discosta dal parent solo quando vi si scrive. Ogni branch è completamente isolato, con il proprio endpoint e i propri dati. Puoi crearne uno in pochi secondi ed eliminarlo altrettanto rapidamente.
Se questo ti ricorda git, è proprio questo il punto. L'analogia che organizza tutto ciò che segue è semplice: un feature branch in git corrisponde a un branch di database in Lakebase. Una PR ottiene il proprio branch di codice e il proprio branch di database, i test vengono eseguiti su entrambi e, quando il lavoro è valido, viene promosso verso la produzione.
C'è un vincolo da tenere presente per la sezione successiva, perché rappresenta una differenza fondamentale rispetto al branching di git. Per ripristinare un branch dal suo parent, devi prima eliminare i child di quel branch. Un parent non può essere ripristinato se vi sono branch che dipendono da esso.
Ecco la progettazione a cui la maggior parte dei team si affida inizialmente, perché rispecchia il modo in cui disegniamo gli ambienti su una lavagna:
Si tratta di una gerarchia pulita: la produzione alla radice, poi lo sviluppo, infine l'accettazione e le feature collegate al di sotto. Tuttavia, introduce anche due modalità di errore prevedibili: i branch divergono (drift) e la soluzione per questa divergenza è così costosa che i team smettono di applicarla.
In primo luogo, lo sviluppo e l'accettazione divergono dalla produzione. La produzione continua a ricevere dati sincronizzati aggiornati e scritture reali dell'applicazione; i branch a lungo termine di sviluppo e accettazione no. Nel giro di uno o due sprint, ti ritroverai a eseguire test su un database che non assomiglia più a quello in cui effettuerai la distribuzione.
La soluzione ovvia è aggiornare lo sviluppo dalla produzione, ed è qui che il vincolo della sezione precedente si trasforma in un costo. Per ripristinare lo sviluppo dal suo parent, devi prima eliminare i child dello sviluppo, il che in questa topologia significa l'accettazione e ogni feature branch collegato allo sviluppo. Quindi, un normale "dammi dati aggiornati" si trasforma in una cascata: elimina l'accettazione e tutti i feature branch, ripristina lo sviluppo, ricrea gli ambienti, ricollega ogni stringa di connessione che puntava ai vecchi branch e riapplica ogni grant di Postgres, perché i grant risiedono sul branch appena eliminato.

Nessuno di questi passaggi è difficile di per sé. Insieme, su base ricorrente, rappresentano un modo sicuro per spingere l'intero team a evitare silenziosamente l'aggiornamento, il che significa che tutti tornano a eseguire test su dati obsoleti e divergenti. Questo era il problema originale. La topologia ingenua non ti costa solo un brutto pomeriggio; scoraggia le buone pratiche che mantengono l'ambiente coerente.
La soluzione è un cambiamento minimo nel modo in cui pensi alla topologia, ma ha effetti straordinari: ogni branch di ambiente a lungo termine è un child della produzione, non di un altro ambiente.
Sviluppo e accettazione sono entrambi branch creati direttamente dalla produzione. Si trovano l'uno accanto all'altro sotto la produzione, non impilati l'uno sull'altro. Nessuno dei due è il parent dell'altro, quindi l'aggiornamento di uno non costringe mai a eliminare l'altro.
Questo singolo cambiamento disinnesca la trappola. Quando lo sviluppo o l'accettazione divergono, li ripristiniamo dalla produzione (oggi un'operazione tramite UI), all'incirca una volta per sprint o ogni volta che vogliamo dati aggiornati. Poiché nulla è collegato al di sotto dello sviluppo o dell'accettazione, non ci sono child da eliminare prima, stringhe di connessione da ricollegare all'interno del team e nessuna maratona per riapplicare i grant. Il ripristino è economico, quindi lo facciamo davvero, mantenendo gli ambienti coerenti. Se un ripristino dovesse saltare una modifica, la successiva riesecuzione della migrazione la applicherà semplicemente di nuovo.

Animazione: il ciclo di vita per ogni PR. Riprodottto sul blog pubblicato; mostrato come fotogramma statico all'interno di questo documento.
Il ciclo di lavoro quotidiano degli sviluppatori sovrappone branch effimeri a questa topologia stabile:
Un dettaglio merita di essere sottolineato per i team che provengono da un mondo git-centrico. In git manteniamo solo un branch master. Non esiste un branch develop o release a lungo termine nel controllo del codice sorgente. Il percorso di promozione da dev ad acceptance e poi a production è espresso interamente tramite gate approvati manualmente in Azure DevOps, che avviano la CI e spostano una modifica da un branch all'altro. La topologia dell'ambiente risiede in Lakebase e nella pipeline; git rimane semplice.
Ciò che rende tutto questo sicuro è che non uniamo mai i database tra loro. No promuoviamo una modifica copiando i dati da un feature branch a development. La promuoviamo rieseguendo le migrazioni sul branch di destinazione. Utilizziamo vitest per eseguire lo smoke test sullo slot di staging.
In questo modello, i branch sono deliberatamente a breve termine e usa e getta. La descrizione duratura e autorevole di come dovrebbe essere lo schema non è un database a lungo termine che potrebbe aver subito una deriva. È l'insieme ordinato delle migrazioni. Ecco perché un reset è sicuro anche quando sfugge qualcosa: se a un branch aggiornato manca una modifica, la riesecuzione successiva della migrazione la applica di nuovo. Le migrazioni sono così centrali che i database attivi sono sempre riproducibili da esse, anziché essere preziosi a causa loro.
Questa è la vera svolta. Creare branch dalla produzione mantiene i dati aggiornati. Considerare le migrazioni come l'unica fonte di verità mantiene lo schema corretto. Insieme, fanno sì che nessun ambiente sia mai troppo speciale per essere eliminato e ricostruito.
Questa è la parte che riteniamo più utile per gli altri team, perché si tratta di un vero e proprio bivio piuttosto che di una best practice con un'unica risposta corretta. Una volta definita la topologia sopra descritta, occorre ancora decidere quando consentire il merge di una PR. Abbiamo valutato due modelli, che presentano compromessi opposti.
| Opzione 1: Merge dopo il superamento della CI (PR stacking) | Opzione 2: Merge dopo la CD su dev e acceptance (promozione per singola PR) | |
|---|---|---|
| Funzionamento | È possibile eseguire il merge di una PR non appena la CI sul relativo branch pr-xxxx è verde. | Non è possibile eseguire il merge di una PR finché non è stata promossa attraverso development e acceptance. |
| Pro | I membri del team possono basarsi immediatamente sulle modifiche degli altri. | Ogni PR si muove al proprio ritmo; gli hotfix sono prioritari e non devono fare la coda dietro a nessun altro. |
| Contro | Più PR unite si accumulano (stack) quando vengono promosse a dev e acceptance. Un hotfix non può aggirare la coda senza una pipeline parallela. | I membri del team devono attendere che acceptance sia verde prima di potersi basare su una modifica. |
In breve: l'Opzione 1 ottimizza la velocità degli sviluppatori all'interno di un team affiatato. L'Opzione 2 ottimizza l'indipendenza e un percorso di hotfix pulito. Se il vostro team lavora su modifiche sovrapposte e si fida della CI, lo stacking vi permette di muovervi più velocemente. Se avete spesso bisogno di rilasciare una correzione che non può attendere il completamento di altre attività in corso, la promozione per singola PR si rivela la scelta migliore.
Abbiamo scelto l'Opzione 1. Siamo un piccolo team che lavora su modifiche sovrapposte. Attendere che acceptance diventi verde prima che chiunque possa basarsi sul tuo lavoro sarebbe un costo che pagheremmo ogni giorno, mentre il rischio di stacking si concretizza solo occasionalmente. E quando succede, è contenuto: ogni gate di promozione riesegue le migrazioni accumulate su un fork pulito della produzione prima di toccare il branch reale, in modo che lo stack venga convalidato come un'unica unità. Per i casi davvero urgenti, manteniamo una pipeline di emergenza separata che va direttamente in produzione con lo stesso preflight. Velocità di default, via di fuga in standby.
Alcuni argomenti sono emersi ripetutamente durante le sessioni di lavoro. Non rappresentano il fulcro della storia, ma sono i dettagli che determinano se il pattern sopravvive al contatto con la produzione.
Autenticazione del database e rotazione dei token. L'applicazione non conserva un segreto del database a lungo termine. Una password statica in Azure Key Vault viene utilizzata per generare una credenziale del database a breve termine (un token con un TTL di 60 minuti) tramite `databricks postgres generate-database-credential`. Poiché il token scade dopo un'ora, il client deve eseguire l'aggiornamento in modo proattivo; gestiamo questo aspetto con una funzione di password asincrona sul pool di connessioni che memorizza nella cache la credenziale e la ruota prima che scada.
Autorizzazioni e provisioning degli accessi. Non gestiamo il DDL delle tabelle tramite migrazioni e le autorizzazioni a livello di oggetto vengono applicate manualmente oggi anziché tramite codice. È l'elemento più evidente nella nostra lista di miglioramenti. Tuttavia, poiché applichiamo tali autorizzazioni in produzione e i branch dell'ambiente vengono ripristinati tramite un nuovo fork dalla produzione, i branch figli le ereditano automaticamente a ogni aggiornamento. Il passaggio di autorizzazione manuale avviene una sola volta in produzione, non ripetutamente su ogni figlio, quindi rimane abbastanza raro da poterlo gestire per ora.
Accesso al database: un singolo utente dell'applicazione. Non forniamo un ruolo Postgres per utente finale. L'app si connette tramite un singolo utente dell'applicazione e l'autorizzazione per i singoli utenti risiede nel livello applicativo anziché nel database. Questo mantiene semplice il provisioning; il compromesso è che il controllo degli accessi per utente viene applicato sopra Postgres, non da esso.
A caso d'uso che prima richiedeva un intero team e mesi di lavoro ora va online con un piccolo team in pochi giorni. Le nostre iterazioni sono decuplicate e un unico team offre continuamente nuovo valore su una piattaforma che cresce insieme all'organizzazione.—Raymon Veldman, Business Control & IT manager
Il cambiamento più evidente è la velocità con meno ansia. Ogni PR ora ottiene il proprio database configurato come quello di produzione in pochi minuti, quindi gli sviluppatori hanno smesso di coordinarsi su ambienti dev e acceptance condivisi; le conversazioni del tipo "chi ha rotto l'ambiente di acceptance?" sono semplicemente scomparse. Poiché i branch dell'ambiente sono sempre figli della produzione, non dovremo mai più affrontare una giornata di ripristino dal parent: niente eliminazione di branch figli, niente riconfigurazione delle stringhe di connessione, nessun pomeriggio perso a riapplicare le autorizzazioni. Le migrazioni vengono rieseguite a ogni fase di promozione, e distribuiamo la nuova immagine dell'app in uno slot di staging rispetto al branch appena migrato ed eseguiamo l'intera suite di test prima che qualsiasi cosa tocchi un ambiente reale, quindi ciò che raggiunge la produzione si è già dimostrato valido due volte. E quando si presenta un'urgenza, gli hotfix seguono lo stesso percorso sicuro di tutto il resto, solo più velocemente, invece di aggirare la pipeline.
State scegliendo Lakebase per un database applicativo e vi trovate ad affrontare le stesse domande su CI/CD? Iniziate da qui: ambienti a lungo termine derivati direttamente dalla produzione, branch effimeri per ciascuna PR e migrazioni come unica fonte di verità. Scoprite di più su Databricks Lakebase
(Questo post sul blog è stato tradotto utilizzando strumenti basati sull'intelligenza artificiale) Post originale
Iscriviti al nostro blog e ricevi gli ultimi articoli direttamente nella tua casella di posta.