Els tres casos anteriors compartien una xarxa de seguretat que la lliçó anterior va assenyalar en acomiadar-se: eren sistemes moderns, amb proves, amb control de versions sa i amb equips que podien decidir com treballar. Aquí es treu aquesta xarxa. Gestor Citas 4 és el producte anterior de l'empresa, el que Reservalia va venir a substituir: un monòlit Java/JSP construït amb Ant i migrat a mitges a Maven el 2011, desplegat sobre un Tomcat que viu en una màquina virtual amb nom propi, sense ni una sola prova automatitzada, amb desplegament manual per FTP els dissabtes a la nit, la configuració editada a mà directament al servidor, i tres clients grans que encara paguen 4.200 euros al mes per ell i no migraran en dos anys. Ningú no el vol tocar i s'ha de tocar. Aquesta lliçó no tracta de triar entre monorepo i polyrepo ni d'afinar un canari: tracta de decidir per on es comença quan no hi ha res, en quin ordre construir els increments perquè cadascun aporti valor per si sol encara que el següent no arribi mai, quines tàctiques concretes aplicar a cada tipus de deute, com es mesura el progrés quan la línia base és un desplegament cada dos mesos, com es justifica la inversió davant de qui la paga, i —el més oblidat— quan parar.

Contingut

  1. Gestor Citas 4: l'inventari honest
  2. Per què no es reescriu i per què «primer les proves» no arriba mai
  3. L'estratègia d'increments
  4. Increments 1 i 2: control de versions i construcció a CI
  5. Increments 3 i 4: caracterització i golden master
  6. Increment 5: desplegament automatitzat encara que continuï sent manual
  7. Increment 6: contenidoritzar per reproduir l'entorn
  8. Increment 7: strangler fig
  9. Deute concret i les seves tàctiques
  10. Mètriques quan la línia base és un desplegament cada dos mesos
  11. La conversa amb negoci
  12. Quan parar
  13. Fitxa del cas
  14. Errors Comuns i Consells
  15. Exercicis
  16. Conclusió i tancament del mòdul

  1. Gestor Citas 4: l'inventari honest

Reservalia (mòduls 2-4) Gestor Citas 4
Codi TypeScript, monorepo Java 8, JSP, Struts 1, 240.000 línies
Construcció npm ci, reproduïble Ant + Maven a mitges, només funciona en una màquina
Proves Unitàries, integració, E2E Cap d'automatitzada
Desplegament cd.yml, 12 min, 12/setmana FTP manual, dissabte a la nit, ~1 cada 2 mesos
Configuració Terraform + gestor de secrets Fitxers .properties editats al servidor
Entorns dev, staging, prod idèntics Prod, i «el de proves» que ningú no sap si coincideix
Base de dades Migracions versionades Scripts SQL solts en una carpeta compartida
Qui el coneix Tot l'equip Una persona, i ja no treballa aquí
Ingressos 340 negocis 3 clients, 4.200 €/mes, contracte fins al 2028

L'última fila és la que fa que aquesta lliçó existeixi. Un sistema sense proves i amb desplegament manual que no dóna diners s'apaga; un que sí que en dóna i amb contracte signat fins al 2028 cal mantenir-lo en condicions. I la penúltima explica per què fa tanta por: no hi ha ningú a qui preguntar, així que el codi és l'única documentació i qualsevol canvi es fa a cegues.

El que avui passa quan cal canviar alguna cosa: dos dies de desenvolupament, mig dia de proves manuals d'algú que obre l'aplicació i clica, i un dissabte a la nit de tres hores en què es puja un .war per FTP, es reinicia Tomcat i es resa. En els últims dotze mesos, dos dels sis desplegaments van fallar i es va haver de restaurar copiant manualment el .war anterior des d'una carpeta anomenada backup_ok_final.

  1. Per què no es reescriu i per què «primer les proves» no arriba mai

La reescriptura és la resposta que tothom proposa i gairebé ningú no acaba. Els números de Gestor Citas 4: 240.000 línies, dotze anys de regles de negoci acumulades, cap especificació escrita, i comportaments que els tres clients fan servir cada dia sense que ningú sàpiga que existeixen —l'informe de facturació trimestral que un client exporta en un format concret, la integració per SFTP amb el sistema comptable d'un altre—. Una reescriptura hauria de reproduir tot allò a cegues, i mentre dura, el sistema vell continua necessitant canvis que cal fer dues vegades. És l'escenari clàssic en què un projecte de nou mesos es converteix en tres anys i es cancel·la amb les dues versions a mitges.

La segona resposta habitual també falla, i de manera més subtil: «primer escrivim proves, i quan hi hagi cobertura muntem CI/CD». No arriba mai, per tres raons concretes. El codi no és testable —lògica de negoci dins de JSP, new de connexions a base de dades dins dels mètodes, estàtics per tot arreu—, així que escriure la primera prova unitària exigeix refactoritzar, i refactoritzar sense proves és exactament el que es volia evitar. És un cicle tancat. A més, escriure proves no produeix cap benefici visible per a negoci, així que la tasca perd sempre contra qualsevol petició d'un client que paga 4.200 euros al mes. I finalment, el problema més urgent no és la manca de proves: és que ningú no sap reconstruir el sistema, i això s'arregla abans i més barat.

D'aquí ve la inversió de l'ordre que governa tota la lliçó: no es comença per les proves, es comença per la reproduïbilitat. Cada increment ha de complir tres condicions —aportar valor per si sol encara que el següent no es faci mai, no requerir tocar el codi de negoci, i ser reversible— i ordenar-se per relació entre valor i risc.

  1. L'estratègia d'increments

flowchart TD
    I1["1 · Control de versions<br/>i construccio en local"] --> I2["2 · Construccio a CI<br/>compila = primer senyal"]
    I2 --> I3["3 · Caracteritzacio<br/>fum E2E sobre el que dona diners"]
    I3 --> I4["4 · Golden master<br/>per al que no es pot provar"]
    I4 --> I5["5 · Desplegament automatitzat<br/>encara que es dispari a ma"]
    I5 --> I6["6 · Contenidoritzar<br/>reproduir l entorn"]
    I6 --> I7["7 · Strangler fig<br/>treure funcionalitat fora"]
    I2 -.->|"valor: ja es pot parar aqui"| P1["Parada valida"]
    I5 -.->|"valor: ja es pot parar aqui"| P2["Parada valida"]

