Lo que queda ya no es construir, sino entregar. La red de Ribalta funciona, es observable, resiste y está empaquetada, pero sigue viviendo en máquinas de desarrollo: nadie de la ciudad puede alquilar una bicicleta todavía. Esta lección es la bisagra del módulo 8. No contiene los pasos de ninguna plataforma concreta —eso llega en las cuatro siguientes— sino las decisiones que hay que tomar antes de tocar ninguna consola, porque son las mismas en Heroku, en AWS y en Kubernetes, y equivocarse en ellas cuesta mucho más caro que elegir mal el proveedor.

Veremos qué cambia exactamente cuando una aplicación pasa de tu portátil a un servidor que atiende a ciudadanos reales, qué artefacto se despliega y por qué CicloUrbana viaja como imagen, los doce factores aplicados uno a uno a nuestro proyecto, los modelos de alojamiento con sus costes y compromisos, la anatomía completa de un entorno de producción, una lista de comprobación razonada de todo lo que hay que decidir —secretos, migraciones, copias de seguridad, memoria de la JVM, zona horaria, HTTPS, pool de conexiones, apagado ordenado— y las cuatro estrategias de despliegue con lo que cada una exige de la aplicación.

Contenido

  1. Qué significa desplegar
  2. El artefacto: JAR, WAR o imagen
  3. Construir una vez, desplegar en muchos sitios
  4. Los doce factores aplicados a CicloUrbana
  5. Modelos de alojamiento
  6. Anatomía de un entorno de producción
  7. Decisiones previas al despliegue
  8. Estrategias de despliegue
  9. Estado, sesiones y escalado horizontal
  10. Saber si un despliegue ha ido bien, y revertir
  11. Entornos y paridad
  12. Errores Comunes y Consejos
  13. Ejercicios

  1. Qué significa desplegar

Ejecutar ./mvnw spring-boot:run en tu portátil y desplegar CicloUrbana en Ribalta se parecen en una sola cosa: en ambos casos arranca una JVM. Todo lo demás cambia.

Aspecto En desarrollo En producción
Quién arranca el proceso Tú, a mano Un supervisor, un orquestador o una plataforma
Qué pasa si el proceso muere Lo ves y lo relanzas Nadie lo ve: debe reiniciarse solo
Cuántas instancias hay Una Varias, y cambiantes
Dónde está la base de datos En un contenedor local, desechable Gestionada, con datos reales e irrecuperables si se pierden
Quién puede llamar a la API Tú Cualquiera con conexión a Internet
Cuánto duran los datos Hasta el próximo docker compose down -v Años
Cómo se ve un error En la consola En un agregador de logs, quizá horas después
Coste de un fallo Perder cinco minutos Ciudadanos sin bicicleta y una llamada del ayuntamiento
Coste económico Cero Facturado por hora, haya tráfico o no

Desplegar es, por tanto, hacer que una versión concreta del software esté disponible y operativa para sus usuarios reales, de forma repetible y reversible. Las tres cualidades de esa definición son las que importan:

  • Disponible y operativa: no basta con que el proceso esté vivo; tiene que atender peticiones correctamente, con su base de datos, sus secretos y su configuración.
  • Repetible: si el despliegue depende de que alguien recuerde una secuencia de pasos, no es un despliegue, es una ceremonia. El objetivo del módulo es que la secuencia esté escrita y la ejecute una máquina (08-05).
  • Reversible: todo despliegue puede salir mal. Un despliegue del que no se puede volver atrás en minutos es una apuesta, no una entrega.

Conviene separar tres términos que se usan como sinónimos y no lo son:

Término Qué es
Construcción (build) Convertir el código fuente en un artefacto: ./mvnw package, docker build
Publicación (release) Combinar el artefacto con la configuración de un entorno concreto y darle un identificador
Despliegue (deploy) Poner esa publicación en ejecución sustituyendo a la anterior

Esta separación no es académica: es el quinto de los doce factores del apartado 4 y explica por qué el artefacto no puede llevar dentro la configuración de ningún entorno.

  1. El artefacto: JAR, WAR o imagen

La primera decisión es qué cosa exactamente se copia al servidor. Hay tres respuestas históricas y solo dos siguen siendo razonables.

JAR ejecutable WAR en servidor de aplicaciones Imagen de contenedor
Qué contiene Clases, dependencias y un servidor embebido (Tomcat) Clases y dependencias, sin servidor Sistema de ficheros completo: JRE, aplicación, zona horaria, certificados
Cómo se ejecuta java -jar ciclourbana.jar Se despliega en Tomcat/WildFly externo docker run o el orquestador
Quién aporta el JRE La máquina anfitriona La máquina anfitriona La propia imagen
Tamaño típico 55-70 MB 40-55 MB 250-350 MB (con capas compartidas)
Aislamiento Ninguno Comparte JVM con otras aplicaciones Proceso, red y sistema de ficheros aislados
Reproducibilidad Depende de la versión de Java instalada Depende del servidor y su configuración Alta: el entorno viaja dentro
Reversión Guardar el JAR anterior Redesplegar el WAR anterior Arrancar la etiqueta anterior
Dónde se despliega VPS, PaaS, systemd Servidores corporativos heredados PaaS, ECS, Kubernetes, cualquier sitio
Estado en 2026 Vigente y perfectamente válido Solo por obligación organizativa El estándar de la industria

Por qué el WAR está prácticamente muerto. Requiere convertir la aplicación en un war (<packaging>war</packaging>, extender SpringBootServletInitializer, marcar Tomcat como provided), y a cambio se hereda toda la operativa del servidor de aplicaciones: su versión de Java, su configuración global, sus reinicios que afectan a varias aplicaciones a la vez, y la vieja fuente de errores de las bibliotecas compartidas entre despliegues. La única razón legítima para usarlo hoy es que el destino sea un servidor corporativo que no se puede cambiar.

