El que queda ja no és construir, sinó lliurar. La xarxa de Ribalta funciona, és observable, resisteix i està empaquetada, però continua vivint en màquines de desenvolupament: ningú de la ciutat no pot llogar encara una bicicleta. Aquesta lliçó és la frontissa del mòdul 8. No conté els passos de cap plataforma concreta —això arriba a les quatre següents— sinó les decisions que cal prendre abans de tocar cap consola, perquè són les mateixes a Heroku, a AWS i a Kubernetes, i equivocar-s'hi surt molt més car que triar malament el proveïdor.

Veurem què canvia exactament quan una aplicació passa del teu portàtil a un servidor que atén ciutadans reals, quin artefacte es desplega i per què CicloUrbana viatja com a imatge, els dotze factors aplicats un a un al nostre projecte, els models d'allotjament amb els seus costos i compromisos, l'anatomia completa d'un entorn de producció, una llista de comprovació raonada de tot el que cal decidir —secrets, migracions, còpies de seguretat, memòria de la JVM, zona horària, HTTPS, pool de connexions, aturada ordenada— i les quatre estratègies de desplegament amb allò que cadascuna exigeix de l'aplicació.

Contingut

  1. Què significa desplegar
  2. L'artefacte: JAR, WAR o imatge
  3. Construir una vegada, desplegar a molts llocs
  4. Els dotze factors aplicats a CicloUrbana
  5. Models d'allotjament
  6. Anatomia d'un entorn de producció
  7. Decisions prèvies al desplegament
  8. Estratègies de desplegament
  9. Estat, sessions i escalat horitzontal
  10. Saber si un desplegament ha anat bé, i revertir
  11. Entorns i paritat
  12. Errors Comuns i Consells
  13. Exercicis

  1. Què significa desplegar

Executar ./mvnw spring-boot:run al teu portàtil i desplegar CicloUrbana a Ribalta s'assemblen en una sola cosa: en tots dos casos arrenca una JVM. Tota la resta canvia.

Aspecte En desenvolupament En producció
Qui arrenca el procés Tu, a mà Un supervisor, un orquestrador o una plataforma
Què passa si el procés mor Ho veus i el rellances Ningú no ho veu: s'ha de reiniciar sol
Quantes instàncies hi ha Una Diverses, i canviants
On és la base de dades En un contenidor local, llencable Gestionada, amb dades reals i irrecuperables si es perden
Qui pot cridar l'API Tu Qualsevol amb connexió a Internet
Quant duren les dades Fins al pròxim docker compose down -v Anys
Com es veu un error A la consola En un agregador de logs, potser hores després
Cost d'una fallada Perdre cinc minuts Ciutadans sense bicicleta i una trucada de l'ajuntament
Cost econòmic Zero Facturat per hora, hi hagi trànsit o no

Desplegar és, per tant, fer que una versió concreta del programari estigui disponible i operativa per als seus usuaris reals, de manera repetible i reversible. Les tres qualitats d'aquesta definició són les que importen:

  • Disponible i operativa: no n'hi ha prou que el procés estigui viu; ha d'atendre peticions correctament, amb la seva base de dades, els seus secrets i la seva configuració.
  • Repetible: si el desplegament depèn que algú recordi una seqüència de passos, no és un desplegament, és una cerimònia. L'objectiu del mòdul és que la seqüència estigui escrita i l'executi una màquina (08-05).
  • Reversible: tot desplegament pot sortir malament. Un desplegament del qual no es pot tornar enrere en minuts és una aposta, no un lliurament.

Convé separar tres termes que s'utilitzen com a sinònims i no ho són:

Terme Què és
Construcció (build) Convertir el codi font en un artefacte: ./mvnw package, docker build
Publicació (release) Combinar l'artefacte amb la configuració d'un entorn concret i donar-li un identificador
Desplegament (deploy) Posar aquesta publicació en execució substituint l'anterior

Aquesta separació no és acadèmica: és el cinquè dels dotze factors de l'apartat 4 i explica per què l'artefacte no pot portar dins la configuració de cap entorn.

  1. L'artefacte: JAR, WAR o imatge

La primera decisió és quina cosa exactament es copia al servidor. Hi ha tres respostes històriques i només dues continuen sent raonables.

JAR executable WAR en servidor d'aplicacions Imatge de contenidor
Què conté Classes, dependències i un servidor incrustat (Tomcat) Classes i dependències, sense servidor Sistema de fitxers complet: JRE, aplicació, zona horària, certificats
Com s'executa java -jar ciclourbana.jar Es desplega en un Tomcat/WildFly extern docker run o l'orquestrador
Qui aporta el JRE La màquina amfitriona La màquina amfitriona La mateixa imatge
Mida típica 55-70 MB 40-55 MB 250-350 MB (amb capes compartides)
Aïllament Cap Comparteix JVM amb altres aplicacions Procés, xarxa i sistema de fitxers aïllats
Reproductibilitat Depèn de la versió de Java instal·lada Depèn del servidor i la seva configuració Alta: l'entorn viatja dins
Reversió Desar el JAR anterior Redesplegar el WAR anterior Arrencar l'etiqueta anterior
On es desplega VPS, PaaS, systemd Servidors corporatius heretats PaaS, ECS, Kubernetes, qualsevol lloc
Estat el 2026 Vigent i perfectament vàlid Només per obligació organitzativa L'estàndard de la indústria

Per què el WAR és pràcticament mort. Requereix convertir l'aplicació en un war (<packaging>war</packaging>, estendre SpringBootServletInitializer, marcar Tomcat com a provided), i a canvi s'hereta tota l'operativa del servidor d'aplicacions: la seva versió de Java, la seva configuració global, els seus reinicis que afecten diverses aplicacions alhora, i la vella font d'errors de les biblioteques compartides entre desplegaments. L'única raó legítima per fer-lo servir avui és que la destinació sigui un servidor corporatiu que no es pot canviar.

