A les tres lliçons anteriors CicloUrbana va arribar a Heroku, a AWS i a Kubernetes. A totes tres hi va haver un actor implícit que continuava sent humà: algú que executava git push heroku main, aws ecs update-service o helm upgrade des del seu portàtil, amb les seves credencials personals, confiant a haver passat les proves abans i a no haver-se deixat res a mig commitejar.

Aquest actor és l'últim punt artesanal del projecte, i aquesta lliçó l'elimina. Construirem una canalització que, davant de cada git push, compili, executi la suite completa del mòdul 6 —inclosos els Testcontainers—, analitzi la qualitat, construeixi i escanegi la imatge, la publiqui etiquetada amb el SHA del commit, la desplegui a pre, passi una prova de fum i, després d'una aprovació explícita, la porti a producció. Ningú no toca un servidor. I amb això, les proves que vam escriure al mòdul 6 deixen de ser un exercici de disciplina personal per convertir-se en una porta que ningú no es pot saltar.

Contingut

  1. Integració, lliurament i desplegament continus
  2. Per què la CI és el que dona valor a les proves
  3. Anatomia d'una canalització
  4. GitHub Actions: els conceptes
  5. El flux d'integració: ci.yml
  6. El flux de lliurament: cd.yml
  7. Secrets a la canalització
  8. Qualitat dins de la canalització
  9. Versionatge i publicació
  10. Desplegar a pre i a prod, i revertir
  11. Migracions de base de dades a la canalització
  12. Que la canalització sigui ràpida i fiable
  13. Alternatives i mètriques DORA
  14. Errors Comuns i Consells
  15. Exercicis

  1. Integració, lliurament i desplegament continus

Els tres termes es fan servir com a sinònims i designen coses diferents. La diferència és fins on arriba l'automatització.

Integració contínua (CI) Lliurament continu (CD) Desplegament continu
Què automatitza Compilar i provar cada canvi integrat a la branca principal Tot l'anterior + deixar cada canvi llest per desplegar Tot l'anterior + desplegar a producció sense intervenció
On acaba Un veredicte: verd o vermell Un artefacte publicat i validat a pre El canvi en mans dels ciutadans
Qui decideix desplegar Ningú: no es desplega Una persona, prement un botó Ningú: si està verd, surt
Què exigeix de l'equip Suite de proves fiable i ràpida; integrar a diari A més: entorns automatitzats i esquema compatible A més: confiança total, observabilitat, reversió automàtica
Risc si falta maduresa Baix Mitjà Alt

La distinció pràctica que convé retenir: lliurament continu significa que podries desplegar en qualsevol moment; desplegament continu significa que ho fas sempre. La diferència entre tots dos és una decisió de negoci i de confiança, no de tecnologia: és exactament el pas d'aprovació manual de l'apartat 10.

Què construïm per a CicloUrbana: integració contínua completa, lliurament continu fins a pre de manera automàtica, i desplegament a prod amb aprovació. És l'elecció sensata per a un servei municipal en el seu primer any. Quan la canalització porti mesos sense sorpreses i l'observabilitat del mòdul 9 sigui al seu lloc, treure aquesta aprovació serà un pas petit.

  1. Per què la CI és el que dona valor a les proves

Al mòdul 6 vam escriure una suite considerable: unitàries de les tarifes de Ribalta, dobles de Mockito, talls @WebMvcTest i @DataJpaTest, la matriu d'accés amb spring-security-test i proves *IT contra PostgreSQL 16 real amb Testcontainers. Tot això s'executa amb ./mvnw verify.

El problema és que ./mvnw verify l'executa qui se'n recorda. I apareixen els patrons coneguts: algú té pressa i puja sense executar-les; algú les executa però només el mòdul que ha tocat; una prova porta dues setmanes fallant i l'equip ha normalitzat el vermell; una prova passa al portàtil de qui la va escriure perquè té una fila a la base de dades local que ningú més no té.

La CI talla els quatre d'arrel:

Sense CI Amb CI
Les proves les executa qui vol S'executen sempre, a cada push i cada pull request
A l'entorn de cadascú En un entorn net i reproduïble
El resultat el coneix qui les va llançar El resultat és públic i bloqueja la fusió
Una prova trencada pot conviure setmanes La branca principal no accepta un canvi en vermell
«A la meva màquina passa» Si no passa a la CI, no passa

Aquest últim punt és el canvi cultural de fons: la CI és l'àrbitre. I hi ha un detall tècnic decisiu per a nosaltres: el runner de GitHub Actions ja té Docker instal·lat i en marxa, així que els Testcontainers de 06-05 funcionen sense configurar res. Les proves d'integració contra PostgreSQL real, que eren la peça més valuosa del mòdul 6, són també les que la CI executa amb més fidelitat.

  1. Anatomia d'una canalització

