Volver a proyectos

Seis semanas para desplegar con menos estrés mientras el equipo seguía entregando

Un producto financiero en uso, despliegues manuales y cuatro desarrolladores que seguían entregando funcionalidades. Me encargué de toda la reforma durante seis semanas: versiones probadas, despliegues controlados, procedimientos de rollback y configuración documentada. Las compilaciones pasaron de 17 minutos a 4. El siguiente desarrollador tuvo la aplicación funcionando en 15 minutos sin ayuda.

10 min de lectura
  • Docker
  • GitHub Actions
  • Google Cloud
  • PostgreSQL
  • Turborepo
  • Turbopack
  • DevOps
CompartirXEmailFacebookWhatsApp

Durante mi incorporación, un desarrollador me contó, medio en broma, que había dejado de apagar el ordenador desde que entró en el proyecto. Lo dejaba en reposo porque no le apetecía abrir 15 terminales a la mañana siguiente para arrancar la aplicación.

El producto tenía un frontend en Next.js, un backend en NestJS, Keycloak para la autenticación, 11 procesos en segundo plano en Node.js, PostgreSQL y Redis. Algunas partes corrían en Docker; otras, directamente en el equipo. Cada servicio podía usar una versión distinta de Yarn.

Y, como suele pasar con proyectos que empezaron como un MVP, había poca documentación, el conocimiento estaba en la cabeza de los desarrolladores y preguntar por el proceso de despliegue solía provocar una sonrisa nerviosa.

Durante el onboarding, le pregunté al compañero que me estaba ayudando por qué la aplicación daba errores al arrancar.

Compañero: "Ah, sí... Se me había olvidado. Tienes que cambiar un flag en el archivo .env. Es deuda técnica que ya conocemos."

Nico: "Vale, ¿tenemos estas cosas documentadas en algún sitio? ¿Aunque sea en los issues de GitHub?"

Compañero: "La verdad es que no... pero deberíamos empezar."

Aquello resumía bastante bien la situación.

Poner todo en marcha requería 17 pasos de configuración, unas 15 terminales y una colección de casos particulares que, por algún motivo, se suponía que tenías que recordar.

Sinceramente, uno de los peores entornos de desarrollo con los que me he encontrado.

El producto había crecido. El proceso tenía que ponerse al día.

Me incorporé en agosto de 2026. El producto ya gestionaba datos financieros de empresas reales. No es precisamente el tipo de software donde te apetece descubrir problemas de despliegue en producción.

Una versión con fallos podía perjudicar a los clientes y dañar la reputación del propietario. Y con tantos pasos manuales, cada despliegue dependía de que alguien recordara el procedimiento correcto.

Normal que hubiera que ir con pies de plomo cada vez.

La configuración venía de una etapa en la que la prioridad era sacar el MVP cuanto antes. Había cumplido su función. Ahora la empresa necesitaba despliegues repetibles y un sistema que, en el futuro, pudiera ofrecer para alojarlo en los servidores de sus clientes.

Me contrataron para resolver tres cosas:

  • Convertir el despliegue en un procedimiento documentado y repetible.
  • Reducir los tiempos de compilación.
  • Hacer que arrancar la aplicación completa fuera sencillo.

Tras una primera fase de análisis e incorporación, propuse el enfoque y el resultado esperado. El responsable del proyecto por parte del cliente lo aprobó, y me encargué de toda la implementación hasta la entrega, a mediados de septiembre.

Mientras tanto, los otros cuatro desarrolladores seguían creando funcionalidades y corrigiendo errores. Teníamos que hacer todo esto sin parar el desarrollo del producto.

Primero, que empezar a trabajar no fuera un proyecto en sí mismo

Al analizar el uso del entorno, quedó clara una cosa: no todo el mundo necesitaba tener el sistema entero funcionando todo el tiempo.

Quien trabajaba en el frontend no necesitaba 11 procesos en segundo plano consumiendo recursos. Quien modificaba esos procesos sí los necesitaba. Y, de vez en cuando, teníamos que reproducir el comportamiento de la aplicación en producción.

Así que introduje cuatro modos de arranque:

Modo Qué ejecuta
hybrid-app Las aplicaciones principales con recarga en caliente. PostgreSQL, Redis y Keycloak en Docker. Sin procesos en segundo plano.
hybrid-all La misma configuración, con todos los procesos en segundo plano ejecutándose en local.
prod-app Los servicios principales como imágenes Docker de producción. Sin recarga en caliente ni procesos en segundo plano.
prod-all Todo el sistema como imágenes Docker de producción, incluidos todos los procesos en segundo plano.