Les dues «parades vàlides» són deliberades i hi tornarem a l'apartat 12: hi ha productes per als quals arribar a l'increment 2 ja és un èxit, i molts per als quals el 5 és la destinació final raonable. Un pla de modernització que només aporta valor si es completa sencer és un pla que fracassarà, perquè la prioritat canviarà abans d'acabar-lo.

  1. Increments 1 i 2: control de versions i construcció a CI

Increment 1 — Ficar-lo a git i reconstruir-lo fora de «la màquina». El punt de partida real va ser una carpeta en un disc de xarxa amb GestorCitas_v4_2_FINAL, GestorCitas_v4_2_FINAL_2 i GestorCitas_v4_2_parcheJulio. La feina, dues setmanes d'arqueologia: identificar quina versió correspon al .war que hi ha en producció —comparant dates i descompilant dues classes dubtoses—, crear el repositori amb aquesta com a primer commit, i reconstruir la construcció en una màquina neta, anotant cada cosa que falta. Aquí surt la primera llista de deute real: tres JAR que no són en cap repositori públic i estaven només al lib/ local, una variable d'entorn GC_HOME que algú va posar a mà fa anys, i una tasca d'Ant que copia fitxers des d'una ruta absoluta del disc C.

El criteri d'èxit d'aquest increment és exacte i verificable: el .war construït des del repositori en una màquina neta té el mateix contingut funcional que el que hi ha en producció. Comparar-lo sencer byte a byte no val —les marques de temps dins del ZIP difereixen sempre—, així que es comparen les llistes de classes i els hashos dels recursos. Quan això quadra, s'ha recuperat la propietat més valuosa que s'havia perdut: saber quin codi s'està executant.

Increment 2 — Que el construeixi el CI. Sense ni una sola prova:

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 i empaquetar
        run: mvn -B -s .mvn/settings.xml clean package -DskipTests         # 2
      - name: Publicar l'artefacte
        uses: actions/upload-artifact@v4
        with:
          name: gc4-${{ github.sha }}                                      # 3
          path: target/gestorcitas.war
          retention-days: 90
  1. Java 8 explícit, perquè el codi no compila amb versions posteriors i descobrir-ho amb un error críptic costa una tarda. Actualitzar la versió de Java és un projecte a part, no un requisit per tenir CI.
  2. -s .mvn/settings.xml apunta al repositori intern on s'han pujat els tres JAR orfes (apartat 9). -B desactiva el mode interactiu, que a CI penja el job esperant una resposta que ningú no donarà.
  3. El .war es publica com a artefacte amb el SHA del commit. Això és artefacte immutable i traçable (02-06) sense haver canviat ni una línia del producte: per primera vegada en dotze anys es pot respondre a «quina versió hi ha en producció?» amb un identificador en lloc d'una data.

Sembla poc i és moltíssim. «Compila» és el primer senyal automàtic que aquest projecte té a la seva vida, i atrapa des del primer dia la mena d'error que abans es descobria el dissabte a la nit a les onze. A Gestor Citas 4 va trobar, els dos primers mesos, tres commits que no compilaven perquè algú havia editat un JSP directament al servidor i després l'havia copiat al repositori a mà.

  1. Increments 3 i 4: caracterització i golden master

Ara sí que arriben les proves, i arriben per on ningú no espera: no per baix amb unitàries, sinó per dalt amb fum d'extrem a extrem. El motiu és que les proves de dalt no exigeixen tocar el codi, i tocar el codi és justament el que encara no es pot fer amb seguretat. La piràmide de la 02-04 continua sent l'objectiu a llarg termini, però en un legacy es comença per la punta i es baixa.

Les proves de caracterització tenen un propòsit diferent de les proves normals: no verifiquen que el sistema fa el correcte, verifiquen que fa exactament el que feia abans. Si hi ha un comportament estrany, la prova el documenta tal qual, inclòs el bug, perquè un client pot portar vuit anys depenent-ne.

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

    driver.get(BASE + "/agenda.do?data=2026-10-25");
    driver.findElement(By.cssSelector("td[data-hora='10:30'] a")).click();  // 1
    driver.findElement(By.name("nomClient")).sendKeys("Client Fictici 1");
    driver.findElement(By.name("desar")).click();

    assertTrue(driver.getPageSource().contains("Cita desada correctament"));    // 2
    assertEquals(1, comptarFiles("SELECT 1 FROM CITES WHERE HORA='10:30'"));    // 3
}
  1. Selectors per atributs de dades existents, perquè l'HTML generat per JSP és fràgil i no es redissenyarà. En un legacy no es refactoritza la vista per fer-la testable; es pren tal com és.
  2. Es comprova el text exacte que veu l'usuari, encara que sigui lleig. La prova documenta el comportament real, no el desitjable.
  3. Es verifica també l'efecte a la base de dades, perquè en aquest sistema hi ha lògica dins de procediments emmagatzemats i una comprovació només per interfície se'n deixa la meitat.

Quantes? Entre sis i deu, i triades per diners, no per cobertura. A Gestor Citas 4: iniciar sessió, crear una cita, cancel·lar-la, el llistat del dia, l'informe de facturació trimestral —el que un client exporta cada tres mesos i que si es trenca genera una trucada del director— i l'exportació per SFTP. Amb vuit proves es cobreix el que fa que tres clients paguin; la cobertura de línies resultant serà de l'11 % i no importa gens ni mica.

Increment 4 — Golden master per al que no admet prova unitària. El càlcul de facturació són 1.800 línies en una classe amb estàtics, accés directe a base de dades i System.out. Aïllar-la per provar-la exigeix refactoritzar-la; refactoritzar-la sense proves és el que fa por. La tècnica que trenca el cicle:

