Nel tempo, le aziende risolvono numerosi problemi solo per trovarsi poi di fronte a nuove difficoltà fino ad allora sconosciute. Abbiamo imparato costantemente a sviluppare codice in team diversi, favorendo lo scambio di conoscenze tramite linee guida e community di pratica. Ma questo non basta. Prima o poi, le aziende si ritrovano con uno snowflake di stack tecnologici e strategie di deployment differenti, che rendono difficile l’onboarding di un nuovo membro del team, rendono il progetto difficile da mantenere e portano alla comparsa di codebase obsolete o legacy. In più, se ogni volta che nasce un nuovo progetto bisogna pensare da zero a come gestire le risorse di infrastruttura del progetto, la sicurezza, l’osservabilità o la scalabilità in ogni team, si finisce per rimpiangere i problemi “semplici” del passato.
È qui che entra in gioco la Platform Engineering. L’obiettivo della platform engineering è fornire un modo semplice e condiviso tra i team per sviluppare e distribuire applicazioni. Avere un unico modo in azienda per gestire il ciclo di vita delle codebase permette di affrontare i problemi appena descritti una sola volta: durante la creazione della piattaforma interna per sviluppatori.
Clustero, la internal developer platform (IDP) di buildo, punta a unificare il modo in cui sviluppiamo e distribuiamo applicazioni in buildo. Qui raccontiamo come abbiamo affrontato uno dei problemi emersi durante la sua creazione: i database.
Primo passo: individuare le esigenze degli utenti
Dopo aver verificato che i database fossero davvero necessari e utili agli sviluppatori, abbiamo chiesto ai team quale fosse lo stato dell’arte. Quali DBMS vengono generalmente usati dai team? Come vengono utilizzati? I team eseguono operazioni manuali di manutenzione, backup e ripristino, migrazioni, accedono direttamente ai database di sviluppo dalle proprie macchine, o altro?
Volevamo conoscere i requisiti che i team si aspettano normalmente da una soluzione di database, così da crearne una basata sulle esigenze degli sviluppatori. In fondo stiamo costruendo una piattaforma interna per sviluppatori, e i nostri utenti sono proprio loro.
I risultati, naturalmente, sono stati eterogenei tra i diversi team. Abbiamo cercato di conciliarli in una soluzione unica e semplice da cui partire, mantenendo però la possibilità di espandere. Sapevamo che nulla è perfetto al primo tentativo e che i requisiti cambiano nel tempo, quindi volevamo che questa flessibilità fosse prevista dall’inizio.
Scelta delle tecnologie