flowchart TD
    A[git push / pull request] --> B[checkout]
    B --> C[memoria cau de dependencies Maven]
    C --> D[compilar]
    D --> E[proves unitaries · Surefire]
    E --> F[analisi estatica · Sonar/SpotBugs]
    F --> G[proves d'integracio · Failsafe + Testcontainers]
    G --> H[cobertura · jacoco:check]
    H --> I[construir imatge]
    I --> J[escanejar imatge · Trivy]
    J --> K[publicar al registre]
    K --> L[desplegar a pre]
    L --> M[proves de fum]
    M --> N{aprovacio}
    N -->|si| O[desplegar a prod]
    N -->|no| P[fi]
    O --> Q[verificar i vigilar]

Dos principis governen aquest ordre. El primer: el ràpid i el que més falla, primer. Compilar triga segons i detecta l'error més comú; les proves d'integració triguen minuts. Posar el que és lent al principi fa que un error ximple costi deu minuts en lloc de trenta.

El segon: cada etapa és una porta. Si una falla, no s'executen les següents. Una canalització que tira endavant «perquè només era l'anàlisi estàtica» no és una porta, és un adorn.

  1. GitHub Actions: els conceptes

Concepte Què és A CicloUrbana
Workflow Un fitxer YAML a .github/workflows/ amb un procés complet ci.yml i cd.yml
Trigger L'esdeveniment que el dispara push, pull_request, workflow_dispatch, release
Job Conjunt de passos que corren a la mateixa màquina verificar, imatge, desplegar-pre
Step Una acció reutilitzable o una ordre de shell actions/checkout, ./mvnw verify
Runner La màquina que executa el job ubuntu-latest, amb Docker ja disponible
Matriu Repetir un job amb combinacions de paràmetres Provar amb Java 21 i 25
Artefacte Fitxer que un job produeix i un altre consumeix El JAR, els informes de proves
Secret Valor xifrat injectat en temps d'execució Credencials del registre, del clúster
Environment Destinació amb regles pròpies: aprovació, branques, secrets pre i prod

Dues propietats importants: cada job arrenca en una màquina neta (d'aquí la necessitat de memòries cau i artefactes per passar coses entre jobs), i els jobs corren en paral·lel llevat que es declari needs:, que és el que imposa l'ordre de les portes.

  1. El flux d'integració: ci.yml

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]                    # cada integracio a la branca principal
  pull_request:
    branches: [main]                    # i cada proposta de canvi, abans de fusionar

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true              # si arriben dos push seguits, cancel·la l'anterior

permissions:
  contents: read                        # minim privilegi: nomes llegir el codi
  checks: write                         # i publicar l'informe de proves

jobs:
  verificar:
    name: Compilar, provar i analitzar
    runs-on: ubuntu-latest
    timeout-minutes: 25                 # cap execucio no pot penjar-se indefinidament

    steps:
      - name: Descarregar el codi
        uses: actions/checkout@v4
        with:
          fetch-depth: 0                # historial complet: el necessiten Sonar i git-commit-id

      - name: Preparar Java 21 amb memoria cau de Maven
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '21'
          cache: maven                  # desa ~/.m2/repository segons el hash del pom.xml

      - name: Verificar el format del codi
        run: ./mvnw -B spotless:check

      - name: Compilar, provar i empaquetar
        run: ./mvnw -B verify
        env:
          TESTCONTAINERS_RYUK_DISABLED: 'false'
          SPRING_PROFILES_ACTIVE: test

      - name: Publicar l'informe de proves
        if: always()                    # tambe quan les proves fallen: es quan mes importa
        uses: mikepenz/action-junit-report@v4
        with:
          report_paths: '**/target/*-reports/TEST-*.xml'

      - name: Publicar la cobertura de JaCoCo
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: cobertura-jacoco
          path: target/site/jacoco/
          retention-days: 14

      - name: Desar el JAR per als fluxos seguents
        uses: actions/upload-artifact@v4
        with:
          name: ciclourbana-jar
          path: target/ciclourbana.jar
          retention-days: 7

Línia a línia, el que cal entendre:

  • on: push i pull_request sobre main és la definició operativa d'integració contínua: es verifica el que es proposa integrar i el que ja es va integrar.
  • concurrency amb cancel-in-progress evita gastar minuts de runner verificant un commit ja superat: en un repositori actiu estalvia fàcilment un terç del consum. I timeout-minutes: 25 impedeix que un procés penjat consumeixi hores.
  • permissions explícits. Per defecte el GITHUB_TOKEN pot tenir permisos amplis; declarar-los al mínim evita que una acció de tercers compromesa escrigui al repositori.
  • fetch-depth: 0. Per defecte checkout porta un sol commit; Sonar necessita l'historial per atribuir línies noves, i el plugin git-commit-id que alimenta l'/actuator/info de 07-01 necessita les dades de Git.
  • cache: maven és l'ajust amb millor relació entre esforç i benefici: sense ell, cada execució torna a descarregar tot l'arbre de dependències —Spring Boot, Hibernate, Spring Security, MapStruct, els drivers— i afegeix dos o tres minuts a cada construcció.
  • ./mvnw -B verify és el cor del flux. -B (batch) desactiva el color i les barres de progrés, que en un log de CI només generen soroll. I verify, no test: test executa només Surefire (les *Test), mentre que verify executa a més Failsafe (les *IT), que són les de Testcontainers de 06-05. Fer servir test a la CI significaria deixar fora precisament les proves que més s'assemblen a producció.
  • Els Testcontainers funcionen sense configurar res perquè el runner ubuntu-latest porta Docker instal·lat i el dimoni en marxa. La primera execució descarrega la imatge postgres:16-alpine (uns 15 segons) i a partir d'aquí les *IT s'executen contra PostgreSQL real.
  • if: always() als informes és subtil i decisiu: sense ell, un verify fallit avorta el job i no es publica l'informe, que és just el que cal per saber què va fallar. Amb always(), l'informe de JUnit apareix anotat al pull request, línia a línia.

Quan calen serveis contenidor. GitHub Actions permet declarar serveis auxiliars al job:

    services:
      postgres:
        image: postgres:16-alpine
        env: { POSTGRES_PASSWORD: prova, POSTGRES_DB: ciclourbana }
        ports: ['5432:5432']
        options: >-
          --health-cmd pg_isready --health-interval 10s --health-retries 5

Amb Testcontainers no cal, i és important entendre per què: Testcontainers arrenca i atura el contenidor des del mateix codi de la prova, amb la mateixa configuració a la CI i al portàtil del desenvolupador — que és exactament la paritat d'entorns que buscàvem. Els services tenen sentit quan les proves no fan servir Testcontainers, o per a dependències que no s'aixequen des de Java.

I la porta. Un flux que informa però no bloqueja no serveix de res. A Settings → Branches → Branch protection rules cal exigir que el check verificar passi abans de poder fusionar a main. Sense aquesta configuració, la canalització és un informe; amb ella, és una garantia.

  1. El flux de lliurament: cd.yml

# .github/workflows/cd.yml
name: CD

on:
  push:
    tags: ['v*']                        # v2.4.1 dispara el lliurament
  workflow_dispatch:                    # i es pot llancar a ma des de la interficie

permissions:
  contents: read
  packages: write                       # publicar a GHCR
  id-token: write                       # OIDC: credencials temporals, sense claus desades

env:
  REGISTRE: ghcr.io
  IMATGE: ${{ github.repository_owner }}/ciclourbana

jobs:
  imatge:
    name: Construir, escanejar i publicar la imatge
    runs-on: ubuntu-latest
    outputs:
      etiqueta: ${{ steps.meta.outputs.version }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: '21', cache: maven }

      - name: Empaquetar sense repetir les proves
        run: ./mvnw -B -DskipTests package
        # Les proves ja van passar a ci.yml sobre aquest mateix commit.

      - name: Calcular etiquetes i metadades
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRE }}/${{ env.IMATGE }}
          tags: |
            type=semver,pattern={{version}}
            type=sha,prefix=sha-,format=short
            type=raw,value=latest,enable={{is_default_branch}}

      - name: Autenticar-se al registre
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRE }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}   # temporal, generat per a aquesta execucio

      - name: Construir i publicar
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64        # imprescindible per a Fargate (08-03)
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Escanejar la imatge
        uses: aquasecurity/[email protected]
        with:
          image-ref: ${{ env.REGISTRE }}/${{ env.IMATGE }}:${{ steps.meta.outputs.version }}
          severity: 'HIGH,CRITICAL'
          exit-code: '1'                # una vulnerabilitat greu ATURA el lliurament

  desplegar-pre:
    needs: imatge
    runs-on: ubuntu-latest
    environment:
      name: pre
      url: https://pre.ciclourbana.ribalta.example
    steps:
      - uses: actions/checkout@v4
      - name: Desplegar a pre
        run: |
          helm upgrade --install ciclourbana ./charts/ciclourbana \
            -n ciclourbana-pre -f values-pre.yaml \
            --set image.tag=${{ needs.imatge.outputs.etiqueta }} \
            --atomic --timeout 8m
      - name: Prova de fum
        run: |
          curl -fsS --retry 10 --retry-delay 6 --retry-all-errors \
            https://pre.ciclourbana.ribalta.example/actuator/health/readiness
          curl -fsS https://pre.ciclourbana.ribalta.example/api/v1/estacions | jq -e 'length == 4'

  desplegar-prod:
    needs: [imatge, desplegar-pre]
    runs-on: ubuntu-latest
    environment:
      name: prod                        # entorn PROTEGIT: exigeix aprovacio manual
      url: https://ciclourbana.ribalta.example
    steps:
      - uses: actions/checkout@v4
      - name: Desplegar a produccio
        run: |
          helm upgrade ciclourbana ./charts/ciclourbana \
            -n ciclourbana-prod -f values-prod.yaml \
            --set image.tag=${{ needs.imatge.outputs.etiqueta }} \
            --atomic --timeout 10m
      - name: Verificar la versio desplegada
        run: |
          curl -fsS https://ciclourbana.ribalta.example/actuator/info \
            | jq -e '.build.version == "${{ needs.imatge.outputs.etiqueta }}"'