@Test
public void factura_trimestral_no_canvia() throws Exception {
    for (String cas : llistarCasos("src/test/recursos/casos/")) {       // 1
        carregarDades(cas);
        String sortida = FacturacioLegacy.generarInforme(2026, 3);      // 2
        String esperat = llegir("src/test/recursos/golden/" + cas + ".txt");
        assertEquals("Cas " + cas, normalitzar(esperat), normalitzar(sortida));  // 3
    }
}
  1. Molts casos d'entrada, capturats de dades reals anonimitzades (04-06): trenta escenaris que cobreixen descomptes, prorrateigs, mesos partits i els casos rars que van aparèixer en dotze anys.
  2. La sortida completa es compara amb un fitxer de referència generat amb el codi actual. Ningú no ha llegit les 1.800 línies ni ha decidit què és correcte: es congela el que fa avui.
  3. normalitzar elimina el que canvia entre execucions —data d'emissió, número d'informe— perquè si no la prova falla sempre. És el mateix emmascarament que a la regressió visual de la 05-01.

A partir d'aquí, la classe es pot refactoritzar amb una xarxa de debò: qualsevol canvi de comportament produeix un diff exacte de la línia afectada. I un advertiment important: el golden master congela també els bugs. Quan se'n descobreix un i es decideix corregir-lo, s'actualitza el fitxer de referència expressament i en un PR a part, amb el diff visible a la revisió. Aquest diff és la millor documentació de comportament que aquest sistema ha tingut mai.

  1. Increment 5: desplegament automatitzat encara que continuï sent manual

Aquí hi ha el retorn més gran per unitat d'esforç de tota la lliçó, i la idea que més costa acceptar: automatitzar el desplegament no significa desplegar automàticament. Es pot tenir un desplegament enterament scriptat, repetible i amb tornada enrere que el continuï disparant una persona un dissabte. Es conserven la revisió i el control humans i desapareix la part que falla: l'execució manual.

Comparem el dissabte d'abans amb el nou:

Abans Després
Qui decideix Una persona La mateixa persona
Com s'executa FTP, clics, reinici a mà workflow_dispatch amb el SHA a desplegar
Què es desplega El .war que hi havia a la carpeta L'artefacte exacte que va construir el CI
Còpia prèvia A vegades, amb nom inventat Sempre, versionada i amb retenció
Configuració Editada a mà al servidor Plantilla + valors per entorn
Tornada enrere Copiar el .war vell, si es troba Un workflow_dispatch amb el SHA anterior
Durada 3 hores 7 minuts
Verificació Obrir la web i mirar Smoke test automàtic
name: gc4-desplegar
on:
  workflow_dispatch:                                   # 1 · el dispara una persona
    inputs:
      sha:    { description: 'SHA a desplegar', required: true }
      entorn: { description: 'proves | prod', required: true, default: proves }

jobs:
  desplegar:
    runs-on: ubuntu-22.04
    environment: gc4-${{ inputs.entorn }}              # 2 · revisors per a prod
    steps:
      - uses: actions/download-artifact@v4             # 3 · no es reconstrueix
        with: { name: gc4-${{ inputs.sha }}, github-token: '${{ secrets.GITHUB_TOKEN }}',
                run-id: '${{ inputs.sha }}' }

      - name: Còpia de seguretat del war actual
        run: |
          ssh "$HOST" "cp /opt/tomcat/webapps/gc.war \
            /opt/copies/gc-\$(date +%F-%H%M).war"      # 4

      - name: Generar configuració des de 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 i reiniciar
        run: |
          scp gestorcitas.war "$HOST:/opt/tomcat/webapps/gc.war.nou"
          scp gc.properties   "$HOST:/opt/gc/conf/gc.properties"
          ssh "$HOST" "systemctl stop tomcat && \
                       mv /opt/tomcat/webapps/gc.war.nou /opt/tomcat/webapps/gc.war && \
                       systemctl start tomcat"          # 6

      - name: Smoke test
        run: |
          for i in $(seq 1 30); do
            curl -fsS "https://$DOMINI/gc/salut.jsp" && exit 0     # 7
            sleep 5
          done
          echo "El servei no ha respost després de 150 s"; exit 1
  1. workflow_dispatch manté la decisió en mans d'una persona. És el pas intermedi entre el manual i el desplegament continu, i per a aquest producte pot ser la destinació final.
  2. L'entorn de GitHub amb revisors (03-02) substitueix el «avisa la Marta per Slack abans de pujar». Queda registrat qui va aprovar i quan.
  3. Es descarrega l'artefacte, no es reconstrueix. Encara que el pipeline sigui rudimentari, la regla de construir una vegada (02-06) s'aplica des del primer dia: és gratis i elimina tota una classe de sorpreses.
  4. La còpia de seguretat forma part del desplegament, amb nom determinista i no backup_ok_final. És el que fa que la tornada enrere sigui real.
  5. La configuració es genera des d'una plantilla versionada i els valors vénen de secrets. Amb això desapareix el .properties editat a mà al servidor, que era la causa que «el de proves» no s'assemblés a producció.
  6. Aturar, substituir, arrencar. No és elegant i hi ha uns 40 segons de tall, però és determinista. Un desplegament sense indisponibilitat per a aquest sistema exigiria un segon servidor i un balancejador; és una millora posterior, no un requisit per automatitzar. Aquí és on convé resistir la temptació de fer-ho tot bé a la primera.
  7. El smoke test amb reintents (03-02) converteix «obrir la web i mirar» en una verificació objectiva que a més decideix si el job acaba en verd.

Resultat mesurat a Gestor Citas 4: el desplegament va passar de tres hores a set minuts, la tornada enrere de «mitja hora buscant el .war correcte» a un workflow_dispatch de set minuts, i els dissabtes a la nit van deixar d'existir perquè un desplegament de set minuts amb tornada enrere verificada es pot fer un dimarts a les cinc de la tarda. Aquesta última conseqüència és la que l'equip va notar de debò.

  1. Increment 6: contenidoritzar per reproduir l'entorn

