La lliçó anterior va deixar totes les decisions preses i cap executada. Aquesta és la primera vegada que CicloUrbana surt d'una màquina de desenvolupament i respon en una adreça pública d'Internet. I ho fa pel camí més curt que existeix: una plataforma com a servei, on algú s'ocupa del sistema operatiu, del servidor, del certificat TLS, de reiniciar el procés si mor i de mantenir PostgreSQL, i a nosaltres ens queda el codi i unes quantes variables.
Heroku és el PaaS que va inventar bona part del vocabulari que fa servir avui tota la indústria —el Procfile, els buildpacks, les config vars, el git push com a desplegament— i per això continua sent el millor lloc per aprendre'n els conceptes, encara que el seu nivell gratuït va desaparèixer el novembre de 2022. Tot el que veurem aquí es trasllada gairebé literalment a Railway, Render, Fly.io, Cloud Run o Azure App Service, i a l'apartat 3 hi ha la taula d'equivalències perquè puguis seguir la lliçó a la plataforma que prefereixis.
Contingut
- Què és un PaaS i què et dona fet
- Els conceptes de Heroku
- Heroku avui: preus i alternatives equivalents
- Requisits previs
- Preparar CicloUrbana:
system.propertiesiProcfile - Crear l'aplicació i desplegar
- La base de dades: Heroku Postgres i
DATABASE_URL - Configuració i secrets amb config vars
- Migracions de Flyway a la release phase
- Escalat, dynos i el reinici diari
- Logs i sistema de fitxers efímer
- Domini propi i TLS
- Alternativa: desplegar la imatge de contenidor
- Comprovar la salut i monitorar
- Costos i neteja
- Els límits d'un PaaS
- Errors Comuns i Consells
- Exercicis
- Què és un PaaS i què et dona fet
Una plataforma com a servei és un model d'allotjament en què lliures codi —o una imatge— i la plataforma s'ocupa absolutament de tot el que hi ha per sota. A la taula de models de 08-01 era la fila d'«esforç operatiu molt baix».
| El que et dona fet un PaaS | El que perds en control |
|---|---|
| Sistema operatiu i les seves actualitzacions de seguretat | No tries la distribució ni el kernel |
| Instal·lació del JRE (a partir d'una declaració de versió) | Ajustos fins de la JVM limitats per la memòria del pla |
| Construcció de l'artefacte als seus servidors | Poc control sobre l'entorn de construcció |
| Arrencada, supervisió i reinici del procés | No hi ha systemd ni accés persistent a la màquina |
| Balancejador HTTP i certificat TLS automàtic | La terminació TLS és seva; no tries la configuració de xifrats |
| Encaminament a diverses instàncies | Sense control fi de l'algorisme de balanceig |
| Base de dades gestionada com a add-on | Menys paràmetres del motor ajustables |
| Recollida i consulta de logs | Retenció curta llevat que hi hagi add-on de pagament |
| Mètriques bàsiques i alertes | Observabilitat limitada davant d'un stack propi |
| Reversió a la publicació anterior en una ordre | — |
El tracte és explícit: cedeixes control a canvi de temps. Per a un projecte com CicloUrbana en la seva fase inicial —un desenvolupador, una demostració a l'ajuntament, zero persones dedicades a infraestructura— és un tracte excel·lent. El punt on deixa de ser-ho es tracta a l'apartat 16.
- Els conceptes de Heroku
| Concepte | Què és | A CicloUrbana |
|---|---|---|
| App | La unitat de desplegament: codi, configuració, add-ons i domini | ciclourbana-ribalta |
| Dyno | El contenidor Linux lleuger on corre un procés | Un dyno web executant el JAR |
| Tipus de dyno | Mida: Eco, Basic, Standard-1X/2X, Performance-M/L |
Basic per a la demostració; Standard-1X amb 512 MB per a ús real |
| Tipus de procés | La classe de procés declarada: web, release, worker |
web (l'API) i release (les migracions) |
| Slug | L'artefacte comprimit que resulta de la construcció i es copia als dynos | El JAR més el JRE, uns 90 MB |
| Buildpack | L'script que detecta el tipus de projecte i el construeix | heroku/java, que detecta el pom.xml |
Procfile |
Fitxer a l'arrel que declara quina ordre arrenca cada tipus de procés | web: i release: |
| Config var | Variable d'entorn de l'app, gestionada per la plataforma | SPRING_PROFILES_ACTIVE, JWT_SECRET |
| Add-on | Servei de suport connectable (factor 4 de 08-01) | Heroku Postgres, Papertrail |
| Release | Una combinació immutable de slug + config vars, numerada (v42) |
La unitat de reversió |
| Release phase | Un procés que s'executa després de construir i abans d'activar la publicació | On correran les migracions de Flyway |
| Pipeline | Encadenament d'apps per etapa (staging → production) |
ciclourbana-pre → ciclourbana-ribalta |
| Review app | App efímera creada automàticament per cada pull request | Validar un canvi abans de fusionar-lo |
Dos conceptes mereixen un matís. El primer: una release és slug + configuració, així que canviar una config var crea una publicació nova i reinicia els dynos. És exactament el model de 08-01: l'artefacte és un, la publicació combina artefacte i entorn.
El segon: el dyno no és una màquina virtual amb nom, és un contenidor efímer que pot reiniciar-se, moure's de màquina o duplicar-se en qualsevol moment. Tot el que vam descriure a 08-01 sobre processos llencables i sense estat s'aplica aquí de manera literal, i amb més rigor que en altres plataformes.
- Heroku avui: preus i alternatives equivalents
Avís important: Heroku va eliminar el seu nivell gratuït el 28 de novembre de 2022. Ja no existeixen els dynos gratuïts ni el pla hobby-dev de Postgres. Per seguir aquesta lliçó a Heroku cal una targeta i la despesa és real des de la primera hora: el pla més barat amb base de dades ronda els 10-15 dòlars al mes. Existeix el programa Heroku for GitHub Students amb crèdit, però requereix sol·licitud.
Com que els conceptes són universals, aquesta és la taula de traducció:
| Plataforma | Model | Equivalències | Nivell gratuït | Notes |
|---|---|---|---|---|
| Railway | PaaS de contenidors | Servei ≈ app · Variables ≈ config vars · Plugin ≈ add-on | Crèdit mensual limitat | Molt proper a Heroku; detecta el pom.xml |
| Render | PaaS | Web Service ≈ app · Environment Group ≈ config vars · render.yaml ≈ Procfile + app.json |
Sí, amb suspensió per inactivitat | Postgres gestionat; el pla gratuït caduca |
| Fly.io | Contenidors a la vora | Machine ≈ dyno · fly.toml ≈ Procfile · Secrets ≈ config vars |
Crèdit limitat | Desplega imatges; desplegament en diverses regions |
| Google Cloud Run | Contenidors sense servidor | Service ≈ app · Revision ≈ release · Secrets des de Secret Manager | Quota gratuïta mensual generosa | Escala a zero; compte amb l'arrencada en fred de la JVM |
| Azure App Service | PaaS | App Service ≈ app · App Settings ≈ config vars · Deployment Slot ≈ blue-green | Nivell F1 gratuït molt limitat | Suporta JAR de Java 21 directament |
| Clever Cloud | PaaS europeu | Application ≈ app · Environment variables ≈ config vars | No | Allotjament a la UE, rellevant per a dades municipals |
Els conceptes es traslladen gairebé tal qual: en totes elles cal declarar la versió de Java, escoltar al port que indiqui la plataforma per variable d'entorn, posar els secrets com a variables, connectar una base de dades gestionada per URL i escriure els logs a stdout. Si segueixes la lliçó a Railway o Render, canvia les ordres de la CLI i la resta encaixa.
- Requisits previs
# 1. Compte a heroku.com amb verificacio de targeta
# 2. Instal·lar la CLI (macOS amb Homebrew)
brew tap heroku/brew && brew install heroku
# 2 bis. Linux
curl https://cli-assets.heroku.com/install.sh | sh
# 3. Comprovar i autenticar-se (obre el navegador)
heroku --version
heroku loginheroku login desa un testimoni d'API a ~/.netrc. Aquest fitxer és una credencial: no el copiïs a cap repositori ni a cap imatge. Per a entorns automatitzats existeix heroku authorizations:create, que genera un testimoni revocable amb permisos acotats —la forma correcta de donar accés a una canalització (08-05)— en lloc de reutilitzar les teves credencials personals.
També necessites el repositori de CicloUrbana amb el mvnw versionat i commit net: el desplegament és literalment un git push.
- Preparar CicloUrbana:
system.properties i Procfile
system.properties i ProcfileEl buildpack de Java (heroku/java) detecta el projecte per la presència de pom.xml i executa ./mvnw -DskipTests clean install. Se salten les proves de manera deliberada: la construcció de la plataforma no és el lloc on s'executa la suite del mòdul 6, això passa a la canalització (08-05). Però el buildpack necessita dues coses que cal declarar.
system.properties —a l'arrel del repositori— fixa la versió de Java. Sense ell, el buildpack fa servir una versió per defecte que pot no ser la 21 i l'aplicació fallarà amb UnsupportedClassVersionError:
Procfile —també a l'arrel, sense extensió i amb aquesta majúscula exacta— declara els tipus de procés:
Cada part importa:
web:és un nom reservat: identifica el procés que rep trànsit HTTP extern. Qualsevol altre nom (worker,release) no rep peticions.$PORTés obligatori i no negociable. Heroku assigna a cada dyno un port arbitrari en temps d'arrencada i el publica en aquesta variable; l'encaminador hi envia el trànsit. Una aplicació que escolti al 8080 fix no rep res i al cap de 60 segons Heroku la mata amb l'errorR10 Boot timeout. Aquest és, amb diferència, la fallada més comuna del primer desplegament.MaxRAMPercentage=75aplica aquí el mateix que al contenidor de 07-04: un dynoBasicté 512 MB i superar-los produeix errorsR14 Memory quota exceededi una degradació brutal per swap.target/ciclourbana.jarés el camí dins del slug; coincideix amb elfinalNamedelpom.xml.
Com a alternativa a -Dserver.port, Spring Boot pren la variable SERVER_PORT per relaxed binding (02-05), així que web: java -jar target/ciclourbana.jar funciona si es defineix SERVER_PORT=$PORT. La forma explícita del Procfile és preferible perquè deixa la dependència a la vista.
I una comprovació prèvia que estalvia un viatge: l'application.yml no ha de fixar server.port: 8080 de manera que guanyi a la línia d'ordres. No ho fa —la línia d'ordres té més precedència (02-04)— però convé verificar-ho abans de culpar la plataforma.
- Crear l'aplicació i desplegar
# Des de l'arrel del repositori
heroku create ciclourbana-ribalta
# Creating ⬢ ciclourbana-ribalta... done
# https://ciclourbana-ribalta-1a2b3c4d5e6f.herokuapp.com/
# https://git.heroku.com/ciclourbana-ribalta.gitL'ordre fa tres coses: reserva el nom (globalment únic a tota la plataforma; si està agafat, tria'n un altre), assigna un domini herokuapp.com amb TLS ja funcionant, i afegeix un remot de Git anomenat heroku al teu repositori local. Comprova-ho amb git remote -v.
git add system.properties Procfile
git commit -m "Preparar desplegament a Heroku"
git push heroku mainAquest git push és el desplegament. El que passa a continuació, llegit al log de construcció:
remote: -----> Building on the Heroku-24 stack
remote: -----> Determining which buildpack to use for this app
remote: -----> Java app detected
remote: -----> Installing JDK 21... done
remote: -----> Executing Maven
remote: $ ./mvnw -DskipTests clean install
remote: [INFO] BUILD SUCCESS
remote: -----> Discovering process types
remote: Procfile declares types -> release, web
remote: -----> Compressing... done, 92.4M
remote: -----> Launching... done, v3
remote: https://ciclourbana-ribalta-1a2b3c4d5e6f.herokuapp.com/ deployed to HerokuCom llegir-ho, línia a línia:
| Línia | Què significa | Què revisar si falla |
|---|---|---|
Java app detected |
Va trobar el pom.xml |
Si no apareix, el pom.xml no és a l'arrel |
Installing JDK 21 |
Va llegir system.properties |
Si instal·la una altra versió, el fitxer falta o té una errada |
Executing Maven |
Construcció real | Aquí surten els errors de compilació i de dependències |
Procfile declares types |
Va llegir el Procfile |
Si diu (none), el fitxer no és a l'arrel o es diu procfile |
Compressing... 92.4M |
Mida del slug | El límit dur és 500 MB; si s'hi acosta, revisa què s'està empaquetant |
Launching... v3 |
Número de publicació | És l'identificador per revertir |
Compte amb la branca. Només es desplega el que s'empeny al remot heroku. Si treballes a desenvolupament, git push heroku desenvolupament:main és la manera de desplegar aquesta branca sobre la principal de Heroku.
Encara no funciona: falta la base de dades.
- La base de dades: Heroku Postgres i
DATABASE_URL
DATABASE_URLheroku addons:create heroku-postgresql:essential-0 --app ciclourbana-ribalta
# Creating heroku-postgresql:essential-0 on ⬢ ciclourbana-ribalta... ~$5/month
# Database has been created and is available
heroku pg:info --app ciclourbana-ribaltaL'add-on crea una instància gestionada de PostgreSQL i defineix automàticament una config var anomenada DATABASE_URL. Aquí arriba el detall que trenca el primer desplegament de tota aplicació Spring Boot a Heroku:
postgres://usuari123:[email protected]:5432/d9fk2l1m3nAquest format no és una URL JDBC vàlida. Segueix la convenció dels dotze factors —un únic valor amb esquema, credencials i destinació— però el driver de PostgreSQL espera jdbc:postgresql://host:port/base amb usuari i contrasenya per separat. Si l'aplicació intenta fer servir DATABASE_URL tal qual, l'arrencada falla amb Driver claims to not accept jdbcUrl.
Hi ha dues formes correctes de resoldre-ho.
Opció A (recomanada): definir les tres variables explícites. Es llegeixen els valors de l'add-on i es tradueixen a les propietats estàndard de Spring:
# Veure el valor actual
heroku config:get DATABASE_URL --app ciclourbana-ribalta
# Traduir a les propietats de Spring
heroku config:set \
SPRING_DATASOURCE_URL="jdbc:postgresql://ec2-10-20-30-40.compute-1.amazonaws.example:5432/d9fk2l1m3n?sslmode=require" \
SPRING_DATASOURCE_USERNAME="usuari123" \
SPRING_DATASOURCE_PASSWORD="contrasenyaSecreta" \
--app ciclourbana-ribaltaÉs explícit, es depura fàcilment i no afegeix codi. El seu inconvenient és real: Heroku rota les credencials de la base de dades en manteniments i actualitzacions, i quan ho fa canvia DATABASE_URL però no les teves variables copiades. Cal estar atent als avisos de manteniment.
Opció B: transformar DATABASE_URL en arrencar. Un EnvironmentPostProcessor converteix el valor abans que es creï el DataSource:
package com.ciclourbana.comu.config;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
/**
* Tradueix la DATABASE_URL de Heroku (postgres://usuari:clau@host:port/base)
* a les propietats estandard de Spring. S'executa molt aviat a l'arrencada,
* abans que es construeixi el DataSource.
*/
public class TraductorDatabaseUrl implements EnvironmentPostProcessor {
@Override
public void postProcessEnvironment(ConfigurableEnvironment entorn, SpringApplication app) {
String valor = entorn.getProperty("DATABASE_URL");
if (valor == null || valor.startsWith("jdbc:")) {
return; // no som a Heroku, o ja ve traduida
}
URI uri = URI.create(valor);
String[] credencials = uri.getUserInfo().split(":", 2);
Map<String, Object> propietats = new HashMap<>();
propietats.put("spring.datasource.url",
"jdbc:postgresql://%s:%d%s?sslmode=require"
.formatted(uri.getHost(), uri.getPort(), uri.getPath()));
propietats.put("spring.datasource.username", credencials[0]);
propietats.put("spring.datasource.password", credencials[1]);
entorn.getPropertySources()
.addFirst(new MapPropertySource("heroku-datasource", propietats));
}
}I es registra a src/main/resources/META-INF/spring.factories:
org.springframework.boot.env.EnvironmentPostProcessor=\
com.ciclourbana.comu.config.TraductorDatabaseUrlDetalls que expliquen el codi: es fa servir EnvironmentPostProcessor i no un @Bean perquè s'ha d'executar abans que l'autoconfiguració construeixi el DataSource (02-06); addFirst dona a aquestes propietats la màxima precedència; el return primerenc fa que la classe sigui innòcua fora de Heroku, de manera que el mateix artefacte serveix per a tot arreu —principi de 08-01—; i sslmode=require és obligatori, perquè Heroku Postgres només accepta connexions xifrades.
Còpies de seguretat. L'add-on fa backups automàtics, i a més es poden gestionar a mà:
heroku pg:backups:schedule DATABASE_URL --at "03:00 Europe/Madrid" --app ciclourbana-ribalta
heroku pg:backups:capture --app ciclourbana-ribalta # copia puntual
heroku pg:backups # llistar
heroku pg:backups:download b012 # descarregar un bolcat
heroku pg:backups:restore b012 DATABASE_URL # restaurar (DESTRUCTIU)Recorda el principi de 08-01: una còpia que mai no s'ha restaurat no és una còpia. Prova pg:backups:restore en una altra app, no a la de producció.
| Pla | Cost aprox. | Connexions | Emmagatzematge | Ús |
|---|---|---|---|---|
essential-0 |
5 $/mes | 20 | 1 GB | Pràctica i demostració |
essential-2 |
20 $/mes | 40 | 32 GB | Producció petita |
standard-0 |
50 $/mes | 120 | 64 GB | Producció amb rèplica i PITR |
Fixa't en la columna de connexions i recorda el càlcul de 08-01: amb essential-0 i 20 connexions, dos dynos amb el pool per defecte de 10 esgoten el límit sense deixar marge per a migracions ni administració. Si has d'escalar a dos dynos, baixa maximum-pool-size a 5.
- Configuració i secrets amb config vars
Aquí és on els perfils de 07-02 encaixen amb la plataforma:
heroku config:set \
SPRING_PROFILES_ACTIVE=prod \
JWT_SECRET="$(openssl rand -base64 48)" \
TZ=Europe/Madrid \
JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError" \
--app ciclourbana-ribalta
heroku config --app ciclourbana-ribalta # llistar
heroku config:unset VARIABLE_OBSOLETA # eliminarPunts importants:
SPRING_PROFILES_ACTIVE=prodactiva l'application-prod.ymlversionat (sense secrets) de 07-02: Swagger tancat, logs enINFO,ddl-auto: validate, CORS restringit.openssl rand -base64 48genera el secret HS256 al teu terminal i l'envia sense que quedi escrit en cap fitxer. No reutilitzis mai el secret de desenvolupament.- Cada
config:setcrea una publicació nova i reinicia els dynos. Agrupa els canvis en una sola ordre per no provocar diversos reinicis seguits. TZ=Europe/Madridresol allò de l'apartat 7.5 de 08-01: els dynos corren en UTC.
Advertiment de seguretat. Les config vars estan xifrades en repòs, però són visibles per a qualsevol col·laborador de l'app i apareixen a heroku config. Aplica mínim privilegi a l'equip, i rota el secret JWT si algú deixa el projecte o si sospites exposició:
Conseqüència pràctica de rotar: tots els testimonis emesos amb la clau anterior deixen de validar, així que els ciutadans s'hauran d'autenticar de nou. És una molèstia acceptable i la raó per la qual convé que el JWT tingui vida curta i hi hagi un refresh token.
- Migracions de Flyway a la release phase
CicloUrbana té set migracions (V1…V7) i ddl-auto: validate (04-08). Hi ha dues formes d'aplicar-les, i és el mateix dilema de l'apartat 7.2 de 08-01.
En arrencar és el que ja fa l'aplicació: spring.flyway.enabled: true i Flyway migra a l'arrencada del context. Funciona amb un dyno. Amb diversos, tots intenten migrar alhora i Flyway serialitza amb un bloqueig: correcte, però amb dos efectes molestos —els dynos que esperen consumeixen la seva finestra d'arrencada de 60 segons, i si la migració falla tots els dynos entren en bucle de reinici, deixant l'app caiguda encara que la versió anterior funcionés.
A la release phase és la manera segura. Es declara un tipus de procés release al Procfile:
release: java -Dspring.flyway.enabled=true -Dspring.main.web-application-type=none -jar target/ciclourbana.jar
web: java -Dserver.port=$PORT -Dspring.flyway.enabled=false -jar target/ciclourbana.jarCom funciona: després de construir el slug i abans de commutar el trànsit a la nova publicació, Heroku executa el procés release en un dyno d'un sol ús, amb les config vars de l'app. web-application-type=none fa que l'aplicació arrenqui sense Tomcat: s'aixeca el context, Flyway migra i el procés acaba. Si surt amb codi diferent de zero, la publicació es cancel·la i els dynos continuen executant la versió anterior.
| En arrencar | Release phase | |
|---|---|---|
| Amb diversos dynos | Tots esperen el bloqueig | Ja està migrat quan arrenquen |
| Si la migració falla | Tots els dynos cauen en bucle | La publicació s'avorta, la versió prèvia continua viva |
| Visibilitat del resultat | Barrejada al log del dyno | Un pas propi, amb el seu propi codi de sortida |
| Temps d'arrencada | Migració + context dins dels 60 s | Només el context |
| Migració llarga | Pot esgotar l'arrencada | Té el seu propi temps |
La release phase és clarament preferible, i és l'equivalent exacte de la tasca puntual d'ECS (08-03) i del Job de Kubernetes (08-04): el factor 12 dels dotze factors, processos d'administració executats com a processos efímers del mateix codi.
Detall pràctic: en desactivar Flyway al procés web, ddl-auto: validate continua actuant de xarxa de seguretat —si per la raó que sigui l'esquema no correspon, l'aplicació no arrenca en lloc de fallar consulta a consulta.
- Escalat, dynos i el reinici diari
heroku ps --app ciclourbana-ribalta # estat actual
heroku ps:scale web=2 --app ciclourbana-ribalta
heroku ps:type web=standard-1x --app ciclourbana-ribalta
heroku ps:restart --app ciclourbana-ribalta| Tipus de dyno | RAM | Cost aprox./mes | S'adorm | Ús |
|---|---|---|---|---|
Eco |
512 MB | 5 $ (paquet d'hores) | Sí, als 30 min | Pràctiques |
Basic |
512 MB | 7 $ | No | Demostracions |
Standard-1X |
512 MB | 25 $ | No | Producció petita, mètriques incloses |
Standard-2X |
1 GB | 50 $ | No | Quan 512 MB estrenyen |
Performance-M |
2,5 GB | 250 $ | No | Càrrega alta |
512 MB són justos per a Spring Boot. CicloUrbana amb Hibernate, Spring Security i el pool arrenca al voltant de 250-350 MB de heap més metaespai. Amb MaxRAMPercentage=75 hi cap, però sense marge per a un pic. Si apareixen errors R14 al log, la resposta correcta és Standard-2X, no baixar el percentatge fins a l'absurd.
El reinici diari (dyno cycling). Heroku reinicia tots els dynos almenys una vegada cada 24 hores, a més de quan canvia una config var, quan es desplega o quan la màquina amfitriona ho necessita. No és una fallada: és la plataforma imposant el factor 9, llencabilitat. Conseqüències per al que ja tenim construït:
- Res en memòria no sobreviu. Ja ho complim: sense sessió gràcies al JWT (05-04).
- Res al disc no sobreviu (apartat 11).
- Les tasques programades de 07-03 es veuen afectades. Una tasca amb
@Scheduled(fixedDelay = ...)reinicia el seu comptador a cada reinici del dyno; i amb dos dynos, tots dos tenen el planificador actiu, així que elCaducadorLloguersi elRecalculadorOcupacios'executarien per duplicat. ShedLock salva exactament aquesta situació: la taulashedlockde la migracióV7fa que només el primer dyno que prengui el bloqueig executi la tasca i l'altre la salti. Sense ShedLock, escalar aweb=2significaria duplicar informes i recalcular l'ocupació dues vegades per minut.
Un matís sobre les tasques amb cron: si un reinici coincideix just amb l'hora programada, aquesta execució es pot perdre. Per a tasques crítiques convé un planificador extern —Heroku Scheduler és un add-on— que invoqui un endpoint o llanci un procés puntual, en lloc de dependre que el dyno estigui viu en aquell segon exacte.
- Logs i sistema de fitxers efímer
heroku logs --tail --app ciclourbana-ribalta
heroku logs --num 500 --source app --app ciclourbana-ribalta
heroku logs --dyno web.1 --tailEl sistema de fitxers d'un dyno és efímer: cada dyno té la seva pròpia còpia del slug amb una capa escrivible que es destrueix a cada reinici, i dos dynos no comparteixen res. D'aquí se segueixen tres regles:
- Els logs van a
stdout. És el factor 11 de 08-01 i el que Logback ja fa per defecte a CicloUrbana. Si l'aplicació escrivís alogs/ciclourbana.log, aquest fitxer desapareixeria a cada reinici i seria diferent a cada dyno. - Els fitxers pujats per usuaris no es poden desar al dyno. Si demà CicloUrbana admet fotos d'incidències, van a un emmagatzematge d'objectes (S3 o equivalent), no al disc.
- La retenció de Heroku és de 1.500 línies o una setmana, el que arribi abans. Per a producció real cal un add-on d'agregació (Papertrail, Logtail) o reenviament a un sistema propi — el tema complet de 09-05.
Un detall de format: els logs de Heroku estan multiplexats entre dynos i processos, i una traça d'excepció de Java apareix com a desenes de línies independents. És un bon argument per adoptar logs en JSON (09-05) i perquè el FiltreRastreig amb MDC de 03-06 hi sigui: amb l'identificador de rastreig a cada línia, reconstruir una petició entre milers de línies barrejades deixa de ser un exercici de paciència.
- Domini propi i TLS
El domini herokuapp.com funciona amb HTTPS des del primer moment. Per al domini de l'ajuntament:
heroku domains:add ciclourbana.ribalta.example --app ciclourbana-ribalta
# Configure your app's DNS provider to point to the DNS Target:
# ciclourbana.ribalta.example -> tranquil-otter-9x8y7z6w.herokudns.example
heroku certs:auto:enable --app ciclourbana-ribalta
heroku certs:auto --app ciclourbana-ribalta # veure l'estatAl proveïdor DNS es crea un CNAME cap a la destinació indicada. Mai un registre A: l'adreça IP de Heroku canvia. Amb el CNAME propagat, Automated Certificate Management sol·licita i renova el certificat per Let's Encrypt automàticament. Requereix un pla de pagament (Basic o superior).
Dos ajustos a l'aplicació tanquen el cercle amb 08-01 i 05-05:
L'encaminador de Heroku termina el TLS i parla HTTP amb el dyno, afegint X-Forwarded-Proto: https. Sense aquesta propietat, les capçaleres Location dels 201 Created i les URL de springdoc sortirien amb http:// i un amfitrió intern. I el CORS de 05-05 ha de permetre l'origen definitiu https://ciclourbana.ribalta.example, no el domini de proves.
- Alternativa: desplegar la imatge de contenidor
A 07-04 vam construir una imatge acurada: multietapa, per capes, amb usuari no root i JAVA_TOOL_OPTIONS. Heroku pot desplegar aquesta imatge en lloc de construir amb el buildpack:
heroku stack:set container --app ciclourbana-ribalta
heroku container:login
heroku container:push web --app ciclourbana-ribalta
heroku container:release web --app ciclourbana-ribaltaRequereix un heroku.yml a l'arrel:
build:
docker:
web: Dockerfile
release: Dockerfile
release:
command:
- java -Dspring.flyway.enabled=true -Dspring.main.web-application-type=none -jar /app/ciclourbana.jar
run:
web: java -Dserver.port=$PORT -jar /app/ciclourbana.jar| Buildpack (JAR) | Contenidor (imatge) | |
|---|---|---|
| Què controles | Poc: versió de Java i poca cosa més | Tot: base, paquets, usuari, zona horària |
| Reproductibilitat | Construeix Heroku, entorn opac | La imatge és idèntica a tot arreu |
Paritat amb pre/prod |
Depèn de la plataforma | Total: la mateixa imatge que ECS o Kubernetes |
| Esforç | Zero configuració | Mantenir el Dockerfile |
| Encaix amb 08-03 i 08-04 | Cap | Directe |
Si el pla a mitjà termini és sortir a ECS o Kubernetes, desplegar la imatge des del principi fa que el pas següent no sigui un salt: canvia la plataforma, no l'artefacte. Compte amb $PORT també aquí: l'ENTRYPOINT de la imatge de 07-04 escolta al 8080 fix, així que cal respectar el run: web: del heroku.yml o parametritzar el port.
- Comprovar la salut i monitorar
curl -s https://ciclourbana.ribalta.example/actuator/health | jq
# {"status":"UP"}
curl -s https://ciclourbana.ribalta.example/actuator/health/readiness
curl -s https://ciclourbana.ribalta.example/actuator/info | jq '.build'
# { "version": "2.4.0", "time": "2026-09-01T09:14:22Z" }Aquest /actuator/info amb build-info (07-01) respon la pregunta de 08-01: quina versió està corrent realment a Ribalta.
Un detall de configuració específic de Heroku: el port de gestió separat (8081) de 07-01 no funciona aquí, perquè un dyno només pot exposar un port, el de $PORT. A Heroku cal deixar Actuator al port principal sota la cadena de seguretat que ja vam escriure, amb /actuator/health i /actuator/info públics i la resta exigint ADMIN:
# application-heroku.yml (o dins del perfil prod, condicionat)
management:
server:
port: ${MANAGEMENT_PORT:} # buit = mateix port que l'aplicacioComplements de monitoratge: els dynos Standard inclouen mètriques (memòria, temps de resposta, throughput) al panell; existeixen add-ons d'APM; i heroku ps més el log mostren els codis d'error de la plataforma, que convé conèixer:
| Codi | Significat | Causa habitual |
|---|---|---|
| R10 | Boot timeout: no va escoltar a $PORT en 60 s |
El Procfile no passa $PORT, o l'arrencada triga massa |
| R14 | Quota de memòria superada | Heap mal dimensionat per al tipus de dyno |
| H12 | Request timeout als 30 s | Consulta lenta o crida externa sense timeout (07-06) |
| H10 | App crashed | Excepció a l'arrencada; mira el log complet |
- Costos i neteja
Advertiment de cost. Tot el d'aquesta lliçó es factura per hora prorratejada des del moment en què existeix, amb trànsit o sense.
| Recurs | Pla mínim | Cost aprox./mes |
|---|---|---|
Dyno Basic |
1 dyno | 7 $ |
Dyno Standard-1X |
1 dyno | 25 $ |
| Heroku Postgres | essential-0 |
5 $ |
| Certificat automàtic | Inclòs amb pla de pagament | 0 $ |
| Add-on de logs | Pla bàsic | 0-7 $ |
| Total mínim realista | ~12-32 $/mes |
Neteja obligatòria en acabar la pràctica. Destruir l'app elimina també els seus add-ons i les seves còpies de seguretat:
# 1. (Opcional) Descarregar una copia abans de destruir res
heroku pg:backups:capture --app ciclourbana-ribalta
heroku pg:backups:download --app ciclourbana-ribalta
# 2. Veure el que es facturara
heroku addons --app ciclourbana-ribalta
heroku ps --app ciclourbana-ribalta
# 3. Destruir (demana confirmacio escrivint el nom)
heroku apps:destroy --app ciclourbana-ribalta --confirm ciclourbana-ribalta
# 4. Verificar que no queda res
heroku apps
heroku addonsheroku apps:destroy és irreversible i s'endú la base de dades i les seves còpies. Si el domini propi hi estava apuntant, esborra també el CNAME al proveïdor DNS per no deixar un registre penjant cap a una destinació inexistent.
- Els límits d'un PaaS
Un PaaS és l'elecció correcta fins que deixa de ser-ho. Els senyals:
| Límit | Manifestació | Quan apareix |
|---|---|---|
| Cost per unitat de capacitat | Quatre Standard-2X costen més que la infraestructura equivalent |
En escalar de debò |
| Sense xarxa privada pròpia | No pots aïllar la base de dades en subxarxes teves ni connectar amb sistemes interns de l'ajuntament | Requisits de xarxa o compliment |
| Control limitat de la JVM i del sistema | No hi ha ajustos fins de l'amfitrió, ni versions concretes de biblioteques del sistema | Problemes de rendiment específics |
| Reinici diari forçat | Incompatible amb processos llargs que no tolerin interrupcions | Treballs per lots pesants |
| Tancament de connexions als 30 s (H12) | Peticions llargues o SSE necessiten un altre disseny | Descàrregues grans, streaming |
| Dependència del proveïdor | Add-ons, Procfile, release phase i CLI són seus |
En voler migrar |
| Regió i compliment | Dades municipals que han de residir a la UE | Requisit legal des del dia u |
L'últim punt és especialment rellevant per a CicloUrbana: les dades dels ciutadans de Ribalta estan subjectes al RGPD i l'ajuntament pot exigir allotjament a la Unió Europea. Heroku té regió europea, però és una decisió que cal prendre en crear l'app (heroku create --region eu), no després.
Quan algun d'aquests senyals apareix, la destinació natural és un model de contenidors gestionats amb xarxa pròpia: exactament el que fa la lliçó següent.
Errors Comuns i Consells
Escoltar en un port fix. Sense $PORT, el dyno no rep trànsit i mor amb R10 als 60 segons. És l'error número u.
procfile en minúscula o dins d'una subcarpeta. Heroku no el troba, el log diu Procfile declares types -> (none) i l'app no arrenca. S'ha de dir Procfile, a l'arrel, sense extensió.
Fer servir DATABASE_URL com si fos JDBC. La fallada és Driver claims to not accept jdbcUrl. Tradueix amb variables explícites o amb l'EnvironmentPostProcessor, i no oblidis sslmode=require.
Oblidar SPRING_PROFILES_ACTIVE=prod. L'app arrenca amb la configuració de desenvolupament a Internet: Swagger obert, logs DEBUG i potser H2. És l'escenari exacte de l'exercici 3 de 07-02.
Escalar a dos dynos sense revisar el pool. Amb essential-0 (20 connexions) i maximum-pool-size: 10, dos dynos esgoten el límit i el tercer procés —inclosa la release phase— falla amb too many clients.
Escalar a dos dynos sense ShedLock. Els informes surten duplicats i l'ocupació es recalcula dues vegades. La migració V7 de 07-03 ja hi és: només cal verificar que les tasques porten @SchedulerLock.
Escriure fitxers al dyno. Desapareixen al pròxim reinici i no es comparteixen entre dynos.
Consell: fes servir un pipeline amb pre i prod. Dues apps encadenades i heroku pipelines:promote mouen el slug ja construït d'una a l'altra, sense reconstruir. És el principi de 08-01 implementat per la plataforma.
Consell: heroku releases i heroku rollback són el teu pla de reversió. heroku releases llista les publicacions numerades i heroku rollback v41 torna a l'anterior en segons. Amb la salvetat coneguda: la base de dades no torna, així que l'esquema ha de continuar sent compatible cap enrere.
Consell: heroku run bash per inspeccionar. Obre un dyno puntual amb el slug i les config vars. Perfecte per depurar i també un recordatori de per què els secrets són secrets: qui pot executar això, els veu tots.
Exercicis
Exercici 1
Prepara el repositori de CicloUrbana per a Heroku sense desplegar res encara: escriu system.properties i un Procfile amb els processos web i release, decideix quines config vars calen i en quin ordre s'executa tot des de git push fins que l'app atén la seva primera petició. Explica què passaria si faltés cadascun dels dos fitxers.
Exercici 2
Es desplega CicloUrbana i el log mostra, en aquest ordre: Java app detected, BUILD SUCCESS, Launching... v3, i tot seguit un bucle de at=error code=H10 desc="App crashed" amb l'excepció java.lang.IllegalStateException: Cannot load driver class: org.postgresql.Driver ... jdbcUrl is required with driverClassName. L'app té l'add-on heroku-postgresql:essential-0 creat i heroku config mostra DATABASE_URL. Diagnostica, corregeix de les dues formes possibles i explica quina triaries per a un projecte que planeja migrar a AWS d'aquí a sis mesos.
Exercici 3
CicloUrbana porta un mes a Heroku amb web=1, dyno Basic i essential-0. L'ajuntament demana alta disponibilitat, així que s'escala a web=2. En 48 hores apareixen tres problemes: (a) l'informe nocturn d'ocupació arriba dues vegades per correu, (b) el log mostra FATAL: sorry, too many clients already de manera intermitent i (c) durant els desplegaments alguns ciutadans reben 503. Explica la causa de cadascun i proposa la correcció completa, indicant què canvia a l'aplicació, què a la configuració i què al pla de la plataforma.
Solucions
Solució 1
system.properties a l'arrel:
Procfile a l'arrel:
release: java -Dspring.flyway.enabled=true -Dspring.main.web-application-type=none -jar target/ciclourbana.jar
web: java -Dserver.port=$PORT -XX:MaxRAMPercentage=75 -jar target/ciclourbana.jarConfig vars necessàries:
| Variable | Valor | Per què |
|---|---|---|
SPRING_PROFILES_ACTIVE |
prod |
Activa application-prod.yml (07-02) |
SPRING_DATASOURCE_URL |
jdbc:postgresql://...?sslmode=require |
Traducció de DATABASE_URL |
SPRING_DATASOURCE_USERNAME / _PASSWORD |
de l'add-on | Ídem |
JWT_SECRET |
openssl rand -base64 48 |
Signatura HS256 (05-04); mai el de desenvolupament |
TZ |
Europe/Madrid |
Els dynos corren en UTC (07-03) |
JAVA_TOOL_OPTIONS |
-XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError |
Evitar R14 |
Seqüència completa: git push heroku main → el buildpack detecta el pom.xml → instal·la el JDK 21 llegit de system.properties → executa ./mvnw -DskipTests clean install → comprimeix el slug → llegeix el Procfile i descobreix els tipus release i web → executa el procés release en un dyno d'un sol ús, que aixeca el context sense Tomcat, aplica V1…V7 i acaba amb codi 0 → si el codi és 0, activa la publicació v3 → arrenca el dyno web, que escolta a $PORT, valida l'esquema amb ddl-auto: validate i queda llest → l'encaminador comença a enviar-li trànsit.
Si falta system.properties: el buildpack instal·la el seu JDK per defecte. Si és anterior al 21, la construcció falla en compilar codi de Java 21, o —pitjor— construeix i l'arrencada mor amb UnsupportedClassVersionError.
Si falta el Procfile: el buildpack de Java intenta endevinar l'ordre d'arrencada a partir del JAR. Pot arribar a funcionar, però sense $PORT, així que el dyno no escolta on ha d'escoltar i mor amb R10 Boot timeout. I no hi hauria release phase, amb la qual cosa les migracions s'haurien d'aplicar en arrencar.
Solució 2
Diagnòstic. La construcció va anar bé i l'app va arrencar, així que el problema és de configuració en temps d'execució. El missatge jdbcUrl is required ve d'HikariCP: no va trobar spring.datasource.url. L'add-on va definir DATABASE_URL, però Spring Boot no la reconeix com a propietat seva —espera SPRING_DATASOURCE_URL— i encara que la llegís, postgres://usuari:clau@host:5432/base no és una URL JDBC: li falta el prefix jdbc: i porta credencials incrustades que el driver no accepta aquí.
Correcció A — variables explícites:
heroku config:get DATABASE_URL --app ciclourbana-ribalta
# postgres://u9k2:[email protected]:5432/d3n1
heroku config:set \
SPRING_DATASOURCE_URL="jdbc:postgresql://ec2-10-20-30-40.compute-1.amazonaws.example:5432/d3n1?sslmode=require" \
SPRING_DATASOURCE_USERNAME="u9k2" \
SPRING_DATASOURCE_PASSWORD="pw7x" \
--app ciclourbana-ribaltaCorrecció B — EnvironmentPostProcessor: la classe TraductorDatabaseUrl de l'apartat 7, registrada a META-INF/spring.factories, que tradueix a l'arrencada i no fa res fora de Heroku.
Quina triar amb AWS a sis mesos vista: l'A. Raonament: l'opció B introdueix codi específic de Heroku dins de l'artefacte, just el que 08-01 demana evitar. És una peça que caldria mantenir, provar i, previsiblement, esborrar en la migració. L'opció A resol el problema fora del binari, amb les tres propietats estàndard de Spring, que són exactament les mateixes que es faran servir amb RDS: migrar consistirà a canviar-ne els valors. L'inconvenient conegut —Heroku rota les credencials— es mitiga documentant-ho al runbook i subscrivint-se als avisos de manteniment de l'add-on.
Si l'horitzó fos quedar-se anys a Heroku amb rotacions freqüents, la B seria defensable; fins i tot llavors, aïllada en un paquet d'integració i activada per una condició explícita.
Solució 3
(a) L'informe arriba dues vegades. Tots dos dynos tenen @EnableScheduling actiu i cadascun executa la seva pròpia còpia de la tasca. És el problema de 07-03 amb diverses instàncies, ara en producció. La causa concreta és que la tasca de l'informe no porta @SchedulerLock, o que falta la configuració de ShedLock. Correcció a l'aplicació:
@Scheduled(cron = "0 0 3 * * *", zone = "Europe/Madrid")
@SchedulerLock(name = "informeOcupacioDiari",
lockAtMostFor = "PT30M", lockAtLeastFor = "PT5M")
public void generarInformeDiari() { ... }lockAtMostFor allibera el bloqueig si el dyno mor a mitges; lockAtLeastFor evita que un segon dyno l'executi si el primer va acabar en mil·lisegons per rellotge desajustat. La taula shedlock ja existeix des de la migració V7, així que no cal res de nou a la base de dades. Verificació: el log del dyno que no executa ha de mostrar la traça de ShedLock indicant que no va obtenir el bloqueig.
(b) too many clients already. El pla essential-0 permet 20 connexions i maximum-pool-size és 10: dos dynos consumeixen les 20 exactes, sense deixar-ne cap per a la release phase, per a heroku pg:psql ni per al monitoratge de l'add-on. Per això és intermitent: falla just quan alguna cosa més demana connexió. És el càlcul de 08-01:
Correcció en dos fronts. A la configuració:
spring:
datasource:
hikari:
maximum-pool-size: ${HIKARI_POOL_SIZE:6}
minimum-idle: 2
connection-timeout: 3000Amb 2 × 6 = 12 queden 8 connexions de marge. I al pla: pujar a essential-2 (40 connexions) si el rendiment amb 6 no n'hi ha prou. Recorda el principi de 08-01: un pool gran no és més ràpid; amb 6 connexions per dyno i consultes indexades, CicloUrbana atén folgadament el trànsit d'una ciutat petita.
(c) 503 durant els desplegaments. En desplegar, Heroku envia SIGTERM als dynos antics i arrenca els nous, però l'encaminador pot continuar enviant peticions al dyno que s'està aturant durant una finestra breu. Si el procés deixa d'acceptar connexions de cop, aquestes peticions fallen. Correccions:
L'aturada ordenada (01-05) fa que les peticions en curs acabin. Heroku concedeix 30 segons entre SIGTERM i SIGKILL, així que 25 s deixa marge. Complements: escalar a web=3 perquè durant el relleu sempre hi hagi capacitat sobrant, i evitar reinicis innecessaris agrupant els config:set en una única ordre en lloc d'encadenar-ne diversos, cadascun amb la seva publicació i el seu reinici.
Resum del pla. A l'aplicació: @SchedulerLock a les tasques i shutdown: graceful amb el seu temps. A la configuració: maximum-pool-size baixat a 6 i connection-timeout explícit. Al pla de la plataforma: essential-2 si el pool reduït estreny, i valorar web=3. I una conclusió de fons: els tres símptomes van aparèixer el dia que hi va haver més d'una instància, que és el moment en què els factors 6, 8 i 9 deixen de ser teoria.
Conclusió
CicloUrbana ja és a Internet. Qualsevol amb l'adreça pot consultar les estacions de Ribalta, autenticar-se i llogar una bicicleta, sobre HTTPS, amb una base de dades gestionada i còpies de seguretat automàtiques, i tot això s'ha aconseguit sense administrar ni un sol servidor. Saps què et dona fet un PaaS i quin control cedeixes a canvi, i manejes el seu vocabulari complet —app, dyno i els seus tipus, slug, buildpack, Procfile, config var, release, release phase, pipeline i review app—, amb l'advertiment clar que Heroku ja no té nivell gratuït i la taula d'alternatives on aquests mateixos conceptes apareixen amb un altre nom.
Has preparat el projecte amb system.properties i un Procfile que respecta la regla que trenca més desplegaments —escoltar a $PORT—, has llegit el log de construcció línia a línia sabent què revisar a cadascuna, i has resolt el problema clàssic de la DATABASE_URL de Heroku, que segueix els dotze factors però no és una URL JDBC vàlida, amb les dues solucions i el criteri per triar-ne una. Els perfils de 07-02 van encaixar amb les config vars, el secret del JWT es va generar fora del repositori i saps per què i com es rota. Les migracions de Flyway van passar a la release phase, que avorta la publicació si fallen en lloc de deixar tots els dynos en bucle: el factor 12 en la seva forma més concreta.
També coneixes el que la plataforma imposa: dynos amb memòria justa on MaxRAMPercentage deixa de ser un detall, un reinici diari que converteix la llencabilitat en un fet i que obliga que ShedLock estigui ben posat abans d'escalar a dos dynos, un sistema de fitxers efímer que confirma per què els logs van a stdout, i el càlcul del pool d'HikariCP davant del límit de connexions del pla. Has afegit el domini de l'ajuntament amb certificat automàtic i forward-headers-strategy perquè les URL surtin bé darrere del proxy, has comprovat amb /actuator/health i /actuator/info quina versió corre realment, i —molt important— has destruït l'app en acabar la pràctica, perquè tot això es factura per hora.
I saps on són els límits: cost per unitat de capacitat en escalar, absència de xarxa privada pròpia, control reduït de l'entorn, el tall als 30 segons, la dependència del proveïdor i la qüestió de la regió per a dades municipals subjectes al RGPD. Quan aquests senyals apareixen, el pas següent és una infraestructura real que continuï sent gestionada. La lliçó següent, Desplegant a AWS, la construeix: una imatge publicada a ECR, PostgreSQL a RDS dins de subxarxes privades on Internet no arriba, secrets a Secrets Manager, un servei d'ECS Fargate amb la seva definició de tasca, i un balancejador amb certificat d'ACM que comprova la salut contra /actuator/health/readiness. Amb el mateix advertiment de sempre, i aquí més de debò: RDS i el balancejador es facturen per hora encara que ningú no faci servir l'aplicació.
Curs de Spring Boot
Mòdul 1: Introducció a Spring Boot
- Què és Spring Boot?
- Configuració del teu entorn de desenvolupament
- Creant la teva primera aplicació Spring Boot
- Entenent l'estructura del projecte
- L'arrencada i el cicle de vida de l'aplicació
Mòdul 2: Conceptes bàsics de Spring Boot
- Anotacions de Spring Boot
- Injecció de dependències a Spring Boot
- Àmbit i cicle de vida dels beans
- Configuració de Spring Boot
- Propietats de Spring Boot
- Autoconfiguració i starters per dins
Mòdul 3: Construint serveis web RESTful
- Introducció als serveis web RESTful
- Creant controladors REST
- Gestió dels mètodes HTTP
- Validació de dades d'entrada
- DTOs i mapatge entre capes
- Gestió d'excepcions a REST
- Documentar l'API amb OpenAPI
Mòdul 4: Accés a dades amb Spring Boot
- Introducció a Spring Data JPA
- Configuració de fonts de dades
- Creació d'entitats JPA
- Relacions entre entitats
- Ús de repositoris de Spring Data
- Mètodes de consulta a Spring Data JPA
- Transaccions i gestió de la persistència
- Migracions d'esquema amb Flyway
Mòdul 5: Seguretat a Spring Boot
- Introducció a Spring Security
- Configuració de Spring Security
- Autenticació i autorització d'usuaris
- Implementació d'autenticació JWT
- Seguretat a nivell de mètode i enduriment de l'API
Mòdul 6: Proves a Spring Boot
- Introducció a les proves
- Proves unitàries amb JUnit
- Simulació amb Mockito
- Proves d'integració
- Proves amb Testcontainers
Mòdul 7: Funcions avançades de Spring Boot
- Spring Boot Actuator
- Perfils de Spring Boot
- Tasques programades i execució asíncrona
- Spring Boot amb Docker
- Spring Boot i microserveis
- Comunicació entre serveis i tolerància a fallades
Mòdul 8: Desplegament d'aplicacions Spring Boot
- Introducció al desplegament
- Desplegant a Heroku
- Desplegant a AWS
- Desplegant a Kubernetes
- Integració i lliurament continus
Mòdul 9: Rendiment i monitoratge
- Ajust de rendiment
- Memòria cau amb Spring Cache
- Monitoratge amb Spring Boot Actuator
- Ús de Prometheus i Grafana
- Gestió de registres i logs
- Traçabilitat distribuïda