Las instrucciones de arranque pasaron a ser:

  1. Clonar el repositorio.
  2. Ejecutar un comando para crear los archivos de entorno local a partir de los ejemplos incluidos.
  3. Elegir un modo y arrancar la aplicación.

Había otro motivo para meter todo el sistema en contenedores.

El cliente tenía previsto ofrecer alojamiento en servidores propios. No era una prioridad inmediata, pero quería hacerlo más adelante.

Como ya estaba reorganizando la forma de arrancar y ejecutar la aplicación, tenía sentido tenerlo en cuenta desde ese momento. Los mismos cambios que simplificaban el desarrollo local también preparaban la aplicación para funcionar fuera de la infraestructura cloud existente.

Al principio, algunos desarrolladores se mostraron reacios. Conocían el proceso anterior y tampoco veían tanta necesidad de cambiarlo. Yo asumí que el problema era que no tenían tiempo para la implementación, no que la propuesta les pareciera mala.

Resulta que me equivocaba.

Lo que les preocupaba era cambiar una configuración incómoda pero predecible por otra que pudiera romperles el entorno local al integrar cualquiera de mis PRs. A nadie le apetece pasar la primera hora del día peleándose con Docker en vez de trabajar en el producto.

Una preocupación muy razonable, la verdad. No se lo puedo reprochar.

Así que entregué primero una prueba pequeña. Solo la aplicación principal en modo híbrido, sin los procesos en segundo plano. Algo que pudieran usar en su trabajo diario mientras yo seguía construyendo el resto.

Les pedía feedback por Slack y sacaba el tema en las reuniones diarias. Probablemente más de lo que les hubiera gustado.

Pero empezaron a usarlo. Y, para mi sorpresa, y seguramente también la suya, la nueva configuración les gustó incluso más de lo que yo esperaba.

Fue una lección útil a la que vuelvo bastante a menudo.

Publicar una versión no debería depender de quién se acuerda de los pasos

Antes, los desarrolladores construían las imágenes de las aplicaciones en local, las subían a Google Cloud manualmente y ejecutaban allí las migraciones de la base de datos a mano. Suena aterrador. Lo era.

Durante el trabajo también descubrimos que los esquemas de las bases de datos de pruebas y producción habían dejado de coincidir. Probar sobre un esquema distinto hace más difícil confiar en lo que realmente demuestra un test que pasa.

Pasé los cambios de la base de datos a archivos de migración de PostgreSQL versionados en el repositorio, que solo avanzaban hacia delante. Los entornos podían seguir el mismo historial de migraciones y podíamos recrear el esquema desde cero. Para las bases con datos existentes, tuve que preparar algunas migraciones específicas y corregir manualmente las diferencias entre ellas.

Después construí el proceso de publicación con GitHub Actions y Google Cloud.

Para empezar un despliegue, alguien elegía un commit de main. Google Cloud se encargaba de construir las imágenes porque nuestras pruebas habían demostrado que era más rápido y barato para nuestra forma de trabajar.

A continuación, el proceso:

  1. Aplicaba las migraciones pendientes de la base de datos.
  2. Creaba una instancia nueva de la aplicación sin tráfico de clientes.
  3. Ejecutaba pruebas de extremo a extremo contra la URL de esa instancia.
  4. Dejaba la versión probada lista para un paso posterior de promoción.

Si fallaba cualquiera de los pasos, el proceso se detenía y conservaba los registros y artefactos necesarios para investigar. La nueva versión no recibía tráfico de clientes automáticamente.

Por decisión del cliente, la promoción seguía siendo manual. Queríamos que la automatización quitara trabajo repetitivo y redujera errores humanos, pero la decisión de exponer una versión nueva a los clientes seguía en manos del equipo. Podían elegir qué porcentaje del tráfico enviarle, empezar con una parte pequeña antes de mover a todos los usuarios o entrar ellos mismos para hacer comprobaciones adicionales.

Desplegábamos en pruebas unas 2 o 3 veces por semana, y en producción, 1 o 2. No necesitábamos compilar y desplegar cada cambio que llegaba a main.

Volver a una versión anterior también necesitaba un procedimiento claro

Las versiones anteriores de la aplicación seguían disponibles, así que otra acción de GitHub podía devolverles el tráfico. ¿Encontrábamos un fallo? Lanzábamos el rollback manualmente. Todos contentos. Sencillo.