Amb el desplegament automatitzat queda un problema obert: el servidor és únic i irreproduïble. Ningú no sap quina versió de Tomcat corre, quins paràmetres de JVM té, ni què s'hi va instal·lar a mà el 2019. Ficar el legacy en una imatge no el modernitza, però converteix «el servidor» en un fitxer versionat.

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/salut.jsp || exit 1
  1. La versió exacta de Tomcat i de Java es fixa a la primera línia, cosa que respon per sempre una pregunta que abans exigia entrar per SSH al servidor.
  2. S'esborren les aplicacions d'exemple que porta la imatge, que són una superfície d'atac coneguda. És el mínim privilegi de la 04-03 aplicat al que s'hereta.
  3. La configuració del contenidor és al repositori i passa per revisió, en lloc de viure editada dins de la màquina.
  4. La zona horària explícita mereix menció a part: en un sistema de cites, que la JVM prengui la zona del sistema amfitrió és una font clàssica de bugs que només apareixen en moure el servidor. Fixar-la aquí és tapar un forat que portava dotze anys obert.

Els beneficis immediats no són de desplegament sinó de desenvolupament i proves: qualsevol aixeca el sistema complet amb docker compose up —aplicació més base de dades amb dades sintètiques— i les proves de caracterització de l'increment 3 passen a executar-se a CI contra aquell entorn efímer en lloc de contra un servidor compartit que a vegades està caigut. Aleshores és quan les vuit proves de fum passen de ser un ritual manual a una porta automàtica.

Un advertiment honest: contenidoritzar un legacy no sempre surt bé. Si l'aplicació escriu en rutes absolutes del disc, depèn d'una impressora local o desa estat al sistema de fitxers del servidor, el contenidor ho destapa i cal resoldre-ho. Això és bo a mitjà termini —és deute que estava amagat— però pot convertir un increment de dues setmanes en un de dos mesos. Convé explorar-ho amb una prova de concepte abans de comprometre dates.

  1. Increment 7: strangler fig

L'últim increment no modernitza el legacy: comença a treure-li funcionalitat. El patró de la figuera estranguladora consisteix a posar un intermediari al davant que enruta per camí, i moure funcionalitats una a una cap a serveis nous amb CI/CD modern, fins que el vell queda buit o es queda amb el poc que no compensa moure.

flowchart LR
    U["Clients"] --> R["Proxy invers<br/>enruta per ruta"]
    R -->|"/gc/informes/*"| NEW["Servei informes<br/>pipeline modern"]
    R -->|"resta"| OLD["Gestor Citas 4<br/>Tomcat"]
    NEW --> BD[("BD compartida<br/>nomes lectura")]
    OLD --> BD
location /gc/informes/ {
    proxy_pass http://informes-nou:8080/;      # 1
}
location /gc/ {
    proxy_pass http://tomcat-legacy:8080/;     # 2
}
  1. Una ruta concreta va al servei nou, amb el seu pipeline complet dels mòduls 2 i 3. Revertir és canviar una línia de configuració del proxy i recarregar: el rollback més barat possible, i és el que fa segur el patró.
  2. Tota la resta continua al legacy, sense assabentar-se'n.

Què se'n treu primer, amb criteri: el que més canvia —si l'informe de facturació es toca cada trimestre i tota la resta està congelada, moure l'informe dóna benefici a cada canvi—, el que té fronteres clares i poca lògica compartida, i el que es pot llegir sense escriure, perquè un servei de només lectura sobre la mateixa base de dades evita el problema difícil de la doble escriptura. El que no se'n treu primer: el nucli de l'agenda, que és el més acoblat i el que té més risc.

I la trampa que cal anomenar: la fase intermèdia, amb dos sistemes convivint, és més complexa que qualsevol dels dos extrems. Cal decidir per endavant fins on s'arriba i en quin termini, perquè un estrangulament a mitges abandonat durant anys és pitjor que el monòlit original. Amb un producte de tres clients i contracte fins al 2028, la resposta raonable a Gestor Citas 4 va ser moure dues funcionalitats concretes i no més.

  1. Deute concret i les seves tàctiques

Deute Símptoma Tàctica
JAR sense repositori públic La construcció només funciona amb el lib/ d'una màquina Pujar-los a un repositori intern (04-02) amb el seu checksum; documentar-ne origen i versió
Sense lockfile equivalent LATEST i rangs oberts al POM Fixar totes les versions a valors exactes, incloses les transitives amb dependencyManagement
Construcció dependent de la màquina Rutes absolutes, GC_HOME, tasques d'Ant locals Substituir per rutes relatives al projecte; el CI en màquina neta ho verifica a cada commit
Secrets en fitxers versionats Contrasenya de BD a gc.properties a git Rotar primer, després extreure a secrets i plantilla; assumir que l'històric està compromès
BD sense migracions Carpeta amb SQL solt i ordre de memòria Adoptar Flyway amb baseline sobre l'esquema existent
Sense entorn de proves fiable «El de proves» difereix de prod Contenidoritzar (increment 6) i aixecar-lo efímer
Java 8 i llibreries sense suport Vulnerabilitats conegudes sense pedaç Escanejar (04-03), prioritzar per explotabilitat real, no per número

Dos mereixen desenvolupament, perquè tenen una trampa concreta.

Secrets versionats. L'instint és esborrar la línia i fer commit. Això no serveix de res: la contrasenya continua a l'historial de git i qualsevol amb accés al repositori la recupera. L'ordre correcte és rotar primer —canviar la contrasenya a la base de dades i desplegar la nova pel mecanisme de l'increment 5—, després extreure el valor a la plantilla, i només aleshores decidir si val la pena reescriure l'historial. Amb un repositori intern i accés controlat, gairebé mai no compensa; el que sí que cal fer és afegir l'escaneig de secrets de la 04-03 al CI perquè no n'hi torni a entrar cap.

Base de dades sense migracions. No es pot començar de zero: hi ha un esquema en producció amb dades de dotze anys. L'adopció es fa amb baseline:

# 1 · marcar l'esquema actual com a punt de partida, sense executar res
flyway -url="$JDBC_URL" -baselineVersion=4.2.0 \
       -baselineDescription="Esquema existent GC4" baseline
