Il Modulo 7 ha trattato le pratiche che trasformano l’architettura in sistemi che girano. Strategie di branching, trunk-based development, CI, CD, infrastructure as code: ogni lezione ha aggiunto un livello al quadro di come un team consegna davvero il software. Questa lezione aggiunge il livello di runtime su cui vivono ormai la maggior parte delle piattaforme dati moderne, sia che il team l’abbia scelto deliberatamente sia che l’abbia ereditato dal resto dell’azienda. Kubernetes.
L’inquadramento conta. Kubernetes non è un tool per i dati. È un orchestratore distribuito general-purpose che schedula container attraverso una flotta di macchine. I data engineer finiscono per usarlo perché il resto dell’organizzazione lo sta già usando, perché l’ecosistema cloud-native ha convertito su di lui, e perché le alternative (gestire le proprie VM, incollare insieme servizi cloud-specific, girare su una piattaforma Spark gestita come Databricks o EMR) hanno i loro costi. La domanda giusta è raramente “dovremmo usare Kubernetes”. La domanda giusta è “dato che Kubernetes è il substrato, come facciamo girare i workload dati sopra di lui bene, e a cosa stiamo dicendo sì”.
Questa lezione attraversa quella domanda in tre parti: in cosa Kubernetes è bravo, in cosa è cattivo, e cosa lo rende la scelta necessaria per la maggior parte dei team dati di taglia media o grande. La sezione di mezzo copre l’operator pattern e le due integrazioni che contano di più in pratica, Spark on Kubernetes e Airflow on Kubernetes.
Cos’è davvero Kubernetes
Togli il marketing e Kubernetes è un pugno di primitive messe insieme con cura.
Un pod è uno o più container schedulati insieme sullo stesso nodo. I pod sono l’unità di deployment. Condividono il networking e una piccola quantità di storage locale.
Un nodo è una macchina (di solito una VM) che fa girare i pod. Un cluster è un insieme di nodi più un control plane che decide dove i pod vengono schedulati, li riavvia quando falliscono, ed espone le API con cui il resto del sistema parla.
Un deployment è una dichiarazione di stato desiderato: “voglio tre repliche di questo pod, con questa immagine, questa allocazione di CPU e memoria, questa variabile d’ambiente”. Consegni il deployment al cluster. Il lavoro del cluster è far combaciare la realtà con la dichiarazione. Se un pod crasha, il cluster ne fa partire uno nuovo. Se cambi l’immagine, il cluster rolla fuori la nuova versione tenendo viva la vecchia abbastanza a lungo da evitare downtime.
Un service è un indirizzo di rete stabile davanti a un insieme di pod. I pod vanno e vengono; il service rimane. I client parlano al service, il service fa load-balancing tra i pod.
Il modello è dichiarativo fino in fondo. Descrivi cosa vuoi; il cluster riconcilia verso quello. Quel ciclo di riconciliazione è il cuore di Kubernetes, ed è anche il cuore dell’operator pattern, che è dove entra in scena il tooling per i dati.
Il buono
Standardizzazione attraverso gli ambienti. Un cluster Kubernetes su AWS (EKS), Google Cloud (GKE), Azure (AKS) o on-premises ha lo stesso aspetto dal lato deployment. Lo stesso YAML funziona in tutti, con un piccolo strato di differenze cloud-specific in load balancer, storage class e IAM. I team che hanno vissuto una migrazione multi-cloud o cloud-to-on-prem sanno quanto è rara questa portabilità.
Auto-scaling. Kubernetes può scalare i pod in base a CPU, memoria o metriche custom tramite l’Horizontal Pod Autoscaler. Il cluster stesso può scalare aggiungendo nodi tramite il Cluster Autoscaler o Karpenter. Per workload dati a raffica (un job Spark che ha bisogno di duecento executor per dieci minuti e poi nulla) questa elasticità è la storia operativa che rende abbordabili i cluster per-job.
Self-healing. Un pod che crasha viene rischedulato automaticamente. Un nodo che diventa irraggiungibile fa evict dei suoi pod che vengono rischedulati altrove. Il sistema assorbe una classe di fallimenti che, in un setup di VM fatte a mano, paginerebbe un essere umano alle tre del mattino.
L’operator pattern. Kubernetes ti permette di definire le tue risorse custom e scrivere un controller (un operator) che le guarda e riconcilia verso lo stato desiderato. Questo è quello che trasforma Kubernetes da scheduler di container in piattaforma per costruire astrazioni di livello più alto. Una risorsa SparkApplication, una PostgresCluster, una KafkaTopic: ognuna è un kind custom che un operator sa come materializzare in pod, service e config. Dalla prospettiva dell’utente, crei una SparkApplication; l’operator si occupa del driver pod, degli executor pod, dei service account, della configurazione del logging, della pulizia al completamento.
Ecosistema. Ogni tool di infrastruttura moderno spedisce con un’integrazione Kubernetes, di solito una Helm chart. Prometheus, Grafana, Jaeger, ArgoCD, Flux, Vault, cert-manager, external-dns: lo stack cloud-native standard si compone sopra Kubernetes. Un team che adotta Kubernetes eredita di default una toolchain matura.
Il cattivo
Complessità operativa. Far girare un cluster Kubernetes sano è un lavoro. Il control plane ha il suo database (etcd), il suo strato di networking, la sua rotazione di certificati, la sua storia di upgrade. Anche i team su offerte gestite (EKS, GKE, AKS) spendono tempo serio sulle operazioni del cluster: dimensionamento dei node group, upgrade di versione, scelta del network plugin, integrazione IAM. Un team dati che condivide un platform team con il resto dell’azienda è a posto. Un team dati che fa girare il proprio cluster dedicato deve mettere a budget il costo operativo.
Networking. Pod-a-pod, pod-a-service, ingress, egress, network policy, service mesh. Ognuno è un argomento a sé. Debuggare una connessione che dovrebbe funzionare ma non funziona significa capire quale layer è responsabile: il plugin CNI, la definizione del service, la network policy, l’ingress controller, il load balancer cloud, la risoluzione DNS. La maggior parte dei data engineer impara le parti che tocca ed evita il resto.
Proliferazione di YAML. Una singola applicazione potrebbe richiedere un deployment, un service, una config map, un secret, un ingress, un horizontal pod autoscaler, un service account e un role binding. Sono sette file YAML per un’app hello-world. Helm chart e Kustomize esistono per gestire la proliferazione, e aiutano, ma la verità sotto è che Kubernetes è verboso. I team che scrivono YAML grezzo a mano affogano. I team che adottano un tool di templating presto rimangono a galla.
Resource accounting. Ogni pod dichiara request di CPU e memoria (la quantità che lo scheduler riserva) e limit (il massimo che il pod può usare prima di essere throttlato o ammazzato). Settare questi correttamente è una disciplina. Setta le request troppo basse e i pod vengono schedulati su nodi sovraccarichi e diventano vicini rumorosi. Settale troppo alte e il cluster finisce lo spazio mentre i nodi stanno a far niente. Setta i limit troppo bassi e il kernel ti ammazza l’executor Spark in mezzo a uno shuffle con un errore di out-of-memory che, dalla prospettiva dell’applicazione, sembra un fallimento di nodo. I valori giusti vengono dall’osservazione e dall’iterazione; i default sono quasi sempre sbagliati.
Il necessario: tooling per i dati su Kubernetes
Il motivo per cui la maggior parte dei team dati finisce su Kubernetes non è entusiasmo per la piattaforma. È che i tool per i dati che vogliono far girare hanno integrazioni Kubernetes di prima classe, e quelle integrazioni ora sono migliori delle alternative.
Spark on Kubernetes. Il Kubernetes Operator for Spark (originalmente di Google, ora mantenuto dalla community) è il modo moderno di far girare Spark. Ogni job Spark diventa una risorsa custom SparkApplication. L’operator crea un driver pod, il driver chiede executor pod al cluster, gli executor fanno il lavoro, e al completamento tutto viene ripulito. Il job ha girato su un cluster effimero di pod, i pod sono spariti, e il cluster reclama la capacità. Questo è il modello “cluster per-job” che era goffo da raggiungere con YARN ed è ora il default con Kubernetes.
I vantaggi si compongono. L’isolamento delle risorse è per-pod. Versioni Spark diverse possono girare fianco a fianco. Lo stesso stack di observability che guarda il resto del cluster guarda i job Spark. L’attribuzione dei costi è per-namespace. I fallimenti sono fallimenti di pod, con la stessa storia di restart e rescheduling di tutto il resto.
Airflow on Kubernetes. Airflow ha due integrazioni rilevanti. Il KubernetesExecutor fa girare ogni task nel proprio pod, invece che in un processo worker dentro un worker Celery long-lived. Questo dà isolamento delle risorse per-task, scaling per-task, e flessibilità d’immagine per-task (un task può specificare l’immagine container di cui ha bisogno, il che disaccoppia gli upgrade di Airflow dagli upgrade del codice dei task). Il KubernetesPodOperator è più generale: è un operator Airflow che lancia un container arbitrario, il che permette a un DAG di orchestrare qualunque tool con un’immagine Docker, non solo cose con operator Airflow nativi.
La combinazione è potente. Un DAG Airflow che gira sul KubernetesExecutor può usare il KubernetesPodOperator per lanciare un job Spark (via Spark Operator), una run dbt, uno script Python e un job Flink, ognuno nel proprio container con il proprio budget di risorse, tutto orchestrato come un normale workflow Airflow.
Argo Workflows. Un workflow engine Kubernetes-native, un’alternativa ad Airflow per i team che vogliono che il loro orchestratore sia nativo a Kubernetes quanto il resto del loro stack. Ogni step in un workflow Argo è un pod. Il DAG è una risorsa custom. Tutta la cosa riconcilia come tutto il resto nel cluster. Argo è la scelta per i team che trovano il modello Python-first di Airflow non necessario e vogliono i loro workflow definiti in YAML accanto alle altre loro risorse Kubernetes.
Flink on Kubernetes. Stesso pattern di Spark. Il Flink Kubernetes operator gestisce risorse FlinkDeployment. Ogni deployment è un cluster Flink di pod. I job di streaming long-running si mappano su deployment long-lived; i job batch ad-hoc si mappano su deployment short-lived.
Il pattern è coerente. I tool per i dati hanno concordato che Kubernetes è il substrato, e hanno costruito operator che trasformano le primitive Kubernetes-native nelle astrazioni di workload a cui i data engineer tengono. La vittoria architetturale non è Kubernetes in sé; è che i tool hanno convertito su un runtime comune.
Un tipico cluster Kubernetes per i dati
flowchart TB
subgraph cluster[Kubernetes cluster]
subgraph ns_airflow[Namespace: airflow]
AF_WEB[Webserver]
AF_SCHED[Scheduler]
AF_TASK[Task pods<br/>per DAG task]
end
subgraph ns_spark[Namespace: spark-operator]
SO[Spark Operator]
SD[Driver pods]
SE[Executor pods]
end
subgraph ns_mon[Namespace: monitoring]
PROM[Prometheus]
GRAF[Grafana]
LOKI[Loki]
end
subgraph ns_jobs[Namespace: data-jobs]
DBT[dbt run pods]
PY[Python job pods]
end
end
AF_SCHED --> AF_TASK
AF_TASK --> SO
AF_TASK --> DBT
AF_TASK --> PY
SO --> SD
SD --> SE
PROM -.scrapes.-> AF_SCHED
PROM -.scrapes.-> SD
PROM -.scrapes.-> SE
GRAF --> PROM
Diagramma da creare: una versione rifinita del layout del cluster sopra, con quattro namespace chiaramente delimitati, le frecce di control flow da Airflow ai workload pod, lo Spark Operator che materializza driver e executor pod, e il namespace di monitoring che fa scrape delle metriche da tutto. Il punto visivo è che i namespace sono l’unità di separazione e che l’operator pattern è quello che fa apparire e sparire i workload pod.
La forma generalizza. La maggior parte dei cluster Kubernetes per i dati ha questo aspetto: qualche namespace per i componenti della piattaforma (Airflow, Spark Operator, monitoring, a volte Argo o Flink), uno o più namespace per i workload veri, e un control flow che va da Airflow (o Argo) attraverso gli operator, che a loro volta creano i workload pod. Il platform team possiede i namespace della piattaforma. I data engineer lavorano nei namespace dei workload.
Quando NON usare Kubernetes
Tre casi in cui Kubernetes è la risposta sbagliata.
Workload single-VM. Un piccolo job ETL che gira una volta al giorno su una sola macchina non ha bisogno di un orchestratore che schedula container attraverso una flotta. Un’entry cron su una VM, o una funzione serverless, è più semplice e più economico. Kubernetes aggiunge complessità operativa che ripaga solo quando hai abbastanza workload e abbastanza scala da ammortizzarla.
Team minuscoli senza infra dedicata. Un team dati di due persone senza supporto di piattaforma spenderà un terzo del proprio tempo sulle operazioni del cluster se fa girare il proprio Kubernetes. Lo stesso team su una piattaforma Spark gestita (Databricks, Snowflake, BigQuery con Dataform) scambia costo-per-job con quasi zero lavoro operativo. Quel trade è di solito corretto a piccola scala.
Workload con equivalenti serverless. AWS Lambda, Google Cloud Run, AWS Batch e Azure Container Instances esistono per un motivo. Un workload che sta dentro i loro vincoli (taglia, runtime, tolleranza al cold start) è spesso più facile da far girare lì che su Kubernetes. La regola del pollice architetturale: Kubernetes è per workload che beneficiano di un cluster; il serverless è per workload che non ne beneficiano.
La sintesi onesta
La maggior parte dei team dati nelle aziende medio-grandi finisce su Kubernetes che lo voglia o no. Il resto dell’azienda gira lì. Il platform team lo supporta. L’ecosistema cloud-native lo assume. I tool per i dati hanno integrazioni di prima classe per lui. Combattere quella corrente raramente vale la pena.
La mossa giusta, dato questo, è appoggiarsi ai tool standard invece di reinventare. Usa lo Spark Operator invece di scrivere il tuo collante di submission Spark-su-k8s. Usa il KubernetesExecutor per Airflow invece del CeleryExecutor. Usa un’offerta gestita (EKS, GKE, AKS) invece di far girare il tuo control plane. Adotta presto un tool di templating (Helm, Kustomize). Azzecca request e limit di risorse, e rivisitali quando il workload cambia.
L’obiettivo è lo stesso obiettivo verso cui il Modulo 7 si è mosso da sempre: rendere la cosa giusta la cosa facile. Kubernetes aggiunge un costo iniziale ripido e ripaga con una lunga coda piatta di benefici operativi. Per la maggior parte dei team dati a scala, il trade vale; per i team piccoli, di solito no. Sapere da che lato di quella linea ti trovi è la decisione architetturale per cui il resto della lezione si è preparato.
La prossima lezione chiude il Modulo 7 con un caso di studio: la pipeline di deployment di Stripe, e cosa rivelano le loro pratiche di engineering pubblicate su CI/CD quando il sistema gestisce soldi veri a scala. Il Modulo 8 partirà con uno sguardo più profondo sull’orchestrazione, con Airflow come esempio ricorrente e Kubernetes come substrato sotto.