Ir al contenido principal
Producto

Ramificar bases de datos como código: un patrón de CI/CD para Lakebase, en producción en Glaspoort

Cómo Glaspoort implementa cambios en la base de datos con el mismo rigor que el código de aplicación al ramificar cada entorno desde producción, crear bases de datos efímeras por PR y tratar las migraciones como la única fuente de verdad.

por Hadi Farhat, Gideon Spierings, Ricardo de Vries y Raymon Veldman

  • Cómo diseñó Glaspoort su configuración de ramificación de Lakebase, con el desarrollo y la aceptación ramificados directamente desde producción en lugar de apilarse uno encima del otro, para evitar la "trampa de restablecer desde el elemento principal" donde actualizar un entorno obsoleto te obliga a eliminar y reconstruir todo lo que está debajo.
  • El flujo de CI/CD por PR: cómo cada pull request obtiene su propia rama de Lakebase nueva y desechable copiada de producción, cómo las migraciones se vuelven a ejecutar y se prueban con una imagen de la aplicación en vivo antes de que algo toque un entorno real, y por qué las migraciones en sí (no las bases de datos) se consideran la fuente de verdad.
  • El equilibrio entre dos modelos de promoción: fusionar tan pronto como se apruebe la CI frente a fusionar solo después de que un PR se haya promovido por completo a través de aceptación. El artículo explica por qué Glaspoort eligió el enfoque de "velocidad primero" y las salvaguardas que agregaron (revalidación de pila y un pipeline de crisis) para mantenerlo seguro.

El problema que no podíamos ignorar

Glaspoort construye y opera infraestructura de fibra en los Países Bajos. Todo gira en torno a aumentar el número de conexiones de fibra y, durante mucho tiempo, el equipo de datos pasó sus días creando informes de BI para respaldar ese objetivo, mientras que la pregunta detrás de cada informe ya estaba desactualizada para cuando se terminaba el informe. El resultado fue una proliferación de informes únicos y usuarios que no tenían a dónde dirigir sus preguntas de seguimiento.

Así que rompimos la dependencia. En lugar de entregar el siguiente informe, creamos una aplicación front-end personalizada en la que los gerentes de proyecto ven directamente dónde se encuentran las oportunidades para sus proyectos. Lo nuevo está bajo el capó: usamos los productos de Databricks directamente como bloques de construcción en la aplicación. Genie, para chatear con los datos y generar análisis rápidos. AI/BI Dashboards, para obtener información y análisis de autoservicio. Flujos de trabajo automatizados con Agent Bricks, que alertan a los gerentes de proyecto en el momento en que algo destaca en sus proyectos, y Lakebase, la base de datos OLTP de Databricks, para los datos transaccionales de la aplicación.

Esa combinación une dos mundos que hasta hace poco vivían separados: el entorno analítico y un front-end operativo, donde los datos analíticos se encuentran con los datos transaccionales. El equipo de datos ahora dedica su tiempo a los espacios de Genie y los metadatos en lugar de a informes únicos. Pero nada de esto se mantiene rápido sin una base sólida debajo: CI/CD, pruebas de calidad de datos, infraestructura como código y gobernanza de datos. Una parte de esa base requirió el diseño más cuidadoso, y es la parte de la que trata el resto de esta historia: cómo enviamos los cambios a la base de datos detrás de la aplicación.

Detrás de esa aplicación se encuentra una base de datos Databricks Lakebase. Es Postgres OLTP serverless, que se ejecuta junto al lakehouse en lugar de estar acoplado a él. El flujo de datos es sencillo de describir y, como descubrimos, más interesante de operar de lo que parece:

  • Los datos seleccionados del lakehouse se sincronizan en una rama de producción de Lakebase, donde aterrizan en un esquema de aplicación de solo lectura.
  • La aplicación escribe su propio estado de vuelta en un esquema separado en esa misma rama, de modo que los datos leídos del lakehouse y los datos escritos por la aplicación coexisten sin colisionar.
  • Además de eso, ejecutamos tres entornos lógicos: desarrollo, aceptación y producción. Enviamos cambios a esta base de datos de la misma manera que enviamos cambios al código de la aplicación, a través de pull requests, CI y promoción controlada.

image3.png

En ese último punto es donde reside la pregunta interesante. En el momento en que decides que una base de datos OLTP merece el mismo rigor que el código de la aplicación, tienes que responder a una pregunta difícil:

¿Cómo permitimos que cada PR realice pruebas contra una base de datos que se parezca a la de producción, sin que las personas se interfieran entre sí y sin perder los datos frescos que hacen que la prueba sea significativa en primer lugar?