-- 2 · a partir d'aquí, tot canvi és un fitxer versionat
-- db/migration/V4.2.1__index_cites_data.sql
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_cites_data ON CITES(DATA);
  1. baseline no toca l'esquema: crea la taula de control de Flyway i registra que la versió 4.2.0 «ja està aplicada». És l'equivalent exacte de la taula migracions_aplicades de la 04-06, adoptada a mig camí.
  2. Des d'aquell punt, tota la disciplina de la 04-06 s'aplica sense canvis: fitxers versionats, revisió, execució des del pipeline, CONCURRENTLY, lock_timeout i expand and contract per a qualsevol canvi incompatible. L'esquema històric no es documenta ni es recrea; simplement es declara punt de partida i s'avança des d'allà. Un detall pràctic: convé bolcar l'esquema actual (pg_dump --schema-only) al repositori com a V4.2.0__baseline.sql no executable però sí versionat, perquè hi hagi una referència llegible d'on es va començar.

  1. Mètriques quan la línia base és un desplegament cada dos mesos

Les mètriques DORA de la 01-05 continuen valent, però comparar Gestor Citas 4 amb Reservalia no informa de res: són productes amb propòsits diferents. La comparació útil és contra si mateix.

Mètrica Abans Després de l'increment 2 Després de l'increment 5 Objectiu 12 mesos
Freqüència de desplegament 1 / 2 mesos 1 / 2 mesos 2 / mes 1 / setmana si cal
Lead time (commit → prod) ~45 dies ~45 dies 6 dies 3 dies
Change failure rate 33 % (2 de 6) 33 % 11 % < 15 %
Time to restore ~2 h, incert 2 h 7 min 7 min
Durada del desplegament 3 h 3 h 7 min 7 min
«Compila a CI»
«Es pot reconstruir en màquina neta»

Tres lectures. Primera: l'increment 2 no va moure cap mètrica DORA i tot i així va ser el més valuós de tots, perquè va fer possibles els altres. Per això les dues últimes files —binàries, no numèriques— apareixen al quadre: en un legacy, les mètriques útils de les primeres fases són capacitats adquirides, no números. Mesurar només DORA en aquesta etapa fa que la feina fundacional sembli inútil, i és la manera més ràpida que negoci la cancel·li.

Segona: un change failure rate de l'11 % seria inacceptable a Reservalia i aquí és un èxit rotund, perquè venia del 33 %. Les mètriques es comparen amb la línia base pròpia, mai amb les d'un altre producte ni amb les de la indústria.

Tercera: l'objectiu a dotze mesos no és «desplegar deu vegades al dia». És «poder desplegar en una setmana quan calgui». Per a un producte en manteniment amb tres clients, la capacitat de respondre ràpid a un problema val molt més que la freqüència, i confondre les dues coses porta a invertir on no toca.

  1. La conversa amb negoci

Cap d'aquests increments no es finança parlant de tecnologia. El que no funciona: «cal posar CI/CD», «el codi és un desastre», «tenim deute tècnic». Són afirmacions que qui decideix el pressupost no pot avaluar i sonen a preferència de l'equip.

El que sí que funciona és traduir a risc, cost i temps de resposta:

En comptes de dir Dir
«No hi ha proves automatitzades» «Cada canvi ens costa mig dia de proves manuals i tot i així dos dels últims sis desplegaments van fallar»
«La construcció no és reproduïble» «Si el portàtil de qui construeix es trenca, no podem publicar una correcció urgent. Avui no sabem en quant de temps ho recuperaríem»
«Cal automatitzar el desplegament» «Passem de tres hores un dissabte a set minuts un dimarts; una fallada urgent es corregeix el mateix dia en lloc d'esperar la finestra»
«El sistema té deute tècnic» «Un canvi que a Reservalia costa dos dies, aquí costa dues setmanes. Amb 4.200 €/mes d'ingrés, cada petició es menja el marge»
«Els secrets són a git» «La contrasenya de la base de dades d'un client és visible per a qualsevol amb accés al repositori; això és notificable»

I tres tàctiques que a Gestor Citas 4 van funcionar. Vincular cada increment a un incident real ocorregut: el desplegament fallit de març que va deixar un client sense servei quatre hores justifica l'increment 5 millor que qualsevol argument abstracte. Demanar porcions petites i demostrables: dues setmanes per a l'increment 1, amb un resultat ensenyable —«aquí tens el sistema construint-se sol»— en lloc d'un projecte de sis mesos. I presentar el risc del «no fer res» amb un número: si el contracte val 4.200 €/mes fins al 2028, són uns 100.000 € en risc; trenta dies de feina per reduir substancialment la probabilitat de perdre'l és una decisió fàcil quan es planteja així.

Marta: «A negoci no li véns un pipeline. Li véns que el dissabte a la nit no existeixi.»

  1. Quan parar

La pregunta que gairebé cap material sobre modernització no respon. Gestor Citas 4 no tindrà desplegament continu, ni canari, ni feature flags, ni observabilitat distribuïda, i això és una decisió correcta, no una rendició. El nivell d'automatització raonable depèn de tres variables:

Variable Gestor Citas 4 Reservalia
Freqüència de canvi 1-2 al mes Diària
Vida restant prevista 2 anys (contracte 2028) Indefinida
Cost d'una fallada 3 clients, alt per client 340+ negocis
Persones que el toquen 1, a temps parcial 3, a temps complet
Nivell raonable Fins a l'increment 5-6 Tot el mòdul 4

La regla, formulada per endur-se-la: l'automatització es justifica per la freqüència d'ús multiplicada per la vida restant. Un canari automàtic que evitaria un incident cada dos anys, en un sistema que s'apaga d'aquí a dos anys, no es paga mai. Un desplegament automatitzat que es fa servir dues vegades al mes durant dos anys —48 usos, estalviant gairebé tres hores cadascun— es paga el segon mes.

