Los tres casos anteriores compartían una red de seguridad que la lección anterior señaló al despedirse: eran sistemas modernos, con pruebas, con control de versiones sano y con equipos que podían decidir cómo trabajar. Aquí se quita esa red. Gestor Citas 4 es el producto anterior de la empresa, el que Reservalia vino a sustituir: un monolito Java/JSP construido con Ant y migrado a medias a Maven en 2011, desplegado sobre un Tomcat que vive en una máquina virtual con nombre propio, sin una sola prueba automatizada, con despliegue manual por FTP los sábados por la noche, la configuración editada a mano directamente en el servidor, y tres clientes grandes que aún pagan 4.200 euros al mes por él y no van a migrar en dos años. Nadie quiere tocarlo y hay que tocarlo. Esta lección no trata de elegir entre monorepo y polyrepo ni de afinar un canary: trata de decidir por dónde se empieza cuando no hay nada, en qué orden construir los incrementos para que cada uno aporte valor por sí solo aunque el siguiente nunca llegue, qué tácticas concretas aplicar a cada tipo de deuda, cómo se mide el progreso cuando la línea base es un despliegue cada dos meses, cómo se justifica la inversión ante quien la paga, y —lo más olvidado— cuándo parar.

Contenido

  1. Gestor Citas 4: el inventario honesto
  2. Por qué no se reescribe y por qué "primero las pruebas" nunca llega
  3. La estrategia de incrementos
  4. Incrementos 1 y 2: control de versiones y build en CI
  5. Incrementos 3 y 4: caracterización y golden master
  6. Incremento 5: despliegue automatizado aunque siga siendo manual
  7. Incremento 6: contenerizar para reproducir el entorno
  8. Incremento 7: strangler fig
  9. Deuda concreta y sus tácticas
  10. Métricas cuando la línea base es un despliegue cada dos meses
  11. La conversación con negocio
  12. Cuándo parar
  13. Ficha del caso
  14. Errores Comunes y Consejos
  15. Ejercicios
  16. Conclusión y cierre del módulo

  1. Gestor Citas 4: el inventario honesto

Reservalia (módulos 2-4) Gestor Citas 4
Código TypeScript, monorepo Java 8, JSP, Struts 1, 240.000 líneas
Construcción npm ci, reproducible Ant + Maven a medias, solo funciona en una máquina
Pruebas Unitarias, integración, E2E Ninguna automatizada
Despliegue cd.yml, 12 min, 12/semana FTP manual, sábado noche, ~1 cada 2 meses
Configuración Terraform + gestor de secretos Ficheros .properties editados en el servidor
Entornos dev, staging, prod idénticos Prod, y "el de pruebas" que nadie sabe si coincide
Base de datos Migraciones versionadas Scripts SQL sueltos en una carpeta compartida
Quién lo conoce Todo el equipo Una persona, y ya no trabaja aquí
Ingresos 340 negocios 3 clientes, 4.200 €/mes, contrato hasta 2028

La última fila es la que hace que esta lección exista. Un sistema sin pruebas y con despliegue manual que no da dinero se apaga; uno que sí lo da y con contrato firmado hasta 2028 hay que mantenerlo en condiciones. Y la penúltima explica por qué da tanto miedo: no hay nadie a quien preguntar, así que el código es la única documentación y cualquier cambio se hace a ciegas.

Lo que hoy pasa cuando hay que cambiar algo: dos días de desarrollo, medio día de pruebas manuales de alguien que abre la aplicación y clica, y un sábado por la noche de tres horas en el que se sube un .war por FTP, se reinicia Tomcat y se reza. En los últimos doce meses, dos de los seis despliegues fallaron y hubo que restaurar copiando manualmente el .war anterior desde una carpeta llamada backup_ok_final.

  1. Por qué no se reescribe y por qué "primero las pruebas" nunca llega

La reescritura es la respuesta que todo el mundo propone y casi nadie termina. Los números de Gestor Citas 4: 240.000 líneas, doce años de reglas de negocio acumuladas, ninguna especificación escrita, y comportamientos que los tres clientes usan a diario sin que nadie sepa que existen —el informe de facturación trimestral que un cliente exporta en un formato concreto, la integración por SFTP con el sistema contable de otro—. Una reescritura tendría que reproducir todo eso a ciegas, y mientras dura, el sistema viejo sigue necesitando cambios que hay que hacer dos veces. Es el escenario clásico en el que un proyecto de nueve meses se convierte en tres años y se cancela con las dos versiones a medias.

La segunda respuesta habitual también falla, y de forma más sutil: "primero escribimos pruebas, y cuando haya cobertura montamos CI/CD". Nunca llega, por tres razones concretas. El código no es testable —lógica de negocio dentro de JSP, new de conexiones a base de datos dentro de los métodos, estáticos por todas partes—, así que escribir la primera prueba unitaria exige refactorizar, y refactorizar sin pruebas es exactamente lo que se quería evitar. Es un ciclo cerrado. Además, escribir pruebas no produce ningún beneficio visible para negocio, así que la tarea pierde siempre contra cualquier petición de un cliente que paga 4.200 euros al mes. Y por último, el problema más urgente no es la falta de pruebas: es que nadie sabe reconstruir el sistema, y eso se arregla antes y más barato.

De ahí la inversión del orden que gobierna toda la lección: no se empieza por las pruebas, se empieza por la reproducibilidad. Cada incremento debe cumplir tres condiciones —aportar valor por sí solo aunque el siguiente nunca se haga, no requerir tocar el código de negocio, y ser reversible— y ordenarse por relación entre valor y riesgo.

  1. La estrategia de incrementos

flowchart TD
    I1["1 · Control de versiones<br/>y build en local"] --> I2["2 · Build en CI<br/>compila = primera senal"]
    I2 --> I3["3 · Caracterizacion<br/>humo E2E sobre lo que da dinero"]
    I3 --> I4["4 · Golden master<br/>para lo no testable"]
    I4 --> I5["5 · Despliegue automatizado<br/>aunque se dispare a mano"]
    I5 --> I6["6 · Contenerizar<br/>reproducir el entorno"]
    I6 --> I7["7 · Strangler fig<br/>sacar funcionalidad fuera"]
    I2 -.->|"valor: ya se puede parar aqui"| P1["Parada valida"]
    I5 -.->|"valor: ya se puede parar aqui"| P2["Parada valida"]

