Published

Runbook · P0 — OpenMetadata en una VM con Docker Compose

Connect any source, model it as an ontology, transform it, and operationalize it, analytics, automation and machine learning, under one governed, self-hostable roof. --- Most teams stitch the...

Runbook · P0 — OpenMetadata en una VM con Docker Compose

Fase P0 de semantic-context-graph.md,
bajo el norte agentic-data-cloud.md.
Qué desbloquea: que OpenMetadata sea la fuente de verdad de la metadata del
ecosistema (decisión N2), construida desde la ingesta.

Decisión de despliegue (tomada): una VM propia con docker compose, no cuatro
servicios en Railway. Railway sigue alojando los servicios de Carbon (ml-runner,
karma-server, duck-server, Lakekeeper). Motivos en §0.

⚠️ Convención de este runbook: lo marcado [confirmar] son detalles que cambian
entre versiones de OM y se verifican contra el fichero descargado o la doc oficial en el
momento de ejecutar
. Lo demás es alcance y decisión, que es lo que no cambia. Mismo
criterio que lakekeeper-standup.md.


0 · Por qué una VM y no Railway

Tres razones concretas, no de gusto:

  1. El parámetro del kernel. El buscador (OpenSearch/Elasticsearch) necesita vm.max_map_count=262144 — cuántas regiones de memoria puede mapear un proceso; Lucene mapea muchísimos ficheros. Es un ajuste del kernel del host, y en un PaaS normalmente no se puede tocar. En una VM es una línea. (El vm. ahí significa «virtual memory», nada que ver con máquinas virtuales.)
  2. La forma de la memoria. Son cuatro servicios con estado y suelos de memoria propios. En Railway pagas cuatro huellas reservadas; en una VM comparten la RAM de la máquina — el buscador aprovecha la caché que la BD no usa en ese instante. Para una carga a ráfagas como la ingesta, compartir es mucho más eficiente.
  3. El compose ya existe. OM publica el suyo. En una VM se usa casi tal cual; en Railway habría que despiezarlo en cuatro servicios y recablearlo a mano.

0.1 · Airflow entra en el stack, y su UI no se expone

Se evaluó ejecutar los conectores en los workers de Carbon (el paquete openmetadata-ingestion sobre ml-runner) para ahorrarse el orquestador. Decisión: no. El flujo se monta sobre OpenMetadata — conectores a 130+ servicios, modelo de entidades dirigido por esquema, linaje y gestión de ingestas ya resueltos. Reescribir eso no es producto.

Lo que esa decisión no cambia: el usuario nunca ve Airflow. Las ingestas se crean desde el asistente de nueva fuente que Carbon ya tiene (Data Gateway), que llama a la API de OM. Airflow es el ejecutor, no una superficie: su UI se queda dentro de la red de Docker y no se publica.

Salida de emergencia: si el buscador da guerra operativa, la ruta siguiente es alquilarlo gestionado (Bonsai / Elastic Cloud / AWS OpenSearch) y dejar en la VM sólo servidor + BD + ingesta. No hay que rehacer nada: son variables de entorno.


1 · La máquina

RecomendadoPor qué
EjemploHetzner CCX23 · GCP e2-standard-4en Hetzner US la línea dedicada sale más barata que la compartida para la misma RAM — comprobado en consola
vCPU / RAM4 dedicados / 16 GiBlos cuatro servicios en marcha; ver §2
Disco160 GB SSDíndice + BD + imágenes Docker + logs
SOUbuntu 24.04 LTSlos repos de Docker y la doc de OM están probados contra ella

Los cuatro servicios, incluido el orquestador. Airflow entra en el stack por decisión de producto (§0.1): el flujo se monta sobre OpenMetadata. Eso fija el tamaño — sin orquestador cabría en 8 GB, con él no.

vCPU dedicado, no compartido. Sin steal time, las pausas del recolector de basura de las dos JVM (buscador y servidor) son predecibles. En vCPU compartido, un vecino ruidoso se traduce en timeouts de ingesta que parecen bugs de OM y no lo son.

Redimensionar no es una puerta de un solo sentido: Hetzner Rescale (coger la opción de CPU y RAM sin tocar el disco — ampliar disco no se deshace) o, en GCP, parar → cambiar tipo → arrancar.

Cualquier proveedor con VM sirve (Hetzner, DigitalOcean, EC2, Compute Engine). No un plan «compartido» o de tipo contenedor: hace falta el kernel.