Por qué CicloUrbana se despliega como imagen. El JAR es una opción digna —Heroku lo usa en 08-02, y Elastic Beanstalk también— pero deja fuera de la caja una parte del entorno: qué versión exacta del JRE hay en la máquina, qué zona horaria tiene el sistema, qué certificados raíz conoce, qué locale está configurado. La imagen que construimos en 07-04 mete todo eso dentro y con ello consigue tres cosas que el módulo necesita:

  1. El mismo artefacto se ejecuta idéntico en el portátil, en pre y en prod. No hay «funcionaba en preproducción».
  2. Es la unidad que entienden las plataformas de los próximos capítulos: ECS Fargate (08-03), Kubernetes (08-04) y las canalizaciones de entrega (08-05) despliegan imágenes, no ficheros.
  3. La reversión es trivial y exacta: ciclourbana:2.3.0 sigue existiendo en el registro, byte a byte, y volver a ella no reconstruye nada.

La regla práctica: construye el JAR siempre —es el paso intermedio— y publica la imagen como artefacto de despliegue.

  1. Construir una vez, desplegar en muchos sitios

Este principio ya apareció en 07-02, pero aquí es donde se vuelve operativo. Enunciado formalmente:

El artefacto se construye una sola vez, a partir de un commit concreto, y esa misma copia binaria recorre pre y prod sin volver a compilarse. Lo único que cambia entre entornos es la configuración inyectada desde fuera.

flowchart LR
    C[commit 9f3a2b1] --> B[Construcción única<br/>ciclourbana:2.4.0]
    B --> R[(Registro de imágenes)]
    R --> P1[pre<br/>config de pre]
    R --> P2[prod<br/>config de prod]
    P1 -->|mismo binario| OK1[Probado aquí...]
    P2 -->|mismo binario| OK2[...es lo que corre aquí]

Por qué importa tanto. Si se recompila para producción, lo que se despliega es un binario que nadie ha probado. Puede diferir por una dependencia que resolvió otra versión, por un perfil de Maven distinto, por una variable del entorno de construcción, o simplemente porque entre las dos compilaciones alguien hizo un commit. Las pruebas del módulo 6 se ejecutaron sobre un artefacto y a Ribalta llega otro: la garantía se evapora.

La consecuencia directa: el artefacto no puede llevar configuración de entorno dentro. Nada de application-prod.yml con la contraseña real empaquetado en el JAR, nada de un Dockerfile con ENV SPRING_PROFILES_ACTIVE=prod, nada de perfiles de Maven que produzcan un JAR «de producción» distinto del «de pre». Si el binario contiene una decisión de entorno, deja de ser un binario y pasa a ser varios.

Lo que sí puede —y debe— viajar dentro:

Va dentro del artefacto Va fuera, en el entorno
El código y las dependencias Contraseñas, secretos, claves de API
Los application-*.yml sin secretos (valores por entorno) Qué perfil se activa (SPRING_PROFILES_ACTIVE)
Las migraciones de Flyway URL y credenciales de la base de datos
Los valores por defecto razonables Tamaños de pool, límites de memoria, nivel de log
El JRE y la zona horaria (en la imagen) Direcciones de servicios externos

Ese es el motivo por el que en 07-02 los ficheros de perfil se versionaron vacíos de secretos, con marcadores ${JWT_SECRETO} sin valor por defecto: el fichero describe la forma de la configuración, el entorno aporta el contenido peligroso.

  1. Los doce factores aplicados a CicloUrbana

The Twelve-Factor App es una metodología publicada en 2011 por el equipo de Heroku que describe cómo debe construirse una aplicación para ser desplegable, escalable y operable en plataformas modernas. Quince años después sigue siendo la mejor lista de comprobación previa a un despliegue. Aplicada a nuestro proyecto:

# Factor Qué exige Situación de CicloUrbana
1 Código base Un repositorio, muchos despliegues Un repositorio Git; pre y prod son despliegues del mismo código en distinto commit
2 Dependencias Declaradas explícitamente, nunca implícitas del sistema pom.xml con versiones gestionadas por el BOM de Spring Boot; nada instalado a mano en el servidor
3 Configuración En el entorno, no en el código SPRING_PROFILES_ACTIVE, JWT_SECRETO, SPRING_DATASOURCE_* como variables (07-02)
4 Servicios de respaldo PostgreSQL, correo o pagos son recursos conectables, intercambiables por URL Cambiar de PostgreSQL local a RDS es cambiar SPRING_DATASOURCE_URL, sin tocar código
5 Construir, publicar, ejecutar Tres etapas separadas y estrictas mvn verify → docker build+push → despliegue de una etiqueta concreta
6 Procesos Sin estado, sin nada que compartir en memoria o disco local La sesión no existe: el JWT de 05-04 lleva la identidad; nada se guarda en disco
7 Asignación de puertos La aplicación expone su servicio por un puerto, ella misma Tomcat embebido en 8080, gestión en 8081 (07-01); no hay servidor externo que la contenga
8 Concurrencia Escalar añadiendo procesos, no hilos dentro de un proceso gigante Se añaden réplicas de la imagen tras el balanceador
9 Desechabilidad Arranque rápido y apagado ordenado SIGTERM → graceful shutdown de 01-05, con preStop y periodo de gracia holgado
10 Paridad de entornos dev, pre y prod lo más parecidos posible Testcontainers con PostgreSQL 16 en pruebas (06-05) y PostgreSQL 16 gestionado en producción
11 Logs Un flujo de eventos a stdout, que la plataforma recoge Logback escribe a consola; nada de ficheros rotados por la aplicación (se amplía en 09-05)
12 Procesos de administración Tareas puntuales como procesos efímeros del mismo código Las migraciones de Flyway como paso previo del despliegue, con la misma imagen

Vale la pena detenerse en los tres que más despliegues rompen:

Factor 3 (configuración). La prueba del algodón es sencilla: ¿podrías hacer público el repositorio ahora mismo sin comprometer nada? Si la respuesta es no, la configuración está en el código.