Abbiamo scelto due tecnologie diverse per la nostra soluzione: CloudNativePG e Metacontroller.
CloudNativePG è una delle soluzioni per la gestione di database PostgreSQL in ambienti Kubernetes. Permette agli utenti di Kubernetes di creare un cluster PostgreSQL immediatamente, semplicemente definendo una risorsa YAML. Abbiamo valutato anche altre soluzioni, come il postgres-operator di Crunchy Data, ma abbiamo speso molto tempo a configurare correttamente l’operatore invece di poter contare su impostazioni predefinite sensate (ad esempio, WAL in crescita infinita). Abbiamo anche provato il postgres-operator di Zalando, ma abbiamo trovato la semplicità e l’assenza di vincoli vendor-specific di CloudNativePG un vantaggio decisivo. Punto bonus: è un progetto orgogliosamente 🇮🇹 italiano — chi non vorrebbe un tocco di la dolce vita nel proprio stack Kubernetes?
Metacontroller è per noi una piccola perla dell’ecosistema Kubernetes. Per il nostro utilizzo ci consente di creare rapidamente operatori in qualsiasi linguaggio di programmazione, astraendo molti concetti di Kubernetes e richiedendo solo di fornire una semplice funzione, in qualunque linguaggio, che prende come input la risorsa padre di Kubernetes e restituisce le risorse figlie. Certo, non ha la stessa potenza espressiva di un operatore completo scritto con gli SDK Go o Rust, ma per noi non è stato un problema.
E Helm? Alcuni potrebbero sostenere che Helm sia più che sufficiente per astrarre risorse YAML in un insieme di risorse diverse. In effetti, prima di usare Metacontroller abbiamo avuto l’occasione di sfruttare Helm per definire risorse di alto livello: ad esempio è possibile definire in Helm un semplice values.yaml che rappresenta un Database, e poi creare tutte le risorse necessarie nei templates. Il problema è che questo approccio è intrinsecamente discreto: non essendo un operatore, non esiste un ciclo di riconciliazione. Di conseguenza, la risorsa Database viene tradotta in più risorse YAML solo quando qualcuno o qualcosa (ad esempio una pipeline CI) esegue un comando helm install o helm template. L’impatto è che per qualsiasi modifica infrastrutturale è necessario rilanciare un comando helm. Diventa quindi impossibile cambiare in modo trasparente qualcosa nelle risorse sotto l’astrazione, come la storage class o l’aggiornamento della versione minor o patch del motore di database.
Operator SDK è un altro progetto di Red Hat dedicato alla creazione di operatori. Tra le altre funzionalità, permette di convertire un chart Helm in un operatore. È però decisamente più rigido e complesso da testare (anche se helm-unittest esiste) rispetto al codice scritto in un linguaggio di programmazione tradizionale. Stiamo valutando di esplorare questa tecnologia nel prossimo futuro.
Analisi della soluzione
Anche se le Custom Resources (CR) di CloudNativePG sono abbastanza semplici per chi conosce Kubernetes, non lo sono altrettanto per gli sviluppatori che non lo conoscono. Inoltre, le CR di CloudNativePG non seguono (né dovrebbero seguire) le linee guida di Buildo per la gestione dei database, perché naturalmente ogni azienda ha linee guida diverse. Per questo abbiamo dovuto avvolgerle in una nostra astrazione opinata, e abbiamo scelto Metacontroller.
Abbiamo quindi combinato questi due strumenti per creare un operatore di database che unisce i sane default (quelli di CloudNativePG) a quelli opinati di Buildo.
Nel caso più semplice, agli sviluppatori basta scrivere una CR come la seguente:
apiVersion: database.buildo.io/v1alpha1
kind: PostgreSQL
metadata:
name: my-db
spec: {}
Nota il campo spec vuoto: ogni valore ha un default.
Ma cosa succede se agli sviluppatori serve un database temporaneo con cui sperimentare, per esempio in una Pull Request? O una dimensione di storage specifica? O una particolare versione di PostgreSQL? Possono usare una CR come questa:
apiVersion: database.buildo.io/v1alpha1
kind: PostgreSQL
metadata:
name: my-db
spec:
version: 15
storage:
type: ephemeral
size: 10Gi
Garantire flessibilità
Gli esempi seguenti mostrano come il nostro team ha affrontato problemi posti da team diversi, sfruttando la flessibilità della soluzione. Ogni caso rappresenta una sfida distinta: uno riguarda l’integrazione trasparente di estensioni PostgreSQL, l’altro l’uso efficace di backup di database esistenti. Questi scenari mettono in luce il nostro obiettivo: fornire soluzioni flessibili ed efficienti che rispondano a bisogni specifici, preservando l’integrità e il funzionamento dei nostri sistemi.
Caso 1: estensioni PostgreSQL
Dopo un po’ di tempo i team hanno iniziato a sperimentare con la versione alpha della nostra soluzione di database, e si sono improvvisamente imbattuti in un piccolo dettaglio che nessuno aveva considerato fino a quel momento: come posso avere l’estensione uuidv4 nel mio database PostgreSQL?
Questa esigenza non era prevista. Abbiamo scelto di rendere flessibile l’intera soluzione proprio per casi come questo, indipendentemente da quanto precise possano essere le analisi.
In un primo momento abbiamo pensato di aggiungere un campo extensions alla nostra Custom Resource. Ma presto sono emerse altre personalizzazioni necessarie all’inizializzazione del database, non legate alle estensioni. Abbiamo quindi esteso l’operatore per permettere quanto segue:
apiVersion: database.buildo.io/v1alpha1
kind: PostgreSQL
metadata:
name: my-db
spec:
initSql:
- CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
È molto semplice ma comunque molto efficace: gli sviluppatori sanno scrivere SQL, e qui diamo loro un’interfaccia per inizializzare i database con qualsiasi SQL serva. Naturalmente non è una soluzione per eseguire le migrazioni SQL, che restano affidate ad altri strumenti, ma risolve molti piccoli problemi di personalizzazione del database all’avvio.
Caso 2: utilizzare un backup esistente
Un team ci ha sottoposto il seguente problema: eseguiva regolarmente backup e attività di anonimizzazione sul database di produzione, per ottenerne copie più piccole e anonimizzate. Voleva usare una di queste copie per il proprio ambiente.
Abbiamo quindi lavorato insieme per trovare la soluzione con il valore più alto e il costo più basso. Il risultato, dal punto di vista degli sviluppatori, è un campo molto semplice nella spec:
apiVersion: database.buildo.io/v1alpha1
kind: PostgreSQL
metadata:
name: my-db
spec:
initFromBackup: path/of/the/backup
Questa CR avvia un database di base, ripristinando schemi e dati dal backup indicato nel path.
Ma che cos’è quel path? Poiché i dati del database di produzione erano anonimizzati, i backup potevano essere spostati da un bucket S3 gestito dal team a uno gestito dalla piattaforma, configurato direttamente sull’operatore invece che nella CR: questo ci ha permesso di avere una CR più pulita. Così i team devono solo scrivere i backup nel path del bucket S3 consentito e poi indicarlo nella CR.
Bonus: dietro le quinte di CloudNativePG 🇮🇹
Durante il Kubernetes Community Days Italy 2023 abbiamo conosciuto le persone del team di CloudNativePG, parlato del futuro del progetto e scambiato idee su miglioramenti e trucchi per il loro software open source.
Durante la conferenza abbiamo conosciuto anche Gabriele Bartolini del team CloudNativePG, che ha tenuto un talk su “Postgres and Kubernetes: past, present and future”. Ci ha poi raccontato la storia di come CloudNativePG sia nato a Prato e la sua lunga esperienza con PostgreSQL fin dagli inizi. Partecipare come community a questi progetti e parlare con persone così appassionate è stato un vero piacere. Non vediamo l’ora di tornare al KCD Italy il prossimo anno!
Se ti è piaciuto questo articolo e vuoi vedere altri progetti su cui stiamo lavorando, leggi il nostro post di approfondimento Creare valore per gli sviluppatori con una Internal Developer Platform. Raccontiamo come stiamo rendendo più semplice per gli sviluppatori sperimentare in autonomia, le soluzioni che abbiamo implementato e cosa arriverà in futuro.