Per què CicloUrbana es desplega com a imatge. El JAR és una opció digna —Heroku el fa servir a 08-02, i Elastic Beanstalk també— però deixa fora de la caixa una part de l'entorn: quina versió exacta del JRE hi ha a la màquina, quina zona horària té el sistema, quins certificats arrel coneix, quin locale està configurat. La imatge que vam construir a 07-04 fica tot això dins i amb això aconsegueix tres coses que el mòdul necessita:

  1. El mateix artefacte s'executa idèntic al portàtil, a pre i a prod. No hi ha cap «funcionava a preproducció».
  2. És la unitat que entenen les plataformes dels pròxims capítols: ECS Fargate (08-03), Kubernetes (08-04) i les canalitzacions de lliurament (08-05) despleguen imatges, no fitxers.
  3. La reversió és trivial i exacta: ciclourbana:2.3.0 continua existint al registre, byte a byte, i tornar-hi no reconstrueix res.

La regla pràctica: construeix sempre el JAR —és el pas intermedi— i publica la imatge com a artefacte de desplegament.

  1. Construir una vegada, desplegar a molts llocs

Aquest principi ja va aparèixer a 07-02, però aquí és on es torna operatiu. Enunciat formalment:

L'artefacte es construeix una sola vegada, a partir d'un commit concret, i aquesta mateixa còpia binària recorre pre i prod sense tornar-se a compilar. L'única cosa que canvia entre entorns és la configuració injectada des de fora.