Factor 6 (procesos sin estado). El día que una instancia guarda algo en memoria que otra necesita —un carrito, una sesión, una caché de escritura, un fichero subido en /tmp— el escalado horizontal deja de funcionar y aparecen errores intermitentes imposibles de reproducir: dependen de a qué instancia haya ido a parar la petición.

Factor 11 (logs como flujo). La aplicación no debe abrir ficheros, no debe rotarlos y no debe saber dónde acaban sus mensajes. Escribe a stdout y es la plataforma —Heroku, CloudWatch, el agente de Kubernetes— quien los recoge. Si la aplicación escribe en /var/log/ciclourbana.log dentro de un contenedor, esos logs desaparecen con el contenedor.

  1. Modelos de alojamiento

Modelo Qué gestionas tú Esfuerzo operativo Coste típico (mes) Cuándo elegirlo
Servidor propio / VPS Sistema operativo, Java, actualizaciones, seguridad, TLS, backups, arranque Muy alto 5-40 € Presupuesto mínimo, una sola instancia, alguien que sepa administrar Linux
PaaS (Heroku, Render, Railway) Solo el código y las variables Muy bajo 25-120 € Primer despliegue, equipos pequeños, tiempo escaso (08-02)
Contenedores gestionados (ECS Fargate, Cloud Run, App Runner) La imagen y su definición de tarea Bajo-medio 60-250 € Producción real sin querer operar un orquestador (08-03)
Kubernetes (EKS, GKE, AKS) Manifiestos, versiones del clúster, complementos Alto 200 € + clúster Muchos servicios, varios equipos, requisitos de portabilidad (08-04)
Funciones sin servidor (Lambda) Solo el código de cada función Bajo Por invocación Cargas esporádicas y muy irregulares

Qué elegiría un proyecto como CicloUrbana en cada momento de su vida:

  • Prototipo y demostración al ayuntamiento: un PaaS. Un git push y hay URL con HTTPS. El coste de aprendizaje es casi cero y el foco sigue en el producto.
  • Servicio real de la ciudad, un solo equipo: contenedores gestionados. Es el punto de equilibrio: infraestructura seria (red privada, base de datos gestionada, balanceador, autoescalado) sin la carga de operar un clúster. Es la opción que 08-03 desarrolla en detalle.
  • La red crece a varios servicios y equipos: Kubernetes, cuando el coste de coordinar despliegues supere al de aprender el orquestador.
  • Nunca, en nuestro caso: funciones sin servidor. Una aplicación Spring Boot con un pool de conexiones a PostgreSQL encaja mal en un modelo de procesos efímeros; el arranque en frío de la JVM (mitigado, no eliminado, por SnapStart) y la multiplicación de conexiones a la base de datos son problemas reales.

Y una advertencia que atraviesa todo el módulo: estos servicios se facturan, casi siempre por hora de recurso activo y no por tráfico. Una base de datos gestionada y un balanceador cuestan lo mismo con cero peticiones que con mil. Cuando termines una práctica de 08-02, 08-03 o 08-04, destruye los recursos. Al final de cada lección hay una lista exacta de qué eliminar.

  1. Anatomía de un entorno de producción

Un despliegue serio nunca es «un servidor con la aplicación». Es un conjunto de piezas con responsabilidades separadas:

flowchart TD
    U[Ciudadanos de Ribalta] --> DNS[DNS<br/>ciclourbana.ribalta.example]
    DNS --> LB[Balanceador<br/>terminación TLS · certificado]
    LB --> A1[CicloUrbana réplica 1]
    LB --> A2[CicloUrbana réplica 2]
    LB --> A3[CicloUrbana réplica 3]
    A1 --> BD[(PostgreSQL 16 gestionado<br/>red privada · backups)]
    A2 --> BD
    A3 --> BD
    A1 -.lee al arrancar.-> S[Almacén de secretos]
    A2 -.-> S
    A3 -.-> S
    REG[(Registro de imágenes<br/>ciclourbana:2.4.0)] -.despliega.-> A1
    A1 -.logs y métricas.-> O[Observabilidad]
    A2 -.-> O
    A3 -.-> O
Pieza Responsabilidad Qué pasa si falta
DNS Traducir el nombre público a la dirección del balanceador Los usuarios tendrían que conocer una IP que además cambia
Balanceador / terminación TLS Repartir tráfico, comprobar salud, cerrar el HTTPS Sin él no hay varias réplicas ni certificado gestionado
Instancias de la aplicación Ejecutar la imagen; son ganado, no mascotas Una sola instancia significa parada en cada despliegue
Base de datos gestionada Persistencia, backups, actualizaciones menores, alta disponibilidad Operar PostgreSQL a mano es la tarea que más tiempo consume y peor sale
Almacén de secretos Guardar y rotar credenciales fuera del repositorio Los secretos acaban en Git o en un chat
Registro de imágenes Guardar cada versión publicada e inmutable No hay reversión exacta
Observabilidad Logs, métricas y trazas agregados Un fallo en producción se investiga a ciegas (módulo 9)

Dos ideas que conviene fijar. La primera: las instancias son desechables. Ninguna tiene nombre propio, ninguna guarda nada valioso, cualquiera puede morir en cualquier momento y ser sustituida. Todo lo que hay que conservar vive en la base de datos. La segunda: la base de datos nunca está expuesta a Internet. Vive en una red privada y solo acepta conexiones de las instancias de la aplicación. Es la regla de seguridad que más veces se incumple en despliegues improvisados.

  1. Decisiones previas al despliegue

Esta es la lista de comprobación que hay que responder antes de elegir plataforma. Cada punto tiene una respuesta correcta con matices, no una preferencia.

7.1 ¿Cómo se gestionan los secretos?

CicloUrbana necesita al menos tres: la contraseña de PostgreSQL, el secreto de firma HS256 del JWT (05-04) y la clave de la pasarela de pagos. Opciones, de peor a mejor:

