@ :~/lab/openbricks-piattaforma-dati-open-source-openshift$
← cd ~/lab

./run openbricks-piattaforma-dati-open-source-openshift

Openbricks: quando il piano di governance è crollato al contatto con la realtà

Diario tecnico di Openbricks: un lakehouse open-source su OpenShift bare-metal via GitOps. Il piano 'Ranger unifica Trino e Spark' è crollato — ecco come l'identità Keycloak è diventata la spina dorsale.

status
draft
project
openbricks
updated
2026-07-07
tags
#databricks alternativa#openshift#trino#spark#keycloak#gitops
generate cover # alt: Stack a strati di componenti open-source su rack bare-metal, con un livello di governance evidenziato che tiene insieme Trino e Spark tramite identità

Databricks è comodo. È anche una scatola chiusa su cloud altrui con una bolletta che cresce. La domanda che mi ha fatto partire: quanto di quella comodità si ricostruisce da pezzi open-source, in casa, sul proprio ferro? Openbricks è il tentativo — un lakehouse a 8 strati deployato al 100% via GitOps su OpenShift 4.18 bare-metal, air-gapped. E la storia vera è come il piano di governance sia morto e rinato.

Lo stack in breve

Otto strati, dall’identità allo storage: Keycloak (OIDC federato a LDAP/AD) e OPA in cima; poi il router HAProxy di OpenShift; le app (JupyterHub, MLflow, Airflow 3, Superset, Kestra); il processing (Spark 4.0.2 via operatore K8s, Trino 479); il catalogo (Unity Catalog OSS, OpenMetadata); lo storage (Delta Lake su MinIO, PostgreSQL HA via CloudNativePG, Ceph); il control plane (ArgoCD, monitoring); e il deploy GitOps. Cluster: 3 worker, ~288 core, ~768 Gi RAM.

Come scorrono i dati (la prima decisione)

Il pattern è “Unity Catalog come metastore passivo”:

  • Scrittura: Spark scrive tabelle Delta col connettore UC (registra i metadati) ma scrive i byte direttamente su MinIO con le proprie credenziali.
  • Lettura: Trino legge i metadati via UC Iceberg REST API e legge i byte direttamente da MinIO.
  • Perché passivo: UC OSS v0.4.0 non fa credential vending verso endpoint S3-compatibili come MinIO (PR upstream ancora aperta). Quindi UC non valida né distribuisce token S3; gli engine tengono le proprie credenziali, e lo storage è protetto a livello infra. Questo tiene UC sostituibile a costo zero.

Il pivot: Ranger fuori, tre pezzi dentro

Il piano era elegante: Apache Ranger come piano di policy unico per Trino e Spark, più OPA per Kubernetes. Un solo posto per dire chi vede cosa. Poi la realtà:

Ranger 2.7 ha un plugin nativo per Trino, ma non esiste un plugin Ranger per Spark 4.0. Ranger coprirebbe l’engine di query, non quello di scrittura — quindi non può essere il “piano unico”. Rimosso dallo stack.

Rifatto come autorizzazione multi-livello, ognuno bravo in ciò che sa fare: Keycloak+oauth2-proxy per l’accesso alla piattaforma, OPA Gatekeeper per l’admission K8s, la file-based ACL di Trino per l’accesso ai dati in query, i GRANT di Unity Catalog (pronti ma disabilitati — la UI di UC non ha login Keycloak, altra issue upstream), RBAC/SCC di OpenShift per l’infra.

Come restano coerenti i permessi tra engine

Non c’è un motore ABAC unico che copra Trino e Spark. La coerenza si ottiene facendo dell’identità la spina dorsale condivisa e vincolando dove ogni engine può scrivere:

  1. Identità unificata a Keycloak. Ogni engine autentica la stessa persona contro lo stesso realm, dietro un solo AD. Il gruppo è la fonte di verità: team = gruppo AD, mappato nel claim groups del token.
  2. Enforcement Trino. Trino gira con access-control.name=file più un group-provider. Gli umani entrano via OAuth2; i servizi non interattivi presentano un Bearer token Keycloak validato via JWKS — con una furbizia necessaria: il principal si legge da un claim dedicato trino_user, non da preferred_username (che per un service account collide come service-account-…).
http-server.authentication.type=oauth2,jwt
http-server.authentication.jwt.principal-field=trino_user   # claim dedicato, non preferred_username
http-server.authentication.jwt.required-audience=trino
  1. Enforcement Spark. Niente authorizer Spark: si limita il raggio d’azione. L’unico servizio autorizzato a scrivere nello schema ingest è l’identità di ingest governata; tutti gli altri hanno sola lettura sul percorso di query. La governance sta sul query path (Trino) e sull’identità di scrittura, dando permessi effettivi coerenti senza un plugin Spark.

La ACL di Trino, in forma sanitizzata — deny-by-default via catch-all a privilegi vuoti:

{ "tables": [
  { "group": "platform-ingest|admin", "catalog": "delta", "schema": "ingest",
    "table": ".*", "privileges": ["SELECT","INSERT","DELETE","UPDATE","OWNERSHIP"] },
  { "group": "hr|admin", "catalog": "delta", "schema": "sensitive_hr",
    "table": ".*", "privileges": ["SELECT"] },
  { "user": ".*", "catalog": "delta", "schema": "sensitive_hr",
    "table": ".*", "privileges": [] } ] }

La prova che funziona: un test end-to-end sul cluster vivo — il service JWT scrive in ingest = OK; scrive fuori = PERMISSION_DENIED; JWT invalido = 401; e il login umano OAuth2 resta intatto perché i due autenticatori coesistono.

GitOps e air-gap

Tutto è app-of-apps ArgoCD: una Application radice possiede ~15 figlie, ordinate con sync-wave (foundation → processing → app). Kustomize base + overlay testing/production. Essendo air-gapped, ogni chart Helm è vendorizzato come artefatto OCI su un Quay interno — niente pull da Docker Hub al sync. I secret sono SealedSecrets committati in git; l’unico applicato a mano è la credenziale del repo verso Quay (sigillarla creerebbe un deadlock circolare).

Gotcha onesti

  • UC Iceberg REST è read-only e una sua DDL usa BINARY(16) che PostgreSQL rifiuta.
  • Due sistemi di gruppi disgiunti (Keycloak vs group.txt di Trino), additivi — un CronJob che rigenera group.txt da Keycloak è l’unificazione pianificata.
  • group.txt montato via subPath non fa hot-reload: serve rollout restart del coordinator.
  • La CA aziendale non pubblica ovunque, gestita in quattro modi diversi: JKS truststore per i componenti Java, CA cotta nell’immagine per Python (Superset), skip-verify per Go (oauth2-proxy).
  • OpenShift ≠ K8s vanilla: Route non Ingress, SCC non PodSecurity, oc non kubectl.

Cosa sto imparando

Che “open-source” non vuol dire “gratis di sforzo”. I mattoncini sono liberi, ma il cemento che li tiene insieme — identità, policy coerenti tra engine, riproducibilità del deploy — lo devi mettere tu. È esattamente quel cemento che paghi quando paghi una piattaforma chiusa. E che un piano di architettura elegante vale finché non incontra “non esiste il plugin per la tua versione”.

Lo costruisco per un contesto reale con requisiti reali. Nomi e destinatario restano fuori: qui interessa il come, non il per chi. Prossima tappa: chiudere la governance e passare allo strato ML.