Els senyals que cal parar són concrets: quan el següent increment costa més que el problema que resol; quan la millora afecta una cosa que no canvia mai —automatitzar les proves d'un mòdul que ningú no ha tocat en cinc anys és feina llençada—; quan existeix una data d'apagada creïble i signada; i quan l'equip comença a modernitzar per gust tècnic i no per un problema mesurable. Aquesta última és la més difícil de reconèixer des de dins, i per això convé que cada increment neixi amb la seva justificació escrita en termes de l'apartat 11: si no es pot redactar la frase de la columna dreta d'aquella taula, probablement aquell increment no toca.

  1. Fitxa del cas

Context Monòlit Java 8/JSP del 2011, 240.000 línies, sense proves, FTP manual, 3 clients, 4.200 €/mes
Què continua valent tal qual Artefacte immutable (02-06), entorns amb revisors (03-02), smoke test (03-02), escaneig de seguretat (04-03), migracions versionades (04-06)
Decisió 1 No reescriure; no esperar a tenir proves; començar per la reproduïbilitat
Decisió 2 Set increments, cadascun amb valor propi i dues parades vàlides declarades
Decisió 3 Vuit proves de caracterització triades per ingrés, no per cobertura; golden master per al que no es pot provar
Decisió 4 Desplegament automatitzat però disparat a mà amb workflow_dispatch i revisors
Decisió 5 Flyway amb baseline sobre l'esquema existent; secrets rotats abans d'extreure'ls
Decisió 6 Strangler fig limitat a dues funcionalitats, amb termini escrit
Cost ~30 dies de feina repartits en 8 mesos
Efecte en DORA Lead time 45 → 6 dies; CFR 33 % → 11 %; restore 2 h → 7 min; desplegament 3 h → 7 min
Què t'endús a qualsevol projecte El primer increment no són les proves: és poder reconstruir el sistema i saber què hi ha desplegat

Errors Comuns i Consells

Error 1: proposar la reescriptura completa. Amb 240.000 línies sense especificació, és el projecte que dura tres anys i es cancel·la amb dos sistemes a mitges. Error 2: esperar a tenir cobertura per muntar CI; el codi no és testable, refactoritzar-lo sense proves és el que es vol evitar, i el cicle no es trenca sol. Error 3: començar per les proves unitàries en lloc de pel fum d'extrem a extrem, que és l'única cosa que no exigeix tocar el codi.

Error 4: corregir els bugs descoberts durant la caracterització. Una prova de caracterització documenta el que fa el sistema, inclosos els seus errors; corregir-los és una altra tasca, amb la seva decisió i el seu PR a part. Error 5: esborrar un secret del fitxer i fer commit creient que s'ha resolt: continua a l'historial i sense rotar. Error 6: intentar el desplegament sense tall des del principi, quan 40 segons d'indisponibilitat són perfectament acceptables en un sistema que es desplega dues vegades al mes.

Error 7: mesurar només DORA a les fases inicials, amb la qual cosa la feina fundacional sembla inútil i es cancel·la just abans de donar fruit. Error 8: comparar les mètriques del legacy amb les del producte modern en lloc de amb la seva pròpia línia base. Error 9: deixar un strangler fig a mitges sense termini ni abast escrits: la fase intermèdia és més complexa que qualsevol dels dos extrems. Error 10: continuar modernitzant per gust tècnic un sistema amb data d'apagada.

Consell 1: el primer criteri d'èxit és reconstruir l'artefacte de producció des del repositori en una màquina neta. Consell 2: tria les proves de fum per ingrés, no per cobertura; vuit de ben triades valen més que dues-centes repartides. Consell 3: automatitza el desplegament abans que el dispar, que és on hi ha gairebé tot el benefici. Consell 4: escriu la justificació de negoci de cada increment abans de començar-lo; si no surt la frase, aquell increment no toca.

Exercicis

Exercici 1

T'incorpores a mantenir Gestor Citas 4 i la primera setmana arriba una petició urgent: un client necessita un camp nou a l'informe de facturació trimestral per al tancament del mes que ve. No hi ha proves, la construcció només funciona en un portàtil que ja no existeix i l'últim desplegament va ser fa set setmanes. Descriu què fas les dues primeres setmanes, en quin ordre i per què, distingint el que és imprescindible per lliurar la petició del que és inversió.

Exercici 2

Durant l'increment 4, el golden master del càlcul de facturació revela que un descompte per volum s'aplica dues vegades quan el client té més de tres seus, cosa que passa des de fa almenys quatre anys. Un dels tres clients té cinc seus. Analitza la situació tècnica i de negoci i proposa un pla d'actuació complet.

Exercici 3

Després de completar l'increment 5, un company proposa continuar fins al final: contenidoritzar, muntar desplegament continu amb canari, afegir observabilitat completa amb SLO i pressupost d'error, i començar l'strangler fig sobre el mòdul d'agenda. Estima esforç i benefici de cada proposta amb els criteris de l'apartat 12 i dóna una recomanació prioritzada.

Solucions

Solució 1. La clau és separar lliurar de millorar, i acceptar que no es pot fer tot. Amb la petició a sobre, l'ordre que jo seguiria:

Dies 1-3 — Reconstruir la construcció (imprescindible, no inversió). Sense poder generar un .war no hi ha lliurament possible, així que això no és opcional encara que sembli feina de fons. Es recupera el codi que correspon a producció, es crea el repositori, i es documenta cada obstacle: els JAR orfes es pugen a un repositori intern, les rutes absolutes se substitueixen per relatives, la variable d'entorn es declara al POM. El criteri de parada és concret: el .war construït té les mateixes classes i recursos que el desplegat. Dies 3-4 — CI que compila. Mitja jornada de feina, i a partir d'aquí cada commit produeix un artefacte identificat per SHA. És barat i transforma la resta de les dues setmanes: ja no hi ha dubtes sobre què s'està construint. Dies 4-6 — Golden master de l'informe, només de l'informe. Aquí hi ha la decisió més important de l'exercici: com que el canvi toca el càlcul de facturació, es congela la seva sortida actual sobre vint casos de dades anonimitzades abans de tocar una línia. No és «inversió de futur»: és l'única manera de saber que el camp nou no altera els imports existents. Sense això, la verificació seria que algú miri un PDF i li sembli bé. Dies 6-9 — Implementar el canvi, amb el golden master executant-se a cada iteració. Qualsevol diferència diferent del camp nou apareix com un diff exacte. Quan el canvi és correcte, s'actualitza el fitxer de referència expressament, en un commit separat i amb el diff visible. Dies 9-11 — Automatitzar el desplegament. El primer desplegament després de set setmanes és el moment de més risc de l'any, així que scriptar workflow_dispatch + còpia de seguretat + smoke test es paga en aquest mateix lliurament. Abans s'assaja almenys una vegada contra l'entorn de proves. Dies 11-12 — Desplegar un dimarts a la tarda, amb el smoke test verificant i la tornada enrere provada per endavant.

