Queda el quart front, i és el que més desplegaments trenca. Tot el que s'ha construït en tres mòduls descansa sobre una propietat que l'esquema de la base de dades no té: l'artefacte reservalia/api:a3f9c21 és immutable i se substitueix per l'anterior en quatre minuts, però una columna esborrada no torna. El rollback.yml de la 03-05 sap tornar a un digest anterior; no sap desfer un DROP COLUMN, i per això la mateixa lliçó ho va deixar escrit com l'únic motiu pel qual el botó de tornada enrere podria no funcionar. Aquesta lliçó tanca aquell forat i, amb ell, el mòdul. Veurem per què l'esquema és estat compartit i què implica això; com es versionen les migracions i per què s'executen des del pipeline i mai des del portàtil d'en Diego; desenvoluparem pas a pas el patró expand and contract sobre un cas real —reanomenar hora_inici a inici_utc sense tallar el servei—; classificarem els canvis d'esquema en segurs, perillosos i prohibits en calent; entendrem per què un ALTER TABLE pot tombar producció i com evitar-ho; col·locarem la migració al seu lloc exacte dins de cd.yml; i acabarem amb les dades de prova, l'anonimització i un advertiment de compliment normatiu que convé llegir sencer.
Contingut
- L'esquema és estat compartit i no es reverteix
- Migracions versionades: convenció, eina i taula de control
- Per què s'executen des del pipeline i mai des d'un portàtil
- Expand and contract: el cas
hora_inici→inici_utc - Classificació dels canvis d'esquema
- Migracions i bloquejos: com un
ALTER TABLEtomba producció - Backfill per lots sense saturar la base
- On encaixa la migració a
cd.yml - Dades de prova i anonimització
- Errors Comuns i Consells
- Exercicis
- Conclusió i tancament del mòdul
- L'esquema és estat compartit i no es reverteix
Compara les dues meitats del sistema que Reservalia desplega:
L'artefacte (reservalia/api) |
L'esquema (RDS PostgreSQL) | |
|---|---|---|
| Naturalesa | Immutable, identificat per digest; una còpia per tasca ECS | Mutable i únic per entorn |
| Tornar enrere | Desplegar el digest anterior: 4 min | Depèn del canvi; a vegades impossible |
| Si surt malament | Se substitueix i no queda rastre | Les dades perdudes no tornen |
| Qui el comparteix | Ningú: cada tasca té el seu | Totes les versions del codi alhora |
L'última fila és la clau de tota la lliçó. Durant un rolling update de la 03-04 —o durant un canary al 10 %— conviuen en producció dues versions del codi parlant amb una sola base de dades. Si la migració deixa l'esquema en un estat que la versió antiga no entén, la meitat de les peticions falla mentre dura el desplegament. I si cal revertir el codi, la versió antiga es trobarà un esquema que ja no és el seu. Per això rollback.yml no salva una migració destructiva, i convé veure'n el cas concret. Suposa un desplegament que executa ALTER TABLE cites DROP COLUMN hora_inici i desplega el codi nou. Deu minuts després es detecta una fallada greu i la Nuria llança el rollback: la imatge anterior torna en quatre minuts… i falla a cada petició, perquè el seu codi pregunta per una columna que ja no existeix. El rollback de l'artefacte s'ha completat amb èxit i el servei continua caigut. Restaurar una còpia de seguretat tampoc no és la resposta: significa perdre totes les reserves creades des del DROP.
Nuria: "Un desplegament que no es pot desfer en cinc minuts no és un desplegament, és una aposta." I una migració destructiva converteix qualsevol desplegament en això.
La conclusió que ordena la resta: l'esquema ha de canviar de manera que cap versió desplegada no es trenqui, ni la nova ni l'anterior. Això no és una restricció del pipeline, és una restricció de disseny de cada canvi.
- Migracions versionades: convenció, eina i taula de control
Una migració és un canvi d'esquema escrit com un fitxer versionat al repositori, no com una ordre teclejada a psql. Reservalia les desa a apps/api/src/db/migracions/ —la ruta que ja apareixia al CODEOWNERS de la 02-07, exigint revisió de la Marta o la Nuria— amb aquesta convenció de noms:
apps/api/src/db/migracions/
20260715093000_expandir_inici_utc.sql
20260716101500_backfill_inici_utc.sql
20260722084500_contraure_hora_inici.sqlMarca de temps + descripció en snake_case. La marca ordena l'execució de manera determinista i evita el problema clàssic dels números correlatius: dues persones que treballen alhora creen 005_ i 005_, i el conflicte només apareix en fusionar. Amb marques de temps, dues migracions simultànies s'ordenen soles. L'eina —node-pg-migrate a Reservalia, però el mecanisme és universal (Flyway, Liquibase, Alembic, ActiveRecord)— manté una taula de control dins de la mateixa base de dades:
CREATE TABLE IF NOT EXISTS migracions_aplicades (
nom text PRIMARY KEY, -- 1 · el nom del fitxer
hash text NOT NULL, -- 2 · empremta del contingut
aplicada_el timestamptz NOT NULL DEFAULT now(),
duracio_ms integer NOT NULL
);- El nom com a clau primària és el que fa la migració idempotent: en arrencar, l'eina compara els fitxers del repositori amb les files d'aquesta taula i executa només el que falta. Tornar a llançar el job no repeteix res, que és el requisit de la 03-02.
- El hash del contingut detecta l'error més perillós: que algú editi una migració ja aplicada. Si el hash del fitxer no coincideix amb el registrat, l'eina s'atura amb un error explícit. Una migració aplicada és història: es corregeix amb una migració nova, mai editant l'anterior, perquè l'entorn on ja es va executar no la tornarà a llegir.
- I
duracio_msno hi és per curiositat. Una migració que astagingtriga 200 ms i aprodtriga 40 segons està dient que la taula és molt més gran i que el bloqueig de l'apartat 6 farà mal.
- Per què s'executen des del pipeline i mai des d'un portàtil
A la 01-04, el ritual del divendres incloïa en Diego llançant migracions a mà a psql connectat a producció. Allò va desaparèixer, i val la pena enumerar per què no ha de tornar ni tan sols "només aquesta vegada":
| Migració a mà | Migració des del pipeline |
|---|---|
| Ningú no sap què es va executar exactament | Queda el fitxer, el commit i el log |
| L'ordre depèn de la memòria d'una persona | El determina la taula de control |
staging i prod divergeixen sense que ningú ho noti |
Els tres entorns executen el mateix |
| Requereix credencials de producció en un portàtil | El pipeline fa servir OIDC, sense claus permanents |
| No hi ha revisió prèvia, i si falla s'improvisa | Passa per PR amb CODEOWNERS, amb procediment escrit |
L'argument decisiu és el tercer. La deriva entre entorns de la 03-03 —el problema que va resoldre Terraform per a la infraestructura— existeix també a l'esquema, i és pitjor: una migració que a staging es va executar amb una variant i a prod amb una altra fa que les proves de staging deixin de significar res, i ningú no ho descobreix fins al dia de l'incident. Si l'esquema no està sota control de versions, no tens entorns equivalents per molt Terraform que escriguis.
- Expand and contract: el cas
hora_inici → inici_utc
hora_inici → inici_utcEl problema real de Reservalia: la columna cites.hora_inici és un timestamp sense zona horària, i amb negocis que comencen a operar fora de la Península els càlculs de calcularForats s'estan equivocant en els canvis d'hora. Cal passar a inici_utc de tipus timestamptz. És, en aparença, un ALTER TABLE ... RENAME COLUMN. I fer-ho així trencaria el servei: en l'instant del reanomenament, totes les tasques que encara corren la versió antiga comencen a fallar.
El patró expand and contract (expandir i contraure) resol això convertint un canvi incompatible en una seqüència de canvis compatibles. Quatre fases, cadascuna desplegable i reversible per separat:
flowchart TD
F1["FASE 1 · Expandir<br/>afegir inici_utc, nullable<br/>+ trigger de sincronia"] --> F2["FASE 2 · Migrar dades<br/>backfill per lots"]
F2 --> F3["FASE 3 · Desplegar codi<br/>llegeix i escriu inici_utc"]
F3 --> F4["FASE 4 · Contraure<br/>treure trigger i DROP hora_inici"]
F1 -.->|"codi vell continua OK"| F2
F3 -.->|"esperar dies, verificar"| F4
Fase 1 · Expandir. S'afegeix la columna nova sense tocar la vella, i un trigger manté les dues sincronitzades mentre convisquin:
-- 20260715093000_expandir_inici_utc.sql
ALTER TABLE cites ADD COLUMN inici_utc timestamptz; -- 1 · nullable, sense default
CREATE OR REPLACE FUNCTION sincronitzar_inici() RETURNS trigger AS $$
BEGIN
IF NEW.inici_utc IS NULL AND NEW.hora_inici IS NOT NULL THEN
NEW.inici_utc := NEW.hora_inici AT TIME ZONE 'Europe/Madrid'; -- 2
ELSIF NEW.hora_inici IS NULL AND NEW.inici_utc IS NOT NULL THEN
NEW.hora_inici := NEW.inici_utc AT TIME ZONE 'Europe/Madrid'; -- 3
END IF;
RETURN NEW;
END $$ LANGUAGE plpgsql;
CREATE TRIGGER trg_sincronitzar_inici
BEFORE INSERT OR UPDATE ON cites FOR EACH ROW EXECUTE FUNCTION sincronitzar_inici();- Nullable i sense valor per defecte: és el que fa que aquest
ALTER TABLEsigui instantani i no reescrigui la taula (apartat 6). - El codi vell continua escrivint
hora_inicii el trigger ompleinici_utc. La versió antiga no s'assabenta de res. - El codi nou escriurà
inici_utci el trigger ompliràhora_inici, de manera que si cal revertir el codi, la columna vella està al dia. Aquesta direcció del trigger és la que fa el rollback segur, i és justament la que s'oblida. Després d'aquesta migració conviuen sense problema el codi vell, que escriuhora_inici, i el nou quan es desplegui, que escriuràinici_utc.
Fase 2 · Migrar les dades. El trigger només cobreix les files que es toquen; les 640.000 cites històriques cal omplir-les amb un backfill per lots (apartat 7). En acabar, inici_utc està completa. Fase 3 · Desplegar el codi nou. Ara, i no abans, apps/api/src/domini/agenda.ts passa a llegir i escriure inici_utc. El desplegament és un rolling update normal de la 03-04: durant uns minuts conviuen les dues versions, i totes dues funcionen perquè ambdues columnes estan completes i sincronitzades. I aquí hi ha la propietat que justifica tot el patró: si el canary detecta un problema, rollback.yml funciona sense tocar la base de dades.
Fase 4 · Contraure. Només quan han passat uns quants dies, el codi nou és estable a prod i s'ha verificat que ningú no llegeix ja la columna vella:
-- 20260722084500_contraure_hora_inici.sql
DROP TRIGGER IF EXISTS trg_sincronitzar_inici ON cites;
DROP FUNCTION IF EXISTS sincronitzar_inici();
ALTER TABLE cites ALTER COLUMN inici_utc SET NOT NULL; -- 1
ALTER TABLE cites DROP COLUMN hora_inici; -- 2- La restricció
NOT NULLs'afegeix al final, quan ja se sap que no hi ha nuls. Posar-la a la fase 1 hauria trencat totes les escriptures del codi vell. - Aquest és l'únic pas irreversible del procés, i per això va en una migració a part, una setmana després i amb la seva pròpia revisió. Abans d'executar-lo convé una comprovació objectiva: buscar
hora_inicial codi desplegat i consultar als logs si alguna consulta l'esmenta els últims set dies.
El cost és real i cal dir-ho: quatre pull requests i una setmana per al que semblava un reanomenament d'un minut. A canvi, zero segons d'indisponibilitat i un rollback possible en tot moment llevat dels cinc segons finals de la fase 4. Per a una taula amb 640.000 files i 340 negocis de pagament, l'intercanvi és evident.
- Classificació dels canvis d'esquema
| Canvi | Categoria | Per què | Alternativa segura |
|---|---|---|---|
ADD COLUMN nullable, sense default |
Segur | Metadades: instantani | — |
CREATE TABLE nova · CREATE INDEX CONCURRENTLY |
Segur | Ningú no la fa servir encara · no bloqueja escriptures | — |
ADD COLUMN amb default volàtil |
Perillós | Reescriu la taula sencera sota bloqueig | Afegir nullable + backfill per lots |
CREATE INDEX sense CONCURRENTLY |
Perillós | Bloqueja escriptures mentre dura | Fer servir CONCURRENTLY |
ALTER COLUMN ... TYPE |
Perillós | Reescriu i bloqueja | Columna nova + expand/contract |
ADD CONSTRAINT (FK, CHECK) o SET NOT NULL |
Perillós | Recorre i valida tota la taula sota bloqueig | NOT VALID i després VALIDATE CONSTRAINT |
DROP COLUMN en ús |
Prohibit en calent | Trenca el codi antic; irreversible | Expand and contract |
RENAME COLUMN o RENAME TABLE |
Prohibit en calent | Cap versió no sobreviu a l'instant del canvi | Expand and contract |
DROP TABLE |
Prohibit en calent | Irreversible i sense rollback | Deixar de fer-la servir, esperar, i esborrar |
Dos matisos sobre les files més traïdores. ADD COLUMN amb default depèn de la versió i del tipus de default: PostgreSQL 11 i posteriors gestionen un default constant sense reescriure la taula, però un default volàtil —now(), gen_random_uuid()— continua obligant a reescriure cada fila sota un bloqueig exclusiu. I el truc de NOT VALID val per a gairebé totes les restriccions: s'afegeix la restricció sense validar el que ja existeix (instantani, i ja s'aplica a les files noves) i després s'executa VALIDATE CONSTRAINT, que recorre la taula amb un bloqueig molt més suau que permet continuar escrivint.
- Migracions i bloquejos: com un
ALTER TABLE tomba producció
ALTER TABLE tomba produccióAquest és el mecanisme que convé entendre de debò, perquè explica incidents que semblen inexplicables. Un ALTER TABLE necessita un bloqueig exclusiu (ACCESS EXCLUSIVE) sobre la taula, i per obtenir-lo ha d'esperar que acabin les transaccions en curs. Fins aquí, res greu. El problema és el que passa mentre espera:
flowchart LR
A["Consulta lenta<br/>sobre cites · 30 s"] --> B["ALTER TABLE espera<br/>el bloqueig exclusiu"]
B --> C["Tota peticio nova<br/>fa cua al darrere"] --> D["El pool queda exhaurit"]
D --> E["La API deixa de respondre<br/>i ALTER encara no ha comencat"]
La cua de bloquejos de PostgreSQL respecta l'ordre d'arribada, així que un ALTER TABLE en espera bloqueja tots els que arriben després, inclosos els simples SELECT. El resultat és una caiguda completa provocada per una migració que encara no ha executat res. Tres defenses, i totes tres són barates:
SET lock_timeout = '3s'; -- 1 · no esperar indefinidament
SET statement_timeout = '30s'; -- 2 · no executar indefinidament
ALTER TABLE cites ADD COLUMN inici_utc timestamptz;lock_timeoutfa que la migració falli de pressa en lloc de bloquejar la cua. Una fallada neta als tres segons és infinitament millor que una caiguda: es reintenta més tard, quan no hi hagi una consulta llarga en curs. És la línia que més incidents evita de tota la lliçó.statement_timeoutprotegeix del cas contrari: una sentència que sí que va obtenir el bloqueig però triga deu minuts reescrivint la taula.- La tercera defensa és
CREATE INDEX CONCURRENTLY, imprescindible en taules grans: construeix l'índex sense bloquejar escriptures, a costa de trigar més i de dues passades sobre la taula. Té dues peculiaritats que cal conèixer: no es pot executar dins d'una transacció —la majoria d'eines de migració emboliquen cada fitxer en una, així que cal desactivar-ho explícitament per a aquella migració—, i si falla deixa un índex invàlid que cal esborrar a mà abans de reintentar.
- Backfill per lots sense saturar la base
Omplir 640.000 files amb un sol UPDATE és l'altra manera clàssica de tombar producció: una transacció gegant que bloqueja files durant minuts, fa créixer el WAL, dispara el retard de la rèplica i satura la CPU. L'alternativa és avorrida i funciona: lots petits, amb pausa entre ells.
#!/usr/bin/env bash
# infra/scripts/backfill-lots.sh
set -euo pipefail
LOT=${LOT:-1000}; PAUSA=${PAUSA:-0.2} # 1
while true; do
N=$(psql "$DATABASE_URL" -tA <<SQL | grep -c 1 || true
WITH pendents AS (
SELECT id FROM cites WHERE inici_utc IS NULL
ORDER BY id LIMIT $LOT
FOR UPDATE SKIP LOCKED -- 2
)
UPDATE cites c SET inici_utc = c.hora_inici AT TIME ZONE 'Europe/Madrid'
FROM pendents p WHERE c.id = p.id RETURNING 1;
SQL
)
echo "Actualitzades $N files"
[ "$N" -eq 0 ] && break # 3
sleep "$PAUSA" # 4
done- Lots de 1.000 files i pausa de 200 ms: cada transacció dura mil·lisegons i allibera els bloquejos de seguida. Els valors s'ajusten mirant la latència p95 del panell
reservalia-api-prodmentre corre. FOR UPDATE SKIP LOCKEDevita esperar per files que una altra transacció està tocant: se salten i s'agafaran a la volta següent. Sense això, el backfill competeix amb el trànsit real.- La condició de parada és "no queden files pendents", no un comptador. Això fa l'script represa-ble: si s'interromp a mig fer, es torna a llançar i continua on era. És la mateixa idempotència de la 03-02, aplicada a dades.
- La pausa és el que fa el backfill amable. Sense ella el bucle satura la base encara que cada lot sigui petit. És preferible un backfill que triga dues hores i que ningú no nota, que un de quatre minuts que dispara l'alerta
ApiLatenciaReserves.
I dues regles més: el backfill s'executa per separat del desplegament, com un job sota demanda, perquè un procés de dues hores no bloquegi el pipeline; i es vigila mentre corre, amb el panell obert i la disposició a aturar-lo, que és trivial perquè es pot reprendre.
- On encaixa la migració a
cd.yml
cd.ymlLa migració és un job propi, anterior al desplegament del codi i amb les seves pròpies regles:
migrar-prod:
needs: [desplegar-staging]
environment: prod-migracions # 1 · aprovacio propia
runs-on: ubuntu-22.04
timeout-minutes: 15
permissions: { id-token: write, contents: read }
steps:
- uses: ./.github/actions/preparar-node # de la 04-05
- uses: aws-actions/configure-aws-credentials@v4
with: { role-to-assume: '${{ secrets.AWS_ROLE_MIGRAR }}', aws-region: eu-west-1 }
- name: Migracions pendents # 2
run: npm run migrate:status --workspace apps/api
- name: Aplicar migracions
env: { PGOPTIONS: '-c lock_timeout=3s -c statement_timeout=60s' } # 3
run: npm run migrate --workspace apps/api
desplegar-prod:
needs: [migrar-prod] # 4 · l ordre importa
environment: prod- Un entorn propi,
prod-migracions, amb la seva pròpia llista de revisors. Aprovar un desplegament de codi i aprovar un canvi d'esquema són decisions diferents: la primera es desfà en quatre minuts, la segona pot no desfer-se. Separar-les fa que l'aprovador vegi el SQL abans de dir que sí. migrate:statusabans d'aplicar imprimeix quines migracions s'executaran. És informació per a l'aprovador i queda al log del run: la resposta a "què es va canviar el dia 15?".- Els temps d'espera es fixen per connexió amb
PGOPTIONS, de manera que s'apliquen a totes les sentències de la migració sense haver d'escriure'ls a cada fitxer. - La migració va abans del desplegament del codi, i això només és correcte perquè el patró de l'apartat 4 garanteix que l'esquema nou és compatible amb el codi antic. Si una migració no compleix aquella propietat, l'ordre no la salva: el problema és la migració.
Què fer si falla a mig fer. Primer, entendre què significa exactament: l'eina executa cada fitxer dins d'una transacció, així que una migració individual és atòmica, o s'aplica sencera o no s'aplica. El que no és atòmic és la sèrie: si n'hi ha tres de pendents i falla la segona, la primera va quedar aplicada i registrada. La taula migracions_aplicades diu exactament on es va aturar. I el procediment, que convé tenir escrit abans de necessitar-lo: (1) no reintentar a cegues —si va fallar per lock_timeout, reintentar en un moment tranquil és correcte; si va fallar per un error de SQL, reintentar dona el mateix error—; (2) el desplegament del codi no s'ha executat, perquè desplegar-prod depèn de migrar-prod, així que producció continua amb la versió anterior i l'esquema anterior més el que s'apliqués: si el patró es va respectar, això és un estat funcional; (3) corregir amb una migració nova, mai editant la que va fallar; i (4) anotar-ho com a incident a la taula incidents de la 03-06, perquè compta per al change failure rate. Un cas a part: un CREATE INDEX CONCURRENTLY no pot anar en una transacció, així que aquella migració no és atòmica i, si falla, deixa un índex invàlid que cal esborrar abans de reintentar. Mereix un comentari dins del mateix fitxer per a qui el trobi a les tres de la matinada.
- Dades de prova i anonimització
La manera més ràpida de tenir un staging realista és copiar la base de dades de producció. No es fa. I no només per normativa:
| Risc | Què significa a Reservalia |
|---|---|
| Legal | Les dades de les cites inclouen nom, telèfon i a vegades el motiu de la visita: són dades personals, i algunes poden ser de salut |
| Superfície d'exposició | staging té menys controls, més accessos i a vegades logs més verbosos |
| Fuites per integracions | Un entorn de proves amb dades reals pot enviar SMS o correus a clients reals |
| Retenció | Qui exerceix el seu dret de supressió continua estant a la còpia de staging |
L'alternativa és generar dades sintètiques amb un script que crea negocis, horaris i cites amb distribucions semblants a les reals —incloent-hi els casos difícils: horari partit, canvis d'hora, reserves solapades— però amb dades inventades. L'avantatge afegit és que són reproduïbles: les proves E2E de la 02-04 poden donar per fet que el negoci demo existeix amb una agenda coneguda.
// apps/api/src/db/llavors/generar.ts (fragment)
export function generarCites(negociId: number, n: number) {
return Array.from({ length: n }, (_, i) => ({
negociId,
client: `Client Fictici ${i}`, // 1
telefon: `+34 600 000 ${String(i).padStart(3, '0')}`, // 2
iniciUtc: new Date(Date.UTC(2026, 9, 25, 8 + (i % 9), 0)), // 3
}));
}- Noms clarament falsos: si alguna cosa es filtra en un log o en una captura, es veu a l'instant que no és real.
- Telèfons d'un rang no assignable, de manera que un enviament accidental d'SMS no arribi a ningú.
- Dates triades expressament al voltant d'un canvi d'hora, que és justament el cas que va motivar la migració d'aquesta lliçó.
Si tot i així cal partir de dades reals —a vegades és l'única manera de reproduir un problema de rendiment amb volum real—, aleshores anonimització abans que les dades surtin de producció: substituir noms, telèfons i correus per valors generats, amb cura de mantenir les propietats estadístiques que importen (quantes cites per negoci, quina distribució horària). I amb dues cauteles: l'anonimització mal feta és reversible —un identificador que es conserva pot recompondre la identitat—, i el procés s'ha d'executar en un entorn tan protegit com producció, perquè durant una estona maneja dades reals.
Advertiment. Aquest apartat explica principis tècnics, no assessorament legal. El tractament de dades personals està regulat —RGPD a la Unió Europea, i normativa sectorial addicional si hi ha dades de salut— i les decisions sobre què es pot copiar, anonimitzar o conservar s'han de prendre amb el criteri d'un professional de protecció de dades i del responsable de compliment normatiu de la teva organització. Un curs tècnic no substitueix aquella revisió.
Errors Comuns i Consells
Error 1: fer RENAME COLUMN o DROP COLUMN en un desplegament normal. Trenca totes les tasques que encara corren la versió anterior i deixa el rollback inservible. Error 2: editar una migració ja aplicada en lloc d'escriure'n una de nova: l'entorn on ja es va executar no la tornarà a llegir, i els entorns divergeixen en silenci.
Error 3: contraure el mateix dia que s'expandeix. El valor sencer del patró és a l'espera entre la fase 3 i la 4; sense ella només has escrit més SQL per al mateix risc. Error 4: ALTER TABLE sense lock_timeout, que converteix una migració en espera en una caiguda completa per la cua de bloquejos. Error 5: backfill en un únic UPDATE, amb bloquejos llargs, WAL disparat i rèplica endarrerida; per lots i amb pausa.
Error 6: CREATE INDEX sense CONCURRENTLY sobre una taula gran en producció. Error 7: executar migracions des d'un portàtil "només aquesta vegada", que és exactament així com els entorns comencen a divergir. Error 8: copiar dades de producció a staging, amb tot el que implica en risc legal i de fuites.
Consell 1: revisa el SQL de les migracions amb la mateixa atenció que el codi, i amb CODEOWNERS que ho garanteixi. Consell 2: assaja la migració sobre una còpia amb el volum de producció i anota quant triga; és l'única dada que prediu el bloqueig. Consell 3: escriu a cada migració destructiva un comentari amb la data en què la columna va deixar de fer-se servir i qui ho va verificar, i Consell 4: tingues el procediment de fallada escrit abans de necessitar-lo.
Exercicis
Exercici 1
En Diego necessita afegir a cites una columna estat de tipus text, obligatòria i amb valor per defecte 'confirmada', sobre una taula de 640.000 files i amb trànsit real. Escriu la seqüència completa de migracions i desplegaments, indicant quines versions del codi conviuen a cada moment i en quin punt exacte deixa de ser possible el rollback.
Exercici 2
A les 10:15 d'un dimarts, un desplegament executa CREATE INDEX idx_cites_negoci ON cites(negoci_id) sobre una taula de 640.000 files. Als 40 segons, l'API deixa de respondre completament, incloses les peticions que no toquen cites. Explica el mecanisme exacte de la fallada, per què afecta peticions alienes a aquella taula i com s'hauria evitat.
Exercici 3
Un equip vol reproduir a staging un problema de rendiment que només apareix amb volum real, i proposa restaurar-hi una còpia de seguretat de producció "només durant dos dies". Argumenta la resposta i proposa una alternativa concreta que resolgui la necessitat tècnica.
Solucions
Solució 1. L'error de partida seria una sola migració amb ADD COLUMN estat text NOT NULL DEFAULT 'confirmada'. Encara que el default sigui constant i PostgreSQL modern no reescrigui la taula, el NOT NULL immediat trenca el codi antic, que fa INSERT sense aquella columna… i en realitat no el trenca, perquè el default l'omple; el problema real és l'invers i més subtil: si després cal revertir el codi, no passa res, però si la columna fos NOT NULL sense default, tots els INSERT del codi antic fallarien. La seqüència segura, que funciona en tots dos casos, és:
- Migració 1 (expandir):
ALTER TABLE cites ADD COLUMN estat text;— nullable, sense default. Instantani. Conviuen: codi vell (ignora la columna) i codi nou si estigués desplegat. Rollback: trivial. - Backfill per lots: omplir
estat = 'confirmada'a les 640.000 files existents amb l'script de l'apartat 7. Sense bloquejos llargs, es pot reprendre. - Desplegament del codi nou: escriu sempre
estati tolera llegirNULLa les files que encara no s'hagin omplert si el backfill continua en curs. Conviuen les dues versions i totes dues funcionen; el rollback continua sent segur. - Migració 2 (endurir): quan el backfill ha acabat i el codi nou porta dies estable,
ALTER TABLE cites ALTER COLUMN estat SET DEFAULT 'confirmada';i desprésSET NOT NULL—precedit, en una taula gran, d'unCHECK (estat IS NOT NULL) NOT VALID+VALIDATE CONSTRAINT, per no recórrer la taula sota bloqueig exclusiu—.
El rollback deixa de ser possible al pas 4, i només parcialment: revertir el codi continua funcionant —la versió antiga simplement ignora la columna—, però el NOT NULL impedeix tornar a un codi que inserís files sense estat. Per això el pas 4 va a part i uns quants dies després.
Solució 2. El mecanisme, en tres temps. (1) CREATE INDEX sense CONCURRENTLY pren un bloqueig que impedeix les escriptures sobre cites durant tota la construcció de l'índex, que en 640.000 files són desenes de segons. (2) Totes les escriptures sobre cites fan cua al darrere. (3) Cada petició encuada reté una connexió del pool; en quaranta segons, amb el trànsit d'un dimarts al matí, el pool queda exhaurit. I aquí hi ha la resposta a per què afecta peticions alienes a cites: el pool de connexions és un recurs compartit de tota l'aplicació, així que una petició a /api/negocis que només llegeix una altra taula tampoc no aconsegueix connexió i falla igual. És exactament el senyal de saturació que la 03-06 descrivia com l'únic que avisa abans: l'alerta PoolConnexionsAlt hauria saltat, encara que amb quinze minuts de finestra probablement massa tard.
Com s'hauria evitat, en tres capes: (a) CREATE INDEX CONCURRENTLY, que no bloqueja escriptures —recordant que no pot anar dins d'una transacció i que si falla deixa un índex invàlid per esborrar—; (b) SET lock_timeout = '3s', que hauria fet fallar la migració netament en lloc d'encuar tota l'aplicació, convertint una caiguda en un job vermell; i (c) haver assajat la migració sobre una còpia amb volum de producció, que hauria mostrat els 40 segons de construcció abans de tocar res. La primera és la solució, la segona és la xarxa de seguretat i la tercera és el que evita la sorpresa.
Solució 3. La resposta és no, i l'argument no és només normatiu. Les dades de les cites de Reservalia inclouen nom, telèfon i a vegades el motiu de la visita —potencialment dades de salut—; staging té menys controls d'accés, més persones amb permisos i logs més verbosos; i hi ha un risc específic i molt concret: si l'entorn té configurat el proveïdor d'SMS, un cicle de proves pot enviar recordatoris a clients reals, amb el flag recordatoris_sms activat sense pensar. El "només dos dies" tampoc no ajuda: els dos dies es converteixen en dos mesos, i una còpia de seguretat de staging presa durant aquella finestra pot sobreviure anys. L'alternativa que sí que resol la necessitat tècnica —que és legítima: reproduir un problema de rendiment exigeix volum realista— té tres peces. (1) Generar volum sintètic: l'script de l'apartat 9 escalat a 640.000 cites amb les mateixes distribucions que producció (cites per negoci, concentració horària, proporció d'horaris partits). El rendiment depèn del volum i la forma de les dades, no que els noms siguin verdaders. (2) Si el problema depèn d'una distribució molt específica, extreure de producció només les estadístiques —histogrames, cardinalitats— i fer-les servir per parametritzar el generador; se'n porten els números, no les dades. (3) Si res de tot això no basta, un bolcat anonimitzat en origen, dins del perímetre de producció, amb substitució irreversible de tots els camps personals, amb data de caducitat automàtica de l'entorn, amb les integracions externes desactivades per configuració, i amb l'aprovació prèvia del responsable de protecció de dades. És l'últim recurs, no el primer, i és precisament la decisió que un curs no pot prendre per tu.
Conclusió i tancament del mòdul
El quart front està tancat. Reservalia ja no tracta l'esquema com un artefacte més: sap que és estat compartit, que una sola base de dades serveix totes les versions del codi desplegades alhora, i que per això el rollback.yml de la 03-05 —que retorna un digest en quatre minuts— no pot desfer un DROP COLUMN. Les seves migracions són fitxers versionats a apps/api/src/db/migracions/, amb marca de temps al nom, revisió obligatòria per CODEOWNERS i una taula migracions_aplicades que les fa idempotents i detecta per hash si algú edita una de ja aplicada. S'executen des del pipeline i mai des d'un portàtil, en un job migrar-prod amb el seu propi entorn d'aprovació —perquè aprovar codi i aprovar un canvi d'esquema són decisions diferents—, amb migrate:status al davant de l'aprovador i lock_timeout i statement_timeout fixats per connexió. I sobretot, Reservalia sap dissenyar el canvi perquè cap versió no es trenqui. El patró expand and contract va convertir el reanomenament d'hora_inici a inici_utc en quatre fases compatibles —expandir amb trigger bidireccional, migrar les dades per lots, desplegar el codi, i contraure una setmana després—, de manera que en tot moment el rolling update de la 03-04 i el canary conviuen amb un esquema que totes dues versions entenen i el rollback continua sent possible. Al voltant d'aquell patró hi ha una classificació clara de què és segur, perillós i prohibit en calent, amb l'alternativa de cada cas; la comprensió de per què un ALTER TABLE en espera tomba una aplicació sencera a través de la cua de bloquejos i del pool de connexions; un backfill per lots, pausat i represa-ble; i una política sobre dades de prova que descarta copiar producció i genera dades sintètiques, amb l'advertiment que aquella decisió es pren amb un professional de protecció de dades i no amb un curs.
Amb això, els quatre fronts que el mòdul 3 va deixar oberts estan resolts:
| Front | Estat | Què el va resoldre |
|---|---|---|
| Pipeline lent | ✅ | Mesurar, memòria cau, sharding, execució selectiva: de 6 a 3 min (04-04) |
| Dependències sense control | ✅ | Lockfile, npm ci, dependabot.yml i política d'actualització (04-02) |
| Seguretat del subministrament | ✅ | Job seguretat, OIDC, accions per SHA, SBOM i signatura (04-03) |
| Migracions d'esquema | ✅ | Migracions versionades i expand and contract (04-06) |
I el número que ho resumeix, mesurat amb la mateixa vara de la línia base del mòdul 1: el change failure rate ha baixat del 6,5 % al 3,8 %, per sota de l'objectiu del 5 % que era l'únic que quedava sense complir. Les quatre mètriques DORA són ara en verd —12 desplegaments per setmana, 3,5 hores de lead time, 3,8 % de fallades i 9 minuts de recuperació—, i les fallades que queden ja no són de procés: són les que qualsevol equip tindrà sempre, les que es descobreixen només quan el programari es troba amb usuaris reals.
Marta: "M'estimo més desplegar deu vegades al dia i que cada desplegament sigui avorrit." Aquell era l'objectiu des de la primera lliçó, i la feina d'aquests tres mòduls va consistir, exactament, a fer avorrit el que abans ocupava tres hores d'un divendres.
El que ve ara canvia de registre. Fins aquí hem construït un pipeline per a un producte, prenent a cada punt la decisió que convenia a Reservalia: monorepo de Node, GitHub Actions, contenidors sobre ECS, trunk-based amb squash. Totes aquelles decisions van ser raonades, però cap no és universal. El mòdul 5, Implementació de CI/CD en Projectes Reals, agafa el que s'ha après i ho sotmet a contextos que no s'assemblen al de Reservalia: una aplicació mòbil amb signatures, botigues i revisions que triguen dies; un sistema de microserveis on el graf de desplegament té desenes de nodes; i un projecte heretat sense proves al qual cal portar CI/CD sense poder-lo reescriure. Comença pel més proper, Cas d'Estudi: Projecte Web, que aterra el pipeline complet sobre una aplicació web real de principi a fi —inclosa la part d'apps/web que hem tractat sempre com a acompanyant de l'API— i serveix de pont entre el sistema que hem construït i els tres casos que el posaran a prova.
Curs de CI/CD: Integració i Desplegament Continu
Mòdul 1: Introducció al CI/CD
- Conceptes Bàsics de CI/CD
- Beneficis del CI/CD
- Eines Populars de CI/CD
- El Projecte del Curs: l'Aplicació que Automatitzarem
- Mètriques DORA: Com es Mesura el Lliurament de Programari
Mòdul 2: Integració Contínua (CI)
- Introducció a la Integració Contínua
- Configuració d'un Entorn de CI
- Automatització de la Construcció
- Proves Automatitzades
- Qualitat de Codi i Anàlisi Estàtica
- Artefactes, Versionat i Promoció
- Integració amb el Control de Versions
Mòdul 3: Desplegament Continu (CD)
- Introducció al Desplegament Continu
- Automatització del Desplegament
- Infraestructura com a Codi i Entorns Reproduïbles
- Estratègies de Desplegament
- Feature Flags, Rollback i Recuperació davant Errors
- Monitoratge i Retroalimentació
Mòdul 4: Pràctiques Avançades de CI/CD
- Pipelines de CI/CD
- Gestió de Dependències
- Seguretat en CI/CD
- Escalabilitat i Rendiment
- Pipeline as Code: Plantilles, Reutilització i Proves del Pipeline
- Bases de Dades al Pipeline: Migracions Segures
Mòdul 5: Implementació de CI/CD en Projectes Reals
- Cas d'Estudi: Projecte Web
- Cas d'Estudi: Aplicació Mòbil
- Cas d'Estudi: Microserveis
- Cas d'Estudi: Modernitzar un Projecte Legacy
Mòdul 6: Eines i Tecnologies
- Jenkins
- GitLab CI/CD
- CircleCI
- Travis CI
- Docker i Kubernetes
- GitHub Actions a Fons
- Comparativa i Criteris per Triar Eina
Mòdul 7: Exercicis Pràctics
- Exercici 1: Configuració d'un Pipeline Bàsic
- Exercici 2: Integració de Proves Automatitzades
- Exercici 3: Desplegament en un Entorn de Producció
- Exercici 4: Monitoratge i Retroalimentació
- Exercici 5: Enfortir el Pipeline amb Seguretat i Secrets
- Projecte Final: Pipeline Complet d'Extrem a Extrem