Forma Valoración
En application-prod.yml versionado Inaceptable. Queda en el historial de Git para siempre
En la imagen (ENV del Dockerfile) Inaceptable. Cualquiera con acceso al registro los lee con docker history
Variable de entorno puesta a mano en la plataforma Aceptable como mínimo viable; sin rotación ni auditoría
Almacén de secretos (Secrets Manager, SSM, Vault, Sealed Secrets) Lo correcto. Cifrado, auditado, rotable, con permisos por identidad

Y tres reglas invariables: nunca versionar credenciales; rotar cualquier secreto que haya podido quedar expuesto, aunque el commit se borrase después; y aplicar mínimo privilegio: el usuario de PostgreSQL de la aplicación no necesita ser superusuario ni poder crear bases de datos.

7.2 ¿Cómo se ejecutan las migraciones de Flyway?

Hay dos modelos y la elección cambia con el número de instancias.

Al arrancar la aplicación Paso previo del despliegue
Cómo spring.flyway.enabled: true, se aplican en el arranque Un proceso efímero (Job, tarea puntual, release phase) ejecuta las migraciones y luego se despliegan las instancias
Con una instancia Perfecto Correcto, algo más de maquinaria
Con varias instancias Flyway toma un bloqueo en la base de datos: una migra y las demás esperan Las instancias arrancan con el esquema ya listo
Si la migración falla La aplicación no arranca, y puede reintentar en bucle El despliegue se detiene antes de tocar las instancias vivas
Migración larga (índice sobre millones de filas) Bloquea el arranque y dispara las sondas de salud Se ejecuta con su propio tiempo, sin sondas encima
Visibilidad Mezclada en el log de arranque Un paso propio, con su resultado explícito
Permisos de base de datos La aplicación necesita permisos DDL siempre La aplicación puede correr con permisos solo de datos

La recomendación del curso: migrar al arrancar es perfectamente válido mientras haya una sola instancia o el despliegue sea recreate; en cuanto haya varias réplicas y despliegues sin corte, conviene el paso previo. Con ddl-auto: validate de 04-08, además, una instancia que arranque contra un esquema que no coincide falla inmediatamente y de forma clara, en vez de corromper datos.

El matiz importante con varias instancias: aunque Flyway serializa correctamente mediante un bloqueo, durante un rolling update conviven la versión antigua y la nueva contra el mismo esquema. Eso obliga a que cada migración sea compatible hacia atrás —el patrón expand/contract de 04-08— y es el punto que enlaza esta lista con el apartado 8.

7.3 Copias de seguridad y restauración

Una copia de seguridad que nunca se ha restaurado no es una copia de seguridad, es una intención. Tres decisiones:

  • RPO (Recovery Point Objective): cuántos datos se puede permitir perder. Con backups diarios, hasta 24 horas de alquileres. Con recuperación a un punto en el tiempo (PITR), segundos.
  • RTO (Recovery Time Objective): cuánto se puede tardar en volver. Restaurar 20 GB desde una instantánea son decenas de minutos.
  • Prueba de restauración: al menos una vez, restaurar en un entorno aparte y comprobar que la aplicación arranca contra esa copia. Es el único modo de saber que funciona.

Las bases de datos gestionadas (Heroku Postgres, RDS) hacen backups automáticos; solo hay que verificar la ventana de retención y que el borrado de la instancia no se lleve por delante las copias.

7.4 Dimensionado de la memoria de la JVM en contenedor

Ya visto en 07-04, aquí como decisión de despliegue: el contenedor tiene un límite de memoria y la JVM debe respetarlo con margen.

# Variable estándar reconocida por la JVM sin tocar el ENTRYPOINT
JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError"
  • MaxRAMPercentage=75 deja el 25 % restante para metaespacio, pilas de hilos, búferes directos de red y el propio proceso. Fijar el 100 % —o no fijar nada y confiar en el 25 % por defecto de la heurística antigua— es la causa número uno de contenedores OOMKilled sin traza alguna en el log.
  • ExitOnOutOfMemoryError hace que un OutOfMemoryError mate el proceso en lugar de dejar una JVM medio viva que responde mal. Con reinicio automático de la plataforma, eso es lo deseable.
  • Un punto de partida razonable para CicloUrbana: 1 GB de límite de memoria y 1 vCPU por réplica.

7.5 Zona horaria y Locale

Un contenedor sin configurar corre en UTC y con Locale POSIX. Consecuencias reales: los informes programados a las 3 de la madrugada (07-03) se ejecutan a las 5 hora de Ribalta en verano; las fechas formateadas salen en formato anglosajón; y el importe de un alquiler se imprime con punto decimal.

La decisión correcta es doble: fijar TZ=Europe/Madrid en la imagen o el entorno, y almacenar siempre los instantes en UTC (TIMESTAMPTZ en PostgreSQL, Instant en Java), convirtiendo a la zona local solo al presentar. Lo primero evita sorpresas operativas; lo segundo hace el sistema correcto aunque cambie la zona.

7.6 HTTPS y cabeceras de proxy

En producción, el TLS lo termina el balanceador y la aplicación recibe HTTP plano por dentro de la red privada. Eso está bien —es lo habitual y lo eficiente— pero introduce un problema: Tomcat cree que la petición llegó por http al puerto 8080, así que las URLs que genera (redirecciones, Location de un 201 Created, enlaces de Swagger) salen mal.

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

Con esta propiedad, Spring Boot registra un filtro que reconstruye el esquema, el host y el puerto originales a partir de las cabeceras X-Forwarded-* que añade el balanceador. Sin ella, un POST /api/v1/alquileres correcto devuelve Location: http://10.0.2.31:8080/api/v1/alquileres/1042 en lugar de https://ciclourbana.ribalta.example/api/v1/alquileres/1042.

Aviso de seguridad: forward-headers-strategy debe activarse solo cuando hay de verdad un proxy de confianza delante. Si la aplicación es accesible directamente, un cliente puede falsificar X-Forwarded-For y contaminar los logs o las decisiones basadas en IP. En Kubernetes y en ECS, donde el Service o el grupo de seguridad garantizan que solo el balanceador alcanza a la aplicación, es seguro.