Què és imprescindible i què és inversió. Imprescindible: la construcció reproduïble i el golden master de l'informe. Inversió que es paga dins d'aquestes mateixes dues setmanes: el CI i el desplegament automatitzat. Inversió que no faig ara i deixo escrita per a després: les vuit proves de fum completes, la contenidorització, l'extracció de secrets —llevat que en trobi un de crític, cas en què el roto immediatament— i qualsevol refactor del codi de negoci. I una regla que aplicaria des del primer dia: res d'això no es fa a la mateixa branca que el canvi funcional, perquè el lliurament no depengui que la modernització surti bé.

Solució 2. Tècnicament, el descobriment és la prova que el golden master funciona: ha trobat en una tarda un error que portava quatre anys sense detectar-se. Però l'error de bulto seria corregir-lo allà mateix. Un bug de facturació de quatre anys no és una decisió tècnica.

Pas 1 — Quantificar abans de decidir res. Amb una consulta sobre les dades històriques: quins clients tenen més de tres seus, en quants trimestres s'ha aplicat el descompte duplicat i quant suma la diferència. Sense aquest número no es pot prendre cap decisió, i la conversa degenera en opinions. Suposem el resultat plausible: un client afectat, setze trimestres, uns 9.400 € facturats de menys —el descompte duplicat afavoreix el client—.

Pas 2 — Congelar el comportament actual al golden master, amb un comentari explícit. El fitxer de referència es genera amb el bug inclòs i s'afegeix una nota al codi de la prova: què és, des de quan, i que la seva correcció està pendent de decisió de negoci. Això permet continuar treballant al mòdul sense arrossegar una prova en vermell, que és el que acabaria amb tothom ignorant-la.

Pas 3 — Escalar-ho amb les dades. Va a negoci i a qui porti la relació amb el client, no es decideix a l'equip. Les opcions, amb les seves conseqüències: (a) corregir cap endavant i no reclamar el passat —el més habitual: l'import és assumible, evita una conversa desagradable amb un client que aporta un terç dels ingressos del producte, i cal valorar si legalment es pot reclamar retroactivament el que es va facturar de menys—; (b) corregir i regularitzar, que exigeix revisió legal i comptable; (c) no corregir, que només és defensable si es documenta com a comportament acordat, i és la pitjor opció perquè el bug tornarà a aparèixer en qualsevol revisió futura.

Pas 4 — Executar la decisió com un canvi de comportament explícit. Sigui quina sigui, la correcció va en un PR propi el contingut principal del qual és el diff del fitxer golden, que mostra exactament quins imports canvien i en quins casos. Aquest diff és l'evidència que s'ensenya a negoci per aprovar, i queda a l'historial amb data, autor i motiu. Es desplega amb el mecanisme de l'increment 5 i es verifica sobre un cas real conegut abans del següent tancament trimestral.

Pas 5 — Aprendre de la troballa. Si un error de facturació ha sobreviscut quatre anys, la pregunta interessant és què més hi ha. Val la pena ampliar els casos del golden master als escenaris menys freqüents —prorrateigs, altes i baixes a mitja trimestre, canvis de tarifa— abans que els descobreixi un client. És el millor retorn disponible en aquest sistema i surt pràcticament gratis, perquè la maquinària ja està muntada.

Solució 3. Aplicant freqüència d'ús × vida restant, amb dos anys de contracte per davant i 1-2 desplegaments al mes:

Proposta Esforç Benefici en aquest context Veredicte
Contenidoritzar 2-4 setmanes (risc de rutes absolutes) Entorn reproduïble; proves de fum automàtiques a CI; elimina «el servidor màgic» Sí, prioritat 1
Vuit proves de fum a CI 1-2 setmanes Verificació automàtica a cada canvi; és el que falta perquè l'increment 5 rendeixi Sí, prioritat 2
Desplegament continu amb canari 4-6 setmanes + infraestructura nova Es faria servir ~24 vegades en dos anys; requereix segon servidor i balancejador; 40 s de tall ja són acceptables No
Observabilitat completa amb SLO i pressupost d'error 3-5 setmanes Un pressupost d'error governa la cadència de desplegament… que aquí és de 2 al mes: no governa res No — però sí una versió mínima
Strangler fig sobre l'agenda 3-6 mesos, risc alt L'agenda és el nucli més acoblat; amb dos anys de vida no s'amortitza No

Recomanació prioritzada. Primer contenidoritzar, perquè desbloqueja la resta: sense un entorn aixecable no hi ha proves de fum fiables a CI, i a més elimina el risc que el servidor irreproduïble es perdi, que és l'amenaça real de continuïtat d'aquest producte. Amb una prova de concepte de tres dies abans de comprometre termini, pel que la lliçó adverteix sobre rutes absolutes i estat en disc. Segon, les vuit proves de fum executant-se a CI contra aquell entorn efímer: és el que converteix el desplegament de l'increment 5 en un desplegament verificat, i és el complement natural del que ja s'ha fet. Tercer, una versió mínima d'observabilitat que no és la de la 03-06: mètriques bàsiques de disponibilitat i latència, alerta per símptoma —«l'aplicació no respon»—, i retenció de logs. Costa dies, no setmanes, i respon la pregunta que avui no té resposta: quant triguem a assabentar-nos que està caigut. Es descarten el canari, el pressupost d'error i l'strangler fig sobre l'agenda, i val la pena escriure el motiu a la decisió perquè no es torni a plantejar cada trimestre. La frase que resumeix el criteri: l'objectiu d'aquest producte no és desplegar més, és poder desplegar bé quan calgui, i a partir de l'increment 6 aquesta capacitat ja està aconseguida.