Les decisions de fons d'aquest fitxer:

Es dispara amb una etiqueta, no amb cada push. v2.4.1 és un acte deliberat: algú decideix que aquest commit és una versió. És la frontera entre integració contínua (cada canvi) i lliurament (versions).

No es repeteixen les proves. -DskipTests no és una trampa: ci.yml ja les va executar sobre aquest mateix commit. Repetir-les duplicaria el temps sense afegir informació.

Etiquetatge triple. docker/metadata-action produeix alhora 2.4.1 (la versió semàntica, llegible), sha-9f3a2b1 (l'identificador exacte i inequívoc del commit) i latest. És el que fa possible respondre amb certesa a «quin codi està corrent a Ribalta?»: es compara el SHA d'/actuator/info amb el del repositori.

environment: prod és l'aprovació manual. Configurant aquest entorn a GitHub amb required reviewers, el job s'atura i espera que una persona autoritzada l'aprovi a la interfície. És la línia exacta que separa lliurament continu de desplegament continu: treure la protecció de l'entorn converteix l'un en l'altre.

L'escàner atura el lliurament. exit-code: '1' amb severitat HIGH,CRITICAL significa que una vulnerabilitat greu a la imatge impedeix publicar, enllaçant amb l'enduriment de 07-04 i amb OWASP Dependency-Check de 05-05.

La prova de fum amb --retry. Després d'un helm upgrade, els pods triguen a estar llestos; sense reintents, el curl fallaria per arribar aviat. I el jq -e 'length == 4' comprova alguna cosa real —les quatre estacions de Ribalta— i no només que el procés respongui.

  1. Secrets a la canalització

La canalització necessita credencials per publicar imatges i desplegar. És, per definició, un sistema amb permisos elevats i per tant un objectiu.

Mecanisme Què és Valoració
Secret de repositori Valor xifrat a secrets.NOM Acceptable; és una credencial de llarga durada que cal rotar
Secret d'entorn Igual, però lligat a pre o prod Millor: les credencials de producció només existeixen al job de producció
GITHUB_TOKEN Testimoni generat per a cada execució i revocat en acabar Ideal per a GHCR i el mateix repositori
OIDC GitHub emet un testimoni signat que el proveïdor canvia per credencials temporals La millor opció: no hi ha cap clau desada

OIDC amb AWS, que elimina completament les claus de llarga durada:

      - name: Credencials temporals d'AWS sense claus desades
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::111122223333:role/ciclourbanaDesplegamentRol
          aws-region: eu-west-1

No hi ha cap AWS_ACCESS_KEY_ID enlloc. GitHub emet un testimoni OIDC signat que identifica el repositori, el flux i la branca; AWS el valida contra una relació de confiança i retorna credencials que caduquen en una hora. I la política de confiança ha de restringir quin repositori i quina branca poden assumir el rol:

"Condition": {
  "StringEquals": { "token.actions.githubusercontent.com:sub":
      "repo:ajuntament-ribalta/ciclourbana:environment:prod" }
}

Sense aquesta condició, qualsevol repositori de GitHub podria assumir el rol. És l'error de configuració més greu i més freqüent d'OIDC.

Advertiment destacat: no imprimeixis mai un secret als logs. GitHub emmascara els valors registrats com a secrets, però l'emmascarament es trenca amb facilitat: si el secret es transforma (es codifica en base64, es retalla, es concatena), el valor derivat no està emmascarat. Un echo de depuració oblidat, un set -x en un script o una eina que bolca la seva configuració poden deixar una credencial en un log que potser és públic. I si passa, l'única resposta correcta és rotar el secret: esborrar el log no n'hi ha prou, perquè va poder ser llegit o replicat.

Tres regles més: mínim privilegi —el rol de desplegament pot actualitzar el servei, no esborrar la base de dades—; permissions explícits a cada flux; i fixar les accions de tercers per SHA (uses: acme/accio@a1b2c3d) en lloc de per etiqueta mòbil, perquè una etiqueta es pot reapuntar a codi maliciós.

  1. Qualitat dins de la canalització

La canalització és l'únic lloc on una regla de qualitat es compleix sempre. El que convé posar-hi, i en quin ordre:

Control Eina Quan s'executa Trenca la construcció?
Format i errors probables Spotless/Checkstyle; SpotBugs/Error Prone Abans i després de compilar Sí: és objectiu i trivial d'arreglar
Qualitat i deute SonarQube / SonarCloud Després de les proves Sí, per quality gate sobre codi nou
Cobertura jacoco:check A verify Sí, amb llindar realista
Dependències vulnerables OWASP Dependency-Check Nocturn + a main Sí per a CRITICAL
Actualització de dependències Dependabot Programat No: obre pull requests
Vulnerabilitats de la imatge Trivy, docker scout Després de construir la imatge Sí per a HIGH/CRITICAL

El llindar de cobertura de 06-01, ara com a porta real:

<execution>
  <id>comprovar-cobertura</id>
  <goals><goal>check</goal></goals>
  <configuration><rules><rule><element>BUNDLE</element><limits>
    <limit><counter>LINE</counter><value>COVEREDRATIO</value><minimum>0.70</minimum></limit>
    <limit><counter>BRANCH</counter><value>COVEREDRATIO</value><minimum>0.60</minimum></limit>
  </limits></rule></rules></configuration>
</execution>

I l'advertiment que ja es va fer a 06-01, aquí més important perquè ara és obligatori: un llindar massa alt produeix proves escombraria. Si per fusionar cal arribar al 90 %, algú escriurà proves sense assercions que executen codi per pujar el percentatge. Un llindar del 70 % en línies és exigent i honest. I el criteri més útil no és la cobertura global sinó la del codi nou: és el que fa Sonar amb el seu quality gate, i evita que una base heretada amb poca cobertura bloquegi qualsevol avenç.

Dependències vulnerables, reprenent 05-05:

  seguretat:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: '21', cache: maven }
      - name: OWASP Dependency-Check
        run: ./mvnw -B org.owasp:dependency-check-maven:check -DfailBuildOnCVSS=9