7.7 Pool de HikariCP frente a max_connections

Este es el error de dimensionado más frecuente al escalar. HikariCP abre por defecto 10 conexiones por instancia (04-02). Con 4 réplicas son 40 conexiones permanentes; si además se despliega en rolling update, durante unos segundos hay 5 réplicas y 50. Y las migraciones, las tareas puntuales y cualquier herramienta de administración suman más.

Una instancia pequeña de PostgreSQL gestionado suele venir con max_connections entre 80 y 120, y el propio motor reserva unas cuantas para superusuario. La regla:

réplicas_máximas × maximum-pool-size  +  margen (migraciones, admin, monitorización)  <  max_connections
spring:
  datasource:
    hikari:
      maximum-pool-size: 10
      minimum-idle: 5
      connection-timeout: 3000     # falla rápido si no hay conexión libre
      max-lifetime: 1200000        # 20 min, por debajo del corte del balanceador y del motor

Y un principio contraintuitivo que conviene interiorizar: un pool grande no da más rendimiento. PostgreSQL atiende cada conexión con un proceso; pasado el punto en que se saturan los núcleos y el disco, más conexiones solo añaden contención. Un pool de 10 por instancia es casi siempre más rápido que uno de 50.

7.8 Apagado ordenado y preStop

Cuando la plataforma decide retirar una instancia le envía SIGTERM. Con el apagado ordenado de 01-05 configurado, Spring Boot deja de aceptar peticiones nuevas, termina las que están en curso y cierra los ejecutores y el pool.

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

Falta una pieza sutil: el balanceador tarda unos segundos en enterarse de que esa instancia ya no debe recibir tráfico. Si el proceso deja de aceptar conexiones en el mismo instante en que recibe el SIGTERM, las peticiones que el balanceador siga enviando durante esa ventana fallan con 502. La solución universal es dormir unos segundos antes de empezar a apagar, lo que en Kubernetes se expresa con un preStop (08-04) y en otras plataformas con un margen equivalente:

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

Durante esos 10 segundos, la instancia sigue atendiendo con normalidad mientras el balanceador la retira de la rotación. Después empieza el apagado ordenado. El periodo de gracia total debe ser mayor que preStop + timeout-per-shutdown-phase, o la plataforma matará el proceso a mitad.

  1. Estrategias de despliegue

La pregunta es cómo se pasa de la versión N a la N+1 sin dejar a Ribalta sin bicicletas.

flowchart TD
    subgraph Recreate
      R1[v1 v1 v1] --> R2[--- parada ---] --> 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[azul v1 activo · verde v2 en pruebas] --> B2[conmutar tráfico a verde]
    end
    subgraph Canary
      C1[95% a v1 · 5% a v2] --> C2[50/50 si las métricas acompañan] --> C3[100% a v2]
    end
Estrategia Corte de servicio Coste de recursos Complejidad Reversión Exige de la aplicación
Recreate Sí, segundos o minutos Ninguno extra Mínima Volver a desplegar la anterior Nada especial: nunca conviven versiones
Rolling update No +1 instancia temporal Baja (nativa en ECS y Kubernetes) rollout undo o redesplegar la etiqueta previa Convivencia de N y N+1: esquema y API compatibles hacia atrás
Blue-green No El doble durante la transición Media Conmutar de vuelta: segundos Convivencia durante la ventana; dos entornos completos
Canary No +1 instancia Alta: requiere enrutado por porcentaje y métricas por versión Retirar el canario Convivencia prolongada y métricas separadas por versión

Qué exige realmente la convivencia de versiones. Durante un rolling update de CicloUrbana hay, digamos, dos réplicas con la versión 2.4.0 y una con la 2.3.0 hablando con la misma base de datos. Eso obliga a dos compatibilidades:

  1. Esquema hacia atrás. La migración que acompaña a 2.4.0 debe poder aplicarse sin romper a 2.3.0. Renombrar capacidad a plazas_totales en un solo paso mata a la versión antigua en cuanto se aplica. El patrón expand/contract de 04-08 lo resuelve en tres despliegues: primero añadir la columna nueva y escribir en las dos (expand), después desplegar el código que solo usa la nueva, y por último eliminar la vieja (contract), cada paso compatible con el anterior.
  2. API hacia atrás. Si la aplicación móvil de Ribalta está pidiendo /api/v1/alquileres a las tres réplicas, no puede recibir respuestas con formas distintas. Añadir campos es seguro; quitarlos o cambiar su tipo, no.

La elección para CicloUrbana: rolling update como norma —es lo que ECS y Kubernetes hacen por defecto y basta con respetar la compatibilidad hacia atrás—, recreate solo para migraciones destructivas que exijan parar (y con aviso a los ciudadanos), y blue-green si algún día el ayuntamiento exige poder validar la versión nueva con tráfico real antes de conmutar.

  1. Estado, sesiones y escalado horizontal

Con varias réplicas detrás de un balanceador, una misma persona puede caer en la réplica 1 en una petición y en la 3 en la siguiente. Todo lo que esté guardado en la memoria de una instancia deja de existir para las demás.

La solución clásica —sesiones pegajosas (sticky sessions)— consiste en que el balanceador ate a cada usuario a una instancia. Funciona, pero es una mala solución: al retirar esa instancia en un despliegue, todos sus usuarios pierden la sesión; y el reparto de carga se desequilibra. La segunda solución clásica es una sesión compartida en Redis (Spring Session), válida y muy usada, pero añade una pieza de infraestructura más.

CicloUrbana no necesita ninguna de las dos, y la razón está en 05-04: la autenticación es por JWT. El testigo lo guarda el cliente, viaja en cada petición y contiene la identidad y los roles firmados. El servidor no recuerda nada entre peticiones: valida la firma y decide. Cualquier réplica puede atender cualquier petición de cualquier usuario, y una réplica que muere no se lleva ninguna sesión consigo. Es el factor 6 cumplido de verdad, y por eso el escalado horizontal de los próximos capítulos es simplemente «poner más copias».

