La continuous delivery per i servizi web è ben capita. Rilasci una nuova versione di una API, guardi i tassi d’errore e le latenze, e se qualcosa va storto fai rollback. L’intera industria ha convertito su un piccolo insieme di pattern: blue-green, canary, rolling deploy, feature flag. Ci sono libri scritti sul tema, la community SRE è maturata attorno a questo, e la maggior parte delle piattaforme moderne (Kubernetes, ECS, Cloud Run, Lambda) spedisce le primitive già nella scatola.
Le pipeline dati sono diverse. Non perché cambino i principi, ma perché cambia la forma del workload. Un servizio web gestisce milioni di richieste corte e indipendenti; un deploy cattivo produce un breve picco di 5xx che puoi rollback in pochi secondi. Una pipeline o produce grosse fette di dati derivati su uno schedule, oppure gira di continuo e tiene stato. Un deploy cattivo può scrivere righe sbagliate nel warehouse per ore prima che qualcuno se ne accorga, oppure corrompere lo stato di un job di streaming che gira da settimane. Il blast radius è più grande; la disciplina deve essere all’altezza.
Questa lezione attraversa i pattern di deployment e come ognuno si mappa su batch e streaming. L’idea grossa è che i pattern sono gli stessi, ma la meccanica è abbastanza diversa da non poter semplicemente copiare il playbook dei servizi web.
Come è fatta la CD per un servizio web
Per fare contrasto, ecco il flusso standard di un servizio web. Fai merge su main, la CI builda un’immagine, l’immagine viene pushata su un registry, il sistema di deploy la rolla fuori. Rolling deploy significa che i nuovi pod vengono su con la nuova immagine mentre i vecchi vengono drenati; in ogni momento un po’ di traffico colpisce i vecchi, un po’ colpisce i nuovi. Canary significa che instradi una piccola percentuale (5 percento, 10 percento) verso la nuova versione e guardi le metriche prima di promuovere. Blue-green significa che tiri su un ambiente completamente fresco con la nuova versione, fai smoke test, e poi giri il traffico con un cambio sul load balancer.
La proprietà unificante: una richiesta è un’unità piccola e isolata. Se la nuova versione è cattiva, smetti di mandarle richieste e il comportamento cattivo si ferma. Niente persiste tra un deploy e l’altro tranne il database, che entrambe le versioni condividono.
Quest’ultima frase è dove il mondo dei dati comincia a divergere. Per le pipeline, l’unità di deploy è un job, non una richiesta. Il job ha stato. Il job scrive su output da cui dipendono altri job ed esseri umani. E le cose su cui puoi fare rollback variano a seconda che il job sia batch o streaming.
Pipeline batch: deploy attraverso lo schedule
Il batch è il più gentile dei due. Un job batch gira su uno schedule (ogni ora, ogni giorno, ogni volta che i dati upstream arrivano). Quando deployi nuovo codice, la prossima esecuzione schedulata lo prende. Non c’è nessun processo long-running in volo con cui combattere.
Il flusso di deploy di default per una pipeline batch ha questa forma:
- Fai merge di un cambio di codice nel repo.
- La CI builda l’artefatto (un’immagine Docker, un wheel, un JAR) e lo carica dovunque l’orchestratore peschi.
- La prossima volta che il job viene triggerato (la prossima run delle 02:00, il prossimo trigger manuale, il prossimo sensor che scatta), usa il nuovo codice.
Il rollback è simmetrico: revert del commit, redeploy dell’artefatto, la prossima run torna al vecchio comportamento. Se vuoi essere paranoico, ammazza qualunque job attualmente in esecuzione prima di fare rollback, così non finisce sotto il vecchio codice con output nuovi già parzialmente scritti.
L’idempotenza, dalla lezione 38, è quello che rende tutto questo pulito. Se il job è ri-eseguibile in sicurezza, puoi fare rollback e replay delle date interessate senza problemi. Se non lo è, devi rincorrere a mano qualunque output sbagliato la versione cattiva abbia scritto e ripulirlo a mano.
Il punto critico sono le migrazioni di schema. Se il nuovo codice si aspetta una nuova colonna che il vecchio codice non scriveva, fare rollback del codice senza rollback dello schema rompe le letture. Peggio, se hai fatto rollback perché il nuovo schema si è rivelato sbagliato, potresti avere output nella nuova forma che il vecchio codice non riesce a leggere affatto.
La disciplina che rende tutto questo gestibile è la stessa dei servizi online: tieni i cambi di schema retrocompatibili. Aggiungere una colonna è sicuro; droppare o rinominare una colonna no. I deploy che hanno bisogno di un cambio di schema vanno spezzati in due deploy: prima la migrazione additiva, poi il codice che la usa. Se hai bisogno del cambio distruttivo (droppare una colonna, restringere un tipo), lo fai come step separato esplicito molto dopo che il codice che dipendeva dalla vecchia forma è scomparso.
In sintesi: i deploy batch sembrano deploy web ma rallentati alla cadenza dello schedule. Il pattern che conta di più è tenere migrazioni e release di codice indipendenti.
Pipeline di streaming: il problema del job long-running
Lo streaming è dove la storia del deploy diventa interessante. Un job Flink o Spark Structured Streaming è un processo long-running. Sta girando da settimane, tiene stato (finestre, join, tabelle di deduplicazione, stato di modelli di machine learning) che ha richiesto tempo non banale per accumularsi. Non puoi semplicemente rollare fuori nuovo codice come faresti per una API stateless.
Il pattern standard per gli stream processor stateful è il savepoint. Flink, l’esempio canonico, ti permette di triggerare un savepoint in qualunque momento: uno snapshot dell’intero stato del job scritto su storage durabile. Il flusso di deploy diventa:
- Builda il nuovo codice.
- Triggera un savepoint sul job in esecuzione.
- Ferma il job una volta che il savepoint è completo.
- Fai partire il nuovo job dal savepoint, usando il nuovo codice.
Il nuovo job riprende esattamente dove il vecchio aveva lasciato, con lo stato intatto. Gli offset Kafka fanno parte del savepoint, quindi consuma lo stesso messaggio successivo che il vecchio job avrebbe consumato. Niente riprocessing, niente buchi.
Funziona finché il nuovo codice riesce a leggere il vecchio stato. Se hai rinominato un campo dello stato, cambiato il suo tipo, o aggiunto un nuovo operator che prima non esisteva, ti serve uno step di state migration. Flink supporta l’evoluzione di schema per lo stato se ti attieni a serializer compatibili (POJO, Avro), ma nel momento in cui inizi a salvare oggetti Java arbitrari senza schema sei per conto tuo.
I job di streaming stateless (un semplice filtro stateless, un arricchimento stateless) sono molto più facili. Non c’è stato da migrare, quindi puoi fare un rolling restart: fai partire la nuova istanza, lasciala recuperare, ammazza la vecchia. Stesso pattern dei servizi web.
Il diagramma sotto mostra il contrasto.
flowchart TB
subgraph BATCH["Batch deploy"]
B1[Merge code] --> B2[CI builds artefact]
B2 --> B3[Next scheduled run]
B3 --> B4[Uses new code]
end
subgraph STREAM["Streaming deploy with savepoint"]
S1[Merge code] --> S2[CI builds artefact]
S2 --> S3[Trigger savepoint on running job]
S3 --> S4[Stop old job]
S4 --> S5[Start new job from savepoint]
end
Blue-green, canary, dark launch per i dati
I pattern dei servizi web si traducono sulle pipeline dati, con qualche aggiustamento.
Blue-green per i dati. Mantieni due pipeline complete, blue e green. Blue è quella da cui i consumatori leggono. Deployi la nuova versione come green, la fai girare a fianco di blue, e una volta che hai fiducia, giri il puntatore del consumatore (una view, un alias di tabella, la config di input di un job downstream) verso green. Blue rimane in piedi come target di rollback.
Il costo è reale: stai facendo girare due copie della pipeline e stai stoccando due copie dell’output. Per una pipeline piccola va benissimo. Per un lakehouse da petabyte è proibitivo. Il pattern è più utile per i percorsi critici dove non puoi tollerare un incidente di output sbagliato, e accetti il costo doppio come assicurazione.
Canary per i dati. Deployi la nuova versione su una fetta del lavoro. Per un job di streaming alimentato da Kafka, potrebbe essere un sottoinsieme di partizioni o un sottoinsieme di topic. Per un job batch partizionato per tenant, un singolo tenant. Guardi gli output, li confronti con quello che la vecchia versione avrebbe prodotto, promuovi al resto se tutto sembra a posto.
La parte difficile è il confronto. Hai bisogno che entrambe le versioni producano output confrontabili che puoi diffare, il che di solito significa scrivere l’output della nuova versione su una tabella di lato e calcolare diff su un campione. Economico se imposti il tooling una volta sola; tedioso se lo fai da zero ogni volta.
Dark launch. Fai girare la nuova versione in shadow mode: processa gli stessi input della vecchia versione ma scrive su un output separato. Niente downstream lo legge. Calcoli i diff contro il vecchio output finché non ti fidi della nuova versione, poi giri il cutover. Questo è il pattern canary senza rischio in produzione perché il nuovo output non è ancora consumato.
Per un job di streaming stateful, fare dark launch è costoso perché stai facendo girare due copie della pipeline che tengono due copie dello stato. Per un job batch, il dark launch è economico: la nuova versione gira semplicemente una volta al giorno su una tabella diversa.
Feature flag per le pipeline. Meno comuni che nei servizi web ma utili. La pipeline legge un flag a runtime e biforca la sua logica su quello. Puoi girare il flag per un sottoinsieme di partizioni, tenant, o ambienti. Il vantaggio è che girare un flag non richiede un redeploy; lo svantaggio è che il codice della pipeline porta rami morti e la matrice di test cresce.
Il problema del data-is-forever
Un deploy cattivo in un servizio web causa un breve picco di 5xx. Gli utenti vedono errori per qualche minuto, fai rollback, e la vita continua. L’effetto cattivo è limitato nel tempo e nello scope.
Un deploy cattivo in una pipeline scrive dati sbagliati. I dati sbagliati siedono in una tabella che le pipeline downstream leggono, le dashboard mostrano, e i modelli di machine learning ci si allenano sopra. Per quando qualcuno si accorge che i conteggi utenti di ieri sono sbagliati di un fattore due, i dati cattivi sono già stati consumati da una dozzina di sistemi downstream e propagati in qualche dataset derivato. Fare rollback del deploy ferma l’emorragia ma non disfa il danno.
Questo è il motivo per cui la disciplina attorno alla CD per i dati deve essere più stretta che per i servizi online, anche se le conseguenze sembrano meno immediate. Un 500 web è rumoroso e visibile; una riga sbagliata è silenziosa e contagiosa. I pattern di deployment che sembrano esagerati (dark launch, confronto fianco a fianco, blue-green sui percorsi critici) sono quelli che ripagano la prima volta che intercettano un deploy cattivo prima che i consumatori downstream lo vedano.
Cosa significa in pratica
Per un piccolo team che parte, il flusso realistico è:
- I job batch deployano via orchestratore: merge su main, la CI spedisce una nuova immagine, la prossima run schedulata la prende. L’idempotenza rende il rollback sicuro.
- I job di streaming deployano via savepoint-and-restart per il lavoro stateful, rolling restart per quello stateless. La disciplina del savepoint conta più di qualunque feature del framework.
- Le migrazioni di schema sono uno step di deploy separato, additivo di default, mai accoppiato a una release di codice.
- Le pipeline più critiche (revenue, reporting regolatorio, qualunque cosa user-facing nel prodotto) si guadagnano dark launch o blue-green. Quelle minori no.
- Tutto quello che si può rendere idempotente, andrebbe reso tale. L’idempotenza è la proprietà che fa funzionare ogni altro pattern di deployment.
Il resto è tooling: orchestratore, CI, registry, observability, alert sui diff degli output. La lezione 53 copre il pezzo dell’infrastructure as code, che è il livello che rende l’orchestratore e le pipeline stesse riproducibili. La lezione 54 copre il pezzo dei container, che è l’unità con cui la maggior parte dei job dati moderni viene spedita.
Riferimenti
- “What Is Continuous Delivery?” (
https://continuousdelivery.com/, consultato 2026-05-01). - Documentazione Apache Flink, “Savepoints” (
https://nightlies.apache.org/flink/flink-docs-stable/docs/ops/state/savepoints/, consultato 2026-05-01). - “Schema Evolution and State Migration” nella documentazione Flink (
https://nightlies.apache.org/flink/flink-docs-stable/docs/dev/datastream/fault-tolerance/schema_evolution/, consultato 2026-05-01). - Google SRE Book, capitolo sul release engineering (
https://sre.google/sre-book/release-engineering/, consultato 2026-05-01).