-DfailBuildOnCVSS=9 trenca la construcció només davant de vulnerabilitats crítiques. Un llindar baix genera tants falsos positius que l'equip aprèn a ignorar-los, i un control ignorat és pitjor que no tenir-lo. Aquesta anàlisi convé executar-la en un job a part i programat de nit, perquè descarrega la base de dades de vulnerabilitats i pot trigar diversos minuts. I Dependabot (.github/dependabot.yml) complementa obrint pull requests que actualitzen versions: com que cadascun passa per ci.yml, actualitzar Spring Boot deixa de ser un salt de fe.

  1. Versionatge i publicació

Versionatge semàntic (MAJOR.MENOR.PEDAC) aplicat a CicloUrbana:

Canvi Versió Exemple a CicloUrbana
Correcció compatible (PEDAÇ) 2.4.0 → 2.4.1 Arreglar l'arrodoniment d'una tarifa
Funcionalitat compatible (MENOR) 2.4.1 → 2.5.0 Nou endpoint d'incidències
Canvi incompatible (MAJOR) 2.5.0 → 3.0.0 Eliminar un camp de la resposta pública

Per a una API amb clients externs —l'aplicació mòbil de Ribalta— la part MAJOR és un contracte: un canvi incompatible obliga a versionar l'API (/api/v2/...) i a mantenir l'anterior durant un període de gràcia.

./mvnw versions:set -DnewVersion=2.4.1 -DgenerateBackupPoms=false
git commit -am "Versio 2.4.1"
git tag -a v2.4.1 -m "Correccio de l arrodoniment de les tarifes"
git push origin main --follow-tags        # l'etiqueta dispara cd.yml

I el tancament del cercle amb 07-01: el plugin build-info de Spring Boot i git-commit-id graven la versió, el SHA i la data dins de l'artefacte, i Actuator els exposa:

curl -s https://ciclourbana.ribalta.example/actuator/info | jq '.build, .git.commit.id.abbrev'
# { "version": "2.4.1", "time": "2026-09-01T09:14:22Z" }
# "9f3a2b1"

Aquest SHA és la resposta definitiva a «què està desplegat?», i fa verificable el desplegament en lloc de creïble. Les notes de la versió, generades a partir dels missatges de commit des de l'etiqueta anterior, completen el rastre: algú que investigui un incident pot passar del SHA que respon el servidor a la llista exacta de canvis que van entrar.

  1. Desplegar a pre i a prod, i revertir