Queda una excepción que conviene vigilar: las cachés locales. Cuando en 09-02 aparezca Spring Cache, una caché en memoria por instancia significa que cada réplica puede tener un valor distinto y que invalidar en una no invalida en las otras. Es aceptable para datos que toleran estar desactualizados unos segundos, y requiere una caché distribuida cuando no lo toleran.

  1. Saber si un despliegue ha ido bien, y revertir

Un despliegue no termina cuando la plataforma dice «completado». Termina cuando hay evidencia de que la versión nueva se comporta al menos tan bien como la anterior. Qué mirar, en los primeros diez minutos:

Señal Qué indica De dónde sale
Todas las réplicas en readiness 200 El contexto arrancó y la base de datos responde /actuator/health/readiness (07-01)
Ningún reinicio del contenedor No hay OOMKilled ni fallo de liveness La plataforma
Tasa de errores 5xx estable La versión nueva no rompe casos que funcionaban Métricas (09-03)
Latencia p95 y p99 estables No hay una consulta nueva sin índice ni un pool saturado Métricas
Sin excepciones nuevas en el log Fallos que no llegan a 5xx pero rompen funcionalidad Logs agregados (09-05)
/actuator/info con el SHA esperado Lo desplegado es lo que crees que está desplegado build-info y datos de Git (07-01)
Una prueba de humo real Un usuario ficticio se autentica y consulta estaciones La canalización (08-05)

Ese último punto de la tabla —build-info con el SHA del commit— parecía un detalle menor en 07-01 y aquí muestra su valor: es lo que convierte «creo que se desplegó» en un hecho verificable.

Y la reversión. La regla es tener decidido antes de desplegar cómo se vuelve atrás y cuánto se tarda:

  • Aplicación: volver a desplegar la etiqueta anterior de la imagen. Es rápido (la imagen está en el registro) y exacto. Segundos o pocos minutos.
  • Base de datos: no se revierte. Una migración aplicada se queda aplicada; deshacerla con otra migración de vuelta es peligroso y a veces imposible sin perder datos. Por eso el esquema debe ser compatible hacia atrás: para poder revertir la aplicación sin tocar la base de datos.
  • Criterio de decisión: fijado de antemano. «Si la tasa de 5xx supera el 2 % durante cinco minutos, se revierte», y se revierte antes de investigar la causa. Investigar con producción rota es la peor combinación posible.

  1. Entornos y paridad

Entorno Para qué Datos Quién accede Diferencias aceptables con prod
dev Trabajo diario Ficticios, desechables El desarrollador Muchas: Swagger abierto, logs DEBUG, base de datos local
test Ejecución de la suite (módulo 6) Generados por la prueba La canalización Contenedores efímeros con Testcontainers
pre Validar la versión candidata Copia anonimizada o volumen realista Equipo y ayuntamiento Las mínimas: mismo tipo de infraestructura, menos réplicas
prod El servicio real Reales Los ciudadanos —

El décimo factor pide reducir la distancia entre entornos en tres dimensiones: tiempo (que lo escrito hoy se despliegue hoy, no dentro de un mes), personas (que quien escribe el código participe en su despliegue) y herramientas (que el motor, la versión y la configuración sean los mismos).

La tercera es donde más se peca, y donde este curso ya tomó las decisiones correctas: PostgreSQL 16 en todas partes —también en las pruebas, gracias a Testcontainers (06-05), en lugar de H2—, ddl-auto: validate con Flyway en todos los entornos con base de datos persistente (04-08), y la misma imagen recorriendo pre y prod.

Lo que sí puede diferir legítimamente entre pre y prod: el número de réplicas, el tamaño de la instancia de base de datos, el volumen de datos, los servicios externos (simulados en pre, reales en prod) y el nivel de log. Nada de eso cambia el binario.

Errores Comunes y Consejos

Recompilar para cada entorno. Un perfil de Maven que produce un JAR «de producción» distinto del que se probó rompe la garantía de todo el módulo 6. Construye una vez; configura muchas veces.

Meter secretos en la imagen. ENV JWT_SECRETO=... en el Dockerfile queda escrito en una capa y se lee con docker history sin siquiera ejecutar el contenedor. Los secretos entran en tiempo de ejecución.

Exponer la base de datos a Internet «para poder conectarme con DBeaver». Es la puerta de entrada más habitual a una filtración. Se accede por un túnel, un bastión o una sesión gestionada, y siempre con credenciales propias y de solo lectura cuando basten.

Desplegar sin plan de reversión. «Si sale mal ya veremos» significa investigar bajo presión con el servicio caído. Decide el criterio de reversión antes de pulsar el botón.

Confundir liveness con readiness. Si la sonda de vida incluye la base de datos, una caída momentánea del motor reinicia todas las réplicas en cadena y convierte un incidente de dos minutos en uno de veinte. Vida = ¿el proceso está irrecuperable? Disponibilidad = ¿puede atender ahora? (07-01).

Ignorar el pool frente a max_connections. El sistema funciona con dos réplicas y falla al escalar a seis con FATAL: sorry, too many clients already. Calcula el máximo antes de escalar, no después.

Desplegar el viernes por la tarde. No es superstición: es que el coste de un fallo depende de cuánta gente esté disponible para arreglarlo. Cuando la canalización y la reversión son sólidas (08-05), deja de importar; hasta entonces, importa.

Consejo: escribe el runbook antes del primer despliegue. Un documento corto que responda: cómo se despliega, cómo se revierte, dónde están los logs, cómo se restaura la base de datos y a quién se llama. Media hora de escritura que se amortiza el primer día malo.

Consejo: destruye siempre los recursos de práctica. Todo lo que crees en las lecciones siguientes se factura por hora. Una base de datos gestionada olvidada durante un mes es una factura real y evitable.

Ejercicios

Ejercicio 1