Los modos de fallo aquí son familiares para cualquiera que haya compartido una base de datos en un equipo. PR que pasan de forma aislada y se rompen cuando se juntan. Entornos de desarrollo y aceptación que se han desviado silenciosamente de cómo se ve realmente la producción. Y el día de actualización que nadie quiere, donde recuperar datos limpios significa desmantelar entornos, volver a conectar cada cadena de conexión y volver a aplicar cada permiso a mano.

Esta publicación trata sobre cómo evitamos la mayor parte de eso y la única decisión que todavía estamos debatiendo activamente.

Ramificación en Lakebase, en 60 segundos

Si aún no has utilizado las ramas de Lakebase, aquí tienes el único modelo mental que necesitas para esta publicación.

Una rama de Lakebase es una rama de Postgres de tipo copy-on-write a partir de una rama principal. Crear una no copia los datos; bifurca el estado de forma económica e instantánea, y la rama solo diverge de su principal a medida que escribes en ella. Cada rama está completamente aislada, con su propio endpoint y sus propios datos. Puedes crear una en segundos y desecharla con la misma rapidez.

Si eso suena como git, ese es el objetivo. La analogía que organiza todo lo que sigue es simple: una rama de características en git se asigna a una rama de base de datos en Lakebase. Una PR obtiene su propia rama de código y su rama de base de datos, las pruebas se ejecutan en ambas y, cuando el trabajo es correcto, se promueve hacia producción.

Hay una restricción que debes tener en cuenta para la siguiente sección, ya que es una distinción crítica con respecto a la ramificación en git. Para restablecer una rama desde su principal, primero debes eliminar los propios hijos de esa rama. Una rama principal no se puede restablecer si tiene ramas que dependen de ella.

La trampa del restablecimiento desde el principal

Este es el diseño al que la mayoría de los equipos recurren primero, porque refleja la forma en que dibujamos los entornos en una pizarra:

  • Una única rama de desarrollo de larga duración a partir de producción.
  • Una rama de aceptación a partir de desarrollo.
  • Ramas de características a partir de desarrollo.

Es una jerarquía limpia: producción en la raíz, luego desarrollo, y después aceptación y características colgando debajo de ella. También introduce dos modos de fallo predecibles: las ramas se desvían y la solución para la desviación es lo suficientemente costosa como para que los equipos dejen de aplicarla.

Primero, desarrollo y aceptación se desvían de producción. Producción sigue recibiendo datos sincronizados frescos y escrituras reales de la aplicación; las ramas de desarrollo y aceptación de larga duración no. En uno o dos sprints, estarás realizando pruebas en una base de datos que ya no se parece a aquella a la que estás enviando los cambios.

La solución obvia es actualizar desarrollo desde producción, y aquí es donde la restricción de la sección anterior se convierte en un costo. Para restablecer desarrollo desde su principal, primero debes eliminar los hijos de desarrollo, lo que en esta topología significa aceptación y cada rama de características que cuelgue de desarrollo. Así, un "dame datos frescos" de rutina se convierte en una cascada: eliminar aceptación y todas las ramas de características, restablecer desarrollo, volver a crear los entornos, volver a conectar cada cadena de conexión que apuntaba a las ramas antiguas y volver a aplicar cada permiso de Postgres, porque los permisos residen en la rama que acabas de eliminar.

image1.png

Ninguno de esos pasos es difícil por sí solo. Juntos, de forma recurrente, son una forma segura de hacer que todo el equipo evite silenciosamente la actualización, lo que significa que todos vuelven a realizar pruebas con datos obsoletos y desviados. Ese era el problema original. La topología ingenua no solo te cuesta una mala tarde; desalienta la higiene que mantiene la integridad del entorno.

El diseño de Glaspoort: ramificar siempre desde producción

La solución es un cambio de una sola línea en la forma de pensar sobre la topología, y tiene efectos enormes: cada rama de entorno de larga duración es hija de producción, no de otro entorno.

Desarrollo y aceptación son ramas tomadas directamente de producción. Se sitúan una al lado de la otra bajo producción, no apiladas una encima de la otra. Ninguna es la principal de la otra, por lo que actualizar una nunca te obliga a eliminar la otra.

Ese único cambio desactiva la trampa. Cuando desarrollo o aceptación se desvían, los restablecemos desde producción (hoy en día una operación de UI), aproximadamente una vez por sprint o cuando queremos datos frescos. Como nada cuelga debajo de desarrollo o aceptación, no hay hijos que eliminar primero, ni cadenas de conexión que volver a pasar a todo el equipo, ni un maratón para volver a aplicar permisos. El restablecimiento es económico, por lo que realmente lo hacemos, manteniendo así la integridad de los entornos. Si por casualidad un restablecimiento pasa por alto un cambio, la siguiente reproducción de la migración simplemente lo vuelve a aplicar.

image2.gif

Animado: el ciclo de vida por PR. Se reproduce en el blog publicado; se muestra como un fotograma estático dentro de este documento.