El flux de l'apartat 6 implementa la política: pre automàtic, prod amb aprovació. El que la persona que aprova ha de poder veure abans de prémer: que la CI està verda, que pre porta una estona funcionant amb aquesta versió, que la prova de fum va passar i quins canvis entren.

Les estratègies de 08-01, ara automatitzades:

Estratègia Com s'implementa a la canalització
Rolling helm upgrade amb maxUnavailable: 0 (08-04) o minimumHealthyPercent=100 a ECS (08-03)
Blue-green Dos serveis i un pas que commuta la destinació del balancejador
Canary Un desplegament amb pes de trànsit i una pausa que mesura mètriques abans de continuar

La reversió, que és la part que més es descuida, té tres nivells segons la gravetat:

# 1. Automatica, si el desplegament no convergeix: --atomic ja ho fa
# 2. Manual i immediata: tornar a l'etiqueta anterior de la imatge
helm rollback ciclourbana -n ciclourbana-prod
aws ecs update-service --cluster ciclourbana --service ciclourbana-web \
  --task-definition ciclourbana:41 --force-new-deployment
# 3. Per la canalitzacio: rellancar cd.yml amb l'etiqueta anterior
gh workflow run cd.yml -f version=2.4.0

I la condició de fons, repetida per tercera vegada al mòdul perquè és la que arruïna més desplegaments: tornar a la imatge anterior només funciona si l'esquema de base de dades continua sent compatible amb ella.

  1. Migracions de base de dades a la canalització

La migració és l'únic pas de la canalització que no es pot desfer. Tota la resta —la imatge, la configuració, el nombre de rèpliques— torna enrere amb una ordre; una migració aplicada es queda aplicada.

D'aquí la regla que governa tot el mòdul: el patró expand/contract de 04-08 és la condició per poder revertir l'aplicació sense revertir l'esquema.

flowchart LR
    subgraph D1["Desplegament 1 · expand"]
      A1[Migracio: AFEGIR columna nova] --> A2[Codi: escriu a les dues]
    end
    subgraph D2["Desplegament 2 · migrar lectura"]
      B1[Sense migracio] --> B2[Codi: llegeix i escriu nomes la nova]
    end
    subgraph D3["Desplegament 3 · contract"]
      C1[Migracio: ELIMINAR columna vella] --> C2[Codi: sense canvis]
    end
    D1 --> D2 --> D3

A cadascun dels tres desplegaments, la versió anterior de l'aplicació continua funcionant contra l'esquema resultant. Aquesta propietat és el que fa que helm rollback sigui una operació segura en lloc d'una aposta.

Com es tradueix a la canalització:

  1. Un pas propi per a la migració, abans de tocar les instàncies vives: el hook pre-upgrade de Helm (08-04), la tasca puntual d'ECS (08-03) o la release phase de Heroku (08-02). Si falla, el desplegament s'atura i la versió anterior continua servint.
  2. Una comprovació automàtica al pull request que rebutgi migracions amb DROP COLUMN, RENAME o canvis de tipus incompatibles quan arriben juntament amb el codi que les fa servir: deu línies de grep que eviten un incident.
  3. ddl-auto: validate a tots els entorns (04-08): una instància l'esquema de la qual no correspon falla en arrencar, clara i immediatament, en lloc de fallar consulta a consulta.
  4. Verificació a pre amb la versió anterior abans d'aprovar producció: és la prova directa que la reversió serà possible.

  1. Que la canalització sigui ràpida i fiable

Una canalització lenta s'evita, i una canalització que falla sense motiu s'ignora. Les dues patologies tenen el mateix desenllaç: l'equip deixa de confiar-hi i torna a desplegar a mà.

Objectiu Com aconseguir-ho
CI en menys de 10 minuts Memòria cau de Maven, paral·lelitzar jobs, -DskipTests als fluxos que no proven
Retroalimentació primerenca Format i compilació primer; integració després
Paral·lelitzar Jobs independents sense needs: corren alhora: proves, anàlisi, seguretat
Memòries cau efectives cache: maven i cache-from/to: type=gha per a les capes de Docker
Res de proves intermitents Prohibició absoluta: una prova que falla una de cada vint vegades s'arregla o s'esborra
Fallar ràpid timeout-minutes a cada job i cancel-in-progress

Sobre les proves intermitents cal ser taxatiu. Una prova que de vegades falla ensenya l'equip a rellançar l'execució en lloc de llegir l'error, i aquest hàbit destrueix el valor de tota la canalització: el dia que falli de debò, algú prémerà «reintentar». Les causes habituals són conegudes —dependències temporals (Thread.sleep en lloc d'esperar una condició), estat compartit entre proves, ordre d'execució assumit, i dates i zones horàries— i quan arreglar-la trigarà, la resposta correcta és @Disabled amb un enllaç a la incidència, no deixar-la fallant.

Temps objectiu: la CI per sota de 10 minuts i el lliurament complet fins a pre per sota de 20. Per sobre d'això, la gent comença a agrupar canvis per «no gastar una execució», i agrupar canvis és exactament el contrari de la integració contínua.

  1. Alternatives i mètriques DORA

Eina Model Notes
GitHub Actions Allotjat, YAML Integrat amb el repositori; enorme catàleg d'accions
GitLab CI Allotjat o propi, .gitlab-ci.yml Molt complet, amb entorns i registre inclosos
Jenkins Autoallotjat, Jenkinsfile Màxima flexibilitat; cal mantenir-lo
CircleCI / Azure DevOps Allotjats Ràpids i amb bona memòria cau; el segon, fort en entorns Microsoft
Tekton / Argo Workflows Sobre Kubernetes Natius del clúster; encaixen amb GitOps (08-04)