Audita CicloUrbana frente a los doce factores tal como está al terminar el módulo 7. Para cada factor, indica si se cumple, se cumple parcialmente o no se cumple, con una frase de justificación y, cuando no se cumpla, la corrección concreta. Presta atención especial a los factores 3, 5, 6 y 12.

Ejercicio 2

El ayuntamiento de Ribalta plantea tres escenarios y pide para cada uno un modelo de alojamiento, una estrategia de despliegue y un modelo de ejecución de las migraciones, con justificación:

  • (a) Demostración para el pleno municipal dentro de dos semanas, un desarrollador, presupuesto casi nulo, sin datos reales.
  • (b) Servicio en producción para 40.000 ciudadanos, un equipo de cuatro personas, sin especialista en infraestructura, con exigencia de no cortar el servicio en horario diurno.
  • (c) La red crece a cinco aplicaciones (bicicletas, patinetes, aparcamiento, incidencias, portal ciudadano) con tres equipos y requisito de poder cambiar de proveedor de nube.

Ejercicio 3

CicloUrbana se ha desplegado con tres réplicas tras un balanceador. La versión 2.5.0 incluye una migración que renombra la columna capacidad de estaciones a plazas_totales y el código correspondiente. Se lanza un rolling update con las migraciones ejecutándose al arrancar cada instancia. Describe minuto a minuto qué ocurre, qué ven los ciudadanos, por qué revertir la aplicación no arregla el problema, y reescribe el plan completo para que el mismo cambio funcional se entregue sin corte de servicio.

Soluciones

Solución 1

# Factor Estado Justificación y corrección
1 Código base Cumple Un repositorio Git, varios despliegues del mismo código
2 Dependencias Cumple pom.xml con el BOM de Spring Boot; el JRE viaja en la imagen desde 07-04
3 Configuración Parcial 07-02 sacó los secretos a variables, pero se ponen a mano. Corrección: almacén de secretos con rotación (08-03)
4 Servicios de respaldo Cumple PostgreSQL y la pasarela se configuran por URL; cambiar de instancia no toca código
5 Construir, publicar, ejecutar No cumple Hoy no hay separación real: se construye a mano y se ejecuta a mano. Corrección: la canalización de 08-05
6 Procesos Cumple Sin sesión de servidor gracias al JWT; nada en disco local
7 Asignación de puertos Cumple Tomcat embebido, 8080 y 8081
8 Concurrencia Parcial La aplicación lo soporta, pero nunca se ha ejecutado con más de una instancia. Corrección: verificar en pre con dos réplicas, revisando ShedLock y el pool
9 Desechabilidad Cumple shutdown: graceful desde 01-05 y stop_grace_period en Compose (07-04). Pendiente el margen tipo preStop
10 Paridad de entornos Cumple PostgreSQL 16 en pruebas con Testcontainers y en producción; Flyway en ambos
11 Logs Parcial Se escribe a stdout, pero nadie los agrega. Corrección: agregador (09-05)
12 Procesos de administración No cumple Las migraciones se aplican al arrancar y no hay forma estándar de lanzar una tarea puntual. Corrección: un paso previo de despliegue con la misma imagen

Conclusión de la auditoría: la aplicación está bien construida; lo que falta es el proceso que la rodea (factores 5 y 12), que es exactamente el objeto de este módulo.

Solución 2

(a) Demostración para el pleno. Alojamiento: PaaS (08-02). En una tarde hay URL con HTTPS y base de datos gestionada, sin aprender nada de redes ni de IAM. Estrategia: recreate; con una sola instancia y sin usuarios reales, un corte de treinta segundos no importa. Migraciones: al arrancar, por simplicidad, o en la release phase si el PaaS la ofrece. Detalle crítico: destruir la aplicación después del pleno, porque el nivel gratuito ya no existe.

(b) Producción para 40.000 ciudadanos. Alojamiento: contenedores gestionados (ECS Fargate + RDS, 08-03). Razón: se necesita red privada, base de datos con backups y alta disponibilidad, balanceador con TLS y autoescalado, pero el equipo no tiene a nadie que pueda dedicarse a operar Kubernetes; Fargate elimina la gestión de servidores. Estrategia: rolling update con dos réplicas mínimas y minimumHealthyPercent: 100, apoyada en la comprobación de estado sobre /actuator/health/readiness. Migraciones: paso previo, como tarea puntual con la misma imagen, más disciplina de expand/contract; con réplicas conviviendo, es la única opción sensata.

(c) Cinco aplicaciones y tres equipos. Alojamiento: Kubernetes (08-04). Aquí sí compensa: la complejidad se amortiza entre cinco servicios, cada equipo despliega su Deployment sin coordinarse con los demás, y los manifiestos son portables entre proveedores, que era el requisito explícito. Estrategia: rolling update por defecto y canary para los servicios de cara al ciudadano, con métricas por versión. Migraciones: Job de Kubernetes previo al despliegue, uno por servicio, cada uno dueño de su propio esquema. Advertencia honesta: la base de datos sigue siendo gestionada fuera del clúster; correr PostgreSQL dentro de Kubernetes multiplica el riesgo sin ganancia clara.

Solución 3

Minuto a minuto.

  • Minuto 0. El orquestador arranca una instancia con 2.5.0. Al iniciar, Flyway aplica ALTER TABLE estaciones RENAME COLUMN capacidad TO plazas_totales. La migración tarda milisegundos y tiene efecto inmediato para todos.
  • Minuto 0 + 1 segundo. Las dos réplicas de 2.4.0 siguen vivas y siguen ejecutando SELECT ... capacidad ... FROM estaciones. PostgreSQL responde ERROR: column "capacidad" does not exist. Hibernate propaga la excepción, el ManejadorGlobalExcepciones de 03-06 la traduce y dos tercios del tráfico reciben 500.
  • Minuto 1. La instancia nueva pasa su readiness y empieza a recibir tráfico: un tercio de las peticiones funciona. El síntoma que ve el ciudadano es el peor posible: la aplicación falla de forma intermitente, y recargar «a veces arregla» el problema.
  • Minutos 1-4. El rolling update sustituye las otras dos réplicas. Cuando termina, todo vuelve a funcionar. Se han perdido unos minutos de servicio parcial.
  • Si alguien revierte durante la ventana, el desastre es mayor: las tres réplicas vuelven a 2.4.0, que busca capacidad, y la columna ya no existe. El servicio pasa de fallar en dos tercios a fallar en el 100 %.