flowchart LR
    C[commit 9f3a2b1] --> B[Construcció única<br/>ciclourbana:2.4.0]
    B --> R[(Registre d'imatges)]
    R --> P1[pre<br/>config de pre]
    R --> P2[prod<br/>config de prod]
    P1 -->|mateix binari| OK1[Provat aquí...]
    P2 -->|mateix binari| OK2[...és el que corre aquí]

Per què importa tant. Si es recompila per a producció, el que es desplega és un binari que ningú no ha provat. Pot diferir per una dependència que va resoldre una altra versió, per un perfil de Maven diferent, per una variable de l'entorn de construcció, o simplement perquè entre les dues compilacions algú va fer un commit. Les proves del mòdul 6 es van executar sobre un artefacte i a Ribalta n'arriba un altre: la garantia s'evapora.

La conseqüència directa: l'artefacte no pot portar configuració d'entorn a dins. Res d'application-prod.yml amb la contrasenya real empaquetat al JAR, res d'un Dockerfile amb ENV SPRING_PROFILES_ACTIVE=prod, res de perfils de Maven que produeixin un JAR «de producció» diferent del «de pre». Si el binari conté una decisió d'entorn, deixa de ser un binari i passa a ser-ne diversos.

El que sí que pot —i ha de— viatjar a dins:

Va dins de l'artefacte Va fora, a l'entorn
El codi i les dependències Contrasenyes, secrets, claus d'API
Els application-*.yml sense secrets (valors per entorn) Quin perfil s'activa (SPRING_PROFILES_ACTIVE)
Les migracions de Flyway URL i credencials de la base de dades
Els valors per defecte raonables Mides de pool, límits de memòria, nivell de log
El JRE i la zona horària (a la imatge) Adreces de serveis externs

Aquest és el motiu pel qual a 07-02 els fitxers de perfil es van versionar buits de secrets, amb marcadors ${JWT_SECRET} sense valor per defecte: el fitxer descriu la forma de la configuració, l'entorn aporta el contingut perillós.

  1. Els dotze factors aplicats a CicloUrbana

The Twelve-Factor App és una metodologia publicada el 2011 per l'equip de Heroku que descriu com s'ha de construir una aplicació per ser desplegable, escalable i operable en plataformes modernes. Quinze anys després continua sent la millor llista de comprovació prèvia a un desplegament. Aplicada al nostre projecte:

# Factor Què exigeix Situació de CicloUrbana
1 Codi base Un repositori, molts desplegaments Un repositori Git; pre i prod són desplegaments del mateix codi en un commit diferent
2 Dependències Declarades explícitament, mai implícites del sistema pom.xml amb versions gestionades pel BOM de Spring Boot; res instal·lat a mà al servidor
3 Configuració A l'entorn, no al codi SPRING_PROFILES_ACTIVE, JWT_SECRET, SPRING_DATASOURCE_* com a variables (07-02)
4 Serveis de suport PostgreSQL, correu o pagaments són recursos connectables, intercanviables per URL Canviar de PostgreSQL local a RDS és canviar SPRING_DATASOURCE_URL, sense tocar codi
5 Construir, publicar, executar Tres etapes separades i estrictes mvn verify → docker build+push → desplegament d'una etiqueta concreta
6 Processos Sense estat, sense res per compartir en memòria o disc local La sessió no existeix: el JWT de 05-04 porta la identitat; res no es desa a disc
7 Assignació de ports L'aplicació exposa el seu servei per un port, ella mateixa Tomcat incrustat al 8080, gestió al 8081 (07-01); no hi ha servidor extern que la contingui
8 Concurrència Escalar afegint processos, no fils dins d'un procés gegant S'afegeixen rèpliques de la imatge darrere del balancejador
9 Llencabilitat Arrencada ràpida i aturada ordenada SIGTERM → graceful shutdown de 01-05, amb preStop i període de gràcia folgat
10 Paritat d'entorns dev, pre i prod tan semblants com sigui possible Testcontainers amb PostgreSQL 16 a les proves (06-05) i PostgreSQL 16 gestionat en producció
11 Logs Un flux d'esdeveniments a stdout, que la plataforma recull Logback escriu a consola; res de fitxers rotats per l'aplicació (s'amplia a 09-05)
12 Processos d'administració Tasques puntuals com a processos efímers del mateix codi Les migracions de Flyway com a pas previ del desplegament, amb la mateixa imatge

Val la pena aturar-se en els tres que trenquen més desplegaments:

Factor 3 (configuració). La prova del cotó és senzilla: podries fer públic el repositori ara mateix sense comprometre res? Si la resposta és no, la configuració és al codi.

Factor 6 (processos sense estat). El dia que una instància desa en memòria alguna cosa que una altra necessita —un carret, una sessió, una memòria cau d'escriptura, un fitxer pujat a /tmp— l'escalat horitzontal deixa de funcionar i apareixen errors intermitents impossibles de reproduir: depenen de a quina instància hagi anat a parar la petició.

Factor 11 (logs com a flux). L'aplicació no ha d'obrir fitxers, no els ha de rotar i no ha de saber on acaben els seus missatges. Escriu a stdout i és la plataforma —Heroku, CloudWatch, l'agent de Kubernetes— qui els recull. Si l'aplicació escriu a /var/log/ciclourbana.log dins d'un contenidor, aquests logs desapareixen amb el contenidor.

  1. Models d'allotjament

Model Què gestiones tu Esforç operatiu Cost típic (mes) Quan triar-lo
Servidor propi / VPS Sistema operatiu, Java, actualitzacions, seguretat, TLS, backups, arrencada Molt alt 5-40 € Pressupost mínim, una sola instància, algú que sàpiga administrar Linux
PaaS (Heroku, Render, Railway) Només el codi i les variables Molt baix 25-120 € Primer desplegament, equips petits, temps escàs (08-02)
Contenidors gestionats (ECS Fargate, Cloud Run, App Runner) La imatge i la seva definició de tasca Baix-mitjà 60-250 € Producció real sense voler operar un orquestrador (08-03)
Kubernetes (EKS, GKE, AKS) Manifestos, versions del clúster, complements Alt 200 € + clúster Molts serveis, diversos equips, requisits de portabilitat (08-04)
Funcions sense servidor (Lambda) Només el codi de cada funció Baix Per invocació Càrregues esporàdiques i molt irregulars

Què triaria un projecte com CicloUrbana en cada moment de la seva vida:

  • Prototip i demostració a l'ajuntament: un PaaS. Un git push i ja hi ha URL amb HTTPS. El cost d'aprenentatge és gairebé zero i el focus continua al producte.
  • Servei real de la ciutat, un sol equip: contenidors gestionats. És el punt d'equilibri: infraestructura seriosa (xarxa privada, base de dades gestionada, balancejador, autoescalat) sense la càrrega d'operar un clúster. És l'opció que 08-03 desenvolupa en detall.
  • La xarxa creix a diversos serveis i equips: Kubernetes, quan el cost de coordinar desplegaments superi el d'aprendre l'orquestrador.
  • Mai, en el nostre cas: funcions sense servidor. Una aplicació Spring Boot amb un pool de connexions a PostgreSQL encaixa malament en un model de processos efímers; l'arrencada en fred de la JVM (mitigada, no eliminada, per SnapStart) i la multiplicació de connexions a la base de dades són problemes reals.

I un advertiment que travessa tot el mòdul: aquests serveis es facturen, gairebé sempre per hora de recurs actiu i no per trànsit. Una base de dades gestionada i un balancejador costen el mateix amb zero peticions que amb mil. Quan acabis una pràctica de 08-02, 08-03 o 08-04, destrueix els recursos. Al final de cada lliçó hi ha una llista exacta de què cal eliminar.

  1. Anatomia d'un entorn de producció

Un desplegament seriós mai no és «un servidor amb l'aplicació». És un conjunt de peces amb responsabilitats separades:

flowchart TD
    U[Ciutadans de Ribalta] --> DNS[DNS<br/>ciclourbana.ribalta.example]
    DNS --> LB[Balancejador<br/>terminació TLS · certificat]
    LB --> A1[CicloUrbana rèplica 1]
    LB --> A2[CicloUrbana rèplica 2]
    LB --> A3[CicloUrbana rèplica 3]
    A1 --> BD[(PostgreSQL 16 gestionat<br/>xarxa privada · backups)]
    A2 --> BD
    A3 --> BD
    A1 -.llegeix en arrencar.-> S[Magatzem de secrets]
    A2 -.-> S
    A3 -.-> S
    REG[(Registre d'imatges<br/>ciclourbana:2.4.0)] -.desplega.-> A1
    A1 -.logs i mètriques.-> O[Observabilitat]
    A2 -.-> O
    A3 -.-> O
Peça Responsabilitat Què passa si falta
DNS Traduir el nom públic a l'adreça del balancejador Els usuaris haurien de conèixer una IP que a més canvia
Balancejador / terminació TLS Repartir trànsit, comprovar salut, tancar l'HTTPS Sense ell no hi ha diverses rèpliques ni certificat gestionat
Instàncies de l'aplicació Executar la imatge; són bestiar, no mascotes Una sola instància significa aturada a cada desplegament
Base de dades gestionada Persistència, backups, actualitzacions menors, alta disponibilitat Operar PostgreSQL a mà és la tasca que consumeix més temps i surt pitjor
Magatzem de secrets Desar i rotar credencials fora del repositori Els secrets acaben a Git o en un xat
Registre d'imatges Desar cada versió publicada i immutable No hi ha reversió exacta
Observabilitat Logs, mètriques i traces agregats Una fallada en producció s'investiga a cegues (mòdul 9)

Dues idees que convé fixar. La primera: les instàncies són llencables. Cap no té nom propi, cap no desa res valuós, qualsevol pot morir en qualsevol moment i ser substituïda. Tot el que cal conservar viu a la base de dades. La segona: la base de dades mai no està exposada a Internet. Viu en una xarxa privada i només accepta connexions de les instàncies de l'aplicació. És la regla de seguretat que més vegades s'incompleix en desplegaments improvisats.

  1. Decisions prèvies al desplegament

Aquesta és la llista de comprovació que cal respondre abans de triar plataforma. Cada punt té una resposta correcta amb matisos, no una preferència.

7.1 Com es gestionen els secrets?

CicloUrbana en necessita almenys tres: la contrasenya de PostgreSQL, el secret de signatura HS256 del JWT (05-04) i la clau de la passarel·la de pagaments. Opcions, de pitjor a millor:

Forma Valoració
A application-prod.yml versionat Inacceptable. Queda a l'historial de Git per sempre
A la imatge (ENV del Dockerfile) Inacceptable. Qualsevol amb accés al registre els llegeix amb docker history
Variable d'entorn posada a mà a la plataforma Acceptable com a mínim viable; sense rotació ni auditoria
Magatzem de secrets (Secrets Manager, SSM, Vault, Sealed Secrets) El correcte. Xifrat, auditat, rotable, amb permisos per identitat

I tres regles invariables: mai versionar credencials; rotar qualsevol secret que hagi pogut quedar exposat, encara que el commit s'esborrés després; i aplicar mínim privilegi: l'usuari de PostgreSQL de l'aplicació no necessita ser superusuari ni poder crear bases de dades.

7.2 Com s'executen les migracions de Flyway?

Hi ha dos models i l'elecció canvia amb el nombre d'instàncies.

En arrencar l'aplicació Pas previ del desplegament
Com spring.flyway.enabled: true, s'apliquen a l'arrencada Un procés efímer (Job, tasca puntual, release phase) executa les migracions i després es despleguen les instàncies
Amb una instància Perfecte Correcte, una mica més de maquinària
Amb diverses instàncies Flyway pren un bloqueig a la base de dades: una migra i les altres esperen Les instàncies arrenquen amb l'esquema ja llest
Si la migració falla L'aplicació no arrenca, i pot reintentar en bucle El desplegament s'atura abans de tocar les instàncies vives
Migració llarga (índex sobre milions de files) Bloqueja l'arrencada i dispara les sondes de salut S'executa amb el seu propi temps, sense sondes a sobre
Visibilitat Barrejada al log d'arrencada Un pas propi, amb el seu resultat explícit
Permisos de base de dades L'aplicació necessita permisos DDL sempre L'aplicació pot córrer amb permisos només de dades

La recomanació del curs: migrar en arrencar és perfectament vàlid mentre hi hagi una sola instància o el desplegament sigui recreate; així que hi hagi diverses rèpliques i desplegaments sense tall, convé el pas previ. Amb ddl-auto: validate de 04-08, a més, una instància que arrenqui contra un esquema que no coincideix falla immediatament i de manera clara, en lloc de corrompre dades.

El matís important amb diverses instàncies: encara que Flyway serialitzi correctament mitjançant un bloqueig, durant un rolling update conviuen la versió antiga i la nova contra el mateix esquema. Això obliga que cada migració sigui compatible cap enrere —el patró expand/contract de 04-08— i és el punt que enllaça aquesta llista amb l'apartat 8.

7.3 Còpies de seguretat i restauració

Una còpia de seguretat que mai no s'ha restaurat no és una còpia de seguretat, és una intenció. Tres decisions:

  • RPO (Recovery Point Objective): quantes dades es pot permetre perdre. Amb backups diaris, fins a 24 hores de lloguers. Amb recuperació a un punt en el temps (PITR), segons.
  • RTO (Recovery Time Objective): quant es pot trigar a tornar. Restaurar 20 GB des d'una instantània són desenes de minuts.
  • Prova de restauració: almenys una vegada, restaurar en un entorn a part i comprovar que l'aplicació arrenca contra aquesta còpia. És l'única manera de saber que funciona.

Les bases de dades gestionades (Heroku Postgres, RDS) fan backups automàtics; només cal verificar la finestra de retenció i que l'esborrat de la instància no s'endugui per davant les còpies.

7.4 Dimensionament de la memòria de la JVM en contenidor

Ja vist a 07-04, aquí com a decisió de desplegament: el contenidor té un límit de memòria i la JVM l'ha de respectar amb marge.

# Variable estàndard reconeguda per la JVM sense tocar l'ENTRYPOINT
JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError"
  • MaxRAMPercentage=75 deixa el 25 % restant per a metaespai, piles de fils, búfers directes de xarxa i el procés mateix. Fixar el 100 % —o no fixar res i confiar en el 25 % per defecte de l'heurística antiga— és la causa número u de contenidors OOMKilled sense cap traça al log.
  • ExitOnOutOfMemoryError fa que un OutOfMemoryError mati el procés en lloc de deixar una JVM mig viva que respon malament. Amb reinici automàtic de la plataforma, això és el desitjable.
  • Un punt de partida raonable per a CicloUrbana: 1 GB de límit de memòria i 1 vCPU per rèplica.

7.5 Zona horària i Locale

Un contenidor sense configurar corre en UTC i amb Locale POSIX. Conseqüències reals: els informes programats a les 3 de la matinada (07-03) s'executen a les 5 hora de Ribalta a l'estiu; les dates formatades surten en format anglosaxó; i l'import d'un lloguer s'imprimeix amb punt decimal.

La decisió correcta és doble: fixar TZ=Europe/Madrid a la imatge o l'entorn, i emmagatzemar sempre els instants en UTC (TIMESTAMPTZ a PostgreSQL, Instant a Java), convertint a la zona local només en presentar. El primer evita sorpreses operatives; el segon fa el sistema correcte encara que canviï la zona.

7.6 HTTPS i capçaleres de proxy

En producció, el TLS el termina el balancejador i l'aplicació rep HTTP pla per dins de la xarxa privada. Això està bé —és l'habitual i l'eficient— però introdueix un problema: Tomcat creu que la petició va arribar per http al port 8080, així que les URL que genera (redireccions, Location d'un 201 Created, enllaços de Swagger) surten malament.

server:
  forward-headers-strategy: framework   # interpreta X-Forwarded-Proto / -Host / -Port

Amb aquesta propietat, Spring Boot registra un filtre que reconstrueix l'esquema, l'amfitrió i el port originals a partir de les capçaleres X-Forwarded-* que afegeix el balancejador. Sense ella, un POST /api/v1/lloguers correcte retorna Location: http://10.0.2.31:8080/api/v1/lloguers/1042 en lloc de https://ciclourbana.ribalta.example/api/v1/lloguers/1042.

Avís de seguretat: forward-headers-strategy s'ha d'activar només quan hi ha de debò un proxy de confiança al davant. Si l'aplicació és accessible directament, un client pot falsificar X-Forwarded-For i contaminar els logs o les decisions basades en IP. A Kubernetes i a ECS, on el Service o el grup de seguretat garanteixen que només el balancejador arriba a l'aplicació, és segur.

7.7 Pool d'HikariCP davant de max_connections

Aquest és l'error de dimensionament més freqüent en escalar. HikariCP obre per defecte 10 connexions per instància (04-02). Amb 4 rèpliques són 40 connexions permanents; si a més es desplega en rolling update, durant uns segons hi ha 5 rèpliques i 50. I les migracions, les tasques puntuals i qualsevol eina d'administració en sumen més.

Una instància petita de PostgreSQL gestionat sol venir amb max_connections entre 80 i 120, i el motor mateix en reserva unes quantes per a superusuari. La regla:

rèpliques_màximes × maximum-pool-size  +  marge (migracions, admin, monitoratge)  <  max_connections
spring:
  datasource:
    hikari:
      maximum-pool-size: 10
      minimum-idle: 5
      connection-timeout: 3000     # falla ràpid si no hi ha connexió lliure
      max-lifetime: 1200000        # 20 min, per sota del tall del balancejador i del motor

I un principi contraintuïtiu que convé interioritzar: un pool gran no dona més rendiment. PostgreSQL atén cada connexió amb un procés; passat el punt en què se saturen els nuclis i el disc, més connexions només afegeixen contenció. Un pool de 10 per instància és gairebé sempre més ràpid que un de 50.

7.8 Aturada ordenada i preStop

Quan la plataforma decideix retirar una instància li envia SIGTERM. Amb l'aturada ordenada de 01-05 configurada, Spring Boot deixa d'acceptar peticions noves, acaba les que estan en curs i tanca els executors i el pool.

server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 40s

Falta una peça subtil: el balancejador triga uns segons a assabentar-se que aquella instància ja no ha de rebre trànsit. Si el procés deixa d'acceptar connexions en el mateix instant en què rep el SIGTERM, les peticions que el balancejador continuï enviant durant aquesta finestra fallen amb 502. La solució universal és dormir uns segons abans de començar a aturar-se, cosa que a Kubernetes s'expressa amb un preStop (08-04) i en altres plataformes amb un marge equivalent:

lifecycle:
  preStop:
    exec:
      command: ["sh", "-c", "sleep 10"]

Durant aquests 10 segons, la instància continua atenent amb normalitat mentre el balancejador la retira de la rotació. Després comença l'aturada ordenada. El període de gràcia total ha de ser més gran que preStop + timeout-per-shutdown-phase, o la plataforma matarà el procés a mitges.

  1. Estratègies de desplegament

La pregunta és com es passa de la versió N a la N+1 sense deixar Ribalta sense bicicletes.

flowchart TD
    subgraph Recreate
      R1[v1 v1 v1] --> R2[--- aturada ---] --> R3[v2 v2 v2]
    end
    subgraph Rolling
      O1[v1 v1 v1] --> O2[v2 v1 v1] --> O3[v2 v2 v1] --> O4[v2 v2 v2]
    end
    subgraph Blue-Green
      B1[blau v1 actiu · verd v2 en proves] --> B2[commutar trànsit a verd]
    end
    subgraph Canary
      C1[95% a v1 · 5% a v2] --> C2[50/50 si les mètriques acompanyen] --> C3[100% a v2]
    end
Estratègia Tall de servei Cost de recursos Complexitat Reversió Exigeix de l'aplicació
Recreate Sí, segons o minuts Cap d'extra Mínima Tornar a desplegar l'anterior Res d'especial: mai no conviuen versions
Rolling update No +1 instància temporal Baixa (nativa a ECS i Kubernetes) rollout undo o redesplegar l'etiqueta prèvia Convivència de N i N+1: esquema i API compatibles cap enrere
Blue-green No El doble durant la transició Mitjana Commutar de tornada: segons Convivència durant la finestra; dos entorns complets
Canary No +1 instància Alta: requereix encaminament per percentatge i mètriques per versió Retirar el canari Convivència prolongada i mètriques separades per versió

Què exigeix realment la convivència de versions. Durant un rolling update de CicloUrbana hi ha, diguem, dues rèpliques amb la versió 2.4.0 i una amb la 2.3.0 parlant amb la mateixa base de dades. Això obliga a dues compatibilitats:

  1. Esquema cap enrere. La migració que acompanya 2.4.0 s'ha de poder aplicar sense trencar 2.3.0. Reanomenar capacitat a places_totals en un sol pas mata la versió antiga tan bon punt s'aplica. El patró expand/contract de 04-08 ho resol en tres desplegaments: primer afegir la columna nova i escriure a les dues (expand), després desplegar el codi que només fa servir la nova, i finalment eliminar la vella (contract), cada pas compatible amb l'anterior.
  2. API cap enrere. Si l'aplicació mòbil de Ribalta està demanant /api/v1/lloguers a les tres rèpliques, no pot rebre respostes amb formes diferents. Afegir camps és segur; treure'ls o canviar-ne el tipus, no.

L'elecció per a CicloUrbana: rolling update com a norma —és el que ECS i Kubernetes fan per defecte i n'hi ha prou de respectar la compatibilitat cap enrere—, recreate només per a migracions destructives que exigeixin aturar (i amb avís als ciutadans), i blue-green si algun dia l'ajuntament exigeix poder validar la versió nova amb trànsit real abans de commutar.

  1. Estat, sessions i escalat horitzontal

Amb diverses rèpliques darrere d'un balancejador, una mateixa persona pot caure a la rèplica 1 en una petició i a la 3 en la següent. Tot el que estigui desat a la memòria d'una instància deixa d'existir per a les altres.

La solució clàssica —sessions enganxoses (sticky sessions)— consisteix que el balancejador lligui cada usuari a una instància. Funciona, però és una mala solució: en retirar aquella instància en un desplegament, tots els seus usuaris perden la sessió; i el repartiment de càrrega es desequilibra. La segona solució clàssica és una sessió compartida a Redis (Spring Session), vàlida i molt utilitzada, però afegeix una peça d'infraestructura més.

CicloUrbana no necessita cap de les dues, i la raó és a 05-04: l'autenticació és per JWT. El testimoni el desa el client, viatja a cada petició i conté la identitat i els rols signats. El servidor no recorda res entre peticions: valida la signatura i decideix. Qualsevol rèplica pot atendre qualsevol petició de qualsevol usuari, i una rèplica que mor no s'endú cap sessió. És el factor 6 complert de debò, i per això l'escalat horitzontal dels pròxims capítols és simplement «posar més còpies».

Queda una excepció que convé vigilar: les memòries cau locals. Quan a 09-02 aparegui Spring Cache, una memòria cau en memòria per instància significa que cada rèplica pot tenir un valor diferent i que invalidar en una no invalida a les altres. És acceptable per a dades que toleren estar desactualitzades uns segons, i requereix una memòria cau distribuïda quan no ho toleren.

  1. Saber si un desplegament ha anat bé, i revertir

Un desplegament no acaba quan la plataforma diu «completat». Acaba quan hi ha evidència que la versió nova es comporta almenys tan bé com l'anterior. Què mirar, en els primers deu minuts:

Senyal Què indica D'on surt
Totes les rèpliques en readiness 200 El context va arrencar i la base de dades respon /actuator/health/readiness (07-01)
Cap reinici del contenidor No hi ha OOMKilled ni fallada de liveness La plataforma
Taxa d'errors 5xx estable La versió nova no trenca casos que funcionaven Mètriques (09-03)
Latència p95 i p99 estables No hi ha una consulta nova sense índex ni un pool saturat Mètriques
Sense excepcions noves al log Fallades que no arriben a 5xx però trenquen funcionalitat Logs agregats (09-05)
/actuator/info amb el SHA esperat El desplegat és el que creus que està desplegat build-info i dades de Git (07-01)
Una prova de fum real Un usuari fictici s'autentica i consulta estacions La canalització (08-05)

Aquest últim punt de la taula —build-info amb el SHA del commit— semblava un detall menor a 07-01 i aquí mostra el seu valor: és el que converteix «crec que es va desplegar» en un fet verificable.

I la reversió. La regla és tenir decidit abans de desplegar com es torna enrere i quant es triga:

  • Aplicació: tornar a desplegar l'etiqueta anterior de la imatge. És ràpid (la imatge és al registre) i exacte. Segons o pocs minuts.
  • Base de dades: no es reverteix. Una migració aplicada es queda aplicada; desfer-la amb una altra migració de tornada és perillós i de vegades impossible sense perdre dades. Per això l'esquema ha de ser compatible cap enrere: per poder revertir l'aplicació sense tocar la base de dades.
  • Criteri de decisió: fixat per endavant. «Si la taxa de 5xx supera el 2 % durant cinc minuts, es reverteix», i es reverteix abans d'investigar la causa. Investigar amb producció trencada és la pitjor combinació possible.

  1. Entorns i paritat

Entorn Per a què Dades Qui hi accedeix Diferències acceptables amb prod
dev Feina diària Ficticis, llencables El desenvolupador Moltes: Swagger obert, logs DEBUG, base de dades local
test Execució de la suite (mòdul 6) Generades per la prova La canalització Contenidors efímers amb Testcontainers
pre Validar la versió candidata Còpia anonimitzada o volum realista Equip i ajuntament Les mínimes: mateix tipus d'infraestructura, menys rèpliques
prod El servei real Reals Els ciutadans —

El desè factor demana reduir la distància entre entorns en tres dimensions: temps (que el que s'escriu avui es desplegui avui, no d'aquí a un mes), persones (que qui escriu el codi participi en el seu desplegament) i eines (que el motor, la versió i la configuració siguin els mateixos).

La tercera és on més es peca, i on aquest curs ja va prendre les decisions correctes: PostgreSQL 16 a tot arreu —també a les proves, gràcies a Testcontainers (06-05), en lloc d'H2—, ddl-auto: validate amb Flyway a tots els entorns amb base de dades persistent (04-08), i la mateixa imatge recorrent pre i prod.

El que sí que pot diferir legítimament entre pre i prod: el nombre de rèpliques, la mida de la instància de base de dades, el volum de dades, els serveis externs (simulats a pre, reals a prod) i el nivell de log. Res d'això no canvia el binari.

Errors Comuns i Consells

Recompilar per a cada entorn. Un perfil de Maven que produeix un JAR «de producció» diferent del que es va provar trenca la garantia de tot el mòdul 6. Construeix una vegada; configura moltes vegades.

Ficar secrets a la imatge. ENV JWT_SECRET=... al Dockerfile queda escrit en una capa i es llegeix amb docker history sense ni tan sols executar el contenidor. Els secrets entren en temps d'execució.

Exposar la base de dades a Internet «per poder connectar-m'hi amb DBeaver». És la porta d'entrada més habitual a una filtració. S'hi accedeix per un túnel, un bastió o una sessió gestionada, i sempre amb credencials pròpies i de només lectura quan n'hi hagi prou.

Desplegar sense pla de reversió. «Si surt malament ja ho veurem» significa investigar sota pressió amb el servei caigut. Decideix el criteri de reversió abans de prémer el botó.

Confondre liveness amb readiness. Si la sonda de vida inclou la base de dades, una caiguda momentània del motor reinicia totes les rèpliques en cadena i converteix una incidència de dos minuts en una de vint. Vida = el procés és irrecuperable? Disponibilitat = pot atendre ara? (07-01).

Ignorar el pool davant de max_connections. El sistema funciona amb dues rèpliques i falla en escalar a sis amb FATAL: sorry, too many clients already. Calcula el màxim abans d'escalar, no després.

Desplegar el divendres a la tarda. No és superstició: és que el cost d'una fallada depèn de quanta gent hi hagi disponible per arreglar-la. Quan la canalització i la reversió són sòlides (08-05), deixa d'importar; fins llavors, importa.

Consell: escriu el runbook abans del primer desplegament. Un document curt que respongui: com es desplega, com es reverteix, on són els logs, com es restaura la base de dades i a qui es truca. Mitja hora d'escriptura que s'amortitza el primer dia dolent.

Consell: destrueix sempre els recursos de pràctica. Tot el que creïs a les lliçons següents es factura per hora. Una base de dades gestionada oblidada durant un mes és una factura real i evitable.

Exercicis

Exercici 1

Audita CicloUrbana davant dels dotze factors tal com està en acabar el mòdul 7. Per a cada factor, indica si es compleix, es compleix parcialment o no es compleix, amb una frase de justificació i, quan no es compleixi, la correcció concreta. Presta atenció especial als factors 3, 5, 6 i 12.

Exercici 2

L'ajuntament de Ribalta planteja tres escenaris i demana per a cadascun un model d'allotjament, una estratègia de desplegament i un model d'execució de les migracions, amb justificació:

  • (a) Demostració per al ple municipal d'aquí a dues setmanes, un desenvolupador, pressupost gairebé nul, sense dades reals.
  • (b) Servei en producció per a 40.000 ciutadans, un equip de quatre persones, sense especialista en infraestructura, amb exigència de no tallar el servei en horari diürn.
  • (c) La xarxa creix a cinc aplicacions (bicicletes, patinets, aparcament, incidències, portal ciutadà) amb tres equips i requisit de poder canviar de proveïdor de núvol.

Exercici 3

CicloUrbana s'ha desplegat amb tres rèpliques darrere d'un balancejador. La versió 2.5.0 inclou una migració que reanomena la columna capacitat d'estacions a places_totals i el codi corresponent. Es llança un rolling update amb les migracions executant-se en arrencar cada instància. Descriu minut a minut què passa, què veuen els ciutadans, per què revertir l'aplicació no arregla el problema, i reescriu el pla complet perquè el mateix canvi funcional es lliuri sense tall de servei.

Solucions

Solució 1

# Factor Estat Justificació i correcció
1 Codi base Compleix Un repositori Git, diversos desplegaments del mateix codi
2 Dependències Compleix pom.xml amb el BOM de Spring Boot; el JRE viatja a la imatge des de 07-04
3 Configuració Parcial 07-02 va treure els secrets a variables, però es posen a mà. Correcció: magatzem de secrets amb rotació (08-03)
4 Serveis de suport Compleix PostgreSQL i la passarel·la es configuren per URL; canviar d'instància no toca codi
5 Construir, publicar, executar No compleix Avui no hi ha separació real: es construeix a mà i s'executa a mà. Correcció: la canalització de 08-05
6 Processos Compleix Sense sessió de servidor gràcies al JWT; res al disc local
7 Assignació de ports Compleix Tomcat incrustat, 8080 i 8081
8 Concurrència Parcial L'aplicació ho suporta, però mai no s'ha executat amb més d'una instància. Correcció: verificar a pre amb dues rèpliques, revisant ShedLock i el pool
9 Llencabilitat Compleix shutdown: graceful des de 01-05 i stop_grace_period a Compose (07-04). Pendent el marge tipus preStop
10 Paritat d'entorns Compleix PostgreSQL 16 a les proves amb Testcontainers i en producció; Flyway a tots dos
11 Logs Parcial S'escriu a stdout, però ningú no els agrega. Correcció: agregador (09-05)
12 Processos d'administració No compleix Les migracions s'apliquen en arrencar i no hi ha manera estàndard de llançar una tasca puntual. Correcció: un pas previ de desplegament amb la mateixa imatge

Conclusió de l'auditoria: l'aplicació està ben construïda; el que falta és el procés que l'envolta (factors 5 i 12), que és exactament l'objecte d'aquest mòdul.

Solució 2

(a) Demostració per al ple. Allotjament: PaaS (08-02). En una tarda hi ha URL amb HTTPS i base de dades gestionada, sense aprendre res de xarxes ni d'IAM. Estratègia: recreate; amb una sola instància i sense usuaris reals, un tall de trenta segons no importa. Migracions: en arrencar, per simplicitat, o a la release phase si el PaaS l'ofereix. Detall crític: destruir l'aplicació després del ple, perquè el nivell gratuït ja no existeix.

(b) Producció per a 40.000 ciutadans. Allotjament: contenidors gestionats (ECS Fargate + RDS, 08-03). Raó: cal xarxa privada, base de dades amb backups i alta disponibilitat, balancejador amb TLS i autoescalat, però l'equip no té ningú que es pugui dedicar a operar Kubernetes; Fargate elimina la gestió de servidors. Estratègia: rolling update amb dues rèpliques mínimes i minimumHealthyPercent: 100, recolzada en la comprovació d'estat sobre /actuator/health/readiness. Migracions: pas previ, com a tasca puntual amb la mateixa imatge, més disciplina d'expand/contract; amb rèpliques convivint, és l'única opció sensata.

(c) Cinc aplicacions i tres equips. Allotjament: Kubernetes (08-04). Aquí sí que compensa: la complexitat s'amortitza entre cinc serveis, cada equip desplega el seu Deployment sense coordinar-se amb els altres, i els manifestos són portables entre proveïdors, que era el requisit explícit. Estratègia: rolling update per defecte i canary per als serveis de cara al ciutadà, amb mètriques per versió. Migracions: Job de Kubernetes previ al desplegament, un per servei, cadascun amo del seu propi esquema. Advertiment honest: la base de dades continua sent gestionada fora del clúster; córrer PostgreSQL dins de Kubernetes multiplica el risc sense guany clar.

Solució 3

Minut a minut.

  • Minut 0. L'orquestrador arrenca una instància amb 2.5.0. En iniciar-se, Flyway aplica ALTER TABLE estacions RENAME COLUMN capacitat TO places_totals. La migració triga mil·lisegons i té efecte immediat per a tothom.
  • Minut 0 + 1 segon. Les dues rèpliques de 2.4.0 continuen vives i continuen executant SELECT ... capacitat ... FROM estacions. PostgreSQL respon ERROR: column "capacitat" does not exist. Hibernate propaga l'excepció, el GestorGlobalExcepcions de 03-06 la tradueix i dos terços del trànsit reben 500.
  • Minut 1. La instància nova passa el seu readiness i comença a rebre trànsit: un terç de les peticions funciona. El símptoma que veu el ciutadà és el pitjor possible: l'aplicació falla de manera intermitent, i recarregar «de vegades arregla» el problema.
  • Minuts 1-4. El rolling update substitueix les altres dues rèpliques. Quan acaba, tot torna a funcionar. S'han perdut uns minuts de servei parcial.
  • Si algú reverteix durant la finestra, el desastre és més gran: les tres rèpliques tornen a 2.4.0, que busca capacitat, i la columna ja no existeix. El servei passa de fallar en dos terços a fallar en el 100 %.

Per què revertir no arregla res. La reversió retorna el codi, però la migració ja es va aplicar i l'esquema no torna sol. És la lliçó central de l'apartat 8: la base de dades no es reverteix, així que l'aplicació només es pot revertir si l'esquema és compatible amb la versió anterior. Aquí no ho és, i l'únic camí de tornada seria una altra migració que reanomenés la columna una altra vegada, escrita i aplicada sota pressió.

El pla correcte, en tres desplegaments (expand/contract, 04-08).

Desplegament 1 — expand. Migració que afegeix la columna sense treure res:

-- V10__expand_places_totals.sql
ALTER TABLE estacions ADD COLUMN places_totals INTEGER;
UPDATE estacions SET places_totals = capacitat;
ALTER TABLE estacions ALTER COLUMN places_totals SET NOT NULL;

El codi 2.5.0 llegeix capacitat i escriu a les dues columnes (o un disparador manté la còpia). Compatible amb 2.4.0, que continua fent servir capacitat amb normalitat. El rolling update és segur: totes dues versions funcionen contra el mateix esquema.

Desplegament 2 — canvi de lectura. Sense migració. La versió 2.6.0 llegeix i escriu només places_totals. Continua sent compatible cap enrere, perquè capacitat continua existint i actualitzada, així que revertir a 2.5.0 funciona. Aquest és el desplegament que cal deixar reposar uns dies: és el punt de no retorn pràctic.

Desplegament 3 — contract. Només quan cap instància de 2.5.0 no quedi viva i hi hagi confiança en 2.6.0:

-- V11__contract_eliminar_capacitat.sql
ALTER TABLE estacions DROP COLUMN capacitat;

Reforços del pla. Executar les migracions com a pas previ i no en arrencar, perquè el resultat de cadascuna sigui explícit i no quedi sepultat al log d'arrencada; mantenir ddl-auto: validate, que fa fallar immediatament una instància l'esquema de la qual no coincideix en lloc de deixar-la fallant per consulta; i afegir a la revisió de codi una regla senzilla i verificable: cap migració no pot contenir DROP COLUMN, RENAME ni un canvi de tipus incompatible al mateix desplegament que el codi que l'utilitza.

Conclusió

CicloUrbana encara no està desplegada, però ja saps exactament què significa desplegar-la. Distingeixes construcció, publicació i desplegament, i tens clara la definició operativa: disponible, repetible i reversible. Coneixes els tres artefactes possibles i per què el nostre és una imatge de contenidor —el JAR continua sent vàlid, el WAR només per obligació—, i has interioritzat el principi que governa tot el mòdul: construir una vegada, desplegar a molts llocs, amb la conseqüència inevitable que el binari no pot portar dins la configuració de cap entorn.

Tens els dotze factors aplicats un a un al nostre projecte, amb el diagnòstic honest d'on som: l'aplicació els compleix gairebé tots, i el que falla és el procés que l'envolta. Saps quins models d'allotjament existeixen, quant costen, quant esforç operatiu exigeixen i quin correspon a cada moment de la vida de la xarxa de Ribalta. I tens al cap l'anatomia completa d'un entorn de producció: DNS, balancejador amb terminació TLS, rèpliques llencables, base de dades gestionada en xarxa privada, magatzem de secrets, registre d'imatges i observabilitat.

Sobretot, tens la llista de decisions que cal prendre abans de tocar cap consola: com es desen i es roten els secrets, si Flyway migra en arrencar o en un pas previ —i per què això canvia radicalment amb diverses instàncies—, quines còpies de seguretat hi ha i si alguna vegada s'han restaurat, com es dimensiona la memòria de la JVM amb MaxRAMPercentage per no acabar en OOMKilled, per què cal fixar TZ i desar els instants en UTC, com server.forward-headers-strategy arregla les URL darrere d'un balancejador que termina el TLS, com es calcula el pool d'HikariCP davant del max_connections de PostgreSQL, i per què cal un marge tipus preStop abans de l'aturada ordenada. Coneixes les quatre estratègies de desplegament i, el més important, què exigeix cadascuna de l'aplicació: la convivència de versions obliga a compatibilitat cap enrere a l'esquema —expand/contract— i a l'API. I saps per què CicloUrbana escala horitzontalment sense sessions enganxoses ni Redis: perquè el JWT de 05-04 la va deixar sense estat.

Amb aquesta base, la resta del mòdul són execucions concretes de les mateixes idees. La lliçó següent, Desplegant a Heroku, fa el primer desplegament real: un PaaS que s'encarrega del sistema operatiu, del servidor, del certificat i de la base de dades, i on en menys d'una hora https://ciclourbana-ribalta estarà responent amb les seves estacions. En veurem els conceptes —dynos, buildpacks, Procfile, config vars, release phase—, el detall incòmode d'una DATABASE_URL que no és una URL JDBC vàlida, com hi encaixen els perfils i els secrets, i també on són els límits que abans o després obliguen a sortir d'un PaaS.

Curs de Spring Boot

Mòdul 1: Introducció a Spring Boot

Mòdul 2: Conceptes bàsics de Spring Boot

Mòdul 3: Construint serveis web RESTful

Mòdul 4: Accés a dades amb Spring Boot

Mòdul 5: Seguretat a Spring Boot

Mòdul 6: Proves a Spring Boot

Mòdul 7: Funcions avançades de Spring Boot

Mòdul 8: Desplegament d'aplicacions Spring Boot

Mòdul 9: Rendiment i monitoratge

Mòdul 10: Millors pràctiques i consells

© Copyright 2026. Tots els drets reservats