Els conceptes són els mateixos a totes: disparadors, etapes, passos, artefactes, memòries cau, secrets, aprovacions i entorns. Canvia la sintaxi, no el disseny; el que s'ha après amb ci.yml i cd.yml es trasllada a qualsevol d'elles en una tarda.

I què mirar després de desplegar. Les mètriques DORA són el marc estàndard per mesurar la salut del lliurament:

Mètrica Què mesura Referència d'equip saludable
Freqüència de desplegament Cada quant arriba un canvi a producció Almenys setmanal; els millors, a diari
Temps de lliurament De commit a producció Menys d'un dia
Taxa de fallada de canvis Quin percentatge de desplegaments causa un incident Per sota del 15 %
Temps de restauració Quant es triga a recuperar-se Menys d'una hora

El valuós d'aquestes quatre és que s'equilibren entre si: desplegar molt però trencant constantment surt malament a la tercera; no desplegar mai «per prudència» surt malament a les dues primeres i, paradoxalment, també a la quarta, perquè un desplegament gran i rar és molt més difícil de revertir que un de petit i freqüent. La conclusió contraintuïtiva del sector és sòlida: desplegar més sovint fa el sistema més estable, no menys, perquè cada desplegament és petit, comprensible i fàcil de desfer.

I els senyals immediats dels deu minuts posteriors a un desplegament són els de 08-01: readiness en verd a totes les instàncies, zero reinicis, taxa de 5xx i latència estables, sense excepcions noves al log i /actuator/info mostrant el SHA esperat. Mesurar-los bé és l'objecte del mòdul 9.

Errors Comuns i Consells

Fer servir mvn test en lloc de mvn verify. Executa només Surefire i deixa fora les *IT de Testcontainers, que són les proves més valuoses. La canalització queda verda sense haver provat contra PostgreSQL real.

Publicar l'informe sense if: always(). Quan les proves fallen —l'únic moment en què l'informe importa— el pas no s'executa i no hi ha res a llegir.

No protegir la branca principal. Sense branch protection, la CI informa però no impedeix fusionar en vermell: és un adorn car.

Imprimir secrets al log. Un echo de depuració o una transformació del valor trenca l'emmascarament; si passa, rota el secret, perquè esborrar el log no n'hi ha prou. I OIDC sense condició de sub deixa un rol assumible des de qualsevol repositori de GitHub.

Recompilar al flux de lliurament. Si cd.yml reconstrueix des de zero, el que es desplega no és exactament el que es va provar. Reutilitza l'artefacte o construeix una vegada a partir del mateix commit.

Conviure amb proves intermitents. Ensenyen l'equip a prémer «reintentar» i destrueixen la confiança en tota la canalització.

Llindar de cobertura irreal. Un 90 % obligatori produeix proves sense assercions. Mesura la cobertura del codi nou.

Consell: la canalització és codi. Es revisa en un pull request, es comenta i es refactoritza. Un ci.yml de tres-centes línies amb passos duplicats té el mateix problema que una classe de tres-centes línies.

Consell: act o una branca de proves per iterar. Depurar un flux a base de git push és lent i omple l'historial de commits «provant CI»: fes servir una branca llencable i workflow_dispatch. I fixa les accions de tercers per SHA, perquè una etiqueta com @v4 es pot reapuntar i un SHA no.

Exercicis

Exercici 1

Escriu el flux ci.yml de CicloUrbana amb dos jobs paral·lels: un que compili i executi les proves unitàries (Surefire) i un altre que executi les d'integració amb Testcontainers (Failsafe), més un tercer job que depengui de tots dos i publiqui el resum. Explica què es guanya i què es perd respecte al job únic de l'apartat 5, com es comparteix el resultat de la compilació entre jobs, i què caldria configurar al repositori perquè la canalització sigui una porta i no un informe.

Exercici 2

La canalització de CicloUrbana triga 28 minuts i l'equip ha començat a agrupar canvis per no esperar. Els temps mesurats són: descàrrega de dependències 4 min, compilació 1 min, proves unitàries 2 min, anàlisi de SonarCloud 3 min, OWASP Dependency-Check 7 min, proves d'integració amb Testcontainers 6 min, construcció de la imatge 4 min, escaneig amb Trivy 1 min. Dissenya un pla d'optimització que baixi la retroalimentació a menys de 10 minuts, amb el temps estimat després de cada mesura i una justificació de per què cada canvi és segur.

Exercici 3

Es desplega la versió 2.6.0 a producció a les 10:00. A les 10:07 les alarmes mostren un 12 % de 5xx a /api/v1/lloguers i latència p99 disparada. La versió inclou una migració V12__afegir_index_lloguers.sql i un canvi a LloguerService. Descriu la seqüència exacta d'actuació en els primers quinze minuts, decideix si es pot revertir i en quines condicions, i proposa les millores concretes a la canalització que haurien evitat o acotat l'incident.

Solucions

Solució 1

name: CI
on:
  push: { branches: [main] }
  pull_request: { branches: [main] }
concurrency: { group: ci-${{ github.ref }}, cancel-in-progress: true }
permissions: { contents: read, checks: write }

jobs:
  unitaries:
    runs-on: ubuntu-latest
    timeout-minutes: 12
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: '21', cache: maven }
      - name: Compilar i instal·lar sense proves (reutilitzable)
        run: ./mvnw -B -DskipTests install
      - name: Proves unitaries (Surefire)
        run: ./mvnw -B surefire:test
      - uses: actions/upload-artifact@v4
        if: always()
        with: { name: informes-unitaries, path: target/surefire-reports/ }

  integracio:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: '21', cache: maven }
      - name: Proves d'integracio (Failsafe + Testcontainers)
        run: ./mvnw -B verify -DskipUnitTests
      - uses: actions/upload-artifact@v4
        if: always()
        with: { name: informes-integracio, path: target/failsafe-reports/ }

  resum:
    needs: [unitaries, integracio]
    if: always()
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
      - uses: mikepenz/action-junit-report@v4
        with: { report_paths: '**/TEST-*.xml' }