Por qué 16 GiB y no los ~40 de los mínimos publicados

Los números oficiales son de producción, para el patrimonio de metadata de una empresa entera y con el buscador en clúster replicado (1 master + 2 workers). Ninguna de las dos cosas aplica el día uno. Lo que recortamos, y lo que cuesta:

RecorteAhorroLo que se paga
Buscador de un solo nodo~2/3 de su memoriasin réplica: si se corrompe el índice, se reindexa desde la BD — se pierde el descubrimiento mientras dura, no la metadata
Perfilado apagado en los conectoresmucho de la ingestasin estadísticas de columna (nulos, distintos, mín/máx). No afecta a esquema ni linaje
Concurrencia de ingesta 1–2memoria a ráfagaslos conectores van en fila, tardan más

Estos ~10–12 GiB son mi estimación, no un dato publicado. Se mide en §7, y si se queda corta se sube la VM — que es precisamente lo fácil en esta ruta.


2 · Preparar el sistema

# 1. Docker Engine + plugin compose (script oficial)
curl -fsSL https://get.docker.com | sh

# 2. Usuario no-root operando docker (cerrar sesión y volver a entrar después)
sudo usermod -aG docker "$USER"

# 3. El parámetro del kernel que necesita el buscador — PERSISTENTE
echo 'vm.max_map_count=262144' | sudo tee /etc/sysctl.d/99-opensearch.conf
sudo sysctl --system

# 4. Verificar que quedó puesto
sysctl vm.max_map_count   # debe decir 262144

El paso 3 en /etc/sysctl.d/ y no sysctl -w a secas: un -w se pierde al reiniciar y el buscador no arrancaría después del primer reboot, con un error que no apunta a la causa.

Cortafuegos

Ningún puerto de OM abierto al mundo. Sólo 22 (SSH), 80 y 443 (el proxy inverso de §4). Los 8585/8586 del servidor, la BD y el buscador quedan dentro de la red de Docker.


3 · El compose

Los ficheros ya están escritos en infra/openmetadata/:
docker-compose.override.yml, env.example y Caddyfile, con su README de uso. Se
copian a la VM tal cual. Lo que sigue explica por qué son así — que es lo que hay que
entender antes de tocarlos.

mkdir -p ~/openmetadata && cd ~/openmetadata

# Fijar la versión. 1.13.3 es la estable al escribir esto — [confirmar] la vigente en
# https://github.com/open-metadata/OpenMetadata/releases antes de bajar nada.
export OM_VERSION=1.13.3

# El compose oficial. [confirmar] el nombre exacto del fichero en la release: hay variante
# MySQL y variante PostgreSQL, y el nombre ha cambiado entre versiones.
wget "https://github.com/open-metadata/OpenMetadata/releases/download/${OM_VERSION}-release/docker-compose-postgres.yml"

Elegimos la variante PostgreSQL (OM soporta PG ≥ 15 y MySQL ≥ 8.0.42). No por rendimiento —dan igual a esta escala— sino porque el equipo ya opera Postgres en todas partes: cuando algo falle a las tres de la mañana, la diferencia la marca saber leer el log.

⚠️ Esta BD es exclusiva de OpenMetadata. No es el Index, ni el PG de Lakekeeper. Son
tres metastores distintos y mezclarlos es cómo se pierde la capacidad de restaurar uno
sin arrastrar a los otros.

Los cuatro cambios del arranque reducido

Sobre el compose descargado, cuatro ediciones. Los nombres exactos de servicio y de algunas variables [confirmar] contra el fichero:

a) Buscador de un nodo. En el servicio del buscador: discovery.type=single-node, y bajar réplicas a 0. Sin esto intentará formar clúster, no encontrará pares y se quedará en amarillo/rojo esperando quórum.

b) Heaps acotados. Una JVM crece hasta donde le dejes; si no las acotas, buscador y servidor se pelean por la RAM de la máquina y el que pierde muere por OOM.

# buscador
OPENSEARCH_JAVA_OPTS: "-Xms2g -Xmx2g"     # [confirmar] la var de la imagen que use el compose
# servidor OM
OPENMETADATA_HEAP_OPTS: "-Xms2g -Xmx2g"   # [confirmar]

Regla del buscador: heap = la mitad de lo que le asignes; la otra mitad debe quedar libre para la page cache del sistema, que es donde Lucene lee los segmentos.

