⭐⭐⭐ El MAPA DE POSICIÓN — Index Catalog como plano de gobernanza unificado
Qué es este documento. No es un plan ni un approach: es una definición de
hecho. Para cada concepto de gobernanza que la industria ya tiene resuelto,
dice cuatro cosas y siempre las mismas:① EL MODELO · quién es la referencia, qué vocabulario tiene, cuál es su madurez ② LA DERIVACIÓN · de qué dato NUESTRO sale — si no sale de ninguno, es una declaración ③ DÓNDE ESTAMOS · medido, con el comando que lo demuestra ④ LA AUTORIDAD · quién puede decir que NO, y dóndeAbierto: 2026-08-12. Es la base sobre la que giran
plano-gobernado-approach.md(el plan) y
sustrato-07.md(la foto). Donde discrepen, el sustrato manda
en los hechos y esto manda en el encuadre.
1 · La tesis, en tres frases
① Un solo sitio donde la gobernanza se DEFINE: Index.
② N sitios donde se APLICA: la cara, la puerta y el motor.
③ Cero sitios donde se COPIE.
Es literalmente el movimiento de Unity Catalog —govern once, protect everywhere— y también el de Lake Formation, Polaris y Gravitino. La diferencia no está en la idea: está en de qué deriva cada pieza. Y ahí es donde se gana o se pierde.
1·1 · ⚠️ Y una precisión que cambia lo que significa «copiar UC OSS»
Las políticas ABAC de Unity Catalog —row filter, column mask, governed tags— son
del producto GESTIONADO de Databricks, no del proyecto open source. Toda su
documentación vive en docs.databricks.com, no en el repo OSS.
⇒ No hay implementación que instalar. Lo que hay es un modelo público, maduro y GA (mayo 2026) que se puede estudiar entero. Es decir: el §2 del approach —se toma el modelo, se deriva el sustrato, se evita el producto— aquí no es una preferencia, es la única opción disponible.
2 · Los cinco PLANOS DE AUTORIDAD
La pregunta «¿cuál es la naturaleza de su autoridad?» no tiene una respuesta: tiene cinco, y confundirlas es el origen de la mayoría de los errores de diseño.
┌─ ① IDENTIDAD ─────────────────────────────────────────────────────────┐
│ QUIÉN ERES. Clerk (humanos) · Keycloak (servicios) │
│ Autoridad EXTERNA y no negociable: el catálogo la referencia, │
│ no la administra. ⇒ «no modifiques los grupos aquí: usa tu IdP» │
└───────────────────────────────┬───────────────────────────────────────┘
┌─ ② RETÍCULA (grants) ─────────▼───────────────────────────────────────┐
│ QUÉ OBJETOS PUEDES TOCAR. Index · `privilege_grants` │
│ Autoridad DECLARATIVA y ADITIVA: concede, nunca deniega. │
│ Grano GRUESO. Es la frontera del objeto, no de su contenido. │
└───────────────────────────────┬───────────────────────────────────────┘
┌─ ③ POLÍTICA (celda) ──────────▼───────────────────────────────────────┐
│ QUÉ VES DENTRO. filtro de fila · máscara · proyección │
│ Autoridad RESTRICTIVA: sólo quita. Se define en Index y se APLICA │
│ en el motor (o en el catálogo, si algún día planifica). │
│ ⛔ NO EXISTE TODAVÍA │
└───────────────────────────────┬───────────────────────────────────────┘
┌─ ④ CONTRATO (ODCS) ───────────▼───────────────────────────────────────┐
│ QUÉ ES EL DATO Y QUÉ GARANTIZA. semántica · calidad · clasificación │
│ Autoridad del PRODUCTOR, y nace en la INGESTA. Es lo que ALIMENTA ③ │
│ (las etiquetas) y lo que define el TIPO de la ontología. │
│ ⛔ NO EXISTE TODAVÍA │
└───────────────────────────────┬───────────────────────────────────────┘
┌─ ⑤ APLICACIÓN ────────────────▼───────────────────────────────────────┐
│ DÓNDE SE EJERCE. la cara `/api/iceberg` · la puerta Junction · motor │
│ Autoridad OPERATIVA: es el único plano que puede decir «no» de verdad.│
│ Los otros cuatro sólo tienen razón; éste tiene el interruptor. │
└───────────────────────────────────────────────────────────────────────┘
⭐⭐ Y el invariante que los une: los planos ①-④ definen; sólo el ⑤
impide. Un plano superior sin punto de aplicación es exactamente lo que
privilege.ts avisa en su cabecera: «C3 sin C5 es decoración».
⚠️ Lo que hoy NO tiene autoridad, y hay que saberlo:
| Pieza | Qué NO puede decir |
|---|---|
| Cloudflare R2 | Nada. Los bytes no saben de nadie, y está bien así |
| Lakekeeper | Nada de nuestros grants: tiene los suyos y no los cruza |
| OpenMetadata | Nada: es productor de etiquetas, no fuente de verdad |
| OpenFGA | Nada todavía: desplegado e inerte (AUTHZ_BACKEND=allowall) |
| ⚠️ El vendado STS | Lo esquiva todo: mientras se venda credencial, la política de fila es decorativa |
3 · LOS GRANTS QUE PROPONEMOS — qué son, dónde se definen, qué autoridad tienen
3·1 · El vocabulario: 40 privilegios en 6 familias
Definidos en lib/governance/privilege.ts — y esto es una decisión, no un
accidente: el vocabulario vive en TypeScript porque es donde además se sabe
DÓNDE puede concederse cada uno (grantableOn) y QUÉ implica (implies). El
CHECK de la migración es sólo un patrón (^(WORKSPACE|CATALOG|SCHEMA|TABLE|VIEW|POLICY)_[A-Z_]+$):
ataja la basura evidente y no repite la lista, porque el mismo conocimiento en dos
lenguajes ya nos costó una fase.
| Familia | Cuántos | Qué gobierna |
|---|---|---|
WORKSPACE_* | 4 | El inquilino. Nuestro, no de Polaris — tenemos un nivel por encima del catálogo |
CATALOG_* | 7 | Contenido, metadata, propiedades y adjuntar política |
SCHEMA_* | 8 | El namespace lógico (≠ el físico, que es opaco desde N·3) |
TABLE_* | 10 | Incluido ⭐ TABLE_READ_DATA — leer FILAS, que NINGÚN *_METADATA implica |
VIEW_* | 6 | Conviven con TABLE_* sobre el securable dataset |
POLICY_* | 6 | La entidad de política — existe el vocabulario, no el objeto |
3·2 · Dónde viven: Index, y sólo Index
principal_roles ──┬─ principal_role_members (el sujeto: user | service | agent | job)
└─ catalog_role_bindings ──┬─ catalog_roles
└─ privilege_grants ← EL GRANT
(privilege × securable_kind × securable_id)
▼
principal_grants (VISTA que aplana la cadena; no decide nada)
Cuatro securables — workspace › catalog › schema › dataset — el mismo
eje que usan las políticas (C1) y el registro de uso (C2). Que los tres pilares
compartan jerarquía no es elegancia: es lo que permite cruzar «qué se usó»,
«bajo qué política» y «con qué permiso» con un JOIN en vez de con una tabla
de traducción.
⚠️ No hay tabla principals, y es deliberado. Un principal es un par
(kind, id); crear un registro propio obligaría a sincronizarlo con Clerk, con
las cuentas de servicio y con los agentes, y a decidir qué pasa cuando divergen.
La identidad es de otro sistema; aquí sólo se referencia (plano ①).
3·3 · Quién los recibe: roles, nunca personas
| Sujeto | De dónde DERIVA | Rol | Estado |
|---|---|---|---|
| Humanos | workspace_members (que mantiene Clerk) | ws-owner · ws-admin · ws-editor · ws-viewer · ws-guest | 🏁 12 sujetos, 08-12 |
| Servicios | Declarados: el client_id de Keycloak es el principal_id | warehouse-writer · query-engines · etl-engines · maintenance · sql-editor · catalog-profilers · operators | 🏁 368 grants |
| Agentes | ⛔ nada todavía | — | el vocabulario existe ('agent'), el sujeto no |
| Jobs | ⛔ nada todavía | — | ídem |
⭐ Los humanos se DERIVAN y los servicios se DECLARAN, y no es incoherencia: un servicio no está escrito en ninguna otra parte, así que declararlo es la única opción honesta; una persona sí lo está, así que declararla sería duplicar la verdad. La regla no es «derivar siempre»: es «derivar cuando hay de qué».
3·4 · ⭐ La naturaleza de su autoridad — seis propiedades
| Propiedad | Consecuencia, dicha entera | |
|---|---|---|
| ① | ADITIVO, SIN deny | Igual que Polaris. Un forbid explícito es competencia de Cedar/OpenFGA — meterlo aquí serían dos motores de política, que es lo que la decisión del owner descartó |
| ② | HEREDA hacia abajo por la cadena de securables | ⭐⭐ Y es lo que hace que los future grants de Snowflake no hagan falta: un dataset nuevo en un contenedor concedido nace cubierto |
| ③ | IMPLICA transitivamente (CATALOG_MANAGE_CONTENT → 10 más) | Se calcula con visitados: un ciclo accidental sería un error de datos, pero un bucle en el camino de autorización sería una caída |
| ④ | Gana el MÁS ESPECÍFICO | No por precedencia (el modelo es aditivo, cualquiera bastaría) sino para explicar: el nivel más cercano al objeto es el que un humano espera ver |
| ⑤ | Devuelve el PORQUÉ, no un booleano | Sin el grantedBy, una autorización no se puede auditar |
| ⑥ | ⚠️ Grano GRUESO, y sólo eso | Decide si tocas la tabla, nunca qué ves dentro. Eso es el plano ③, y no existe |
3·5 · Dónde se APLICA hoy — dos puertas, dos preguntas distintas
| Punto | Módulo | Pregunta | Estado |
|---|---|---|---|
| La cara Iceberg REST | catalog-authz.ts | «¿puede este MOTOR?» | 🏁 VIVO, fail-closed, con asiento en access_events |
| La puerta (Junction) | user-authz.ts | «¿puede esta PERSONA?» | ⚠️ shadow — mide y registra, no deniega |
⭐ La decisión más importante de la cara, y merece repetirse: loadTable sin
delegación pide TABLE_READ_PROPERTIES; con X-Iceberg-Access-Delegation pide
además TABLE_READ_DATA. Porque son dos cosas: la primera devuelve dónde está la
tabla; la segunda, la llave para leer sus bytes.
3·6 · ⛔ Los seis límites, sin maquillar
- No hay
deny⇒ no se puede expresar «todos menos éste». - No hay etiquetas ⇒ cada política sería por objeto: N tablas = N políticas.
- No hay grano fino ⇒ ni fila, ni columna, ni proyección.
Principal.purposeexiste y no se usa ⇒ no hay acceso por propósito.- ⚠️ Un grant de
workspacealcanza al DATO. UC dice que los grants de metastore no se heredan al dato; nosotros heredamos desde el workspace. Divergencia consciente: nuestroworkspacejuega el papel de contenedor de catálogos, no de metastore. Queda anotada porque el día que exista un grant «de administración del inquilino» habrá que separar los dos ejes. - Nadie puede conceder por API todavía salvo el
operator:WORKSPACE_MANAGE_ACCESSestá sólo enws-owner. La superficie del cliente (F·5) no existe.
4 · ⭐⭐⭐ EL MAPA — dónde estamos, concepto a concepto
Leyenda: 🏁 hecho y medido · 🟡 parcial · ⛔ no existe · ⚠️ existe y engaña
| # | Concepto | ① El modelo (referencia · madurez) | ② De qué dato NUESTRO deriva | ③ Dónde estamos | ④ Quién puede decir NO |
|---|---|---|---|---|---|
| 1 | Retícula de privilegios sobre securables con herencia | UC + Polaris. Maduro y estándar de facto | privilege_grants — ya existe | 🏁 40 privilegios · 368+12 sujetos · herencia y implicaciones probadas | la cara (motor) 🏁 · la puerta (persona) 🟡 shadow |
| 2 | Sujetos = roles del IdP, nunca personas | Snowflake («no ata privilegios a usuarios») · UC (grupos vía SCIM) | workspace_members ← Clerk | 🏁 derivado el 08-12 · reproyecta en los 3 handlers | — |
| 3 | Future grants (que un objeto nazca con permisos) | Snowflake ON FUTURE · LF por herencia de etiqueta | — | 🏁 ⭐⭐ INNECESARIO: nuestra herencia ya lo es (medido, 123/123) | — |
| 4 | Propiedad de primera clase, y de un ROL | Snowflake («los roles poseen objetos») · UC (el dueño tiene todo sobre ESE objeto) | ⛔ datasets.created_by es «quién lo tecleó», no el dueño (14 apuntan a servicios) | ⛔ aplazado con motivo medido | — |
| 5 | Separación de deberes (conceder ≠ crear) | Snowflake (SECURITYADMIN ≠ SYSADMIN) · UC (MANAGE o propiedad) | El propio enum de Clerk: owner ≠ admin | 🏁 sólo ws-owner concede — salió gratis | Index (declarativo) |
| 6 | ⭐⭐ Etiquetas gobernadas (ABAC) | UC, GA mayo 2026: pares clave-valor de nivel CUENTA sobre catálogos, esquemas, tablas, columnas, modelos y volúmenes · LF-Tags (el más antiguo y maduro) | ⛔ nada | ⛔ el multiplicador que falta: sin él, N tablas = N políticas | — |
| 7 | Filtro de fila y máscara de columna | UC (política adjunta a catálogo/esquema/tabla, con WHEN y MATCH COLUMNS) · Snowflake · LF (cell-level) | ⛔ nada | ⛔ | el MOTOR (§7·ter) |
| 8 | Política de proyección (unir por email sin poder leerlo) | Snowflake (objeto aparte) | ⭐ el plan ya distingue lo proyectado de lo usado en predicados | 🟡 casi cae sola — la ventaja que ellos dan por supuesta | el MOTOR |
| 9 | Política de agregación (tamaño mínimo de grupo) | Snowflake | — | ⏳ conocida y aplazada, no olvidada | — |
| 10 | ⭐ Composición de políticas | UC/Snowflake: hay que satisfacerlas TODAS, y una fila excluida por el filtro tampoco cuenta en los agregados | — | ⛔ — y sin esa segunda mitad, la agregación es un canal lateral | — |
| 11 | ⭐ Server-side scan planning (planTableScan) | Spec REST de Iceberg 1.11 (mayo 2026). ⭐⭐ Gravitino 1.2.0 YA LO IMPLEMENTA (marzo 2026); Polaris lo usa para la gobernanza de Snowflake | La cara /api/iceberg/v1 ya es un Iceberg REST gobernado — es donde entraría | ⛔ Lakekeeper NO lo implementa (25 endpoints, ninguno de plan) · cliente 1.11.0 SÍ puesto | el CATÁLOGO, para todos los motores |
| 12 | ⚠️ Vendado vs aplicación | UC: «tablas con filtros o máscaras NO se sirven por credencial vendida» · Dremio lo declara arquitectura | GET …/tables/{t}/credentials es endpoint de primera clase de nuestro catálogo | ⚠️ VENDAMOS SIEMPRE ⇒ cualquier política de fila sería decorativa | ⛔ nadie — es el agujero |
| 13 | Contrato de activo (semántica + gobernanza + calidad) | ODCS v3.1.0 (Bitol/LF) — validación JSON Schema estricta, de facto | ⛔ la ingesta escribe sin contrato y sin etiquetas | ⛔ — y es lo que define el TIPO de la ontología | el productor, en la ingesta |
| 14 | Clasificación automática | UC (agentic data classification, GA 05-2026) → produce las etiquetas | OpenMetadata, degradado a productor | ⛔ | — (necesita revisión humana) |
| 15 | Un solo modelo de tipos y permisos | UC · Foundry (Actions) | ⚠️ DOS planos que no se hablan: action_types tiene su propio permiso | ⚠️ se rehace (4 ejecuciones en toda su historia) | dos motores ⇒ divergen siempre |
| 16 | Policy as code | Cedar / OPA | Decidido Cedar, sin activar · OpenFGA desplegado e inerte | 🟡 la pieza está, no está enchufada | — |
| 17 | Registro de acceso (quién leyó qué, bajo qué política) | UC (audit) · Snowflake | access_events con el mismo eje de securables | 🏁 la cara deja asiento en cada decisión | — |
| 18 | Federación de catálogos | Polaris 1.3 · Gravitino (multi-fuente) | — | ⛔ fuera del norte (decidido 08-11) | — |
4·bis · ⭐⭐⭐ LA MALLA, CENSADA OBJETO A OBJETO (2026-08-13)
scripts/governance/m0-censo-de-la-malla.ts — contra la base viva. La sonda separa
tres estados, y la distinción es la que decide el plan:
⛔ NO EXISTE no hay tabla ⇒ hay que CONSTRUIR la primitiva
🟡 VACÍA la primitiva EXISTE ⇒ hay que POBLARLA / CABLEARLA
🏁 POBLADA hay filas ⇒ queda saber si algo las APLICA (otra pregunta)
| Plano | Objeto | Estado |
|---|---|---|
| ① identidad | workspace_members 12 · principal_roles 96 · principal_role_members 76 | 🏁 |
| ② retícula | catalog_roles 56 · catalog_role_bindings 120 · privilege_grants 248 (principal_grants aplana a 413) | 🏁 |
| · taxonomía | catalogs 10 · schemas 16 · datasets 123 · tenant_warehouses 8 | 🏁 |
| ③ política | ⭐ policies 0 · policy_attachments 0 | 🟡 existe y está vacía |
| ③ política | legacy: retention_policies 0 · egress_policies 0 · credential_egress_policies 0 · file_access_grants 2 | 🟡/🏁 |
| ⭐ etiquetas | tags · tag_assignments | ⛔ |
| ④ contrato | data_contracts | ⛔ (lo más cercano: dataset_schema_versions 71) |
| ⭐ conexión | connections · secrets como securables | ⛔ (hay user_credentials 26 · api_connections 1) |
| ⭐ sharing | shares · share_recipients · dataspaces | ⛔ (sólo dataspace_assets, vacía) |
| ⑤ uso | access_events 4.614 · dataset_transactions 988 | 🏁 |
| ⚠️ ontología | object_types 17 · action_types 4 · objects 27.042 | 🏁 y se rehace |
⛔ primitivas que NO EXISTEN (construir) ..... 11
🟡 primitivas VACÍAS (poblar / cablear) ...... 6
⭐⭐⭐ ① La política NO hay que construirla: está construida y vacía
policies + policy_attachments existen desde la migración 20261266, con el
invariante «un securable tiene como mucho UNA política de cada tipo» sellado en un
trigger, y su resolución con herencia ya escrita y probada (lib/governance/policy.ts,
26 tests). Cero filas y cero consumidores.
⇒ El §7 de este mapa decía «4 · POLÍTICA como objeto». Ese paso no es construir: es
extender un vocabulario. Los tipos declarados hoy son de ciclo de vida y egress
—retention · snapshot_expiry · compaction · orphan_cleanup · egress · tier_expectation · custom—; falta row_filter y column_mask, que es un CHECK
y un zod, no una entidad.
⭐⭐⭐ ② Hay CINCO vocabularios de «etiqueta» y ninguno es la etiqueta gobernada
Lo que el censo encontró sin buscarlo:
| Tabla | Filas | Qué es |
|---|---|---|
object_tags | 10 | object_id · tag · category · confidence · source — etiqueta sobre objects, la copia materializada del plano que se rehace. Tiene la forma de una clasificación automática… y se aplica al objeto equivocado |
ml_run_tags · ml_model_version_tags · ml_registered_model_tags | 0 | Tags de MLflow. Otro dominio |
graph_node_labels · graph_edge_labels | 0 | Del canvas |
gmail_labels | 0 | De la integración |
⭐⭐ Es exactamente el diagnóstico que la migración
20261266hizo con las
políticas —«cinco superficies, cuatro vacías, ninguna aplicable ni heredable»—
repetido con etiquetas, y nadie lo había contado. La etiqueta gobernada no compite
con ninguna de las cinco: se aplica sobre securables (catálogo · esquema ·
dataset · columna), que es justo lo que ninguna hace.
⚠️ Y object_tags merece una nota aparte: su forma (confidence, source) ya
anticipa la clasificación automática del concepto 14. El vocabulario se pensó una
vez; lo que falla es de qué cuelga.
⭐⭐ ③ La conexión y el secreto: dos superficies, ninguna en la retícula
user_credentials 26 ← el alta de fuentes: encrypted_data + metadata.warehouse_destination
api_connections 1 ← OAuth de integraciones: access_token · refresh_token · bot_id · shop_domain
Ninguna de las dos es un securable: no se pueden conceder. ⇒ «¿quién puede ingerir
de dónde?» y «¿quién puede usar esta credencial?» no son preguntas que este plano
sepa contestar hoy — y el estándar las contesta con USE CONNECTION (UC) y READ
sobre el secret (Snowflake). Ver ingesta-como-query-approach.md §4·sexies.
⛔ ④ SHARING no está en este mapa, y es una laguna del encuadre
Los 18 conceptos del §4 cubren cómo entra y cómo se lee el dato. No hay ni un
concepto sobre cómo SALE gobernado hacia fuera — ni Delta Sharing, ni el SHARE de
Snowflake, ni Gaia-X/EDC, del que sólo queda dataspace_assets vacía.
⇒ Se añaden abajo como conceptos 19-21. No para hacerlos ya: para que dejen de parecer resueltos por omisión.
| # | Concepto | ① El modelo | ② De qué deriva | ③ Dónde estamos | ④ Quién dice NO |
|---|---|---|---|---|---|
| 19 | La FUENTE como securable (connection) | UC CONNECTION + FOREIGN CATALOG · Snowflake CATALOG INTEGRATION | user_credentials + api_connections, hoy fuera de la retícula | ⛔ | — |
| 20 | El SECRETO como objeto con privilegio propio | Snowflake SECRET (+READ) · UC credenciales en la connection | encrypted_data, hoy sin sujeto que lo pueda pedir | ⛔ | — |
| 21 | SHARING gobernado hacia fuera | Delta Sharing (protocolo abierto) · Snowflake SHARE/listing · Gaia-X/EDC | ⛔ nada — dataspace_assets vacía | ⛔ ni concepto ni tabla | — |
⇒ Lo que el censo le hace al orden del §7
ANTES DESPUÉS (con lo medido)
1 · encender la aplicación 1 · encender la aplicación (igual)
2 · etiquetas en Index 2 · etiquetas en Index ⭐ sigue siendo el multiplicador
3 · contrato en la ingesta 3 · ⭐ CONNECTION + SECRET como securables
4 · POLÍTICA como objeto ← el estándar lo exige y la ingesta lo necesita
5 · conmutar el vendado 4 · 🟡 política: EXTENDER el tipo, no construir la entidad
6 · el aplicador en el motor 5 · contrato en la ingesta (cuelga de 3)
6 · conmutar el vendado (igual, y sigue siendo el que decide)
7 · el aplicador (igual)
⭐ Dos movimientos, y los dos salen de medir en vez de suponer: la política baja de rango porque ya está construida, y la conexión sube porque el estándar la puso en el centro y nosotros no la teníamos ni como concepto.
5 · Los jugadores, y dónde aplica cada uno
La pregunta que los ordena no es «qué features tiene» sino «dónde está su punto de aplicación» — porque eso decide si su gobernanza sobrevive a cambiar de motor.
| Quién | Dónde aplica | Qué le copiamos | Qué NO |
|---|---|---|---|
| ⭐⭐ Unity Catalog | Catálogo + motor propio | Todo el modelo: securables, herencia, governed tags, ABAC, la regla de composición | El producto: la parte ABAC no es OSS (§1·1) |
| Snowflake Horizon | Su motor + Scan Plan API para los demás | Los grants a roles, la propiedad de rol, la separación de deberes, la taxonomía de 5 políticas | ⚠️ «sólo gobierna dentro de Snowflake» — el modelo que evitamos |
| ⭐ Apache Polaris (TLP feb-2026) | El catálogo | El vocabulario de privilegios (ya calcado) y el policy store externalizado | Su grano fino: no tiene — manda a «la capa del motor» |
| ⭐⭐ Apache Gravitino | El catálogo, y ⭐ planifica en servidor desde 1.2.0 | ⭐ La prueba de que ⑪ es alcanzable hoy en OSS | El producto entero: es un metadata lake federado, otro problema |
| AWS Lake Formation | Catálogo + motores AWS | ⭐ LF-Tags: el ABAC más maduro y la herencia de etiqueta que hace que un dataset nuevo herede políticas | El acoplamiento a IAM |
| Dremio | Catálogo grueso + motor fino | ⭐ El encuadre de tres capas (metadata → catálogo → motor) | — |
| Starburst / Trino | El motor (OPA/Ranger) | El patrón «autorizar desde el plan analizado» — ya adoptado (plan-reader) | El impuesto por motor |
| OpenMetadata / DataHub | En ningún sitio: describen | El sistema de tipos como referencia histórica · producir etiquetas | ⚠️ Ser fuente de verdad — describir no es gobernar |
| ⭐⭐ Apache Atlas | En ningún sitio: describe. Aplica Ranger, y sobre Hive/Impala | ⭐⭐⭐ El PROCESO como entidad y ⭐⭐⭐ la propagación de clasificación por la arista del linaje (§5·bis) | El despliegue (JanusGraph+HBase+Solr+Kafka+ZK) y su segundo modelo de tipos y de permisos |
| Immuta / Privacera | Superposición sobre el motor | — | Una segunda fuente de política |
⭐⭐ Y el patrón que comparten los cinco primeros, que es la conclusión del mapa:
La frontera GRUESA se aplica en el CATÁLOGO. El grano FINO se aplica donde el
catálogo llegue — y todos están MOVIÉNDOLO hacia el catálogo (Snowflake con su
Scan Plan API, UC con ABAC cross-engine, Gravitino con el scan planning offload).
Nadie se está moviendo en la otra dirección.
5·bis · ⭐⭐⭐ ¿ABSORBER APACHE ATLAS? — el modelo SÍ, el sistema NO (2026-08-13)
La propuesta del owner es buena y señala una laguna real de este mapa: los 18+3 conceptos no tienen ni uno de linaje. Y el linaje es justo donde nuestros activos viven, porque —dicho por él— «cambian de estado y naturaleza mediante ETL, ELT, ML, analíticas y operaciones ad hoc».
⭐⭐⭐ Lo que Atlas tiene y nosotros no habíamos nombrado
| Qué | Por qué importa AQUÍ | |
|---|---|---|
| ⭐⭐⭐ | El PROCESO es una entidad de primera clase (Process, con inputs y outputs) | Un ETL/ELT/ML no es una arista: es un objeto gobernable, con dueño, permiso y traza. Es exactamente lo que el owner describe, y hoy nosotros no lo tenemos como objeto |
| ⭐⭐⭐ | La clasificación se PROPAGA por la arista del linaje — y se puede bloquear por arista | El multiplicador del multiplicador: etiquetas el origen y la tabla derivada nace clasificada. Sin esto, cada tabla que produce un pipeline hay que etiquetarla a mano |
| ⭐⭐ | Sistema de tipos EXTENSIBLE (typeDef/entityDef, con herencia) | Tópicos · contenedores · volúmenes · imágenes · modelos · tablas. Es el problema del owner, literal |
⇒ Los tres se adoptan. Como MODELO.
⛔ Y lo que hace que absorber el SISTEMA sea el camino equivocado
| Medido | |
|---|---|
| ⛔⛔ No cabe | Atlas necesita JanusGraph + HBase + Solr + Kafka + ZooKeeper; la propia guía de Solr pide ~32 GB de RAM. Nuestro techo es CPUS_ALL_REGIONS = 12 sobre un nodo al 82 %, y v2.5 aún «promete» un footprint más ligero. La absorción de Gravitino se pagó sacando Trino; para Atlas no hay presupuesto que sacar |
| ⛔⛔ No aplica nada | Atlas describe; quien aplica es Ranger, y Ranger aplica en Hive/Impala (y Trino, que sale). Nuestro motor es Spark sobre Iceberg |
| ⛔ La integración tag→policy es de CLOUDERA | Toda la documentación de «controlar el acceso con etiquetas» vive en docs.cloudera.com. Es el mismo patrón que el ABAC de UC (§1·1): modelo público, producto de un vendor |
| ⛔⛔ Sería un SEGUNDO modelo de tipos y de permisos | Es literalmente el defecto por el que el plano ontológico legacy se rehace (sustrato-07 §3·7) y lo que dice la regla 47. Adoptarlo entero sería repetirlo con mejor vocabulario |
| ⚠️ Su tipo no DERIVA de nuestro catálogo | Regla 46: un tipo que no deriva es una declaración, no un contrato |
⭐⭐ El dato que reordena la conversación: Ranger ya llega al CATÁLOGO
Apache Polaris 1.5.0 (mayo 2026) soporta Ranger como autorizador externo del catálogo Iceberg. ⇒ Si algún día quisiéramos Ranger, el camino es por el catálogo, no por Atlas — que es otra vez el patrón del §5: todos mueven la aplicación hacia el catálogo, nadie en la otra dirección.
🏁 Y lo que YA tenemos de esto, sin saberlo
lib/compute/junction.ts:350 · mergeUpstreamProvenance(sourceRefs), que corre en
cada escritura por la puerta:
resuelve las fuentes → acumula `sourceDatasetIds`
→ PROPAGA los campos de frontera (counterparty · edcTransferId · agreementId)
→ encadena `upstreamOrigins`
→ lo estampa en el metadata del ledger
⇒ Es el mecanismo de Atlas —propagar por la arista— aplicado a la gobernanza de frontera, y ya está vivo. Lo que le falta son tres cosas concretas, no un sistema:
- ⛔ no propaga clasificaciones — porque no hay etiquetas (§4·bis·②);
- ⚠️ vive en el metadata de una TRANSACCIÓN, no en el objeto ⇒ es un rastro, no un estado consultable del dataset;
- ⚠️ una fuente irresoluble no bloquea (
continue) ⇒ el linaje puede quedar incompleto en silencio.
⇒ Los dos conceptos que faltaban en el mapa
| # | Concepto | ① El modelo | ② De qué deriva | ③ Dónde estamos | ④ Quién dice NO |
|---|---|---|---|---|---|
| 22 | El PROCESO como entidad de primera clase (ETL·ELT·ML·ad-hoc con inputs/outputs) | Atlas Process · UC lineage de sistema | dataset_transactions (988) + sourceRefs ya declara las entradas | 🟡 el rastro existe, el OBJETO no | — |
| 23 | ⭐⭐⭐ Propagación de clasificación por el linaje, con bloqueo por arista | Atlas (con Ranger aplicando) | mergeUpstreamProvenance ya propaga — pero campos de frontera, no etiquetas | 🟡 el mecanismo está; le falta qué propagar | el motor, vía política |
⭐⭐ La conclusión, y es la regla 46 en su forma más útil: Atlas no nos sobra por
pequeño, nos sobra por entero. Lo que necesitábamos de él —el proceso como
objeto y la propagación por el linaje— son dos conceptos, y uno de los dos ya
está a medio construir en nuestra puerta.
5·ter · ⭐⭐⭐ LA DERIVACIÓN — el paradigma de Atlas, traído a nuestro sustrato
Decisión del owner: «tomamos su paradigma como modelo y lo derivamos a nuestro
equivalente». De acuerdo — y la prueba del algodón es la de siempre (§2·4 del
approach): ¿de qué dato NUESTRO deriva cada pieza? Lo que no derive de una tabla
que ya existe, no entra.
| Atlas | Nuestro equivalente | ② De qué dato NUESTRO deriva | Estado |
|---|---|---|---|
Referenceable.qualifiedName | La coordenada (workspace, schema_id, name) + ds_<uuid> | Ya sellada: índice único + trigger, y N·5 la hizo direccionable | 🏁 |
Entity / Asset | El securable (kind, id) | Mismo eje que grants, políticas y access_events — los tres cruzan con un JOIN | 🏁 |
DataSet | datasets (123) | — | 🏁 |
⭐⭐⭐ Process (inputs/outputs) | El proceso como securable | ⭐ Se PROYECTA del ledger, no se declara: dataset_transactions (988) ya guarda writer, motor, destino y metadata; sourceRefs ya declara las entradas | 🟡 el rastro está, el objeto no |
| Lineage | Derivado, nunca declarado — igual que en Atlas | mergeUpstreamProvenance ya acumula sourceDatasetIds en cada escritura por la puerta | 🟡 |
⭐⭐⭐ Classification (tipada, propagable) | tags + tag_assignments sobre securables y columnas | Del contrato de la ingesta (concepto 13) y del clasificador (14) | ⛔ |
Label (suelta, sin tipo) | ⛔ NO se deriva, y es deliberado | Ya hay cinco vocabularios de etiqueta sin gobernar (§4·bis·②). Añadir un sexto sin tipo ni propagación empeora el problema que venimos a resolver | ⛔ a propósito |
Relationship.propagateTags (bloqueo por arista) | Un flag en el vínculo de linaje | La arista ya existe implícita en sourceDatasetIds | ⛔ |
BusinessMetadata | datasets.metadata (jsonb) | Ya existe | 🏁 |
Glossary / Term | La ontología proyectada — el norte | Del catálogo, que es lo que la hace contrato y no declaración | ⏳ |
⭐⭐⭐ La decisión de diseño que sale de aquí, y no es la que parecía
Lo más vistoso de Atlas es su sistema de tipos EXTENSIBLE en runtime (typeDef).
Y es justo lo que no hay que derivar:
Atlas tiene tipos extensibles porque su oficio es DESCRIBIR — tiene que poder
representar cualquier sistema que exista. El nuestro es GOBERNAR, y un securable
que ningún punto de aplicación sabe aplicar no gobierna nada: es una fila.
⇒ Conjunto CERRADO y versionado de securables, que es lo que hacen UC (TABLE · VOLUME · MODEL · FUNCTION bajo el esquema) y Gravitino (filesets · models · topics). Regla 55: su problema no es el nuestro.
Pero el punto del owner sigue en pie, y es el que toca el código: tópicos, contenedores, volúmenes, imágenes y tablas no son cuatro niveles, son HERMANOS.
// hoy — lib/governance/policy.ts:29
SECURABLE_HIERARCHY = ['workspace','catalog','schema','dataset'] // lineal
securableDepth(kind) => SECURABLE_HIERARCHY.indexOf(kind) // la profundidad ES el índice
lo que hace falta
workspace › catalog › schema › { dataset | volume | topic | model | image }
└── varios kinds a la MISMA profundidad
⇒ El cambio no es «un sistema de tipos»: es que la profundidad deje de ser el índice
de un array lineal y pase a ser el nivel en un árbol, donde el nivel hoja admite
hermanos. Acotado, pero toca las tres piezas que comparten eje —decidePrivilege, la
resolución de políticas y access_events—, así que se hace una vez y con test de
invariante, no por kind.
⚠️ Y con una condición que decide si esto gobierna o decora: un kind nuevo sólo
entra con su punto de aplicación. Un topic gobernable cuya lectura no pasa por
ninguna puerta es exactamente el plano ontológico otra vez (regla 47).
Las tres salvaguardas — para no repetir el plano ontológico con mejor vocabulario
- ⭐ Cada entidad nueva deriva de una tabla que YA existe (
Process← el ledger; el linaje ←sourceDatasetIds) — o no entra. - ⭐ Ni un segundo modelo de permisos. El
can_execute_action_typedel plano legacy nació así. Todo lo nuevo se autoriza conprivilege_grantsygrantableOn. - ⭐ Ni un segundo almacén. El grafo de linaje se proyecta; no se materializa
en paralelo. Los 27.042
objectsson el recordatorio de lo que cuesta la copia.
5·quater · ⭐⭐⭐ ABSORBER EN VEZ DE RECONSTRUIR — la viabilidad, explorada (2026-08-13)
Propuesta del owner: «en vez de construir linaje nativo, tagging nativo y procesos
de primera clase, absorbemos lo que nos interesa y pasamos a tener estándares maduros
acoplados a gran escala; luego construimos alrededor».Es viable. Y el candidato no es Atlas: son dos piezas, y una ya la tenemos.
⭐⭐ La distinción que lo desbloquea todo: hay DOS planos, y sólo uno se absorbe
PLANO DESCRIPTIVO qué existe · de dónde viene · cómo se llama · qué contiene
⇒ SE ABSORBE. Es estándar, es voluminoso y no decide nada
PLANO DE AUTORIDAD quién puede · bajo qué política · con qué privilegio
⇒ SE DERIVA. Es nuestro, es pequeño y es lo único que dice NO
Este mapa ya lo tenía escrito sin sacarle la consecuencia: «OpenMetadata / DataHub / Atlas — aplican en ningún sitio: describen» (§5). Eso no era una pega: era la señal de que se pueden absorber sin riesgo de retícula doble.
La cadena, y es toda de estándares
Spark (nuestro cómputo) ──spark.extraListeners──▶ OpenLineage
el linaje NO se construye: lo EMITE el motor, y a nivel de COLUMNA
│ eventos (spec abierta, portable)
▼
OpenMetadata ──▶ catálogo · discovery · tags · glossary · lineage col-level
entidades de primera clase: tabla · topic · container · pipeline · ML model · dashboard
| Pieza | Qué aporta | Coste real |
|---|---|---|
| ⭐⭐⭐ OpenLineage | El productor. Un listener de Spark emite linaje a nivel de columna, con soporte de Iceberg. Es una spec, no un producto | Un jar + config en el CR de Spark. No consume CPUS_ALL_REGIONS |
| ⭐⭐ OpenMetadata | El consumidor. Catálogo, discovery, tags/classifications/glossary, contratos y data products; consume OpenLineage por Kafka/Kinesis y tiene agente de Spark | ⚠️ Servidor + base + OpenSearch. Fuera del cluster — ya vivía en su propia VM |
🏁 Y lo que ya teníamos, medido hoy
VM carbon-openmetadata (34.168.67.167) puerto 22 ⛔ · puerto 8585 ⛔ ⇒ CAÍDA
⇒ La absorción no empieza de cero: empieza por reencender lo que ya se pagó. Y este mapa lo tenía degradado a «productor de etiquetas» como si fuera un consuelo — es exactamente el papel correcto, y con OpenLineage delante deja de ser poco.
⭐⭐ Por qué esto NO repite el error del plano ontológico
El plano legacy falló por dos cosas (§3·7): escribía sin pasar por la puerta y tenía su propio modelo de permisos. La absorción sólo es sana con la dirección declarada:
Index ──DERIVA──▶ OpenMetadata (proyección descriptiva · NUNCA al revés)
Index ──DECIDE──▶ la cara · la puerta · el motor (la autoridad no se absorbe)
⛔ Las policies y los roles de OpenMetadata se quedan APAGADOS, igual que el OpenFGA de Lakekeeper. Encenderlos sería la segunda retícula, que es el error de siempre con mejor vocabulario.
⚠️ Y la excepción que hay que decidir a propósito: las etiquetas que OM produce (clasificación automática) vuelven hacia Index. Ése es el único flujo inverso legítimo, y tiene que entrar como propuesta, no como hecho — con revisión humana, que es lo que el propio estándar recomienda para clasificación automática (concepto 14).
La viabilidad, por pasos y con gate
| Paso | Gate | |
|---|---|---|
| 🏁 L·0 | Estado del cluster y de las VMs | MEDIDO (§5·quinquies) — ⛔ la VM de OM no existe |
| 🏁 L·1 | ⭐⭐ ¿emite el motor linaje a nivel de columna? | VERDE (§5·quinquies) |
| L·2 | Levantar OM y conectarle el emisor | Una tabla derivada por la puerta aparece con su origen sin que nadie lo declare |
| L·3 | ⭐⭐⭐ La proyección Index → OM: securables, dueños y coordenada | 🟡 medio resuelto por el estándar: el symlinks ya trae la coordenada (§5·quinquies·③) |
| L·4 | Las etiquetas producidas vuelven como propuesta | Una clasificación de OM llega a Index sin aplicarse sola |
| ⛔ L·5 | (no se hace) encender policies/roles de OM | — |
Lo que NO resuelve, y hay que decirlo
- ⛔ Sigue sin aplicar nada. OM describe; el
NOlo siguen diciendo la cara, la puerta y el motor. La absorción no adelanta la política de celda. - ⚠️ El linaje que emite Spark cubre lo que pasa por Spark. La ingesta actual
(Node + PyIceberg) no emite nada ⇒ otro motivo para que la ingesta sea una
sentencia (
ingesta-como-query-approach.md). - ⚠️ Un segundo almacén de metadatos existe y hay que operarlo. Lo que lo hace aceptable es que es reemplazable: los eventos son de una spec abierta, así que el productor sobrevive al consumidor.
⭐⭐⭐ La regla que esto deja: se absorbe lo que DESCRIBE y se deriva lo que
DECIDE. Y el criterio para saber en cuál estás: si apagar esa pieza deja pasar una
operación que antes se rechazaba, no era descriptiva.
5·quinquies · 🏁 L·0 y L·1 — MEDIDOS (2026-08-13)
① El terreno
GKE 1 nodo n2-standard-4 · CPU 88 % · memoria 87 % (requests)
namespaces con carga: spark 4 · catalog 1 (el IRC de Gravitino) · stackable-operators 6
⛔ Y la VM de OpenMetadata NO EXISTE. No está caída: no está.
ssh 34.168.67.167 puerto 22 ⛔ · puerto 8585 ⛔
gcloud compute instances list trino-k8s → sólo el nodo del GKE
primeval-rain-… → ninguna
compact-idiom-… → Compute API nunca habilitada
⚠️ Pero el stand-up está versionado: infra/openmetadata/ (compose, override, Caddy,
env.example) + docs/runbooks/openmetadata-standup.md. ⇒ L·2 no es investigar, es
reejecutar — con una corrección importante: el camino que OM tenía al Warehouse era
por Trino, y Trino salió del cluster el 08-12. La ruta nueva es mejor y es la de
este approach: el linaje se lo da OpenLineage y la identidad se la da Index, no un
motor de perfilado.
② 🏁 L·1 · el motor EMITE linaje a nivel de columna, y con Spark 4.1.2
infra/gke/l1-openlineage-gate.yaml · Job desechable, --master local[1], sólo
lectura, sin tocar el CR carbon-connect (cablearlo ahí reinicia Spark Connect y con
él la sesión caliente del editor: primero se mide, después se cablea).
GATE 1 el listener CARGA con Spark 4.1.2-stackable26.7.0 ← NO estaba documentado
GATE 3 lectura de carbon.main.default.bronze_clientes OK · 3 filas ← por LA CARA
GATE 4 eventos = 16 · con inputs Y outputs = 6
GATE 5 outputs con columnLineage = 9
GATE FINAL VERDE
⭐ El coste de la absorción es una coordenada Maven y tres --conf — el mismo
mecanismo que ya trajo Iceberg y el driver JDBC:
--packages io.openlineage:openlineage-spark_2.13:1.52.0
--conf spark.extraListeners=io.openlineage.spark.agent.OpenLineageSparkListener
--conf spark.openlineage.transport.type=…
⭐ Y un campo que no buscábamos: cada transformación de columna viene con
"masking": false. El estándar ya modela si una transformación ENMASCARA ⇒ el día
que exista la máscara de columna (concepto 7), el linaje sabrá decir por dónde viajó un
dato enmascarado. No hay que pedirlo: viene puesto.
③ ⭐⭐⭐ La identidad viene RESUELTA — y es nuestro propio paradigma de nombres
Lo primero que emitió fue inquietante: nombra nuestra tabla por su ubicación.
s3://lakehouse :: warehouse/019f1a5f-3a4f-7972-8bea-77e92a58ddbf
Pero el facet symlinks lleva la otra mitad:
{"namespace": "https://app.paladio.io/api/iceberg",
"name": "main.default.bronze_clientes", "type": "TABLE"}
⇒ El namespace es LA CARA y el nombre es la COORDENADA LÓGICA. No hay traductor que escribir: el motor emite el linaje ya atado a nuestro catálogo.
⭐⭐ Y fíjese en la forma: OpenLineage nombra por ubicación y pone la
coordenada en el symlink — es exactamente nombre ≠ ubicación, el paradigma que
N·1–N·5 nos costó cinco iteraciones. El estándar llegó a la misma separación por su
cuenta, y eso es la mejor señal de que el modelo encaja: cuando dos sistemas que no
se conocen resuelven igual, no es una convención, es la estructura del problema.⚠️ Con su reverso, que hay que decir: es la misma trampa de T·1·0. Quien consuma
estos eventos leyendonameen vez desymlinksresolverá por ubicación y perderá
el nombre — el bug que ya nos costó una tarde, esperando en un campo nuevo.
④ ⭐⭐ ¿OM y OL los dos en el cluster? — sí, y el motivo no es la elegancia
Pregunta del owner. La respuesta corta: OpenLineage ya está en el cluster (es un listener dentro del driver de Spark, no un servicio), así que la pregunta real es sólo sobre OpenMetadata. Y sí — pero por una razón que sale de lo medido hoy, no de que quede más limpio:
⭐⭐⭐ Lo que se perdió no fue una máquina: fue un despliegue NO DECLARADO. La VM se
levantó conscp+docker compose upsiguiendo un runbook, y desapareció sin que
nadie se enterara. Un manifiesto en el repo aplicado al cluster se puede volver a
aplicar; un procedimiento manual sólo se puede volver a ejecutar — y sólo si alguien
se acuerda. La reproducibilidad no es una virtud del k8s: es lo que hoy nos faltó.
El presupuesto, medido (no estimado):
CPUS_ALL_REGIONS límite 12 · uso 4 ⇒ quedan 8
europe-west1 CPUS 32 · N2_CPUS 32 (holgado: el techo es el global)
nodo actual n2-standard-4 · CPU 88 % · memoria 87 % ⇒ ⛔ NO cabe aquí
⇒ Cabe un segundo nodo (n2-standard-4 → 8/12, quedan 4 de margen). Y debe ser un
node pool aparte con taint: Spark está al 88 % y OM no puede provocarle desalojos.
Es la misma disciplina de G·1 —«si no cabe, que lo diga el planificador y no lo pague
Spark»— aplicada un nivel más arriba.
Dónde vive cada estado, y el criterio es la CRITICIDAD, no la comodidad:
| Pieza | Dónde | Por qué |
|---|---|---|
| OpenLineage | 🏁 ya en el cluster | Es un jar en el driver. Ni pod ni cuota |
| OM server | node pool nuevo, con taint | Sin estado propio |
| OpenSearch de OM | el cluster, con PVC | Índice reconstruible: perderlo es reindexar ⚠️ pide vm.max_map_count=262144 en el nodo — en GKE/COS eso es un DaemonSet privilegiado, y es fricción real |
| ⭐ La BD de OM | la base gestionada de europe-west1 (§8·quater·bis, opción A) | Ya hay que crearla para el catálogo; un esquema más sale casi gratis y quita un PVC |
⭐⭐ Y el matiz que hace esto distinto de la opción B del §8·quater·bis (que se descartó): allí el estado era la base del catálogo, donde vive la atomicidad del lakehouse; aquí es una proyección descriptiva. Perder OM es reingerir; perder el catálogo es perder el gobierno. El mismo argumento no se aplica a los dos: lo que decide dónde vive un estado es si su pérdida es reconstruible.
5·sexies · ⭐⭐⭐ ¿LA CAPA ONTOLÓGICA/AGÉNTICA SOBRE OM? — NO, y el motivo es estructural
Pregunta del owner: «¿construimos nuestra capa ontológica/agéntica sobre OM? Porque
entonces usamos SUS guardarraíles, y me sigue pareciendo una arquitectura inconexa
—sobre todo si queremos contexto a nivel enterprise».El instinto es correcto y el motivo no es de gusto. Es éste:
⭐⭐⭐ El espacio de ACCIÓN no se puede proyectar
Nuestro norte lo dice ya: «un agente no consume un espacio de exploración: consume un ESPACIO DE ACCIÓN ACOTADO». Y un espacio de acción es, por definición, (lo que existe) ∩ (lo que ESTE principal puede hacer).
lo que EXISTE lo sabe OM (proyección descriptiva)
lo que PUEDES hacer lo sabe INDEX (la retícula, con herencia e implicaciones)
el ESPACIO DE ACCIÓN sólo lo puede calcular quien tiene LAS DOS
⇒ Si el agente pregunta a OM, OM contesta lo que OM sabe, no lo que ese agente puede tocar. Para filtrarlo, OM necesitaría nuestra retícula dentro — y eso es la segunda retícula que llevamos toda la sesión evitando.
⛔ Un contexto sin filtrar no es contexto: es un catálogo de cosas que no puedes
usar. Y para un agente es peor que no tener nada, porque planifica sobre él y
fracasa al ejecutar.
⛔ Y lo que perderíamos es exactamente la CUÑA del producto
La tesis de plataforma es que el cliente compra CONTEXTO y que la ventaja es tener el catálogo NATIVO. Si el contexto lo sirve OM:
- el contexto sale de una proyección, que puede ir rancia, incompleta o desincronizada;
- la ontología derivaría del modelo de OM, no del nuestro ⇒ regla 46: un tipo que no deriva del catálogo es una declaración;
- y el catálogo deja de ser nativo, que es la cuña.
⇒ La incomodidad del owner no es arquitectónica: es de posicionamiento. Construir el producto sobre el modelo de otro es cómo se deja de tener producto.
✅ El reparto que sí funciona — UNA superficie agéntica, la nuestra
┌──────────── LA CAPA AGÉNTICA (nuestra) ────────────┐
agente ─▶│ compone: espacio de acción = catálogo ∩ retícula │
└───────┬──────────────────────────────┬────────────┘
│ pregunta (lectura) │ ejecuta
▼ ▼
OpenMetadata JUNCTION — la puerta
glosario · clasificación decide, firma, deja asiento
contrato · linaje · calidad
- El agente NO habla con OM. Hablamos nosotros. OM es un servicio interno, no una plataforma sobre la que nos apoyamos.
- ⛔ Su MCP no se expone a nuestros agentes. No es un olvido: su MCP autoriza con «any role or policy defined» de OM ⇒ en el momento en que un agente actúe por ahí, apagar OM cambiaría lo que se puede hacer, y por la regla 66 eso demuestra que ya no era descriptivo.
- ⭐ Y así no hay dos superficies agénticas, que era justo lo «inconexo» que el owner señalaba.
🧪 La prueba del algodón de esta absorción
⭐⭐⭐ Si apagar OM nos deja sin contexto, lo pusimos en el sitio equivocado.
Lo que OM produce —etiquetas, contratos, términos— aterriza en Index como propuestas (L·4) y la verdad vive en Index. Si OM desaparece mañana —como desapareció su VM— perdemos la superficie de curación y el perfilador, no el contexto. Eso es lo que hace la absorción reversible, y es la diferencia entre usar una herramienta y depender de ella.
⇒ Y por eso el orden importa: primero el modelo en Index (etiquetas, contrato, proceso como securable), después OM como el sitio donde se curan y se enriquecen. Al revés, el modelo lo acabaría dictando su esquema.
5·septies · ⭐⭐⭐ GOBERNAR ACTIVOS HETEROGÉNEOS — el panorama, y quién ya está en casa
Pregunta del owner: «¿qué alternativas más completas existen que podamos absorber,
teniendo en cuenta que el catálogo necesita gobernar tópicos, imágenes, datos,
modelos y volúmenes bajo un sistema gobernado enterprise e2e?» — y con el añadido de
que los agentes consumen permisos, tipos, guardarraíles y entornos acotados.
Los candidatos, con lo que gobiernan de verdad
| Qué objetos GOBIERNA | Autoridad | Veredicto | |
|---|---|---|---|
| ⭐⭐⭐ Apache Gravitino | CATALOG · SCHEMA · TABLE · COLUMN · **FILESET** · **TOPIC** · ROLE · METALAKE · catálogo de MODELOS (0.9+) · 20+ tipos de catálogo: relacional, fileset, messaging, model | RBAC unificado entre catálogos (1.2.0, marzo 2026) | 🏁 YA ESTÁ EN EL CLUSTER (G·1, el IRC). Su lista es, literalmente, la del owner |
| ⭐⭐ Unity Catalog OSS | tables · views · volumes · functions · models · services, en 3 niveles. Apache-2.0, LF AI & Data | Todo activo es un securable | ⭐ El MODELO de referencia —ya es el que copiamos— pero el servidor está en 0.1 / sandbox, y ⛔ el ABAC no es del OSS (§1·1) |
| Apache Polaris | Iceberg (+ generic tables) | grants propios · Ranger externo (1.5.0) | ⛔ No cubre modelos, volúmenes ni tópicos |
| Apache Atlas | tipos abiertos (cualquier cosa) | ⛔ ninguna — aplica Ranger | ⛔ Descartado (§5·bis): no cabe y no aplica |
| Apache Egeria | metadatos enterprise, muy amplio | frameworks de gobernanza | ⛔ Mismo criterio que Atlas: peso enorme, y su punto de aplicación no es el nuestro |
⭐ Y el dato que mira al futuro del owner: Unity Catalog está llevando sus ABAC
Grant Policies a modelos, y anuncia extenderlas a componentes de IA — servicios
MCP y AGENTES— además de tablas y volúmenes. ⇒ El agente como securable no es una
idea nuestra: es hacia donde va el estándar. Nuestro vocabulario ya lo admite
(PrincipalKind = 'user' | 'service' | 'agent' | 'job'), y sigue sin un solo sujeto de
tipo agent.
⭐⭐⭐ Y aquí aparece la consecuencia que no estaba vista
La regla de §5·ter era: un kind nuevo sólo entra con su punto de aplicación. Ahora mírese dónde están los nuestros:
tabla Iceberg ⇒ la cara /api/iceberg + la puerta 🏁 los tenemos
topic Kafka ⇒ ¿? ⛔ ninguno
fileset/volumen ⇒ ¿? ⛔ ninguno
modelo ⇒ ¿? ⛔ ninguno
⇒ Nuestro punto de aplicación cubre exactamente UN tipo de activo. Gobernar tópicos,
volúmenes y modelos no es añadir filas a securable_kind: es construir tres puertas
nuevas… o usar las que Gravitino ya tiene.
⭐⭐ Por eso el primer activo NO-TABLA que se quiera gobernar FUERZA la decisión que
el §8·quinquies·② dejó abierta. No es una decisión de catálogo: es la única forma de
que untopicgobernado no sea una fila decorativa.
| Rama | Qué implica con activos heterogéneos | |
|---|---|---|
| A · la cara delante, Gravitino detrás e inerte | Habría que construir el punto de aplicación de cada tipo nuevo. Es reconstruir lo que ya existe | |
| ⭐ B · Gravitino aplica, Index proyecta | Se usa su RBAC unificado como destino de proyección — el mismo patrón que él ya usa con Ranger, y que nosotros ya usamos con workspace_members → grants. Index sigue siendo el origen |
⇒ Recomendación: seguir entrando por A para tablas —donde nuestro punto de aplicación ya existe y funciona— y diseñar B para todo lo demás. Es lo que el mapa ya recomendaba («entrar por A y diseñar para B»), sólo que ahora se sabe qué lo dispara: no un plan, sino el primer tópico o el primer modelo que alguien quiera gobernar.
⚠️ Con la condición de siempre, que aquí es más importante que nunca: la retícula de Gravitino es un destino, jamás una segunda fuente. El día que alguien conceda directamente allí, hay dos verdades — y ésa es la avería que este mapa lleva entero intentando evitar.
5·octies · ⭐⭐⭐ LA FRONTERA — Gravitino, Index y quién aplica de verdad
① Gravitino ↔ OpenLineage: es su CONSUMIDOR, y resuelve nuestra identidad
Gravitino trae un framework de linaje pluggable que recibe eventos OpenLineage, con sink a Marquez. Y —lo que más nos toca— un plugin de Spark propio que extrae el linaje y TRADUCE los identificadores de dataset a identificadores de Gravitino, con linaje de columna y a través de catálogos heterogéneos: fileset · Iceberg · Hudi · Paimon · Hive · Model.
⇒ Es exactamente el problema que L·1 midió: el motor nombra por ubicación
(s3://lakehouse/warehouse/<uuid>) y deja la coordenada en symlinks. Gravitino
hace esa traducción de serie.
⚠️ Y con eso aparece un solape que hay que decidir, no ignorar: dos sinks de
linaje. Gravitino y OpenMetadata pueden consumir los mismos eventos. No es
redundancia gratuita —OM aporta glosario, clasificación PII, contratos, calidad y
superficie de curación; Gravitino aporta el linaje unificado entre tipos de
activo— pero mandar los eventos a los dos sin decidir cuál es autoritativo es cómo
se acaba con dos grafos que discrepan.[decidir]
② «RBAC unificado entre catálogos» — qué es exactamente
Privilege × SecurableObject × Role → User | Group
securables: METALAKE · CATALOG · SCHEMA · TABLE · TOPIC · FILESET · ROLE (+ MODEL)
privilegios: de gestión (CREATE, DELETE) y de operación (SELECT_TABLE, MODIFY_TABLE…)
«Unificado» significa un solo vocabulario para TODOS los tipos de catálogo — el
mismo Role concede sobre una tabla Iceberg, un tópico Kafka y un fileset — en vez de
un modelo de permisos por sistema (el de Hive, el de Kafka, el de MySQL…). Es
literalmente nuestro modelo, con otros nombres, extendido a activos que nosotros no
tenemos.
③ ⭐⭐⭐ Y AQUÍ ESTÁ LA RESPUESTA A «¿NOS OPACA?» — Gravitino NO APLICA
De su propia documentación de control de acceso:
«Gravitino doesn't support metadata authentication. It means that Gravitino won't
check the privileges when Gravitino receives the requests.»
⇒ Gravitino ADMINISTRA quién tiene qué; la aplicación la DELEGA («authorization push-down») a Ranger o al control de acceso del sistema subyacente.
Gravitino define QUIÉN puede ⇒ una DECLARACIÓN
Ranger/otro comprueba SI pasa ⇒ la APLICACIÓN
⭐⭐ No es que no queramos que Gravitino aplique: es que NO APLICA. El puesto de
«el que dice NO» sigue vacante, y hoy lo ocupamos nosotros (la cara + la puerta).
Eso responde la pregunta del owner de forma tajante: una implementación madura de
Gravitino no opaca a Index en lo que Index hace de verdad — sólo en el vocabulario.
⚠️ Reserva del instrumento, dicha: eso es la doc de 0.9.0-incubating; en 1.2.0
medimos que planTableScan lleva @AuthorizationExpression, así que su postura ha
cambiado entre versiones. Lo que no cambia es el alcance: donde aplique, aplicará
sobre operaciones de metadatos de su API, nunca sobre los bytes. [medir en 1.2+]
④ Qué de Index quedaría opacado, honestamente
| Pilar de Index | ¿Lo cubre Gravitino? |
|---|---|
Taxonomía (catalogs · schemas · datasets · tenant_warehouses) | ⚠️ SÍ — es su oficio. Aquí hay solape real |
Retícula (privilege_grants + cadena) | ⚠️ Solapa el vocabulario, NO la aplicación ⇒ destino de proyección |
Ledger (dataset_transactions · iceberg_sync_log · schema_versions) | ⛔ NO. Es nuestro control-plane de escritura |
Uso (access_events) | ⛔ NO. El suyo audita su API; el nuestro registra decisiones de nuestra puerta |
Política (policies · policy_attachments) | ⛔ NO. No tiene política de celda (medido en §8·1·f) |
El lazo con el PRODUCTO (project_files, tiers, la coordenada que ve la UI) | ⛔ NO, y no puede. Index no es un catálogo: es un plano de control de producto |
⑤ 🖼️ La vista idílica, con la frontera trazada
┌──────────────────────────────────────────────────────────────────────────────┐
│ ⑤ SUPERFICIE AGÉNTICA (nuestra, única) │
│ espacio de acción = lo que EXISTE ∩ lo que ESTE principal PUEDE │
└───────────────┬─────────────────────────────────────────┬────────────────────┘
│ ejecuta │ pregunta (sólo lee)
┌───────────────▼─────────────────────────────┐ ┌───────▼────────────────────┐
│ ④ APLICACIÓN — el único que dice NO │ │ DESCRIPTIVO (al lado) │
│ la puerta (Junction) · la cara /api/iceberg│ │ OpenMetadata │
│ ¿puede esta PERSONA? · ¿puede este MOTOR? │ │ glosario · clasificación │
└───────────────┬─────────────────────────────┘ │ contrato · calidad │
│ pregunta a… └───────▲────────────────────┘
┌───────────────▼─────────────────────────────┐ │ eventos
│ ③ AUTORIDAD — INDEX │ │
│ retícula · políticas · etiquetas · │ ┌────────┴────────────────────┐
│ contratos · ledger · lazo con el producto│ │ OpenLineage (en el motor) │
│ ⭐ ORIGEN de toda proyección │ │ ya emite, a nivel COLUMNA │
└───────────────┬─────────────────────────────┘ └─────────────────────────────┘
│ proyecta ─────────────┐
┌───────────────▼─────────────────────┐ │
│ ② CATÁLOGO — Gravitino / Lakekeeper│◀┘ su retícula = DESTINO, nunca fuente
│ qué existe · dónde · CAS del commit│
│ ⛔ define, NO aplica │
└───────────────┬─────────────────────┘
┌───────────────▼─────────────────────┐
│ ① BYTES — R2 · no saben de nadie │
└─────────────────────────────────────┘
⑥ ⭐⭐⭐ «¿Gravitino es como Lakekeeper pero más completo?» — SON DOS ARTEFACTOS
⚠️ Y llevamos varias secciones mezclándolos. Lo desplegado (G·1) es el IRC server: Iceberg y nada más. Todo lo hablado en §5·septies —tópicos, filesets, modelos, RBAC unificado, linaje OpenLineage— es del servidor COMPLETO, que no está desplegado. Es la regla 8·ter aplicada a nosotros mismos: no contar como propia una capacidad que no está encendida.
El IRC contra Lakekeeper, medido hoy (GET /iceberg/v1/config desde dentro del
cluster — endpoints[], que es lo autoritativo):
| Lakekeeper (25) | Gravitino IRC (24) | |
|---|---|---|
| Registro + CAS del commit | ✅ el núcleo | ✅ |
⭐ …/tables/{table}/credentials (vending) | ✅ | ✅ MEDIDO — era el riesgo: sin él no podría sustituirlo, porque hoy Spark lee con vended-credentials |
⭐⭐ POST …/tables/{table}/plan | ⛔ | ✅ la diferencia real |
POST …/namespaces/{ns}/register (migrar) | ✅ | ✅ ⇒ el re-registro es viable |
| Vistas (CRUD completo) | ✅ | ✅ |
…/tables/{t}/metrics | ✅ | ✅ (la ruta que nuestra cara deja fuera y ensucia el log de todo motor) |
⛔ POST /v1/{prefix}/transactions/commit | ✅ declarado (sin estrenar) | ⛔ NO lo declara |
| Warehouses por inquilino (9) | concepto propio | son catálogos del metalake ⇒ hay que traducir |
| Soft-delete 7 días (P·3) · OIDC Keycloak | ✅ | ⏳ sin comprobar |
⭐ Y sus defaults ya vienen con R2 resuelto: s3.path-style-access=true ·
client.region=auto · el endpoint de R2 · S3FileIO.
⇒ Respuesta exacta: el IRC de Gravitino es un Lakekeeper con
/plany sin commit
multi-tabla. Un cambio en cada dirección, no una mejora limpia. Lo «más completo»
vive en el metalake — que es otra decisión, otro despliegue y otro coste.⚠️ Y el commit multi-tabla no es un detalle:
sustrato-07§7 lo tiene listado como
«la capacidad ya está comprada» para la deuda ⛔⛔ «una escritura toca UNA tabla».
Migrar hoy la desharía.[medir si 1.2+ lo declara]
Las tres reglas que sostienen la frontera:
- INDEX DEFINE · LA PUERTA APLICA · EL CATÁLOGO REGISTRA · OM DESCRIBE. Cuatro verbos, cuatro sitios, cero solapes de autoridad.
- Todo lo que baja del ③ es una PROYECCIÓN — la retícula de Gravitino, las entidades de OM. Nunca al revés, salvo lo que entra como propuesta (clasificación).
- ⭐ El agente sólo tiene UNA puerta, y es la ⑤. Ni el MCP de OM ni el catálogo son superficie agéntica.
5·nonies · ⭐⭐⭐ VENDING vs POLÍTICA DE FILA — qué dice el estándar, y una corrección propia
Pregunta del owner: «¿quién firma según el estándar —el catálogo, el motor, la cara o
nadie? ¿Por qué tanta fricción entre el vending y la política de fila? ¿Cómo lo
resuelven los jugadores?»⛔ La respuesta desmiente la salida D que este mismo documento proponía.
① Quién firma, según el estándar: NADIE. El catálogo VENDE
El mecanismo de primera clase de la spec Iceberg REST es la credencial vendida:
…/tables/{t}/credentials y el bloque storage-credentials del loadTable. Temporal,
acotada a la tabla, con refresco. Es lo que Lakekeeper hace hoy y lo que C·2·b midió
(855 s).
⚠️ El remote signing NO es el camino estándar: es una extensión del cliente
(S3V4RestSignerClient), no la recomendación de la spec. ⇒ Proponerlo era apartarse
del estándar creyendo acercarse a él.
② Por qué hay fricción: no es de implementación, es estructural
Iceberg es un FORMATO DE FICHEROS.
Quien recibe acceso al fichero —por credencial o por firma— lee el fichero ENTERO.
⇒ filtro de fila y máscara de columna son INCOMPATIBLES con el acceso directo,
salvo que confíes en el motor.
Firmar cada petición no arregla eso: da granularidad de fichero, no de fila.
③ Cómo lo resuelven los jugadores: cortándolo, no resolviéndolo
Unity Catalog, literal en su documentación:
«Tables with row filters or column masks … are not supported with credential
vending.» · «You cannot use Iceberg REST catalog or Unity REST APIs to access tables
with row filters or column masks.»
Y Delta Sharing igual: no se comparten tablas con RLS/CM de nivel tabla.
| Jugador | Qué hace con una tabla que tiene política |
|---|---|
| Unity Catalog | La saca del acceso directo. Ni vending ni Iceberg REST ⇒ sólo por su motor de confianza |
| Snowflake | Igual: su motor. Las Iceberg tables externas van sin políticas |
| Polaris | Manda el grano fino «a la capa del motor» |
| Dremio | Catálogo grueso + motor fino |
| Trino / Starburst | Motor de confianza con OPA/Ranger |
⇒ Nadie aplica grano fino en el catálogo. El patrón universal es: si hay política, el acceso directo se CORTA y sirve un motor de confianza.
⭐ La única excepción, y es de 2026: UC anunció Preview de control de acceso fino para motores externos — «the first and only catalog to support cross-engine ABAC», con filtros y máscaras basados en etiquetas. Está en Preview, y es exactamente la frontera del sector.
④ ⇒ Qué deberíamos hacer nosotros — y la corrección
⛔⛔ La salida D (remote signing) queda RETIRADA como plan. Su premisa era «el
vending hay que abolirlo igual». Falso: el vending temporal es el estándar y
lo que hay que abolir es el acceso directo a las tablas CON política — que es otra
cosa y no cuesta latencia por fichero.
El patrón que sí toca copiar, y del que ya tenemos las dos mitades:
tabla SIN política → vending temporal por el catálogo ← lo de hoy, y es el estándar
tabla CON política → NO se sirve por Iceberg REST
se sirve por LA PUERTA (Junction + Spark), que es
nuestro motor de confianza y ya aplica gobernanza
⭐⭐ Y ésta es la mejor noticia del análisis: no hay que construir el motor de
confianza — lo tenemos. Junction + plan-reader + Spark es exactamente la pieza que
UC llama su motor, y el SQL Editor ya pasa por ahí. Lo que falta no es infraestructura:
es la política (concepto 7) y las etiquetas (concepto 6) que la hacen escalable.
⑤ Lo que esto le hace al cutover
⚠️ El bloqueo de C·2·b sigue, pero por el motivo CONTRARIO al que dije. No es que haya que abolir el vending: es que hay que conservarlo, porque es el estándar — y Gravitino no sabe vender temporales contra R2.
| Salida | Estado | |
|---|---|---|
| ⭐ A | Parar el cutover. El IRC queda en pie, durable, con sus 9 catálogos | la posición |
| ⭐⭐ B | Que Gravitino sepa vender temporales de R2 — contribuir el provider | sube a salida buena: es conservar el estándar, no inventar |
| C | Que el vending lo sirva la cara | Posible, pero es reimplementar lo que el catálogo ya hace |
| ⛔ D | Remote signing | RETIRADA: no es el estándar, y no compra la política de fila |
⭐⭐⭐ La regla que deja este episodio: «esto habría que quitarlo de todas formas»
es la coartada más cómoda para justificar un rodeo. Antes de apoyarse en ella, hay
que comprobar que el estándar también lo quita — y aquí no lo quita: lo tiene como
mecanismo central, y lo que corta es otra cosa.
6 · ⭐⭐⭐ Lo que este mapa CAMBIA de decisiones ya tomadas
6·1 · La salida C de F·0 estaba mal evaluada
plano-gobernado-approach.md §7·bis dice, sobre cambiar de catálogo:
«Ninguno lo tiene maduro todavía: UC lo tiene en beta».
⛔ Eso ya no es cierto. Gravitino 1.2.0 (marzo 2026) implementa scan planning offload en su servidor Iceberg REST, y motores como DuckDB y Spark ya delegan en él. Polaris lo usa para proyectar la gobernanza de Snowflake sobre motores externos.
⇒ No cambia la recomendación —sigue siendo A, aplicar en el motor—, pero cambia su MOTIVO. Ya no es «porque nadie lo tiene»; es «porque migrar el control-plane cuesta más que construir el aplicador si y sólo si el aplicador se construye para mudarse». Y hace que el diseño de F·4 tenga una obligación concreta:
El aplicador recibe (principal, objetos resueltos, política) y devuelve
(filtros, máscaras). Sin saber quién lo llama. El día que el catálogo
planifique —el nuestro o el que lo sustituya— se mueve la llamada, no la lógica.
⚠️ Y abre una pregunta que no estaba planteada: ¿Lakekeeper es el catálogo
definitivo? No hay que contestarla hoy, pero sí dejar de asumir que sí. [medir]
6·2 · Las etiquetas suben de prioridad por encima de las políticas
UC lo confirma con su modelo GA: las etiquetas heredan hacia abajo (una etiqueta en el esquema alcanza a sus tablas; una en el catálogo, a todo) salvo en columnas, que no heredan de su tabla. Ese detalle es el que convierte «etiquetar» en barato.
⇒ Sin etiquetas, cada política es por objeto. Con 123 datasets y creciendo, eso no es un inconveniente: es el techo del sistema.
6·3 · El contrato ODCS no es «documentación»: es la fuente del tipo
ODCS v3.1.0 (validación JSON Schema estricta) hace que un contrato sea interpretable igual por todas las herramientas. Y en nuestro norte —donde la ontología es el espacio de acción— eso significa que el contrato que nace en la ingesta ES la definición del tipo, no un adorno alrededor.
7 · El orden que se deriva del mapa
No es el orden de lo importante: es el orden de lo que desbloquea a lo demás.
🏁 0 · SUJETOS ← hecho (F·2·0). Sin esto no había a quién aplicar
⭐ 1 · ENCENDER la aplicación ← los grants existen y NO gobiernan (`shadow`)
⭐ 2 · ETIQUETAS en Index ← el multiplicador: sin ellas, N tablas = N políticas
3 · CONTRATO en la ingesta ← quien PRODUCE las etiquetas, y define el tipo
4 · POLÍTICA como objeto ← con dueño, versión y `SHOW` privilegiado
5 · CONMUTAR el vendado ← «hay política ⇒ no vendo credencial». Sin esto, 4 es decorativo
6 · EL APLICADOR en el motor ← construido para mudarse al catálogo
7 · UN SOLO plano de tipos ← la Action escribiendo por la puerta
8 · EL GRAFO como proyección ← y sólo entonces
⚠️ El paso 5 es el que se salta todo el mundo, y es el que decide si los pasos 2-4 gobiernan o decoran. Mientras se venda credencial STS para leer el Parquet directo, cualquier política de fila se esquiva pidiendo la llave.
8 · 🏁 LAS SEIS PREGUNTAS, CONTESTADAS CONTRA EL ESTÁNDAR (2026-08-12)
Ninguna se contesta por criterio propio si la industria ya la tiene resuelta. Y
donde la respuesta sale de nuestro propio código, va con el fichero y la línea.
8·1 · ⭐⭐⭐ ¿Gravitino en vez de Lakekeeper?
⛔ CORRECCIÓN (misma fecha, tras medirlo). La primera versión de esta sección
concluía «hoy no» apoyándose en que perderíamos «OpenFGA por defecto, OPA bridge,
Cedar y permisos heredados hasta tabla y vista». Eso era falso: son
capacidades del producto Lakekeeper que nuestro despliegue no usa. Lo corrige
el owner y lo confirma la medición de abajo. Un argumento construido sobre
capacidades que no hemos encendido no compara dos sistemas: compara dos folletos.
8·1·a · ⭐ Lo que de verdad le pedimos a Lakekeeper, medido
| Capacidad del folleto | En NUESTRO despliegue | Evidencia |
|---|---|---|
| OpenFGA como autorizador | ⛔ INERTE — AUTHZ_BACKEND sigue en allowall | docs/runbooks/openfga-standup.md: «F4·0 despliega OpenFGA y NADA MÁS» |
| Cedar | ⛔ NO EXISTE EN EL OSS — no es que no lo usemos: «el workspace sólo tiene crates/authz-openfga, no hay crate de Cedar», y el binario falla con cualquier otro backend | infra/lakekeeper/policies/README.md · docs/runbooks/index-catalog-rest-face.md |
| OPA bridge | ⛔ Es de TRINO — las 14 políticas son trino_* y las consume f1-trino.yaml. Y Trino está fuera del norte (08-09) | infra/gke/opa-bridge/ |
| Remote signing / vending STS | ⛔ Los dos en false | lib/storage/tenant-warehouse.ts:125-126 |
| Herencia de permisos hasta tabla y vista | ⛔ La hace nuestro decidePrivilege, no Lakekeeper | lib/governance/privilege.ts |
⇒ Hoy Lakekeeper es, para nosotros, un almacén de metadatos Iceberg. Toda la gobernanza es nuestra.
Y el coste de salida, medido por los endpoints que llamamos:
/v1/config · /v1/{prefix}/namespaces · …/tables · …/tables/{table} ← spec Iceberg REST
PORTABLE a cualquier IRC
/management/v1/warehouse ← LO ÚNICO propietario
…y sólo lo llama la ruta de
warehouses DEDICADOS, que
hoy no usa ningún inquilino
⇒ ⭐⭐ No hay atadura de API. El coste real de migrar no es el código: es el registro — ~82-84 tablas, 9 warehouses, el OIDC de Keycloak y el perfil de storage.
8·1·b · Los dos argumentos de superficie, retirados
- ⛔ «Servidor JVM». Ya operamos una flota JVM en GKE (Spark Connect + ejecutores; Trino mientras dure). Un catálogo JVM no es una categoría nueva de coste.
- ⛔ «Topología de federación». No federamos: se copia al bucket. Es complejidad que no usaríamos, no un coste que pagaríamos.
8·1·c · Lo que Gravitino da, y encaja con el norte
⭐⭐⭐ /scan nativo | La PRIMERA implementación OSS de Iceberg REST que lo envía (1.2, marzo 2026): evalúa los filtros de partición en el servidor, devuelve la lista de ficheros ya hecha, y cachea el trabajo de metadata. DuckDB y Spark ya delegan |
| ⭐⭐ Un solo RBAC sobre TODO | Privilege · SecurableObject · Role · User · Group — casi nuestro modelo, uno a uno |
| ⭐⭐ La MALLA | Iceberg + Hive + RDBMS + Kafka + ficheros (filesets) + activos de IA / modelos + UDFs, bajo una API |
| Mantenimiento de tablas (TMS) | Compactación y expiración programadas — hoy lo hacemos a mano |
⭐ Y lo que esto tumbaría de nuestro propio plan: con /scan nativo, el
aplicador «en el motor» de F·4 deja de ser el destino y pasa a ser innecesario — y
con él, el impuesto por motor que era su mayor pega.
8·1·d · ⛔ Los DOS riesgos que sí deciden, y ninguno es el que yo puse
| Riesgo | Por qué es serio | |
|---|---|---|
| R1 | ⛔⛔ ¿Soporta Cloudflare R2? Gravitino documenta S3, GCS, OSS y ADLS. Para almacenes S3-compatibles el path-style access es una mejora ABIERTA (issue #6504) — y R2 necesita exactamente eso, más region=auto y endpoint propio | Es la misma razón por la que se descartó Polaris. Si no soporta R2, no hay conversación |
| R2 | ⚠️ Su ABAC es futuro: «implementará un motor ABAC» para etiquetas dinámicas. Y su RBAC empuja los permisos hacia abajo, hoy a Apache Ranger | ⇒ Gravitino NO nos da etiquetas hoy — que es nuestra pieza nº 2 (§7·2). Y «empujar a Ranger» es el producto que decidimos no adoptar |
8·1·e · 🏁 R1 — MEDIDO: SÍ, GRAVITINO HABLA R2 (2026-08-12)
Sonda real: apache/gravitino-iceberg-rest:1.2.0 en Docker, backend jdbc
(SQLite, que la imagen ya trae), apuntando a nuestro bucket con un prefijo
aislado.
gravitino.iceberg-rest.s3-endpoint = <nuestro endpoint R2>
gravitino.iceberg-rest.s3-region = auto
gravitino.iceberg-rest.s3-path-style-access = true
gravitino.iceberg-rest.io-impl = org.apache.iceberg.aws.s3.S3FileIO
🏁 Y verificado LISTANDO EL BUCKET, no creyéndole al servidor:
_probe-gravitino/probe/r1/metadata/00000-6cf0bf0c….metadata.json 731 bytes
_probe-gravitino/probe/r2t/metadata/00000-a900401e….metadata.json 674 bytes
⇒ El riesgo que descartó a Polaris NO aplica a Gravitino. El path-style-access
que R2 exige entró por el issue #6504 → PR #6541 (cerrado, 0.9.0) y está en el
mapa de variables de la imagen (GRAVITINO_S3_PATH_STYLE_ACCESS).
Tres trampas del instrumento, anotadas porque volverán:
| ⛔ | El backend memory NO toca el almacenamiento. Declara la ruta s3://… y sirve de memoria: la primera sonda dio «verde» con cero objetos en el bucket. Un catálogo en memoria no prueba nada de storage |
| ⛔ | :latest (1.3.x) está roto: NoSuchMethodError de Jackson (BufferRecycler.releaseToPool) en cuanto se crea nada. 1.2.0 funciona. Un fallo de dependencias se lee como fallo de R2 si no se mira la causa |
| ⚠️ | El contrato de variables CAMBIÓ entre versiones: 1.2.0 usa GRAVITINO_S3_*, latest usa GRAVITINO_ICEBERG_REST_S3_*. Y el arranque reescribe su propio .conf, así que montarlo read-only revienta |
8·1·f · 🏁 R2 — MEDIDO: el /plan está GOBERNADO, pero es frontera GRUESA
① El endpoint existe, y es la diferencia directa con Lakekeeper — mismo
instrumento, misma disciplina que F·0 (endpoints[], que es lo autoritativo):
Gravitino 1.2.0 → 24 endpoints · POST /v1/{prefix}/tables/{table}/plan
Gravitino latest → 24 endpoints · POST /v1/{prefix}/namespaces/{ns}/tables/{table}/plan
Lakekeeper → 25 endpoints · ⛔ NINGUNO
② Y SÍ autoriza — leído en la fuente
(IcebergTableOperations.java, anotación del propio endpoint):
@Path("{table}/plan")
@AuthorizationExpression(
expression = "ANY(OWNER, METALAKE, CATALOG) || SCHEMA_OWNER_WITH_USE_CATALOG || "
+ "ANY_USE_CATALOG && ANY_USE_SCHEMA && "
+ "(TABLE::OWNER || ANY_SELECT_TABLE || ANY_MODIFY_TABLE)",
accessMetadataType = MetadataObject.Type.TABLE)
public Response planTableScan(…, @HeaderParam(X_ICEBERG_ACCESS_DELEGATION) String accessDelegation)
⇒ Planificar es una operación autorizada por principal sobre la tabla, con la
misma retícula que el resto (owner · SELECT/MODIFY · herencia por catálogo y
esquema). Y acepta la delegación de credencial en el propio plan ⇒ credenciales
con alcance de plan, que es estrictamente mejor que por tabla.
③ ⛔ Pero NO aplica política de celda. En ese camino no hay inyección de filtro
de fila ni de máscara: los filters y projections los trae el cliente. El scan
planning de Gravitino es poda de particiones en servidor + caché + frontera
gruesa, no el «aplica la política una vez para N motores» de UC o Snowflake.
⭐⭐ Y aun así es exactamente lo que necesitábamos, por una razón distinta a la
que buscábamos: no nos da la política, nos da EL SITIO donde ponerla — un punto
del catálogo, ya autorizado por principal, por el que pasa toda lectura de todo
motor. Es literalmente la «costura» que el §6·1 pedía construir «para mudarse»…
sólo que ya existiría, y sería del estándar en vez de nuestra.
⚠️ Reserva honesta del instrumento: mi POST al /plan devolvió 404 con la
forma de ruta de 1.3 y 500 con las dos formas declaradas por 1.2.0 — la ruta
existe (500 ≠ 404) y no conseguí un plan válido sobre una tabla vacía. El veredicto
se apoya en endpoints[] y en el código, no en mi POST — que es la misma corrección
que hubo que hacerle a la sonda de F·0.
8·1·g · ⭐⭐⭐ Y ENTONCES, ¿SE ABSORBE EN KUBERNETES?
Sí, pero no la bestia entera — y ése es el matiz que cambia el coste.
Son DOS artefactos distintos, y sólo uno hace falta:
| Qué es | Chart | Qué nos da | |
|---|---|---|---|
| ⭐ Iceberg REST server (el que medí) | Un servidor IRC solo | iceberg-rest-catalog-chart | /plan · R2 · vending · el catálogo Iceberg |
| Gravitino server completo | El metalake federado: Hive, RDBMS, Kafka, filesets, modelos, UDFs, TMS, UI | chart (k8s 1.29+, Helm 3+, backend MySQL embebido o PostgreSQL externo) | La MALLA |
⇒ La adopción es por etapas, no un salto: entra el IRC server —que es lo que
sustituye a Lakekeeper y trae /plan—, y el metalake completo entra el día que la
malla Data+AI sea el producto, no antes. Empezar por el pequeño hace la decisión
reversible.
Dónde ponerlo: en el cluster de Spark, y por una razón concreta. El scan planning está en el camino de lectura de toda query; un salto de red extra entre el planificador y el motor se paga en cada una. Hoy Lakekeeper vive en Railway y Spark en GKE — co-locarlos es una mejora, no sólo un traslado.
⛔ Y el techo que manda sobre todo esto: CPUS_ALL_REGIONS = 12, con subida
denegada, sobre nodos n2-standard-2 donde el impuesto de sistema es el 77 %.
⇒ Sólo cabe si Trino sale — que ya está decidido (Spark = cómputo · StarRocks =
serving). La salida de Trino no es un ahorro: es el presupuesto de esta operación.
⭐⭐ La decisión de diseño que hay que tomar ANTES de instalar nada
Gravitino trae su propia retícula (Privilege · SecurableObject · Role · User · Group) y empuja los permisos hacia abajo, hoy a Apache Ranger. Si entra sin
decidir esto, tendríamos dos retículas — que es el error del plano ontológico
otra vez, con mejor vocabulario.
Index ──DEFINE──▶ se PROYECTA a la retícula de Gravitino ──APLICA──▶ /plan
(una sola fuente) (destino de proyección, NUNCA 2ª fuente)
⇒ Index sigue siendo el origen; la retícula de Gravitino es un destino de proyección — exactamente el patrón que Gravitino ya usa con Ranger, aplicado un nivel más arriba. Es lo mismo que hicimos con los grants de usuario: derivar, no declarar; sólo que ahora el destino es otro sistema.
⭐ Y la buena noticia sobre el coste de migrar
No hay que mover datos. La spec REST tiene POST …/namespaces/{ns}/register
—que Lakekeeper ya declara— y las tablas son punteros a metadata en R2. Migrar
las ~82-84 tablas es un bucle de re-registro, no una copia. Lo caro no son los
bytes: son el OIDC, el perfil de storage y la cara.
El veredicto, ahora sí con las dos medidas hechas
🏁 R1 verde · R2 verde con matiz ⇒ la objeción que descartaba a Gravitino ha
caído, y la que queda —que no aplica celda— también la incumple Lakekeeper, que
además no tiene ni el sitio donde ponerla.⇒ Gravitino pasa de «no compensa» a ser la opción por defecto para el catálogo,
con dos condiciones: entra sólo el IRC server, y entra con Index como única
fuente de la retícula.
⭐ La salida B queda como respaldo, no como plan: Lakekeeper es Apache-2.0 y Rust,
y /plan es un endpoint de spec estabilizada — contribuirlo sigue siendo posible si
la migración se complica. Pero construir lo que otro ya tiene en producción, para
un catálogo que no vamos a usar en su parte de gobernanza, es el peor de los dos
caminos.
8·2 · ¿La etiqueta se ata al workspace o al catálogo? → TAXONOMÍA GLOBAL, aplicación por securable
Lo que hace el estándar, literal: en UC las governed tags son pares clave-valor definidos a nivel CUENTA y se aplican a catálogos, esquemas, tablas, columnas, modelos y volúmenes. En Lake Formation, las LF-Tags son igualmente del Data Catalog de la cuenta. Los dos separan lo mismo:
la TAXONOMÍA (qué etiquetas existen) → global, de la plataforma
la APLICACIÓN (qué objeto lleva cuál) → por securable, por inquilino
⇒ Para nosotros: la taxonomía es de plataforma, no por workspace. Si cada
inquilino inventara su propio PII, ninguna política sería portable y la
clasificación automática no tendría contra qué clasificar.
⭐ Y el matiz que lo hace viable en multiinquilino —que UC ya resuelve con
permisos, no con jerarquía—: CREATE de etiquetas a nivel cuenta · ASSIGN sobre
la etiqueta y APPLY TAG sobre el objeto para poder ponerla. ⇒ un cliente puede
etiquetar sin poder inventar vocabulario; y si necesita el suyo, entra con el
namespace de su workspace, no en la taxonomía común.
8·3 · ¿Las columnas heredan etiquetas de su tabla? → NO. Y hay que copiar el NO
El estándar es explícito y asimétrico: los securables heredan las etiquetas
de su catálogo o esquema padre, y la herencia se puede sobreescribir en todos los
niveles salvo en columnas; ⭐ las columnas NO heredan de su tabla — hay que
aplicarlas directamente. Y las funciones de introspección lo refuerzan:
has_tag() sobre una tabla mira etiquetas directas e heredadas, pero sobre una
columna sólo las directas.
Por qué, y es la parte que se entiende mal: si una columna heredara PII de su
tabla, etiquetar una tabla enmascararía TODAS sus columnas — y la etiqueta
dejaría de decir qué es sensible para decir sólo dónde hay algo sensible. La
herencia sirve para alcanzar objetos; el grano de columna sirve para
identificarlos, y son cosas distintas.
⇒ Copiamos el modelo entero, asimetría incluida. Y con él viene su coste: la clasificación de columnas no sale gratis — la produce el contrato de la ingesta (§4·13) o un clasificador, nunca la jerarquía.
8·4 · ¿agent y job son principales de primera clase? → SÍ, y la industria va tarde
Lo que dice la industria en 2026: Microsoft, Okta y Google han enviado primitivas de identidad de agente y OWASP publicó su NHI Top 10 — y aun así sólo el 28 % de las organizaciones puede trazar las acciones de un agente hasta un humano patrocinador, y sólo el 18 % confía en que su IAM aguante agentes. La recomendación es unánime y operativa:
Identidad propia por agente, nunca una cuenta de servicio compartida — porque
«la atribución no se puede retrofitear».
Nuestra posición: el vocabulario ya los admite ('user' | 'service' | 'agent' | 'job', el mismo que access_events) y no hay ni un sujeto de esos dos
tipos. Así que el cabo suelto no es el tipo: son dos cosas que faltan alrededor.
- ⭐⭐ El PATROCINADOR humano. Un agente que escribe tiene que poder atribuirse a la persona que lo lanzó. Es la métrica que la industria mide (28 %) y hoy no la tenemos. Es una relación, no una fila — ⇒ es el primer caso de uso real de OpenFGA (§8·5).
- ⭐ El PROPÓSITO acotado.
Principal.purposeexiste en el código y no se usa (§3·6·4). Es literalmente lo que la literatura de 2026 llama delegation and scope: el agente no hereda los permisos de su patrocinador, hereda un subconjunto declarado.
⚠️ Y esto no es futuro lejano: nuestro norte es que los agentes consuman contexto gobernado. El día que uno escriba, la pregunta «¿quién fue?» tiene que tener respuesta, y la atribución no se añade después.
8·5 · OpenFGA: ¿cabida o sobre-iterar? → AMBAS, y depende de POR DÓNDE entre
⭐⭐⭐ El hallazgo que lo decide: Lakekeeper usa OpenFGA como su sistema de autorización por defecto, con permisos «heredados hasta tablas y vistas individuales» y modelos RBAC/ReBAC/ABAC. Es decir: en nuestro stack OpenFGA ya tiene dueño, y no somos nosotros.
| Por dónde entraría | Veredicto |
|---|---|
| ⛔ Como decisor de la RETÍCULA (nuestros grants) | SOBRE-ITERAR. Tendríamos dos ReBAC sobre el mismo árbol —el de Lakekeeper y el nuestro— y el estado de autorización en dos sitios. Es el anti-patrón que la propia literatura nombra: «cada servicio haciendo su propia autorización» y «mantener el estado en varios sitios». Y es exactamente el error que estamos retirando del plano ontológico |
| ⭐⭐ Como decisor del TRAVERSING del grafo | SÍ, y es su caso canónico. «Puedo ver esta entidad porque estoy relacionado con aquélla» es lo que una retícula jerárquica no sabe expresar y ReBAC sí. Cuando la ontología se proyecte, gobernar el recorrido es un problema de relaciones, no de filas |
| 🟡 Como registro del patrocinador de un agente | SÍ (§8·4·1): «este agente actúa por cuenta de esta persona» es una relación |
⇒ OpenFGA no entra por debajo: entra por arriba. Y hasta entonces, que siga
inerte es la decisión correcta, no un pendiente. Lo que sí hay que corregir es la
frase del approach que lo pinta como «el decisor de la retícula que ya existe»:
ese puesto ya está ocupado por decidePrivilege y por el OpenFGA de Lakekeeper.
8·6 · ¿El grant de workspace debe alcanzar al dato? → SÍ. La divergencia era de NOMBRE, no de modelo
El estándar: en UC los grants a nivel metastore NO se heredan a los hijos —
gobiernan operaciones de ámbito metastore (CREATE CATALOG), no el acceso al
dato. Los grants de catálogo, en cambio, sí heredan hacia abajo.
La comparación correcta, que es donde estaba el error:
UC: cuenta → metastore → CATÁLOGO → esquema → tabla
(no hereda) (SÍ hereda al dato)
nosotros: workspace → catalog → schema → dataset
↑ juega el papel del CATÁLOGO de UC, no el del metastore
⇒ Nuestra herencia es correcta y coincide con el estándar. Lo que engaña es el
nombre: workspace suena a cuenta y funciona como contenedor de catálogos.
⚠️ Pero deja un cabo REAL, y es concreto: en el mismo nivel conviven hoy
WORKSPACE_MANAGE_CONTENT (que hereda al dato, correctamente) y
WORKSPACE_MANAGE_ACCESS (que gobierna la retícula, y no debería tener nada que
ver con el dato). Son los dos ejes de UC metidos en un solo nivel. Hoy no hace
daño porque MANAGE_ACCESS no implica nada; el día que se le cuelgue una
implicación, heredaría por donde no debe. ⇒ anotarlo como invariante con test, no
como refactor.
8·7 · ⛔ Y DOS HALLAZGOS EN NUESTRO CÓDIGO QUE TOCAN A LO ANTERIOR
① 🏁 MEDIDA (2026-08-13): SÍ vendamos temporal, ~855 s
scripts/warehouse/c2b-que-vende-lakekeeper.ts— contra el warehouse compartido,
que es el que sirve:s3.session-token (692 chars) · expiration-time · s3.session-token-expires-at-ms client.refresh-credentials-endpoint ⏱️ caduca en 855 s (~14 min)⇒ La afirmación era CIERTA, y ahora tiene comando detrás. Con ella queda
confirmado el §5·3: mientras se venda credencial para leer el Parquet directo,
cualquier política de fila es decorativa. Y bloquea el cutover de catálogo
(irc-cutover-approach.md§6·sexies): Gravitino sólo sabe entregar la llave larga
contra R2 ⇒ migrar sin resolverlo sería una regresión de seguridad.⚠️ Lo de abajo se conserva porque el razonamiento del código sigue siendo válido y
explica por qué la duda era razonable: la ruta que ponests-enabled:falsesólo
corre para warehouses dedicados, y el compartido se configuró fuera de ella.
①·bis El porqué de la duda — la ruta del código que decía lo contrario
lib/storage/tenant-warehouse.ts:125-126 construye el perfil de almacenamiento con:
'sts-enabled': false,
'remote-signing-enabled': false,
Ni vending temporal ni firma remota. Y hay un segundo matiz que lo complica: esa
ruta sólo corre para warehouses dedicados, y hoy no tiene ninguno (pool, no
silo) ⇒ el warehouse compartido —el que de verdad sirve— se configuró fuera de
este código.
⇒ ⛔ sustrato-07 §5·3 y plano-gobernado §4 afirman «vendamos credencial STS
(900 s)» y esa frase no tiene comando detrás. Puede ser cierta, falsa, o cierta por
otra vía. Hay que medirla contra Lakekeeper antes de construir nada encima,
porque el paso 5 entero cuelga de ella. [medir]
② La palanca del paso 5 ya existe, y es un flag
Lakekeeper soporta remote signing para S3 con role assumption — y el estándar lo describe como «control de acceso por fichero más fuerte, a cambio de latencia», frente al vending, que entrega la llave y se aparta. Con firma remota el catálogo sigue en el camino de lectura de cada fichero.
⇒ «Conmutar el vendado» (§7·5) puede no ser una obra: puede ser cambiar
remote-signing-enabled por tabla con política. Eso rebaja radicalmente el coste
del paso que ahora mismo bloquea toda la política de celda. [medir la latencia]
8·bis · Lo que queda ABIERTO de verdad tras contestar las seis
| Cabo | Qué lo cierra | |
|---|---|---|
| 🏁 | SÍ, medido listando el bucket (§8·1·e) | |
| 🏁 | /plan aplica política? | Autoriza por principal, NO aplica celda (§8·1·f). Nos da el sitio, no la política |
| ⭐ | La migración: re-registrar ~82-84 tablas | POST …/register — punteros, no datos. Medir el bucle |
| ⭐ | Sacar Trino del cluster | Es el presupuesto de meter el IRC server bajo CPUS_ALL_REGIONS=12 |
| ⛔ | ¿Vendamos STS de verdad? | Un GET al warehouse compartido de Lakekeeper. Bloquea el paso 5 |
| ⛔ | ¿Cuánto cuesta la firma remota? | Medir latencia de una query real con remote-signing-enabled |
| 🟡 | Contribuir /scan a Lakekeeper | Deja de ser plan B y pasa a ser el plan si R1 sale rojo |
| 🟡 | El patrocinador humano de un agente | Primer caso de uso de OpenFGA, y la métrica que la industria falla (28 %) |
| 🟡 | WORKSPACE_MANAGE_ACCESS vs _CONTENT en el mismo nivel | Un test de invariante, no un refactor |
8·ter · ⭐⭐⭐ La regla que este episodio deja
UN ARGUMENTO CONSTRUIDO SOBRE CAPACIDADES QUE NO HEMOS ENCENDIDO NO COMPARA DOS
SISTEMAS: COMPARA DOS FOLLETOS.La primera versión de §8·1 defendió quedarse con Lakekeeper listando como pérdidas
OpenFGA (inerte), Cedar (no existe en el OSS) y el OPA bridge (es de Trino, que
sale). Las tres estaban en su README y ninguna en nuestrodocker-compose.El antídoto es el mismo del §2·4, aplicado a lo que YA tenemos y no sólo a lo que
vamos a adoptar: antes de contar una capacidad como propia, buscar el comando que
demuestra que está encendida.
8·quater · 🏁 G·1 — EL IRC, ABSORBIDO (2026-08-12)
infra/gke/g1-gravitino-irc.yaml · gate scripts/warehouse/g1-irc-gate.ts
namespace catalog · gravitino-irc 1/1 Running · ClusterIP, SIN ruta externa
nodo: el MISMO que Spark (no hizo falta un segundo nodo)
✅ metadata.json escrito en R2 DESDE DENTRO DEL CLUSTER, verificado listando el bucket
El estado del cluster que lo hizo posible, medido antes: el namespace trino
está vacío — Trino ya salió, y con él bajó de 3 nodos a 1 × n2-standard-4.
El nodo estaba al 82 % de CPU y 79 % de memoria, y el IRC cupo con requests
deliberadamente modestas (250m/1Gi): si no hubiera cabido, quería que lo dijera el
planificador y no que lo pagara Spark con un desalojo.
⚠️ Nace inerte, y «inerte» aquí significa tres cosas concretas: nadie le apunta ·
ClusterIP sin Gateway ni LB · y el backend es SQLite sobre emptyDir, efímero a
propósito — un catálogo que se pierde al reiniciar no puede confundirse con
producción por accidente.
⛔⛔ La decisión que G·1 NO toma, y que es la más grande
El almacén de registro. Un catálogo Iceberg es, sobre todo, el sitio donde se serializa el commit (el CAS sobre el puntero de metadata). Dos medidas lo dejan abierto:
- ⛔ El Postgres de control está en
us-east-1(Supabase) y el cómputo eneurope-west1. El catálogo está en el camino de toda lectura ⇒ apuntarlo ahí es un salto transatlántico por query. (Y ya había una sospecha registrada de que ese cruce mató workers de Trino.) - ⛔ La imagen no trae driver de PostgreSQL — sólo
sqlite-jdbc. Meterlo exige initContainer o imagen propia.
8·quater·bis · ⭐⭐⭐ DÓNDE VIVE LA BASE DEL CATÁLOGO — las opciones, medidas
Los números, tomados desde el cómputo (GKE europe-west1)
| Destino | Conexión TCP | Total |
|---|---|---|
| Lakekeeper (Railway) — el catálogo de HOY | 46 ms | 222 ms |
Supabase us-east-1 — donde vive Index | 119 ms | — |
| Cloudflare R2 (WEUR) — los bytes | 30 ms | 66 ms |
| ⭐ Gravitino IRC, mismo nodo | 1,3 ms | 5,2 ms |
⭐⭐ El catálogo en el cluster es ~40× más rápido que el de hoy. Y no es una
optimización: una query de N tablas hace ≥ N cargas de tabla, así que la
latencia del catálogo se multiplica por el ancho de la consulta. Con 5 tablas,
222 ms pasan a ser más de un segundo antes de leer un solo byte.
⛔ Y el número que descarta una opción entera: 119 ms a us-east-1. Una base de
catálogo ahí haría varios viajes por carga de tabla. Sería peor que hoy.
⛔ La corrección: QUÉ ES INDEX CATALOG (y qué medí mal)
El primer censo midió
pg_database_size()—541 MB— y lo llamó «Index». Es
falso. Eso es el proyecto de Supabase entero: producto (brain_embeddings
293 MB,chat_sessions105 MB), legacy (objects71 MB, del plano ontológico
que se rehace) y los esquemas del propio proveedor.⭐ Index Catalog no es una base de datos: es un conjunto concreto de tablas —
el plano de control del Warehouse. Medir el continente y llamarlo contenido es
la misma clase de error que comparar folletos: el número sale, y no mide lo que
dice medir.
Index Catalog, medido de verdad (g2b-index-catalog-real.ts):
| Pilar | Tablas | Filas |
|---|---|---|
| Taxonomía — qué existe | catalogs · schemas · datasets · tenant_warehouses | 10 · 16 · 123 · 8 |
| Retícula — quién puede | principal_roles · principal_role_members · catalog_roles · catalog_role_bindings · privilege_grants | 96 · 76 · 56 · 120 · 248 |
| Ledger — qué se escribió | dataset_transactions · iceberg_sync_log · dataset_schema_versions | 988 · 237 · 71 |
| Uso — quién tocó qué | access_events | 4.558 |
| Política — bajo qué regla | policies · policy_attachments | 0 · 0 |
INDEX CATALOG ...... 6,5 MB · 6.607 filas · 15 tablas
la BASE que lo aloja 541 MB
⇒ Index Catalog es el 1,21 % de la base donde vive.
⭐⭐⭐ Mover Index Catalog NO es mover Supabase. Son cosas de escala distinta, y
confundirlas hacía parecer una migración de plataforma lo que son 6,5 MB. Las
316 políticas RLS, las 308 funciones y los 6 esquemas del proveedor son de lo
otro, no de esto.
⇒ Y eso pone sobre la mesa la opción que usa el estándar: en UC y en Snowflake el metastore es su propia base, no la base de la aplicación.
Las cinco opciones, sin maquillar
| Opción | Latencia | Coste | Cierra §4·4 | Veredicto | |
|---|---|---|---|---|---|
| A | Cloud SQL PostgreSQL en europe-west1 — sólo para el catálogo | ~1-3 ms | ~25-50 €/mes · backups y PITR incluidos · no consume CPUS_ALL_REGIONS | ⛔ No (Index sigue lejos) | ⭐ El paso inmediato |
| B | PostgreSQL dentro del cluster (StatefulSet + PVC) | ~0,5 ms | ⛔ Consume del nodo (al 82 %) ⇒ 2º nodo ≈ +70 €/mes, y del techo de 12 CPU | ⛔ No | ⛔ No. Seríamos dueños de la HA, los backups y las actualizaciones de la base donde vive la atomicidad del lakehouse — y un PVC ya nos dejó un worker muerto para siempre |
| C | Seguir en Supabase us-east-1 | ⛔ 119 ms × N | 0 | ⛔ No | ⛔ Descartada por medición |
| D | Mover el proyecto de Supabase ENTERO | ~1-3 ms | Alto | ✅ Sí | ⛔ No, y ya no por el motivo que dije: arrastraría 534 MB que no son Index Catalog (producto y legacy), más 316 RLS, 308 funciones y 6 esquemas del proveedor. Es mover otra cosa |
| ⭐⭐ E | INDEX CATALOG y el catálogo, juntos en su propia base en europe-west1 | ~1-3 ms | Como A | ✅ SÍ | ⭐⭐ El destino, y son 6,5 MB. Es lo que hacen UC y Snowflake: el metastore es su propia base, no la de la aplicación |
⇒ La recomendación — y con el número correcto, cambia
PASO 1 · A — una base en europe-west1 para el catálogo de Gravitino
ganas los 40× de latencia, sin tocar nada más, reversible
PASO 2 · E — Index Catalog (6,5 MB · 15 tablas) se muda a ESA MISMA base
⇒ commit y asiento en UNA transacción ⇒ §4·4 cerrada
A es E sin su segunda mitad, así que el paso 1 no es trabajo tirado: es la misma base, ocupada primero por el catálogo y después por la gobernanza.
⭐ Y con 6,5 MB medidos, los dos pasos pueden ser uno solo. Lo que separaba a A
de E no era el tamaño —eso era el error de medición—: es el acoplamiento. Lo
único que hay que medir antes de fusionarlos es cuántas consultas cruzan hoy las 15
tablas de Index Catalog con las tablas de producto (hay una deuda conocida de
«desacoplar dataset de project_files»). Ése es el precio real, y son consultas, no
bytes. [medir]
8·quinquies · ⭐⭐⭐ LAS TRES PREGUNTAS DEL OWNER, Y RESULTAN SER UNA
① «¿IRC e Index son una sola unidad?» → Sí en el PLANO, no en el ESQUEMA
La intuición es correcta y conviene afilar en qué sentido:
| ¿Una unidad? | Por qué | |
|---|---|---|
| Como plano lógico | ⭐ SÍ | Los dos responden «qué existe y quién puede tocarlo». Partirlo es tener dos verdades sobre el mismo objeto |
| Como esquema de base de datos | ⛔ NO | Las tablas del catálogo las versiona su migración. Fundirlas ata nuestras migraciones a las suyas — y es justo el acoplamiento que el §2·③ evita |
| ⭐⭐ Como frontera de FALLO y de LATENCIA | SÍ, Y ES LO QUE HOY NO SE CUMPLE | Ver abajo |
⭐⭐ Y ahí está el hallazgo: hoy Index vive en us-east-1 y el cómputo en
europe-west1. La «unidad» ya está partida, y por un océano. La pregunta
correcta no es si fusionarlos, es dónde vive el par — porque de esa distancia
depende que la comprobación «¿esta tabla tiene política?» sea un JOIN local o una
llamada de red en el camino de toda lectura (§7·q·3·④).
⇒ La unidad es la BASE DE DATOS y la REGIÓN, no el esquema. El catálogo y el Index en el mismo Postgres, en la misma región que el cómputo, en esquemas separados.
② «¿Es Index el que se proyecta a la retícula de Gravitino?» → DEPENDE DE UNA SOLA DECISIÓN
Y no son dos preguntas, son dos ramas de la misma:
RAMA A · LA CARA SIGUE DELANTE (lo de hoy, con Lakekeeper detrás)
motor ─▶ /api/iceberg (autoriza) ─▶ Gravitino IRC ─▶ R2
· Index es la ÚNICA retícula · NO se proyecta NADA · la autorización de
Gravitino se queda APAGADA, igual que la de Lakekeeper hoy
· ⛔ y el Spark Connector NO se puede usar (③)
RAMA B · GRAVITINO APLICA
motor ─▶ Gravitino (autoriza con SU retícula) ─▶ R2
· hay que PROYECTAR Index ─▶ retícula de Gravitino (como él proyecta a Ranger)
· ⭐ se gana que `/plan` autorice de verdad, y el Spark Connector
· ⛔ se gana también un segundo sitio con estado de autorización
Recomendación: entrar por A y diseñar para B. A es un cambio de pieza detrás de
una puerta que no se mueve —el riesgo está acotado y la migración es contenida—.
B es el destino, porque es lo que hace que/planvalga para algo… y por eso el
día que se proyecte, la proyección tiene que ser DERIVADA de Index, no una segunda
declaración. Exactamente el movimiento de F·2·0, con otro destino.
③ El Gravitino Spark Connector — y por qué hoy sería un RETROCESO
Medido en su documentación:
spark.plugins = org.apache.gravitino.spark.connector.plugin.GravitinoSparkPlugin
spark.sql.gravitino.uri = http://<servidor Gravitino>:8090 ← el servidor COMPLETO
spark.sql.gravitino.metalake = <metalake> ← concepto del servidor
spark.sql.gravitino.enableIcebergSupport = true
⇒ ⛔ NO funciona con el IRC standalone (puerto 9001): exige el servidor Gravitino completo y su API de metalake. G·1 no lo habilita, y eso está bien.
Y lo que de verdad importa, que es lo que el owner intuía: sí cambia el modelo de lectura y escritura, y hoy en la dirección equivocada.
| Hoy (catálogo REST detrás de la cara) | Con el Spark Connector | |
|---|---|---|
| Quién configura el catálogo en Spark | Nosotros, apuntando a la cara | Gravitino, que le entrega hasta las propiedades de storage |
| Por dónde pasa la lectura | ⭐ Por la cara, que autoriza | Directo a Gravitino |
| Quién aplica | Index | La retícula de Gravitino |
⛔⛔ El connector PUENTEA la cara. Y la cara es donde vive toda nuestra
gobernanza — su propio módulo lo dice: «no cierra la red; si los motores pueden
apuntar al catálogo directamente, esto no aplica nada».⇒ El Spark Connector no es una mejora que se activa: es la RAMA B entera. Sólo
es adoptable cuando la retícula esté proyectada dentro de Gravitino. Antes de eso,
encenderlo sería quitar el punto de aplicación a cambio de comodidad.
⭐ Y lo que sí gana cuando llegue: federación real en Spark —Hive, JDBC, Paimon,
Glue y Iceberg bajo un USE catalog—, que es el diagrama de ETL del owner y el
sustrato de Carbon Flows. Sus operaciones cubren INSERT/OVERWRITE, MERGE INTO,
DELETE, UPDATE, CALL y time travel. ⚠️ No cubre vistas, tablas de
metadata Iceberg, ni CREATE OR REPLACE atómico — tres cosas que hoy sí usamos.
8·sexies · ⭐⭐ QUÉ HACE LAKEKEEPER HOY — el inventario de lo que se retira
| Papel | ¿Lo usamos? | Al migrar |
|---|---|---|
| ⭐⭐⭐ El catálogo de registro (nombre → puntero de metadata) | SÍ, es el núcleo | Lo asume Gravitino |
| ⭐⭐⭐ El CAS del commit — donde vive la atomicidad | SÍ. Invariante nº4: «Index nunca serializa escrituras; el CAS es suyo» | Lo asume el backend JDBC de Gravitino |
| Los endpoints de la spec REST | SÍ | Idénticos — es estándar |
| Warehouses (9, uno por inquilino) | SÍ | ⚠️ Concepto propio: en Gravitino son catálogos del metalake. Hay que traducir |
| OIDC con Keycloak | SÍ | Hay que rehacerlo |
POST /transactions/commit | Declarado, sin estrenar | Se pierde/gana según lo declare Gravitino [medir] |
| Protección de borrado | Sí, pasiva | Comprobar equivalente |
| OpenFGA / OPA / Cedar | ⛔ NO — inerte, de Trino, e inexistente en el OSS | No se retira nada: no estaba encendido |
/management/v1/warehouse | Sólo la ruta de warehouses dedicados, que no usa nadie | Único acoplamiento de API |
⭐ Y migrar no mueve datos: las tablas son punteros a metadata en R2 y la spec
tiene POST …/namespaces/{ns}/register. Es un bucle de re-registro.
8·septies · ⭐⭐⭐ EL PLANO DE ESCRITURA, VISTO DESDE ARRIBA
escrituras-sustrato-debilidades.md se escribió antes de saber que íbamos a
absorber Gravitino. Releído desde aquí, seis de sus siete debilidades no cambian —
y la séptima se puede cerrar de raíz.
| Debilidad | ¿La toca Gravitino? | |
|---|---|---|
| 4·1 | Una escritura toca una tabla | ⛔ No. El commit multi-tabla es de la spec, no del producto |
| 4·2 | Sin reintento ante conflicto | ⛔ No. Iceberg es optimista: reintentar es del cliente |
| 4·3 | Sin nivel de aislamiento declarado | ⛔ No |
| ⭐⭐ 4·4 | Fallo parcial con reconciliación MANUAL (ControlPlaneUnrecordedError) | 🏁 SÍ — Y ES LA GRANDE |
| 4·5 | Una sentencia por ejecución | ⛔ No. Es nuestro ejecutor |
| 4·6 | El verbo, por la primera palabra del texto | ⛔ No. Es nuestro parser |
| 4·7 | Guardianes con condición caducada | ⛔ No. Es disciplina nuestra |
⭐⭐⭐ Por qué 4·4 es exactamente la intuición de «una sola unidad»
El agujero de 4·4 existe porque el commit y su asiento viven en dos sistemas: el catálogo commitea en su base, e Index anota en la suya. Entre las dos hay una ventana, y cuando falla ahí, alguien reconcilia a mano.
HOY catálogo (su PG) ──✂── Index (nuestro PG) ⇒ ventana ⇒ reconciliación manual
DESTINO catálogo (esquema A) ── Index (esquema B) en el MISMO Postgres
⇒ el commit y su asiento pueden caber en UNA transacción
⭐⭐ «Una sola unidad» no era una preferencia de despliegue: era la única forma de
cerrar 4·4 sin inventar un protocolo de dos fases.
⚠️ Con dos condiciones que hay que decir enteras: ① el backend del catálogo tiene que ser nuestro Postgres (no SQLite ni una base ajena) y ② en la misma región que el cómputo — hoy no se cumple ninguna de las dos. Y ③ escribir en las tablas del catálogo dentro de nuestra transacción es acoplarse a su esquema interno, así que el asiento se hace alrededor, no dentro: misma conexión, misma transacción, tablas distintas.
⇒ El orden se reordena solo: el paso siguiente no es «pasar tablas a Gravitino», es decidir dónde vive su base. De esa decisión cuelgan la latencia de toda lectura y la única debilidad de escritura que la absorción puede cerrar.
9 · Fuentes
- Unity Catalog / ABAC (GA mayo 2026): conceptos núcleo de ABAC · ABAC en UC · anuncio de GA · cuándo ABAC vs filtros de tabla · escalar gobernanza con ABAC
- Iceberg · scan planning en servidor: scan planning en el REST catalog · Iceberg 1.11.0, novedades · estado de los catálogos Iceberg, junio 2026 · soporte de cliente en iceberg-go
- Apache Gravitino: resumen 2025 y hoja de ruta · repo
- Apache Polaris: el estado en julio 2026 — «la capa de gobernanza del lakehouse abierto» · policy store · Polaris 1.3.0
- ODCS / Bitol: ODCS v3.1.0 · anuncio de v3.1.0
- AWS Lake Formation: control de acceso por etiquetas · grano fino a escala
- El panorama: Snowflake Horizon vs Open Catalog · UC vs Snowflake Horizon
- Nuestro:
plano-gobernado-approach.md·sustrato-07.md·lib/governance/privilege.ts·lib/governance/catalog-authz.ts·lib/governance/membership-grants.ts