Què es guanya. El temps total passa de ser la suma a ser el màxim dels dos jobs: si les unitàries triguen 3 minuts i les d'integració 8, el resultat arriba en 8 en lloc d'11. I la retroalimentació és més útil: una fallada unitària apareix als 3 minuts sense esperar els Testcontainers. A més, cada job té el seu propi timeout ajustat al que fa.

Què es perd. Cada job arrenca en una màquina neta, així que la compilació es fa dues vegades —uns 60-90 segons duplicats— i la memòria cau de Maven es descarrega dues vegades (encara que des de la memòria cau de GitHub, que és ràpida). El fitxer és més llarg i hi ha més peces per mantenir. Amb una suite petita, el job únic de l'apartat 5 és més simple i no notablement més lent; la separació comença a compensar quan les d'integració passen de 5 minuts.

Com es comparteix el resultat. Amb actions/upload-artifact i download-artifact, que és el que fa el job resum. Si es volgués evitar la doble compilació, el patró seria un job previ que compili una vegada i pugi target/ com a artefacte; a la pràctica, per a un projecte de la mida de CicloUrbana, l'estalvi no compensa la complexitat. L'if: always() a resum és imprescindible: sense ell, una fallada en qualsevol dels dos jobs impediria publicar l'informe.

Perquè sigui una porta, a Settings → Branches: exigir pull request abans de fusionar a main, marcar unitaries, integracio i resum com a checks obligatoris, exigir que la branca estigui actualitzada respecte a main abans de fusionar (així es prova realment el resultat de la integració), i prohibir el push directe, inclosos els administradors. Sense això últim, la canalització té una porta del darrere.

Solució 2

Diagnòstic. El total seqüencial és 28 minuts, però no tot pertany al camí crític de la retroalimentació. La clau és separar el que un desenvolupador necessita saber en minuts del que es pot saber més tard.

Mesura Canvi Temps després de la mesura
1. Treure OWASP a un job nocturn No és informació que bloquegi un pull request; la seva base de vulnerabilitats canvia a diari, no per commit 28 → 21 min
2. Memòria cau de Maven cache: maven a setup-java: la descàrrega baixa de 4 min a ~30 s 21 → 17,5 min
3. Paral·lelitzar en tres jobs unitaries+Sonar (1+2+3=6 min) i integracio (6 min) alhora 17,5 → ~12 min
4. Treure la imatge a cd.yml Construir-la i escanejar-la no aporta res al pull request: només cal en lliurar 12 → ~7 min
5. Memòria cau de capes de Docker cache-from/to: type=gha a cd.yml: la construcció baixa de 4 min a ~1 (millora cd.yml)
6. cancel-in-progress Deixa de gastar minuts en commits ja superats Efecte sobre el conjunt

Resultat: retroalimentació de pull request en uns 7 minuts, per sota de l'objectiu, amb el lliurament complet (imatge + escaneig + desplegament a pre) en uns altres 6-8 només quan s'etiqueta una versió.

Per què cada canvi és segur:

  • (1) OWASP continua executant-se a diari i a cada push a main, trencant la construcció davant de CVSS ≥ 9; l'única cosa que canvia és que no bloqueja cada pull request, i no ho ha de fer: una vulnerabilitat publicada aquest matí no l'ha introduïda el canvi que es revisa.
  • (2) La memòria cau s'invalida amb el hash del pom.xml, així que un canvi de dependències força la descàrrega completa: no hi ha risc de construir contra artefactes obsolets. (5) Igual amb les capes de Docker: si una capa canvia, es reconstrueix.
  • (3) Els dos jobs són independents; l'única contrapartida és compilar dues vegades, uns 60 segons que el paral·lelisme compensa de sobres.
  • (4) És coherent amb el disseny del mòdul —ci.yml verifica, cd.yml lliura— i el commit etiquetat és el mateix que va passar per ci.yml. (6) Cancel·lar una execució obsoleta no elimina cap verificació: el commit més recent inclou l'anterior.

I una mesura cultural, sense la qual les tècniques no basten: l'objectiu de temps ha de ser explícit i vigilat. Si la CI torna a superar els 10 minuts, es tracta com una incidència, no com una cosa que passa. El senyal d'alarma és el que descriu l'enunciat: quan la gent agrupa canvis per no esperar, la integració ha deixat de ser contínua.

Solució 3

Minuts 0-2: contenir, no investigar. La regla de 08-01 és que el criteri de reversió es decideix abans i s'aplica sense debat. Un 12 % de 5xx supera folgadament qualsevol llindar raonable, així que:

kubectl get pods -n ciclourbana-prod                  # son vius? reiniciant?
curl -s https://ciclourbana.ribalta.example/actuator/info | jq '.build.version'   # confirmar que es 2.6.0

I abans de res, comprovar si es pot revertir, cosa que depèn enterament de la migració:

cat src/main/resources/db/migration/V12__afegir_index_lloguers.sql

Minut 2: la decisió. V12 afegeix un índex. És una migració additiva i compatible cap enrere: la versió 2.5.0 funciona perfectament contra un esquema amb un índex de més. Per tant sí que es pot revertir, i es fa immediatament:

helm rollback ciclourbana -n ciclourbana-prod
kubectl rollout status deploy/ciclourbana -n ciclourbana-prod

Si V12 hagués estat un DROP COLUMN o un RENAME, la resposta seria la contrària: revertir hauria empitjorat la situació —l'escenari de l'exercici 3 de 08-01— i caldria arreglar cap endavant amb un desplegament correctiu urgent.

Minuts 3-8: confirmar la recuperació. Vigilar que la taxa de 5xx torna a la línia base, que la latència p99 baixa i que /actuator/info mostra 2.5.0. Comunicar l'estat: l'ajuntament ha de saber que el servei està restablert abans que ho pregunti.