La parte delicada era la base de datos. Nuestras migraciones basadas en archivos solo avanzaban hacia delante y, por diseño, no queríamos revertirlas. Acordamos que los cambios debían mantener la compatibilidad con la versión anterior de la aplicación y añadimos comprobaciones de compatibilidad a CI para apoyar ese proceso de publicación.

También incorporé pruebas de estrés mediante GitHub Actions, con la opción de ejecutarlas manualmente contra un entorno desplegado. Resultaban especialmente útiles al cambiar la lógica de los procesos en segundo plano o las consultas a la base de datos. Teníamos una forma de comprobar si un cambio soportaba la carga esperada antes de darlo por hecho.

Documenté los procedimientos de migración, promoción y rollback, además de qué investigar cuando fallaba un paso. Documentación pensada para desarrolladores y lista para agentes de IA.

Diecisiete minutos daban para esperar bastante

Al principio, la compilación local completa tardaba unos 17 minutos en un MacBook Air M1 con 16 GB de RAM. No era precisamente el último modelo, pero sí una referencia razonable del equipo con el que podía trabajar un desarrollador.

Introduje caché de compilación de Docker, cambié la configuración y ejecuté pasos independientes en paralelo cuando era posible. También añadí Turborepo y Turbopack para evitar trabajo innecesario durante la compilación.

La compilación completa pasó de 17 minutos a 4. 4,25 veces más rápida. Un 76,5 % menos de tiempo de compilación.

Esa cifra corresponde a la compilación, no al despliegue completo. Las pruebas, las migraciones y la decisión de enviar tráfico a una versión eran pasos separados.

Cuatro personas seguían cambiando el código mientras tanto

Cada cambio técnico, por separado, tampoco era física nuclear. Lo complicado era hacer que todo funcionara mientras 4 desarrolladores seguían entregando funcionalidades. La coordinación y la comunicación fueron lo que más cuidado exigió.

Trabajábamos con trunk-based development e integrábamos cambios en main con frecuencia. Mis cambios de plataforma tenían que encajar con el trabajo de producto del equipo. Cambiar una dependencia podía obligar a recompilar. Otro cambio podía exigir reiniciar la base de datos local. Todo eso con las fechas del Product Owner, que quería ver nuevas funcionalidades y avances, y los plazos de respuesta acordados para las incidencias que pudieran surgir.

Sin instrucciones claras, alguien podía descargarse código que funcionaba y pasarse la mañana intentando entender por qué no arrancaba en su equipo.

Trabajábamos de forma asíncrona, así que dejaba esas instrucciones en los PRs: qué había cambiado, qué ejecutar después de integrarlos y qué esperar. También avisaba en nuestro canal de novedades de Slack con un enlace al PR.

Como poco, fue una operación delicada. Mentiría si dijera que no hubo problemas. Claro que los hubo, es normal cuando el desarrollo va tan rápido. Pero puedo decir con orgullo que cumplimos las 6 semanas y todos los requisitos del negocio.

Entonces llegó alguien nuevo

Una vez entregados los cambios, necesitábamos comprobar que alguien nuevo pudiera entenderlos. De poco serviría toda la documentación si no se podía usar. Significaría que habíamos cambiado el proceso, pero los detalles seguían en las cabezas de los desarrolladores. Mal asunto.

Y tuvimos la oportunidad perfecta: se incorporó un desarrollador nuevo.

Le pasé la documentación y le pedí que intentara configurar la aplicación sin mi ayuda. Si algo no quedaba claro, quería enterarme mientras aún podía corregirlo. Tampoco lo dejé abandonado a su suerte: le pedí que me avisara enseguida si encontraba algún problema.

Me contó que tardó unos 15 minutos desde que clonó el repositorio hasta que vio la aplicación funcionando en su equipo.

Dedicó alrededor de una hora y media a leer la documentación y entender el flujo de trabajo local. No tuvo que hacerme ninguna pregunta sobre la configuración.

Un éxito en toda regla. Esa es la parte que más satisfacción me da.

Alguien que nunca había tocado el proyecto podía llegar, seguir las instrucciones y ponerse a trabajar. El equipo ya había adoptado el nuevo sistema. Las publicaciones de versiones tenían un procedimiento que cualquiera podía consultar y repetir.

Y arrancar la aplicación por la mañana por fin era algo razonable.