El flujo por PR

El ciclo diario del desarrollador añade ramas efímeras sobre esa topología estable:

  1. Un desarrollador abre una PR efímera (TTL configurado en 1 hora).
  2. CI crea una rama pr-xxxx nueva y efímera a partir de producción. Ramificamos todo desde producción y nunca volvemos a fusionar las bases de datos. Estas ramas por PR son desechables: las archivamos en el momento en que se cierra la PR, lo que nos mantiene por debajo del límite predeterminado de Lakebase de 10 ramas sin archivar por proyecto.
  3. CI vuelve a reproducir las migraciones en esa nueva rama. Una comprobación de git diff decide si realmente se necesita un ensayo de migración, por lo que las PR que no cambian las migraciones omiten la reproducción.
  4. No solo probamos la migración de forma aislada. CI despliega la nueva imagen de la aplicación en un slot de staging, la apunta a la rama recién migrada y ejecuta el conjunto completo de pruebas contra ese par.
  5. Cada PR valida la migración y la nueva imagen de la aplicación de forma conjunta, antes de que cualquiera de las dos toque un entorno real.
  6. Cuando se fusiona la PR, la CI lo hace de nuevo. Vuelve a crear una rama desde producción, vuelve a aplicar las migraciones y vuelve a realizar las pruebas, porque el entorno puede haber cambiado desde que la PR se aprobó por primera vez. Si las migraciones tienen éxito, se aplican a la rama de destino, en su lugar. Para la primera fusión, ese destino es desarrollo.
  7. La promoción a aceptación y luego a producción se realiza a través de puertas aprobadas manualmente, cada una de las cuales vuelve a aplicar las migraciones en la siguiente rama de la fila.

Vale la pena destacar un detalle para los equipos que vienen de un mundo centrado en git. Solo mantenemos una rama master en git. No hay ramas de desarrollo (develop) o de lanzamiento (release) de larga duración en el control de código fuente. La ruta de promoción de dev a aceptación y a producción se expresa completamente a través de puertas aprobadas manualmente en Azure DevOps, que son las que inician la CI y mueven un cambio de una rama a la siguiente. La topología del entorno reside en Lakebase y en el pipeline; git se mantiene simple.

Por qué la repetición de migraciones es la fuente de verdad

Lo que hace que todo esto sea seguro es que nunca volvemos a fusionar las bases de datos entre sí. No promovemos un cambio copiando datos de una rama de características (feature branch) a desarrollo. Lo promovemos volviendo a ejecutar las migraciones en la rama de destino. Usamos vitest para ejecutar la prueba de humo (smoke test) en el entorno de pruebas (staging slot).

Las ramas, en este modelo, son deliberadamente efímeras y desechables. La descripción duradera y autoritativa de cómo debería ser el esquema no es una base de datos de larga duración que podría haber sufrido desviaciones. Es el conjunto ordenado de migraciones. Es por eso que un reinicio es seguro incluso cuando se pasa algo por alto: si a una rama actualizada le falta un cambio, la siguiente repetición de la migración lo vuelve a aplicar. Las migraciones son lo suficientemente centrales como para que las bases de datos activas siempre se puedan reproducir a partir de ellas, en lugar de ser valiosas debido a ellas.

Esa es la verdadera clave. Crear ramas desde producción mantiene los datos actualizados. Tratar las migraciones como la fuente de verdad mantiene el esquema correcto. Juntos, significan que ningún entorno es demasiado especial como para no poder desecharlo y reconstruirlo.

La bifurcación: dos modelos de promoción

Esta es la parte que creemos que es más útil para otros equipos, porque es una bifurcación real en lugar de una mejor práctica con una única respuesta correcta. Una vez que tienes la topología anterior, aún debes decidir cuándo se permite fusionar una PR. Evaluamos dos modelos, y cada uno tiene sus ventajas y desventajas en direcciones opuestas.

Opción 1: Fusionar después de que se apruebe la CI (acumulación de PR)Opción 2: Fusionar después del CD a dev y aceptación (promoción por PR)
MecánicaUna PR se puede fusionar tan pronto como la CI en su rama pr-xxxx esté en verde.Una PR no se puede fusionar hasta que haya sido promovida a través de desarrollo y aceptación.
VentajasLos compañeros de equipo pueden trabajar sobre los cambios de los demás de inmediato.Cada PR avanza a su propio ritmo; las correcciones de errores (hotfixes) son prioritarias y no hacen cola detrás de nadie.
DesventajasMúltiples PR fusionadas se acumulan al promoverse a dev y aceptación. Un hotfix no puede evitar la cola sin un pipeline paralelo.Los compañeros de equipo tienen que esperar hasta que la aceptación esté en verde antes de poder trabajar sobre un cambio.