c) Techos de memoria por servicio, para que un servicio desbocado no se lleve la máquina:

deploy:
  resources:
    limits:
      memory: 4g     # buscador 4g · servidor 4g · BD 2g · ingesta 3g

d) Nada de puertos publicados al host salvo el del servidor, y ése sólo en localhost — el proxy de §4 es quien lo expone:

ports: !override
  - "127.0.0.1:8585:8585"

⚠️ El !override no es decorativo. Compose apenda las listas de ports al fusionar ficheros en vez de sustituirlas: sin el tag te quedas con el mapeo público del fichero base además del tuyo, y OM queda escuchando en 0.0.0.0 sin que nada avise. Comprobación obligatoria tras el primer up:

sudo ss -tlnp | grep -E '8585|9200|5432'   # sólo debe salir 127.0.0.1:8585

Si tu versión de Compose no soporta !override, edita los ports en el fichero base y deja constancia de esa edición — es la única que sobrevive a un cambio de versión sin avisar.

Secretos

Cambiar todas las contraseñas por defecto (BD, buscador, Airflow) y la fernet/clave de cifrado que OM usa para guardar credenciales de conectores. Van en un .env junto al compose, con chmod 600, fuera de git. Esas credenciales dan acceso a todas las fuentes del ecosistema: es el fichero más sensible de la máquina.


4 · Exponer a Carbon (proxy inverso + TLS)

Carbon corre en Vercel: son funciones sin IP de salida fija, así que no se puede cerrar por lista de IPs. La consecuencia es directa: OM necesita un endpoint HTTPS público, y el control de acceso real es su autenticación, no la red.

Caddy delante, que resuelve TLS solo:

om.nodeworkspace.com {
    reverse_proxy 127.0.0.1:8585
}