Conclusió i tancament del mòdul

Gestor Citas 4 ha estat el cas que més incomoda perquè treu totes les condicions favorables alhora, i precisament per això ensenya el que els altres tres no poden. La primera lliçó és d'ordre: no es comença per les proves, es comença per la reproduïbilitat, perquè el cicle «cal refactoritzar per poder provar, i provar per poder refactoritzar» no es trenca des de dins, i perquè el problema més urgent no era la manca de cobertura sinó que ningú no sabia reconstruir el sistema. La segona és d'estructura: set increments, cadascun amb valor propi encara que el següent no arribi mai, i dues parades vàlides declarades per endavant. La tercera és de tècnica: fum d'extrem a extrem abans que unitàries, golden master per congelar el que no admet prova —inclosos els seus bugs, que es corregeixen a part i expressament—, desplegament automatitzat encara que se segueixi disparant a mà, contenidorització per convertir un servidor irreproduïble en un fitxer versionat, i strangler fig amb abast i termini escrits. La quarta és de mesura: a les primeres fases les mètriques útils són capacitats binàries —«compila a CI», «es reconstrueix en màquina neta»— i no números DORA, i quan els números arriben es comparen amb la línia base pròpia i amb ningú més. I la cinquena és la que salva més projectes: saber quan parar, perquè l'automatització es paga amb la freqüència d'ús multiplicada per la vida restant, i en un producte amb data de caducitat hi ha un nivell a partir del qual continuar és gust tècnic i no enginyeria.

Amb això es tanca el mòdul. Quatre contextos que no s'assemblen entre si, sotmesos al mateix sistema:

Web (05-01) Mòbil (05-02) Microserveis (05-03) Legacy (05-04)
Unitat desplegable Fitxers estàtics Binari signat a la botiga 5 serveis independents Un .war
Qui controla el lliurament Tu L'usuari i la botiga Cada equip Tu, un dissabte
Rollback Reapuntar HTML: <1 min No existeix: només cap endavant Un per servei workflow_dispatch: 7 min
Porta característica Pressupost de bundle, Lighthouse Escalat amb pausa per crash-free can-i-deploy «Compila»
Problema de compatibilitat Pestanyes d'ahir Desenes de versions vives Contractes entre parells i esdeveniments Cap: un client, el propi
Cost dominant Portes de qualitat Runners macOS i revisió 2 persones de plataforma Arqueologia inicial
El que més va canviar Configuració a runtime Detectar abans en comptes de revertir Comprar el senyal amb contractes L'ordre dels increments

I el que va ser invariant en tots quatre, que és la resposta a què endur-se quan el context no s'assembli a cap dels vistos:

  • Artefacte immutable, construït una vegada i promocionat. Un dist/ amb hash, un AAB signat, cinc imatges per digest o un .war amb el SHA del commit: en els quatre casos es construeix una vegada i es mou, mai no es reconstrueix per desplegar.
  • Traçabilitat de què hi ha desplegat i d'on va sortir. El peu amb la versió a la web, el versionName a l'informe de crash, git log del repositori de desplegaments, l'artefacte identificat per SHA del legacy. La primera pregunta de qualsevol incident és «què ha canviat?», i sempre va tenir resposta objectiva.
  • Retroalimentació com més ràpida i com més a l'esquerra millor. Quan el canal de lliurament és lent —botiga, revisió, finestra del dissabte— la inversió no es fa a recuperar ràpid sinó a detectar abans: beta, escalat, contractes, fum automàtic, pressupostos.
  • Tot com a codi i sota revisió. Workflows, plantilles, infraestructura, esquemes d'esdeveniments, configuració d'entorn, migracions. En cap cas no va quedar res editat a mà en un servidor, i on n'hi havia, treure-ho va ser un dels primers increments.
  • Compatibilitat cap enrere com a disciplina permanent. Pestanyes obertes, apps de fa un any, consumidors que encara no han migrat: la forma canvia, l'obligació no. Expand and contract (04-06) va aparèixer en els quatre casos amb disfressa diferent.

L'específic, en canvi, va ser tota la resta: on viu la configuració, si hi ha rollback o només roll-forward, si la porta és un pressupost de bundle o un can-i-deploy, si la destinació és desplegament continu o un workflow_dispatch disparat a mà. I aquesta és la conclusió del mòdul: els principis no es negocien, les decisions es prenen mirant el context, i saber distingir els uns de les altres és el que separa aplicar CI/CD de copiar el pipeline d'un altre.

Amb els principis assentats i quatre contextos recorreguts, el que falta és el detall de la maquinària. Fins aquí l'eina ha estat gairebé sempre GitHub Actions, amb incursions puntuals quan el cas ho demanava, i sempre com a mitjà i no com a tema. El mòdul 6, Eines i Tecnologies, inverteix el focus: baixa al detall de Jenkins, GitLab CI/CD, CircleCI, Travis CI, Docker i Kubernetes i GitHub Actions a fons, cadascun amb el seu model d'execució, la seva sintaxi, els seus punts forts i els seus límits reals, i acaba amb una comparativa i uns criteris per triar. Comença per Jenkins, que és el que et trobaràs instal·lat més vegades quan arribis a un projecte, i el que millor explica per què la resta d'eines es van dissenyar com es van dissenyar.

Curs de CI/CD: Integració i Desplegament Continu

Mòdul 1: Introducció al CI/CD

Mòdul 2: Integració Contínua (CI)

Mòdul 3: Desplegament Continu (CD)

Mòdul 4: Pràctiques Avançades de CI/CD

Mòdul 5: Implementació de CI/CD en Projectes Reals

Mòdul 6: Eines i Tecnologies

Mòdul 7: Exercicis Pràctics

Mòdul 8: Recursos Addicionals

© Copyright 2026. Tots els drets reservats