La versión corta: la Opción 1 optimiza la velocidad del desarrollador dentro de un equipo muy unido. La Opción 2 optimiza la independencia y una ruta de hotfix limpia. Si tu equipo trabaja en cambios que se superponen y confía en la CI, la acumulación te permite avanzar más rápido. Si con frecuencia necesitas lanzar una corrección que no puede esperar a lo que esté en curso, la promoción por PR vale la pena.

Elegimos la Opción 1. Somos un equipo pequeño que trabaja en cambios que se superponen. Esperar a que la aceptación se apruebe antes de que alguien pueda trabajar sobre tu código es un costo que pagaríamos todos los días, mientras que el riesgo de acumulación solo se materializa ocasionalmente. Y cuando ocurre, está controlado: cada puerta de promoción vuelve a ejecutar las migraciones acumuladas en una bifurcación nueva de producción antes de tocar la rama real, por lo que una pila se valida como una unidad. Para casos realmente urgentes, mantenemos un pipeline de crisis separado que va directo a producción con la misma verificación previa. Velocidad por defecto, vía de escape en espera.

Temas secundarios: lo que surgió en el camino

Algunos temas surgieron repetidamente en las sesiones de trabajo. No son el eje central de la historia, pero son los detalles que deciden si el patrón sobrevive al contacto con la producción.

Autenticación de base de datos y rotación de tokens. La aplicación no guarda un secreto de base de datos de larga duración. Se utiliza una contraseña estática en Azure Key Vault para generar una credencial de base de datos de corta duración (un token con un TTL de 60 minutos) a través de `databricks postgres generate-database-credential`. Como el token expira después de una hora, el cliente tiene que actualizarlo de forma proactiva; manejamos esto con una función de contraseña asíncrona en el grupo de conexiones (connection pool) que almacena en caché la credencial y la rota antes de que caduque.

Permisos (grants) y aprovisionamiento de acceso. No gestionamos el DDL de las tablas a través de migraciones, y los permisos a nivel de objeto se aplican manualmente hoy en día en lugar de a través de código. Es el elemento más claro a mejorar en nuestra lista. Pero debido a que aplicamos esos permisos en producción y las ramas de entorno se reinician volviendo a bifurcar desde producción, las ramas secundarias los heredan automáticamente en cada actualización. El paso de concesión manual ocurre una vez en producción, no repetidamente en cada rama secundaria, por lo que sigue siendo lo suficientemente inusual como para convivir con ello por ahora.

Acceso a la base de datos: un único usuario de la aplicación. No aprovisionamos un rol de Postgres por usuario final. La aplicación se conecta a través de un único usuario de la aplicación, y la autorización para usuarios individuales reside en la capa de la aplicación en lugar de en la base de datos. Esto mantiene el aprovisionamiento simple; la desventaja es que el control de acceso por usuario se aplica por encima de Postgres, no por este.

Lo que esto desbloqueó para Glaspoort

Un caso de uso que antes requería un equipo completo y meses de trabajo ahora se pone en marcha con un equipo pequeño en cuestión de días. Nuestras iteraciones se han multiplicado por diez, y un solo equipo entrega valor continuamente en una plataforma que crece con la organización.—Raymon Veldman, gerente de Business Control e IT

El cambio más visible es la velocidad con menos ansiedad. Ahora, cada PR obtiene su propia base de datos con estructura de producción en minutos, por lo que los desarrolladores dejaron de coordinarse en torno a entornos compartidos de dev y aceptación; las conversaciones de "¿quién rompió aceptación?" simplemente desaparecieron. Debido a que las ramas de entorno siempre son secundarias de producción, nunca volvimos a tener un día de reinicio desde la rama principal: nada de eliminar ramas secundarias, nada de volver a configurar cadenas de conexión, nada de tardes perdidas volviendo a aplicar permisos. Las migraciones se repiten en cada paso de promoción, y desplegamos la nueva imagen de la aplicación en un entorno de pruebas (staging slot) contra la rama recién migrada y ejecutamos el conjunto completo de pruebas antes de que nada toque un entorno real, por lo que lo que llega a producción ya se ha probado con éxito dos veces. Y cuando surge algo urgente, los hotfixes siguen la misma ruta segura que todo lo demás, solo que más rápido, en lugar de saltarse el pipeline.

¿Estás eligiendo Lakebase para una base de datos de aplicación y lidiando con las mismas preguntas de CI/CD? Comienza aquí: entornos de larga duración ramificados directamente desde producción, ramas efímeras por PR y las migraciones como la única fuente de verdad. Obtén más información sobre Databricks Lakebase

(Esta entrada del blog ha sido traducida utilizando herramientas basadas en inteligencia artificial) Publicación original

Recibe las últimas publicaciones en tu bandeja de entrada

Suscríbete a nuestro blog y recibe las últimas publicaciones directamente en tu bandeja de entrada.