Por qué revertir no arregla nada. La reversión devuelve el código, pero la migración ya se aplicó y el esquema no vuelve solo. Es la lección central del apartado 8: la base de datos no se revierte, así que la aplicación solo puede revertirse si el esquema es compatible con la versión anterior. Aquí no lo es, y el único camino de vuelta sería otra migración que renombrara la columna otra vez, escrita y aplicada bajo presión.

El plan correcto, en tres despliegues (expand/contract, 04-08).

Despliegue 1 — expand. Migración que añade la columna sin quitar nada:

-- V10__expand_plazas_totales.sql
ALTER TABLE estaciones ADD COLUMN plazas_totales INTEGER;
UPDATE estaciones SET plazas_totales = capacidad;
ALTER TABLE estaciones ALTER COLUMN plazas_totales SET NOT NULL;

El código 2.5.0 lee capacidad y escribe en las dos columnas (o un disparador mantiene la copia). Compatible con 2.4.0, que sigue usando capacidad con normalidad. El rolling update es seguro: ambas versiones funcionan contra el mismo esquema.

Despliegue 2 — cambio de lectura. Sin migración. La versión 2.6.0 lee y escribe solo plazas_totales. Sigue siendo compatible hacia atrás, porque capacidad continúa existiendo y actualizada, así que revertir a 2.5.0 funciona. Este es el despliegue que hay que dejar reposar unos días: es el punto de no retorno práctico.

Despliegue 3 — contract. Solo cuando ninguna instancia de 2.5.0 quede viva y haya confianza en 2.6.0:

-- V11__contract_eliminar_capacidad.sql
ALTER TABLE estaciones DROP COLUMN capacidad;

Refuerzos del plan. Ejecutar las migraciones como paso previo y no al arrancar, para que el resultado de cada una sea explícito y no quede sepultado en el log de arranque; mantener ddl-auto: validate, que hace fallar de inmediato a una instancia cuyo esquema no coincide en lugar de dejarla fallando por consulta; y añadir a la revisión de código una regla sencilla y verificable: ninguna migración puede contener DROP COLUMN, RENAME ni un cambio de tipo incompatible en el mismo despliegue que el código que lo usa.

Conclusión

CicloUrbana todavía no está desplegada, pero ya sabes exactamente qué significa desplegarla. Distingues construcción, publicación y despliegue, y tienes clara la definición operativa: disponible, repetible y reversible. Conoces los tres artefactos posibles y por qué el nuestro es una imagen de contenedor —el JAR sigue siendo válido, el WAR solo por obligación—, y has interiorizado el principio que gobierna todo el módulo: construir una vez, desplegar en muchos sitios, con la consecuencia inevitable de que el binario no puede llevar dentro la configuración de ningún entorno.

Tienes los doce factores aplicados uno a uno a nuestro proyecto, con el diagnóstico honesto de dónde estamos: la aplicación cumple casi todos, y lo que falla es el proceso que la rodea. Sabes qué modelos de alojamiento existen, cuánto cuestan, cuánto esfuerzo operativo exigen y cuál corresponde a cada momento de la vida de la red de Ribalta. Y tienes en la cabeza la anatomía completa de un entorno de producción: DNS, balanceador con terminación TLS, réplicas desechables, base de datos gestionada en red privada, almacén de secretos, registro de imágenes y observabilidad.

Sobre todo, tienes la lista de decisiones que hay que tomar antes de tocar ninguna consola: cómo se guardan y rotan los secretos, si Flyway migra al arrancar o en un paso previo —y por qué eso cambia radicalmente con varias instancias—, qué copias de seguridad hay y si alguna vez se han restaurado, cómo se dimensiona la memoria de la JVM con MaxRAMPercentage para no acabar en OOMKilled, por qué hay que fijar TZ y guardar los instantes en UTC, cómo server.forward-headers-strategy arregla las URLs tras un balanceador que termina el TLS, cómo se calcula el pool de HikariCP frente al max_connections de PostgreSQL, y por qué hace falta un margen tipo preStop antes del apagado ordenado. Conoces las cuatro estrategias de despliegue y, lo más importante, qué exige cada una de la aplicación: la convivencia de versiones obliga a compatibilidad hacia atrás en el esquema —expand/contract— y en la API. Y sabes por qué CicloUrbana escala horizontalmente sin sesiones pegajosas ni Redis: porque el JWT de 05-04 la dejó sin estado.

Con esa base, el resto del módulo son ejecuciones concretas de las mismas ideas. La siguiente lección, Desplegando en Heroku, hace el primer despliegue real: un PaaS que se encarga del sistema operativo, del servidor, del certificado y de la base de datos, y donde en menos de una hora https://ciclourbana-ribalta estará respondiendo con sus estaciones. Veremos sus conceptos —dynos, buildpacks, Procfile, config vars, release phase—, el detalle incómodo de una DATABASE_URL que no es una URL JDBC válida, cómo encajan ahí los perfiles y los secretos, y también dónde están los límites que antes o después obligan a salir de un PaaS.

Curso de Spring Boot

Módulo 1: Introducción a Spring Boot

Módulo 2: Conceptos Básicos de Spring Boot

Módulo 3: Construyendo Servicios Web RESTful

Módulo 4: Acceso a Datos con Spring Boot

Módulo 5: Seguridad en Spring Boot

Módulo 6: Pruebas en Spring Boot

Módulo 7: Funciones Avanzadas de Spring Boot

Módulo 8: Despliegue de Aplicaciones Spring Boot

Módulo 9: Rendimiento y Monitoreo

Módulo 10: Mejores Prácticas y Consejos

© Copyright 2026. Todos los derechos reservados