Requisitos: un subdominio apuntando a la IP de la VM, y 80/443 abiertos (80 lo necesita Let's Encrypt para emitir).

No saltarse esto. OM sobre HTTP plano expone en claro el token de bot y las
credenciales de cada conector que se configure desde la UI.


3.1 · Lo que el compose de fábrica trae mal (medido, 1.13.3)

Está pensado para un quickstart en localhost. Colgado de un dominio público, cinco defectos. Todos corregidos en el override — se listan para que nadie los reintroduzca al subir de versión:

#Defecto de fábricaCorrección
1POSTGRES_PASSWORD: password literal (no variable) y 5432 en 0.0.0.0contraseña generada en la VM + puerto cerrado
2RSA_PRIVATE_KEY_FILE_PATH: ./conf/private_key.derla clave de firma de JWT viene dentro de la imagen pública: con ella cualquiera forja un token de adminpar RSA propio en ./certs, montado :ro, + JWT_KEY_ID nuevo
3AUTHENTICATION_ENABLE_SELF_SIGNUP: true con registro abierto a todos los dominiosfalse + dominio acotado
4Elasticsearch con xpack.security.enabled=false y 9200/9300 publicadospuertos cerrados; sólo red interna de Docker
5Airflow admin/admin con 8080 publicadocredenciales generadas + puerto cerrado

Se deja a propósito sin tocar: AUTHORIZER_ENFORCE_PRINCIPAL_DOMAIN=false. Ponerlo a true exigiría que todo usuario tuviera correo del dominio — incluido el admin inicial, que es admin@open-metadata.org. Te dejaría fuera en el primer login. Es endurecimiento posterior a crear tus propios usuarios.

No hay FERNET_KEY en 1.13.x. El cifrado de credenciales de conectores va por SECRET_MANAGER: dbla propia base de datos es el secreto. El pg_dump del §8 deja de ser higiene y pasa a ser lo único que hay.

3.2 · Tres trampas que cuestan una tarde

Encontradas ejecutando esto de verdad. Las tres fallan sin decir por qué:

  1. La contraseña del usuario de la BD no se puede cambiar sólo por env. La imagen openmetadata/postgresql crea openmetadata_user con una contraseña horneada en su script de inicialización. Cambiar DB_USER_PASSWORD en el .env cambia lo que el servidor usa para conectar, no lo que la base tiene → password authentication failed. Hay que alinear la base después del primer arranque:
    docker exec -e PGPASSWORD="$PGPW" openmetadata_postgresql \
      psql -U postgres -d openmetadata_db -c "ALTER USER openmetadata_user WITH PASSWORD '$DBPW';"
    
  2. Los certs los tiene que poder leer el UID del contenedor, y el DIRECTORIO también. El servidor corre como UID 1000; en GCP el usuario de la VM es 1001. Y no basta con chown de los ficheros: si certs/ quedó en 700 (un umask 077 al crearlo), el contenedor no puede entrar aunque los ficheros ya sean suyos. Síntoma: AccessDeniedException y el servidor en bucle de reinicio.
    sudo chown 1000:1000 certs certs/*.der && sudo chmod 750 certs
    # y verifícalo DE VERDAD, con el UID real:
    docker run --rm -v ~/openmetadata/certs:/etc/openmetadata/certs:ro \
      --entrypoint sh <imagen-server> -c 'head -c8 /etc/openmetadata/certs/private_key.der >/dev/null && echo OK'
    
  3. execute-migrate-all es un quinto servicio que no aparece en la documentación de arranque. Es un job de un solo uso que corre las migraciones y siembra los bots con sus tokens ⇒ necesita las mismas claves RSA que el servidor. Si no se las montas, los bots quedan firmados con las claves de fábrica y el endurecimiento nº2 es papel mojado.

5 · Arranque y gates

docker compose --env-file ./.env -f docker-compose-postgres.yml up --detach
docker compose -f docker-compose-postgres.yml ps
docker compose -f docker-compose-postgres.yml logs -f openmetadata-server   # [confirmar] nombre

El primer arranque tarda: el servidor corre sus migraciones de esquema antes de escuchar.

Gates de §5:

  1. docker ps — los cuatro contenedores arriba y estables pasados 5 minutos (no basta con que arranquen: el buscador puede morir por OOM justo después).
  2. https://om.nodeworkspace.com carga la UI.
  3. Login con el admin inicial y cambio inmediato de su contraseña. [confirmar] las credenciales por defecto en la doc de la versión.

6 · Token de lectura para Carbon

Por la decisión N2-bis, Carbon escribe en OM configuración (servicios, pipelines de ingesta) pero nunca metadata (glosario, descripciones, clasificaciones). Esa frontera tiene que sostenerse por permisos, no por buena voluntad del código:

Procedimiento verificado (1.13.3). Cuatro llamadas, en este orden — cada una depende de la anterior:

A=http://127.0.0.1:8585/api/v1
H=(-H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json")

# 1) Política — la frontera N2-bis expresada en reglas. En OM el DENY gana al ALLOW.
#    resources válidos: consúltalos en GET /policies/resources
curl -X POST "${H[@]}" "$A/policies" -d '{
  "name":"CarbonBotPolicy",
  "description":"Lee toda la metadata; escribe solo configuracion.",
  "rules":[
    {"name":"carbon-read-all","resources":["all"],"operations":["ViewAll"],"effect":"allow"},
    {"name":"carbon-write-config",
     "resources":["databaseService","dashboardService","messagingService","pipelineService",
                  "mlmodelService","storageService","searchService","apiService",
                  "metadataService","driveService","ingestionPipeline"],
     "operations":["Create","EditAll","Delete","Trigger"],"effect":"allow"},
    {"name":"carbon-deny-metadata-authoring",
     "resources":["glossary","glossaryTerm","tag","classification"],
     "operations":["Create","EditAll","Delete"],"effect":"deny"}]}'

# 2) Rol
curl -X POST "${H[@]}" "$A/roles" \
  -d '{"name":"CarbonBotRole","description":"...","policies":["CarbonBotPolicy"]}'

# 3) USUARIO bot — ⚠️ PRIMERO el usuario. `POST /bots` con un botUser inexistente
#    devuelve 404 "user instance for X not found".  `roles` va por UUID, no por nombre.
curl -X PUT "${H[@]}" "$A/users" -d '{
  "name":"carbon","email":"carbon@nodeworkspace.com","isBot":true,
  "authenticationMechanism":{"authType":"JWT","config":{"JWTTokenExpiry":"Unlimited"}},
  "roles":["<UUID del rol>"]}'

# 4) Entidad bot
curl -X POST "${H[@]}" "$A/bots" -d '{"name":"carbon","botUser":"carbon","description":"..."}'

El JWT del bot se recupera de GET /users/token/{userId} — no de GET /bots/name/{name}, que devuelve 200 pero sin el token. Es el gotcha que cuesta el rato.

⚠️ Trigger es una operación aparte, y no la cubre EditAll. Se descubrió el
2026-08-02 con la primera ingesta real: POST …/ingestionPipelines201,
POST …/deploy/{id}200, POST …/trigger/{id}403
Principal: CatalogPrincipal{name='carbon'} operations [Trigger] not allowed.
Crear el pipeline y desplegarlo sin poder ejecutarlo deja el producto a medias: Carbon
monta la ingesta desde su asistente y luego no puede lanzarla.

Las operaciones válidas de un recurso se consultan —no se adivinan— en
GET /policies/resources. Para ingestionPipeline son: Create, Delete, ViewAll,
ViewBasic, EditAll, EditDescription, EditDisplayName, EditOwners,
EditGlossaryTerms, EditTier, EditIngestionPipelineStatus, Trigger,
CreateIngestionPipelineAutomator.

El bot no puede arreglarse solo: sobre policy sólo tiene ViewAll, así que esto se
corrige con el ADMIN_TOKEN (o desde la UI). En una instancia ya montada:

curl -X PATCH "$A/policies/name/CarbonBotPolicy" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H 'Content-Type: application/json-patch+json' \
  -d '[{"op":"add","path":"/rules/2/operations/-","value":"Trigger"}]'

⚠️ El índice de /rules/N no es el del POST original: OM reordena las reglas. Míralas
antes con GET /policies/name/CarbonBotPolicy?fields=rules y usa el índice de
carbon-write-config. Añádelo también a la verificación: un cuarto curl que dispare
una ingesta y espere 200, junto a los tres de 200/403/201.

El ADMIN_TOKEN se genera en la UI (avatar → Profile → Personal Access Token) y se
revoca al terminar
: dura horas y tiene permisos totales. El token del bot, en cambio,
es permanente y acotado — ése es el que va a Carbon.

Verificación — las tres llamadas, y las tres importan:

OM=https://om.nodeworkspace.com

# 1) Lee metadata → 200
curl -s -o /dev/null -w 'lectura      %{http_code}\n' \
  -H "Authorization: Bearer $OM_TOKEN" "$OM/api/v1/glossaries"

# 2) Escribe metadata → 403.  Si devuelve 201, N2-bis es papel mojado.
curl -s -o /dev/null -w 'glosario     %{http_code}\n' -X POST \
  -H "Authorization: Bearer $OM_TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"canary","description":"debe fallar"}' "$OM/api/v1/glossaries"

# 3) Escribe configuración → 201.  Si devuelve 403, el asistente de nueva
#    fuente de Carbon no podrá registrar nada y P1 se queda sin producto.
#    [confirmar] el cuerpo exacto que pide `services/databaseServices`.
curl -s -o /dev/null -w 'servicio     %{http_code}\n' -X POST \
  -H "Authorization: Bearer $OM_TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"canary-svc","serviceType":"CustomDatabase","connection":{"config":{"type":"CustomDatabase","sourcePythonClass":"x"}}}' \
  "$OM/api/v1/services/databaseServices"

Las dos últimas son las que definen la frontera. Un token que puede escribir glosario convierte la separación entre las dos fuentes de verdad en una convención — y las convenciones se rompen solas. Uno que no puede crear servicios rompe el producto.

Limpia el canario (DELETE …/services/databaseServices/name/canary-svc?hardDelete=true&recursive=true).

Verificado en om-carbon-01 el 2026-08-02: 200 / 403 / 201.

Wiring en Carbon

OPENMETADATA_URL     = https://om.nodeworkspace.com
OPENMETADATA_TOKEN   = <JWT del bot de lectura>
OPENMETADATA_ENABLED = 0        # a 1 cuando P1 esté verde

El gate a 0 es deliberado: mientras esté apagado, la pantalla del Semantic Context Graph debe degradar con un mensaje honesto («OpenMetadata no configurado»), no romper. Mismo patrón de seam fail-safe que KARMA_SQL_URL.


7 · El primer conector: el Warehouse

Uno solo para empezar, y que sea el que sostiene la hidratación de P3: el Warehouse (Iceberg vía Lakekeeper).

  1. Configurar el conector con perfilado APAGADO (§1). Esquema y linaje sí; estadísticas de columna no, todavía.
  2. Concurrencia de ingesta 1–2.
  3. Ejecutarlo y ver aparecer las tablas del Warehouse en OM con su esquema.

Gate de §7 — las dos mitades son obligatorias:

  • una tabla real del Warehouse es visible en OM con su esquema;
  • su FQN es idéntico entre dos ejecuciones seguidas de la ingesta.

La segunda no es opcional. Todo el anclaje de Pods (N4) es por FQN: si una reingesta lo mueve, cada atributo de cada Pod queda apuntando a la nada. Mejor descubrirlo aquí, con una tabla, que en P3 con un catálogo entero.

Medida real — 2026-08-02, VM om-carbon-01 (GCP e2-standard-4, us-west1-b)

En reposo, los cuatro servicios arriba y sanos, sin ningún conector aún:

ServicioUsoTecho%
elasticsearch2,64 GiB4 GiB66%
ingestion (Airflow)2,07 GiB3 GiB69% ← el más apretado
openmetadata-server1,12 GiB4 GiB28%
postgresql0,13 GiB2 GiB6%
Máquina6,7 GiB usados15 GiB8,9 GiB disponibles

Conclusión: la estimación de ~10–12 GiB de §1 era alta; el consumo real en reposo es ~6 GiB. Pero 8 GB seguiría siendo mala idea: dejaría ~1 GiB de holgura para el sistema y para los picos de ingesta, y el servicio más cercano a su techo es justamente Airflow, que es el que crece al ingerir. Los 16 GiB están bien elegidos.

Volver a medir con conectores corriendo — este número es el suelo, no el techo.


8 · Copias de seguridad

Aquí no hay red de seguridad de nadie. En Railway los backups del plugin de BD venían dados; en una VM, si no lo montas tú, no existe.

  • Lo crítico es la BD de OM. Ahí vive la metadata y, cifradas, las credenciales de cada conector. pg_dump diario a un bucket (R2 sirve), con retención.
  • El buscador NO se respalda: se reindexa desde la BD. Es exactamente el trade-off que aceptamos en §1 al dejarlo en un nodo.
  • Respaldar también el .env y el compose editado, en un gestor de secretos — no en el mismo sitio que el dump. Restaurar la BD sin la clave de cifrado deja las credenciales de los conectores ilegibles.

Probar la restauración una vez. Un backup no verificado es una suposición.


9 · Qué vigilar

SíntomaCausa habitualQué hacer
El buscador reinicia en buclevm.max_map_count sin poner, o heap > memoria del contenedor§2 paso 3; bajar -Xmx
La ingesta se queda colgada sin errormemoria; el worker murió por OOMbajar concurrencia, confirmar perfilado off
La UI va lenta y las búsquedas no encuentraníndice degradadoreindexar desde la BD
Un conector falla de golpe tras semanascredencial caducada en la fuenterotarla en OM
Disco llenologs de Docker sin rotarmax-size/max-file en el daemon

10 · Done-criteria de P0

  • VM con Docker, vm.max_map_count persistente, cortafuegos cerrado salvo 22/80/443.
  • Compose de la versión pinneada arriba: servidor + PG + buscador (un nodo) + ingesta, estables pasados 5 minutos, con heaps y techos de memoria acotados.
  • HTTPS por Caddy en un subdominio propio; nada de OM publicado en claro.
  • ss -tlnp confirma que sólo 127.0.0.1:8585 escucha (el !override se aplicó).
  • Contraseñas por defecto cambiadas; .env en 600 y fuera de git.
  • Bot de Carbon con las tres comprobaciones verdes: 200 leyendo, 403 creando glosario, 201 creando servicio. Canario borrado.
  • Conector del Warehouse ingiriendo, con FQN estable entre dos ejecuciones.
  • Consumo real medido y anotado en §7.
  • pg_dump programado y una restauración probada.
  • OPENMETADATA_URL / _TOKEN en Carbon con OPENMETADATA_ENABLED=0.

Cumplido esto, P1 (el read-through) es trabajo de código y ya no depende del operador.


11 · Rollback

Nada de P0 toca Carbon en producción: el gate OPENMETADATA_ENABLED=0 mantiene el código inerte, y Carbon nunca escribe en OM, así que no hay estado externo que compensar.

Deshacerlo es docker compose down -v y destruir la VM. El -v borra los volúmenes — metadata y credenciales de conectores incluidas. Ejecutarlo sólo con el dump de §8 en la mano.