Minuts 8-15: començar a investigar, ja sense pressió. Les dues hipòtesis, per probabilitat:

  1. La creació de l'índex va bloquejar la taula. CREATE INDEX sense CONCURRENTLY pren un bloqueig que impedeix les escriptures a lloguers mentre es construeix. Amb una taula gran són minuts durant els quals tot POST /api/v1/lloguers espera i acaba esgotant el connection-timeout del pool → 5xx i latència disparada. Encaixa perfectament amb el símptoma, i explica per què afecta /lloguers i no la resta.
  2. Una regressió a LloguerService: una consulta N+1 nova (04-06), una transacció massa llarga (04-07) o una excepció no contemplada.

La manera de distingir-les: els logs de la finestra 10:00-10:07 i les mètriques del pool (hikaricp.connections.pending, 09-03). Si hi ha connexions esperant i bloqueigs a PostgreSQL, és la primera; si hi ha excepcions d'aplicació, la segona.

Millores concretes a la canalització:

Millora Què evita
Regla de revisió: tot CREATE INDEX a PostgreSQL ha de portar CONCURRENTLY i executar-se fora de transacció, i tota migració amb DROP/RENAME ha de justificar la seva compatibilitat La causa arrel més probable
Comprovació automàtica al pull request que busqui CREATE INDEX sense CONCURRENTLY i DROP COLUMN a db/migration Que la regla depengui que algú se'n recordi
Assaig de la migració a pre amb volum realista Un índex que triga 20 ms amb 100 files i 4 minuts amb 2 milions
Desplegament canary o per fases en lloc de rolling complet Que el 100 % del trànsit pateixi la fallada des del primer minut
Reversió automàtica per alarma: si els 5xx superen el 5 % durant 3 minuts després d'un desplegament, revertir sense intervenció Els 7 minuts que va trigar algú a adonar-se'n
Finestra de desplegament fora de l'hora punta de lloguers L'impacte sobre el nombre de ciutadans afectats
Prova de fum amb càrrega a pre, no només un curl Que el problema aparegui per primera vegada en producció

I una observació de fons que resumeix el mòdul sencer: la canalització va funcionar exactament com estava dissenyada —va construir, va provar, va escanejar, va desplegar a pre, va demanar aprovació i va desplegar—. El que va faltar no va ser automatització, sinó una verificació que representés les condicions de producció: volum de dades real a pre i una regla que capturés un patró de migració conegudament perillós. Les canalitzacions no eviten els errors per si soles; eviten els errors que algú s'ha molestat a codificar com a comprovació, i cada incident és l'oportunitat d'afegir-ne un més.

Conclusió

El camí del commit fins als ciutadans de Ribalta és complet i automatitzat. Distingeixes amb precisió integració contínua, lliurament continu i desplegament continu, i saps que el que separa les dues últimes no és tecnologia sinó una decisió de confiança —l'environment: prod protegit—. Has vist per què la CI és el que converteix la suite del mòdul 6 en una garantia real: deixa d'executar-la qui se'n recorda, a l'entorn de cadascú, per executar-se sempre, en una màquina neta, amb un veredicte públic que bloqueja la fusió.

Tens la canalització completa de CicloUrbana en dos fitxers: un ci.yml comentat línia a línia, amb memòria cau de Maven, ./mvnw -B verify que executa Surefire i Failsafe amb Testcontainers sobre el Docker que el runner ja porta, informes publicats amb if: always() i la protecció de branca que el converteix en porta; i un cd.yml que es dispara amb una etiqueta, construeix i escaneja la imatge amb Trivy, la publica amb triple etiquetatge —versió semàntica, SHA del commit i latest—, desplega a pre, passa una prova de fum real contra les quatre estacions i espera una aprovació humana abans de tocar producció.

Saps gestionar els secrets amb entorns, GITHUB_TOKEN i OIDC sense claus de llarga durada, amb la condició de sub que evita que qualsevol repositori assumeixi el teu rol, i amb l'advertiment que un secret imprès en un log es rota, no s'esborra. Tens la qualitat com a part de la canalització —Spotless, SpotBugs, Sonar sobre codi nou, jacoco:check amb un llindar honest, OWASP Dependency-Check nocturn, Dependabot i l'escaneig de la imatge—, el versionatge semàntic enllaçat amb el build-info d'Actuator que respon quin SHA corre a Ribalta, la reversió en els seus tres nivells i el patró expand/contract com a condició perquè aquesta reversió sigui possible. I saps què fa que una canalització es faci servir o s'ignori: per sota de deu minuts, sense proves intermitents, amb cada etapa fallant ràpid.

CicloUrbana és en producció i hi arriba sola. La pregunta ja no és si funciona ni com es desplega, sinó com es comporta: quina latència té realment un lloguer en hora punta, quina consulta consumeix el 40 % del temps de base de dades, quantes vegades es recalcula el mateix, què està passant dins de la JVM quan la memòria puja i no baixa. El mòdul 9, Rendiment i Monitoratge, respon a tot això: ajust de rendiment i del pool, memòria cau amb Spring Cache, mètriques de negoci amb Micrometer sobre l'Actuator de 07-01, Prometheus i Grafana, gestió de logs i traçabilitat distribuïda. Fins ara hem construït i lliurat la xarxa de Ribalta; a partir d'ara la veurem funcionar i farem que vagi ràpid.

Curs de Spring Boot

Mòdul 1: Introducció a Spring Boot

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

Mòdul 3: Construint serveis web RESTful

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

Mòdul 5: Seguretat a Spring Boot

Mòdul 6: Proves a Spring Boot

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

Mòdul 8: Desplegament d'aplicacions Spring Boot

Mòdul 9: Rendiment i monitoratge

Mòdul 10: Millors pràctiques i consells

© Copyright 2026. Tots els drets reservats