Las dos "paradas válidas" son deliberadas y volveremos a ellas en el apartado 12: hay productos para los que llegar al incremento 2 ya es un éxito, y muchos para los que el 5 es el destino final razonable. Un plan de modernización que solo aporta valor si se completa entero es un plan que va a fracasar, porque la prioridad cambiará antes de terminarlo.

  1. Incrementos 1 y 2: control de versiones y build en CI

Incremento 1 — Meterlo en git y reconstruirlo fuera de "la máquina". El punto de partida real fue una carpeta en un disco de red con GestorCitas_v4_2_FINAL, GestorCitas_v4_2_FINAL_2 y GestorCitas_v4_2_parcheJulio. El trabajo, dos semanas de arqueología: identificar qué versión corresponde al .war que está en producción —comparando fechas y descompilando dos clases dudosas—, crear el repositorio con esa como primer commit, y reconstruir la build en una máquina limpia, anotando cada cosa que falta. Aquí sale la primera lista de deuda real: tres JAR que no están en ningún repositorio público y estaban solo en el lib/ local, una variable de entorno GC_HOME que alguien puso a mano hace años, y una tarea de Ant que copia ficheros desde una ruta absoluta del disco C.

El criterio de éxito de este incremento es exacto y verificable: el .war construido desde el repositorio en una máquina limpia tiene el mismo contenido funcional que el que está en producción. Compararlo entero byte a byte no vale —las marcas de tiempo dentro del ZIP difieren siempre—, así que se comparan las listas de clases y los hashes de los recursos. Cuando eso cuadra, se ha recuperado la propiedad más valiosa que se había perdido: saber qué código está ejecutándose.

Incremento 2 — Que lo construya el CI. Sin una sola prueba:

name: gc4-ci
on: { push: {}, pull_request: {} }

jobs:
  construir:
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: '8', cache: maven }   # 1
      - name: Compilar y empaquetar
        run: mvn -B -s .mvn/settings.xml clean package -DskipTests         # 2
      - name: Publicar el artefacto
        uses: actions/upload-artifact@v4
        with:
          name: gc4-${{ github.sha }}                                      # 3
          path: target/gestorcitas.war
          retention-days: 90
  1. Java 8 explícito, porque el código no compila con versiones posteriores y descubrirlo con un error críptico cuesta una tarde. Actualizar la versión de Java es un proyecto aparte, no un requisito de tener CI.
  2. -s .mvn/settings.xml apunta al repositorio interno donde se han subido los tres JAR huérfanos (apartado 9). -B desactiva el modo interactivo, que en CI cuelga el job esperando una respuesta que nadie dará.
  3. El .war se publica como artefacto con el SHA del commit. Eso es artefacto inmutable y trazable (02-06) sin haber cambiado ni una línea del producto: por primera vez en doce años se puede responder a "¿qué versión hay en producción?" con un identificador en lugar de una fecha.

Parece poco y es muchísimo. "Compila" es la primera señal automática que este proyecto tiene en su vida, y atrapa desde el primer día la clase de error que antes se descubría el sábado por la noche a las once. En Gestor Citas 4 encontró, en los dos primeros meses, tres commits que no compilaban porque alguien había editado un JSP directamente en el servidor y luego lo había copiado al repositorio a mano.

  1. Incrementos 3 y 4: caracterización y golden master

Ahora sí llegan las pruebas, y llegan por donde nadie espera: no por abajo con unitarias, sino por arriba con humo de extremo a extremo. El motivo es que las pruebas de arriba no exigen tocar el código, y tocar el código es justamente lo que aún no se puede hacer con seguridad. La pirámide de la 02-04 sigue siendo el objetivo a largo plazo, pero en un legacy se empieza por la punta y se baja.

Las pruebas de caracterización tienen un propósito distinto de las pruebas normales: no verifican que el sistema hace lo correcto, verifican que hace exactamente lo que hacía antes. Si hay un comportamiento raro, la prueba lo documenta tal cual, incluido el bug, porque un cliente puede llevar ocho años dependiendo de él.

// src/test/java/caracterizacion/ReservaHumoTest.java
@Test
public void reservar_hueco_libre_genera_cita_y_correo() {
    driver.get(BASE + "/login.do");
    login("[email protected]", "demo");

    driver.get(BASE + "/agenda.do?fecha=2026-10-25");
    driver.findElement(By.cssSelector("td[data-hora='10:30'] a")).click();  // 1
    driver.findElement(By.name("nombreCliente")).sendKeys("Cliente Ficticio 1");
    driver.findElement(By.name("guardar")).click();

    assertTrue(driver.getPageSource().contains("Cita guardada correctamente")); // 2
    assertEquals(1, contarFilas("SELECT 1 FROM CITAS WHERE HORA='10:30'"));     // 3
}
  1. Selectores por atributos de datos existentes, porque el HTML generado por JSP es frágil y no se va a rediseñar. En un legacy no se refactoriza la vista para hacerla testable; se toma como está.
  2. Se comprueba el texto exacto que ve el usuario, aunque sea feo. La prueba documenta el comportamiento real, no el deseable.
  3. Se verifica también el efecto en la base de datos, porque en este sistema hay lógica dentro de procedimientos almacenados y una comprobación solo por interfaz se deja la mitad.

¿Cuántas? Entre seis y diez, y elegidas por dinero, no por cobertura. En Gestor Citas 4: iniciar sesión, crear una cita, cancelarla, el listado del día, el informe de facturación trimestral —el que un cliente exporta cada tres meses y que si se rompe genera una llamada del director— y la exportación por SFTP. Con ocho pruebas se cubre lo que hace que tres clientes paguen; la cobertura de líneas resultante será del 11 % y no importa en absoluto.

Incremento 4 — Golden master para lo que no admite prueba unitaria. El cálculo de facturación son 1.800 líneas en una clase con estáticos, acceso directo a base de datos y System.out. Aislarla para probarla exige refactorizarla; refactorizarla sin pruebas es lo que da miedo. La técnica que rompe el ciclo:

@Test
public void factura_trimestral_no_cambia() throws Exception {
    for (String caso : listarCasos("src/test/recursos/casos/")) {       // 1
        cargarDatos(caso);
        String salida = FacturacionLegacy.generarInforme(2026, 3);      // 2
        String esperado = leer("src/test/recursos/golden/" + caso + ".txt");
        assertEquals("Caso " + caso, normalizar(esperado), normalizar(salida));  // 3
    }
}
  1. Muchos casos de entrada, capturados de datos reales anonimizados (04-06): treinta escenarios que cubren descuentos, prorrateos, meses partidos y los casos raros que aparecieron en doce años.
  2. La salida completa se compara con un fichero de referencia generado con el código actual. Nadie ha leído las 1.800 líneas ni ha decidido qué es correcto: se congela lo que hace hoy.
  3. normalizar elimina lo que cambia entre ejecuciones —fecha de emisión, número de informe— porque si no la prueba falla siempre. Es el mismo enmascarado que en la regresión visual de la 05-01.

A partir de ahí, la clase se puede refactorizar con una red de verdad: cualquier cambio de comportamiento produce un diff exacto de la línea afectada. Y una advertencia importante: el golden master congela también los bugs. Cuando se descubre uno y se decide corregirlo, se actualiza el fichero de referencia a propósito y en un PR aparte, con el diff visible en la revisión. Ese diff es la mejor documentación de comportamiento que este sistema ha tenido nunca.

  1. Incremento 5: despliegue automatizado aunque siga siendo manual

Aquí está el mayor retorno por unidad de esfuerzo de toda la lección, y la idea que más cuesta aceptar: automatizar el despliegue no significa desplegar automáticamente. Se puede tener un despliegue enteramente scriptado, repetible y con vuelta atrás que lo sigue disparando una persona un sábado. Se conservan la revisión y el control humanos y desaparece la parte que falla: la ejecución manual.

Comparemos el sábado de antes con el nuevo:

Antes Después
Quién decide Una persona La misma persona
Cómo se ejecuta FTP, clics, reinicio a mano workflow_dispatch con el SHA a desplegar
Qué se despliega El .war que había en la carpeta El artefacto exacto que construyó el CI
Copia previa A veces, con nombre inventado Siempre, versionada y con retención
Configuración Editada a mano en el servidor Plantilla + valores por entorno
Vuelta atrás Copiar el .war viejo, si se encuentra Un workflow_dispatch con el SHA anterior
Duración 3 horas 7 minutos
Verificación Abrir la web y mirar Smoke test automático
name: gc4-desplegar
on:
  workflow_dispatch:                                   # 1 · lo dispara una persona
    inputs:
      sha:     { description: 'SHA a desplegar', required: true }
      entorno: { description: 'pruebas | prod', required: true, default: pruebas }

jobs:
  desplegar:
    runs-on: ubuntu-22.04
    environment: gc4-${{ inputs.entorno }}              # 2 · revisores para prod
    steps:
      - uses: actions/download-artifact@v4             # 3 · no se reconstruye
        with: { name: gc4-${{ inputs.sha }}, github-token: '${{ secrets.GITHUB_TOKEN }}',
                run-id: '${{ inputs.sha }}' }

      - name: Copia de seguridad del war actual
        run: |
          ssh "$HOST" "cp /opt/tomcat/webapps/gc.war \
            /opt/respaldos/gc-\$(date +%F-%H%M).war"    # 4

      - name: Generar configuración desde plantilla
        run: envsubst < config/gc.properties.tpl > gc.properties   # 5
        env:
          DB_URL:  ${{ secrets.GC4_DB_URL }}
          DB_PASS: ${{ secrets.GC4_DB_PASS }}

      - name: Desplegar y reiniciar
        run: |
          scp gestorcitas.war "$HOST:/opt/tomcat/webapps/gc.war.nuevo"
          scp gc.properties   "$HOST:/opt/gc/conf/gc.properties"
          ssh "$HOST" "systemctl stop tomcat && \
                       mv /opt/tomcat/webapps/gc.war.nuevo /opt/tomcat/webapps/gc.war && \
                       systemctl start tomcat"          # 6

      - name: Smoke test
        run: |
          for i in $(seq 1 30); do
            curl -fsS "https://$DOMINIO/gc/salud.jsp" && exit 0    # 7
            sleep 5
          done
          echo "El servicio no respondió tras 150 s"; exit 1
  1. workflow_dispatch mantiene la decisión en manos de una persona. Es el paso intermedio entre lo manual y el despliegue continuo, y para este producto puede ser el destino final.
  2. El entorno de GitHub con revisores (03-02) sustituye al "avisa a Marta por Slack antes de subir". Queda registrado quién aprobó y cuándo.
  3. Se descarga el artefacto, no se reconstruye. Aunque el pipeline sea rudimentario, la regla de construir una vez (02-06) se aplica desde el primer día: es gratis y elimina toda una clase de sorpresas.
  4. La copia de seguridad forma parte del despliegue, con nombre determinista y no backup_ok_final. Es lo que hace que la vuelta atrás sea real.
  5. La configuración se genera desde una plantilla versionada y los valores vienen de secretos. Con esto desaparece el .properties editado a mano en el servidor, que era la causa de que "el de pruebas" no se pareciera a producción.
  6. Parar, sustituir, arrancar. No es elegante y hay unos 40 segundos de corte, pero es determinista. Un despliegue sin indisponibilidad para este sistema exigiría un segundo servidor y un balanceador; es una mejora posterior, no un requisito para automatizar. Aquí es donde conviene resistir la tentación de hacerlo todo bien a la primera.
  7. El smoke test con reintentos (03-02) convierte "abrir la web y mirar" en una verificación objetiva que además decide si el job termina en verde.

Resultado medido en Gestor Citas 4: el despliegue pasó de tres horas a siete minutos, la vuelta atrás de "media hora buscando el .war correcto" a un workflow_dispatch de siete minutos, y los sábados por la noche dejaron de existir porque un despliegue de siete minutos con vuelta atrás verificada se puede hacer un martes a las cinco de la tarde. Esa última consecuencia es la que el equipo notó de verdad.

  1. Incremento 6: contenerizar para reproducir el entorno

Con el despliegue automatizado queda un problema abierto: el servidor es único e irreproducible. Nadie sabe qué versión de Tomcat corre, qué parámetros de JVM tiene, ni qué se instaló a mano en 2019. Meter el legacy en una imagen no lo moderniza, pero convierte "el servidor" en un fichero versionado.

FROM tomcat:8.5-jdk8-temurin                        # 1
RUN rm -rf /usr/local/tomcat/webapps/*              # 2
COPY docker/server.xml  /usr/local/tomcat/conf/server.xml
COPY docker/setenv.sh   /usr/local/tomcat/bin/setenv.sh    # 3
COPY target/gestorcitas.war /usr/local/tomcat/webapps/gc.war
ENV JAVA_OPTS="-Xms512m -Xmx2048m -Duser.timezone=Europe/Madrid"   # 4
HEALTHCHECK --interval=30s CMD curl -fsS http://localhost:8080/gc/salud.jsp || exit 1
  1. La versión exacta de Tomcat y de Java se fija en la primera línea, lo que responde para siempre a una pregunta que antes exigía entrar por SSH al servidor.
  2. Se borran las aplicaciones de ejemplo que trae la imagen, que son una superficie de ataque conocida. Es el mínimo privilegio de la 04-03 aplicado a lo que se hereda.
  3. La configuración del contenedor está en el repositorio y pasa por revisión, en lugar de vivir editada dentro de la máquina.
  4. La zona horaria explícita merece mención aparte: en un sistema de citas, que la JVM tome la zona del sistema anfitrión es una fuente clásica de bugs que solo aparecen al mover el servidor. Fijarla aquí es tapar un agujero que llevaba doce años abierto.

Los beneficios inmediatos no son de despliegue sino de desarrollo y pruebas: cualquiera levanta el sistema completo con docker compose up —aplicación más base de datos con datos sintéticos— y las pruebas de caracterización del incremento 3 pasan a ejecutarse en CI contra ese entorno efímero en lugar de contra un servidor compartido que a veces está caído. Ahí es cuando las ocho pruebas de humo pasan de ser un ritual manual a una puerta automática.

Una advertencia honesta: contenerizar un legacy no siempre sale bien. Si la aplicación escribe en rutas absolutas del disco, depende de una impresora local o guarda estado en el sistema de ficheros del servidor, el contenedor lo destapa y hay que resolverlo. Eso es bueno a medio plazo —es deuda que estaba escondida— pero puede convertir un incremento de dos semanas en uno de dos meses. Conviene explorarlo con una prueba de concepto antes de comprometer fechas.

  1. Incremento 7: strangler fig

El último incremento no moderniza el legacy: empieza a sacarle funcionalidad. El patrón de la higuera estranguladora consiste en poner un intermediario delante que enruta por camino, y mover funcionalidades una a una hacia servicios nuevos con CI/CD moderno, hasta que el viejo queda vacío o se queda con lo poco que no compensa mover.

flowchart LR
    U["Clientes"] --> R["Proxy inverso<br/>enruta por ruta"]
    R -->|"/gc/informes/*"| NEW["Servicio informes<br/>pipeline moderno"]
    R -->|"resto"| OLD["Gestor Citas 4<br/>Tomcat"]
    NEW --> BD[("BD compartida<br/>solo lectura")]
    OLD --> BD
location /gc/informes/ {
    proxy_pass http://informes-nuevo:8080/;    # 1
}
location /gc/ {
    proxy_pass http://tomcat-legacy:8080/;     # 2
}
  1. Una ruta concreta va al servicio nuevo, con su pipeline completo del módulo 2 y 3. Revertir es cambiar una línea de configuración del proxy y recargar: el rollback más barato posible, y es lo que hace el patrón seguro.
  2. Todo lo demás sigue en el legacy, sin enterarse.

Qué se saca primero, con criterio: lo que más cambia —si el informe de facturación se toca cada trimestre y todo lo demás está congelado, mover el informe da beneficio en cada cambio—, lo que tiene fronteras claras y poca lógica compartida, y lo que se puede leer sin escribir, porque un servicio de solo lectura sobre la misma base de datos evita el problema difícil de la doble escritura. Lo que no se saca primero: el núcleo de la agenda, que es lo más acoplado y lo que más riesgo tiene.

Y la trampa que hay que nombrar: la fase intermedia, con dos sistemas conviviendo, es más compleja que cualquiera de los dos extremos. Hay que decidir de antemano hasta dónde se llega y en qué plazo, porque un estrangulamiento a medias abandonado durante años es peor que el monolito original. Con un producto de tres clientes y contrato hasta 2028, la respuesta razonable en Gestor Citas 4 fue mover dos funcionalidades concretas y no más.

  1. Deuda concreta y sus tácticas

Deuda Síntoma Táctica
JAR sin repositorio público La build solo funciona con el lib/ de una máquina Subirlos a un repositorio interno (04-02) con su checksum; documentar origen y versión
Sin lockfile equivalente LATEST y rangos abiertos en el POM Fijar todas las versiones a valores exactos, incluidas las transitivas con dependencyManagement
Build dependiente de la máquina Rutas absolutas, GC_HOME, tareas de Ant locales Sustituir por rutas relativas al proyecto; el CI en máquina limpia lo verifica en cada commit
Secretos en ficheros versionados Contraseña de BD en gc.properties en git Rotar primero, luego extraer a secretos y plantilla; asumir que el histórico está comprometido
BD sin migraciones Carpeta con SQL suelto y orden por memoria Adoptar Flyway con baseline sobre el esquema existente
Sin entorno de pruebas fiable "El de pruebas" difiere de prod Contenerizar (incremento 6) y levantarlo efímero
Java 8 y librerías sin soporte Vulnerabilidades conocidas sin parche Escanear (04-03), priorizar por explotabilidad real, no por número

Dos merecen desarrollo, porque tienen una trampa concreta.

Secretos versionados. El instinto es borrar la línea y hacer commit. Eso no sirve de nada: la contraseña sigue en el historial de git y cualquiera con acceso al repositorio la recupera. El orden correcto es rotar primero —cambiar la contraseña en la base de datos y desplegar la nueva por el mecanismo del incremento 5—, luego extraer el valor a la plantilla, y solo entonces decidir si merece la pena reescribir el historial. Con un repositorio interno y acceso controlado, casi nunca compensa; lo que sí hay que hacer es añadir el escaneo de secretos de la 04-03 al CI para que no vuelva a entrar ninguno.

Base de datos sin migraciones. No se puede empezar de cero: hay un esquema en producción con datos de doce años. La adopción se hace con baseline:

# 1 · marcar el esquema actual como punto de partida, sin ejecutar nada
flyway -url="$JDBC_URL" -baselineVersion=4.2.0 \
       -baselineDescription="Esquema existente GC4" baseline
-- 2 · a partir de aquí, todo cambio es un fichero versionado
-- db/migration/V4.2.1__indice_citas_fecha.sql
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_citas_fecha ON CITAS(FECHA);
  1. baseline no toca el esquema: crea la tabla de control de Flyway y registra que la versión 4.2.0 "ya está aplicada". Es el equivalente exacto de la tabla migraciones_aplicadas de la 04-06, adoptada a mitad de camino.
  2. Desde ese punto, toda la disciplina de la 04-06 aplica sin cambios: ficheros versionados, revisión, ejecución desde el pipeline, CONCURRENTLY, lock_timeout y expand and contract para cualquier cambio incompatible. El esquema histórico no se documenta ni se recrea; simplemente se declara punto de partida y se avanza desde ahí. Un detalle práctico: conviene volcar el esquema actual (pg_dump --schema-only) al repositorio como V4.2.0__baseline.sql no ejecutable pero sí versionado, para que exista una referencia legible de por dónde se empezó.

  1. Métricas cuando la línea base es un despliegue cada dos meses

Las métricas DORA de la 01-05 siguen valiendo, pero comparar Gestor Citas 4 con Reservalia no informa de nada: son productos con propósitos distintos. La comparación útil es contra sí mismo.

Métrica Antes Tras incremento 2 Tras incremento 5 Objetivo 12 meses
Frecuencia de despliegue 1 / 2 meses 1 / 2 meses 2 / mes 1 / semana si hace falta
Lead time (commit → prod) ~45 días ~45 días 6 días 3 días
Change failure rate 33 % (2 de 6) 33 % 11 % < 15 %
Time to restore ~2 h, incierto 2 h 7 min 7 min
Duración del despliegue 3 h 3 h 7 min 7 min
"Compila en CI"
"Se puede reconstruir en máquina limpia"

Tres lecturas. Primera: el incremento 2 no movió ninguna métrica DORA y aun así fue el más valioso de todos, porque hizo posibles los demás. Por eso las dos últimas filas —binarias, no numéricas— aparecen en el cuadro: en un legacy, las métricas útiles de las primeras fases son capacidades adquiridas, no números. Medir solo DORA en esta etapa hace que el trabajo fundacional parezca inútil, y es la forma más rápida de que negocio lo cancele.

Segunda: un change failure rate del 11 % sería inaceptable en Reservalia y aquí es un éxito rotundo, porque venía del 33 %. Las métricas se comparan con la línea base propia, nunca con las de otro producto ni con las de la industria.

Tercera: el objetivo a doce meses no es "desplegar diez veces al día". Es "poder desplegar en una semana cuando haga falta". Para un producto en mantenimiento con tres clientes, la capacidad de responder rápido a un problema vale mucho más que la frecuencia, y confundir ambas cosas lleva a invertir donde no toca.

  1. La conversación con negocio

Ninguno de estos incrementos se financia hablando de tecnología. Lo que no funciona: "hay que meter CI/CD", "el código es un desastre", "tenemos deuda técnica". Son afirmaciones que quien decide el presupuesto no puede evaluar y suenan a preferencia del equipo.

Lo que sí funciona es traducir a riesgo, coste y tiempo de respuesta:

En vez de decir Decir
"No hay pruebas automatizadas" "Cada cambio nos cuesta medio día de pruebas manuales y aun así dos de los últimos seis despliegues fallaron"
"La build no es reproducible" "Si el portátil de quien construye se rompe, no podemos publicar una corrección urgente. Hoy no sabemos en cuánto tiempo lo recuperaríamos"
"Hay que automatizar el despliegue" "Pasamos de tres horas un sábado a siete minutos un martes; un fallo urgente se corrige el mismo día en lugar de esperar a la ventana"
"El sistema tiene deuda técnica" "Un cambio que en Reservalia cuesta dos días, aquí cuesta dos semanas. Con 4.200 €/mes de ingreso, cada petición se come el margen"
"Los secretos están en git" "La contraseña de la base de datos de un cliente es visible para cualquiera con acceso al repositorio; esto es notificable"

Y tres tácticas que en Gestor Citas 4 funcionaron. Vincular cada incremento a un incidente real ocurrido: el despliegue fallido de marzo que dejó a un cliente sin servicio cuatro horas justifica el incremento 5 mejor que cualquier argumento abstracto. Pedir porciones pequeñas y demostrables: dos semanas para el incremento 1, con un resultado enseñable —"aquí está el sistema construyéndose solo"— en lugar de un proyecto de seis meses. Y presentar el riesgo del "no hacer nada" con un número: si el contrato vale 4.200 €/mes hasta 2028, son unos 100.000 € en riesgo; treinta días de trabajo para reducir sustancialmente la probabilidad de perderlo es una decisión fácil cuando se plantea así.

Marta: "A negocio no le vendes un pipeline. Le vendes que el sábado por la noche no exista."

  1. Cuándo parar

La pregunta que casi ningún material sobre modernización responde. Gestor Citas 4 no va a tener despliegue continuo, ni canary, ni feature flags, ni observabilidad distribuida, y eso es una decisión correcta, no una rendición. El nivel de automatización razonable depende de tres variables:

Variable Gestor Citas 4 Reservalia
Frecuencia de cambio 1-2 al mes Diaria
Vida restante prevista 2 años (contrato 2028) Indefinida
Coste de un fallo 3 clientes, alto por cliente 340+ negocios
Personas que lo tocan 1, a tiempo parcial 3, a tiempo completo
Nivel razonable Hasta el incremento 5-6 Todo el módulo 4

La regla, formulada para llevársela: la automatización se justifica por la frecuencia de uso multiplicada por la vida restante. Un canary automático que evitaría un incidente cada dos años, en un sistema que se apaga en dos años, no se paga nunca. Un despliegue automatizado que se usa dos veces al mes durante dos años —48 usos, ahorrando casi tres horas cada uno— se paga en el segundo mes.

Las señales de que hay que parar son concretas: cuando el siguiente incremento cuesta más que el problema que resuelve; cuando la mejora afecta a algo que no cambia nunca —automatizar las pruebas de un módulo que nadie ha tocado en cinco años es trabajo tirado—; cuando existe una fecha de apagado creíble y firmada; y cuando el equipo empieza a modernizar por gusto técnico y no por un problema medible. Esa última es la más difícil de reconocer desde dentro, y por eso conviene que cada incremento nazca con su justificación escrita en términos del apartado 11: si no se puede redactar la frase de la columna derecha de aquella tabla, probablemente ese incremento no toca.

  1. Ficha del caso

Contexto Monolito Java 8/JSP de 2011, 240.000 líneas, sin pruebas, FTP manual, 3 clientes, 4.200 €/mes
Qué sigue valiendo tal cual Artefacto inmutable (02-06), entornos con revisores (03-02), smoke test (03-02), escaneo de seguridad (04-03), migraciones versionadas (04-06)
Decisión 1 No reescribir; no esperar a tener pruebas; empezar por la reproducibilidad
Decisión 2 Siete incrementos, cada uno con valor propio y dos paradas válidas declaradas
Decisión 3 Ocho pruebas de caracterización elegidas por ingreso, no por cobertura; golden master para lo no testable
Decisión 4 Despliegue automatizado pero disparado a mano con workflow_dispatch y revisores
Decisión 5 Flyway con baseline sobre el esquema existente; secretos rotados antes de extraerlos
Decisión 6 Strangler fig limitado a dos funcionalidades, con plazo escrito
Coste ~30 días de trabajo repartidos en 8 meses
Efecto en DORA Lead time 45 → 6 días; CFR 33 % → 11 %; restore 2 h → 7 min; despliegue 3 h → 7 min
Qué te llevas a cualquier proyecto El primer incremento no son las pruebas: es poder reconstruir el sistema y saber qué está desplegado

Errores Comunes y Consejos

Error 1: proponer la reescritura completa. Con 240.000 líneas sin especificación, es el proyecto que dura tres años y se cancela con dos sistemas a medias. Error 2: esperar a tener cobertura para montar CI; el código no es testable, refactorizarlo sin pruebas es lo que se quiere evitar, y el ciclo no se rompe solo. Error 3: empezar por las pruebas unitarias en lugar de por el humo de extremo a extremo, que es lo único que no exige tocar el código.

Error 4: corregir los bugs descubiertos durante la caracterización. Una prueba de caracterización documenta lo que hace el sistema, incluidos sus errores; corregirlos es otra tarea, con su decisión y su PR aparte. Error 5: borrar un secreto del fichero y hacer commit creyendo que se ha resuelto: sigue en el historial y sin rotar. Error 6: intentar el despliegue sin corte desde el principio, cuando 40 segundos de indisponibilidad son perfectamente aceptables en un sistema que se despliega dos veces al mes.

Error 7: medir solo DORA en las fases iniciales, con lo que el trabajo fundacional parece inútil y se cancela justo antes de dar fruto. Error 8: comparar las métricas del legacy con las del producto moderno en lugar de con su propia línea base. Error 9: dejar un strangler fig a medias sin plazo ni alcance escritos: la fase intermedia es más compleja que cualquiera de los dos extremos. Error 10: seguir modernizando por gusto técnico un sistema con fecha de apagado.

Consejo 1: el primer criterio de éxito es reconstruir el artefacto de producción desde el repositorio en una máquina limpia. Consejo 2: elige las pruebas de humo por ingreso, no por cobertura; ocho bien elegidas valen más que doscientas repartidas. Consejo 3: automatiza el despliegue antes que el disparo, que es donde está casi todo el beneficio. Consejo 4: escribe la justificación de negocio de cada incremento antes de empezarlo; si no sale la frase, ese incremento no toca.

Ejercicios

Ejercicio 1

Te incorporas a mantener Gestor Citas 4 y en la primera semana llega una petición urgente: un cliente necesita un campo nuevo en el informe de facturación trimestral para el cierre del mes que viene. No hay pruebas, la build solo funciona en un portátil que ya no existe y el último despliegue fue hace siete semanas. Describe qué haces en las dos primeras semanas, en qué orden y por qué, distinguiendo lo que es imprescindible para entregar la petición de lo que es inversión.

Ejercicio 2

Durante el incremento 4, el golden master del cálculo de facturación revela que un descuento por volumen se aplica dos veces cuando el cliente tiene más de tres sedes, lo que lleva ocurriendo al menos cuatro años. Uno de los tres clientes tiene cinco sedes. Analiza la situación técnica y de negocio y propón un plan de actuación completo.

Ejercicio 3

Tras completar el incremento 5, un compañero propone continuar hasta el final: contenerizar, montar despliegue continuo con canary, añadir observabilidad completa con SLO y presupuesto de error, y empezar el strangler fig sobre el módulo de agenda. Estima esfuerzo y beneficio de cada propuesta con los criterios del apartado 12 y da una recomendación priorizada.

Soluciones

Solución 1. La clave es separar entregar de mejorar, y aceptar que no se puede hacer todo. Con la petición encima, el orden que yo seguiría:

Días 1-3 — Reconstruir la build (imprescindible, no inversión). Sin poder generar un .war no hay entrega posible, así que esto no es opcional aunque parezca trabajo de fondo. Se recupera el código que corresponde a producción, se crea el repositorio, y se documenta cada obstáculo: los JAR huérfanos se suben a un repositorio interno, las rutas absolutas se sustituyen por relativas, la variable de entorno se declara en el POM. El criterio de parada es concreto: el .war construido tiene las mismas clases y recursos que el desplegado. Días 3-4 — CI que compila. Media jornada de trabajo, y a partir de ahí cada commit produce un artefacto identificado por SHA. Es barato y transforma el resto de las dos semanas: ya no hay dudas sobre qué se está construyendo. Días 4-6 — Golden master del informe, solo del informe. Aquí está la decisión más importante del ejercicio: como el cambio toca el cálculo de facturación, se congela su salida actual sobre veinte casos de datos anonimizados antes de tocar una línea. No es "inversión a futuro": es la única forma de saber que el campo nuevo no altera los importes existentes. Sin esto, la verificación sería que alguien mire un PDF y le parezca bien. Días 6-9 — Implementar el cambio, con el golden master ejecutándose en cada iteración. Cualquier diferencia distinta del campo nuevo aparece como un diff exacto. Cuando el cambio es correcto, se actualiza el fichero de referencia a propósito, en un commit separado y con el diff visible. Días 9-11 — Automatizar el despliegue. El primer despliegue tras siete semanas es el momento de mayor riesgo del año, así que scriptar workflow_dispatch + copia de seguridad + smoke test se paga en esta misma entrega. Antes se ensaya al menos una vez contra el entorno de pruebas. Días 11-12 — Desplegar un martes por la tarde, con el smoke test verificando y la vuelta atrás probada de antemano.

Qué es imprescindible y qué es inversión. Imprescindible: la build reproducible y el golden master del informe. Inversión que se paga dentro de estas mismas dos semanas: el CI y el despliegue automatizado. Inversión que no hago ahora y dejo escrita para después: las ocho pruebas de humo completas, la contenerización, la extracción de secretos —salvo que encuentre uno crítico, en cuyo caso lo roto de inmediato— y cualquier refactor del código de negocio. Y una regla que aplicaría desde el primer día: nada de esto se hace en la misma rama que el cambio funcional, para que la entrega no dependa de que la modernización salga bien.

Solución 2. Técnicamente, el descubrimiento es la prueba de que el golden master funciona: ha encontrado en una tarde un error que llevaba cuatro años sin detectarse. Pero el error de bulto sería corregirlo ahí mismo. Un bug de facturación de cuatro años no es una decisión técnica.

Paso 1 — Cuantificar antes de decidir nada. Con una consulta sobre los datos históricos: qué clientes tienen más de tres sedes, en cuántos trimestres se ha aplicado el descuento duplicado y cuánto suma la diferencia. Sin ese número no se puede tomar ninguna decisión, y la conversación degenera en opiniones. Supongamos el resultado plausible: un cliente afectado, dieciséis trimestres, unos 9.400 € facturados de menos —el descuento duplicado favorece al cliente—.

Paso 2 — Congelar el comportamiento actual en el golden master, con un comentario explícito. El fichero de referencia se genera con el bug incluido y se añade una nota en el código de la prueba: qué es, desde cuándo, y que su corrección está pendiente de decisión de negocio. Esto permite seguir trabajando en el módulo sin arrastrar una prueba en rojo, que es lo que acabaría con todo el mundo ignorándola.

Paso 3 — Escalarlo con los datos. Va a negocio y a quien lleve la relación con el cliente, no se decide en el equipo. Las opciones, con sus consecuencias: (a) corregir hacia delante y no reclamar lo pasado —lo más habitual: el importe es asumible, evita una conversación desagradable con un cliente que aporta un tercio de los ingresos del producto, y hay que valorar si legalmente se puede reclamar retroactivamente lo que se facturó de menos—; (b) corregir y regularizar, que exige revisión legal y contable; (c) no corregir, que solo es defendible si se documenta como comportamiento acordado, y es la peor opción porque el bug volverá a aparecer en cualquier revisión futura.

Paso 4 — Ejecutar la decisión como un cambio de comportamiento explícito. Sea cual sea, la corrección va en un PR propio cuyo contenido principal es el diff del fichero golden, que muestra exactamente qué importes cambian y en qué casos. Ese diff es la evidencia que se enseña a negocio para aprobar, y queda en el historial con fecha, autor y motivo. Se despliega con el mecanismo del incremento 5 y se verifica sobre un caso real conocido antes del siguiente cierre trimestral.

Paso 5 — Aprender del hallazgo. Si un error de facturación ha sobrevivido cuatro años, la pregunta interesante es qué más hay. Merece la pena ampliar los casos del golden master a los escenarios menos frecuentes —prorrateos, altas y bajas a mitad de trimestre, cambios de tarifa— antes de que los descubra un cliente. Es el mejor retorno disponible en este sistema y sale prácticamente gratis, porque la maquinaria ya está montada.

Solución 3. Aplicando frecuencia de uso × vida restante, con dos años de contrato por delante y 1-2 despliegues al mes:

Propuesta Esfuerzo Beneficio en este contexto Veredicto
Contenerizar 2-4 semanas (riesgo de rutas absolutas) Entorno reproducible; pruebas de humo automáticas en CI; elimina "el servidor mágico" Sí, prioridad 1
Ocho pruebas de humo en CI 1-2 semanas Verificación automática en cada cambio; es lo que falta para que el incremento 5 rinda Sí, prioridad 2
Despliegue continuo con canary 4-6 semanas + infraestructura nueva Se usaría ~24 veces en dos años; requiere segundo servidor y balanceador; 40 s de corte ya son aceptables No
Observabilidad completa con SLO y presupuesto de error 3-5 semanas Un presupuesto de error gobierna la cadencia de despliegue… que aquí es de 2 al mes: no gobierna nada No — pero sí una versión mínima
Strangler fig sobre la agenda 3-6 meses, alto riesgo La agenda es el núcleo más acoplado; con dos años de vida no se amortiza No

Recomendación priorizada. Primero contenerizar, porque desbloquea lo demás: sin un entorno levantable no hay pruebas de humo fiables en CI, y además elimina el riesgo de que el servidor irreproducible se pierda, que es la amenaza real de continuidad de este producto. Con una prueba de concepto de tres días antes de comprometer plazo, por lo que la lección advierte sobre rutas absolutas y estado en disco. Segundo, las ocho pruebas de humo ejecutándose en CI contra ese entorno efímero: es lo que convierte el despliegue del incremento 5 en un despliegue verificado, y es el complemento natural de lo ya hecho. Tercero, una versión mínima de observabilidad que no es la de la 03-06: métricas básicas de disponibilidad y latencia, alerta por síntoma —"la aplicación no responde"—, y retención de logs. Cuesta días, no semanas, y responde a la pregunta que hoy no tiene respuesta: cuánto tardamos en enterarnos de que está caído. Se descartan el canary, el presupuesto de error y el strangler fig sobre la agenda, y merece la pena escribir el motivo en la decisión para que no vuelva a plantearse cada trimestre. La frase que resume el criterio: el objetivo de este producto no es desplegar más, es poder desplegar bien cuando haga falta, y a partir del incremento 6 esa capacidad ya está conseguida.

Conclusión y cierre del módulo

Gestor Citas 4 ha sido el caso que más incomoda porque quita todas las condiciones favorables a la vez, y precisamente por eso enseña lo que los otros tres no pueden. La primera lección es de orden: no se empieza por las pruebas, se empieza por la reproducibilidad, porque el ciclo "hace falta refactorizar para poder probar, y probar para poder refactorizar" no se rompe desde dentro, y porque el problema más urgente no era la falta de cobertura sino que nadie sabía reconstruir el sistema. La segunda es de estructura: siete incrementos, cada uno con valor propio aunque el siguiente no llegue nunca, y dos paradas válidas declaradas de antemano. La tercera es de técnica: humo de extremo a extremo antes que unitarias, golden master para congelar lo que no admite prueba —incluidos sus bugs, que se corrigen aparte y a propósito—, despliegue automatizado aunque siga disparándose a mano, contenerización para convertir un servidor irreproducible en un fichero versionado, y strangler fig con alcance y plazo escritos. La cuarta es de medida: en las primeras fases las métricas útiles son capacidades binarias —"compila en CI", "se reconstruye en máquina limpia"— y no números DORA, y cuando los números llegan se comparan con la línea base propia y con nadie más. Y la quinta es la que más proyectos salva: saber cuándo parar, porque la automatización se paga con la frecuencia de uso multiplicada por la vida restante, y en un producto con fecha de caducidad hay un nivel a partir del cual seguir es gusto técnico y no ingeniería.

Con esto se cierra el módulo. Cuatro contextos que no se parecen entre sí, sometidos al mismo sistema:

Web (05-01) Móvil (05-02) Microservicios (05-03) Legacy (05-04)
Unidad desplegable Ficheros estáticos Binario firmado en tienda 5 servicios independientes Un .war
Quién controla la entrega El usuario y la tienda Cada equipo Tú, un sábado
Rollback Repuntar HTML: <1 min No existe: solo hacia delante Uno por servicio workflow_dispatch: 7 min
Puerta característica Presupuesto de bundle, Lighthouse Escalado con pausa por crash-free can-i-deploy "Compila"
Problema de compatibilidad Pestañas de ayer Decenas de versiones vivas Contratos entre pares y eventos Ninguno: un cliente, el propio
Coste dominante Puertas de calidad Runners macOS y revisión 2 personas de plataforma Arqueología inicial
Lo que más cambió Configuración a runtime Detectar antes en vez de revertir Comprar la señal con contratos El orden de los incrementos

Y lo que fue invariante en los cuatro, que es la respuesta a qué llevarse cuando el contexto no se parezca a ninguno de los vistos:

  • Artefacto inmutable, construido una vez y promocionado. Un dist/ con hash, un AAB firmado, cinco imágenes por digest o un .war con el SHA del commit: en los cuatro casos se construye una vez y se mueve, nunca se reconstruye para desplegar.
  • Trazabilidad de qué está desplegado y de dónde salió. El pie con la versión en la web, el versionName en el informe de crash, git log del repositorio de despliegues, el artefacto identificado por SHA del legacy. La primera pregunta de cualquier incidente es "¿qué ha cambiado?", y siempre tuvo respuesta objetiva.
  • Feedback lo más rápido y lo más a la izquierda posible. Cuando el canal de entrega es lento —tienda, revisión, ventana del sábado— la inversión no se hace en recuperar rápido sino en detectar antes: beta, escalado, contratos, humo automático, presupuestos.
  • Todo como código y bajo revisión. Workflows, plantillas, infraestructura, esquemas de eventos, configuración de entorno, migraciones. En ningún caso quedó nada editado a mano en un servidor, y donde lo había, quitarlo fue uno de los primeros incrementos.
  • Compatibilidad hacia atrás como disciplina permanente. Pestañas abiertas, apps de hace un año, consumidores que aún no migraron: la forma cambia, la obligación no. Expand and contract (04-06) apareció en los cuatro casos con distinto disfraz.

Lo específico, en cambio, fue todo lo demás: dónde vive la configuración, si hay rollback o solo roll-forward, si la puerta es un presupuesto de bundle o un can-i-deploy, si el destino es despliegue continuo o un workflow_dispatch disparado a mano. Y esa es la conclusión del módulo: los principios no se negocian, las decisiones se toman mirando el contexto, y saber distinguir unos de otras es lo que separa aplicar CI/CD de copiar el pipeline de otro.

Con los principios asentados y cuatro contextos recorridos, lo que falta es el detalle de la maquinaria. Hasta aquí la herramienta ha sido casi siempre GitHub Actions, con incursiones puntuales cuando el caso lo pedía, y siempre como medio y no como tema. El módulo 6, Herramientas y Tecnologías, invierte el foco: baja al detalle de Jenkins, GitLab CI/CD, CircleCI, Travis CI, Docker y Kubernetes y GitHub Actions a fondo, cada uno con su modelo de ejecución, su sintaxis, sus puntos fuertes y sus límites reales, y termina con una comparativa y unos criterios para elegir. Empieza por Jenkins, que es el que más veces te encontrarás ya instalado cuando llegues a un proyecto, y el que mejor explica por qué el resto de herramientas se diseñaron como se diseñaron.

Curso de CI/CD: Integración y Despliegue Continuo

Módulo 1: Introducción a CI/CD

Módulo 2: Integración Continua (CI)

Módulo 3: Despliegue Continuo (CD)

Módulo 4: Prácticas Avanzadas de CI/CD

Módulo 5: Implementación de CI/CD en Proyectos Reales

Módulo 6: Herramientas y Tecnologías

Módulo 7: Ejercicios Prácticos

Módulo 8: Recursos Adicionales

© Copyright 2026. Todos los derechos reservados