La taula amb què vam tancar 05-04 —què s'executa en desar, al pull request, en desplegar i en producció— és una llista de bones intencions mentre algú s'hagi de recordar d'executar-la. I ningú no se'n recorda un divendres a les set de la tarda amb una correcció urgent entre mans.
Aquesta lliçó converteix aquella llista en maquinària. Empaquetarem l'API de la Botiga Aroma en una imatge Docker que arrenca a qualsevol lloc, aixecarem l'entorn complet amb Redis en una ordre, i escriurem la canalització de GitHub Actions que executa en ordre el linting, Spectral, les proves amb cobertura, npm audit, oasdiff, la construcció de la imatge i Newman contra preproducció. Després veurem el que de debò distingeix un equip que desplega amb tranquil·litat d'un que desplega amb por: migracions retrocompatibles, estratègies de desplegament sense talls, i un pla de retorn enrere que funcioni.
Tot el que vam construir al mòdul 4 reapareix aquí amb un paper operatiu: /salut i /salut/preparat decideixen quan entra el trànsit, les mètriques i els SLO de 04-07 decideixen si el desplegament continua o es reverteix, i l'aturada ordenada de 03-07 és el que permet desplegar sense tallar peticions a mitges.
Contingut
- Integració contínua, lliurament continu i desplegament continu
- Per què la branca principal sempre ha d'estar desplegable
- Empaquetar amb Docker: el
Dockerfilemultietapa .dockerignore, capes i mida de la imatge- L'entorn complet amb
docker-compose.yml - Configuració per entorn i secrets fora de la imatge
- La canalització de CI:
.github/workflows/ci.yml - Matriu de versions i memòria cau de dependències
- Què fa fallar la canalització i per què les portes són inflexibles
- Construir i publicar la imatge
- Migracions: expandir, migrar, contraure
- Estratègies de desplegament
- El paper de liveness i readiness
- Retorn enrere i feature flags
- On desplegar
- Versionat d'artefactes i traçabilitat
- Després del desplegament: proves de fum i vigilància
- Secrets a CI i mínim privilegi
- Integració contínua, lliurament continu i desplegament continu
Tres termes que es fan servir com a sinònims i no ho són:
| Integració contínua (CI) | Lliurament continu (CD) | Desplegament continu | |
|---|---|---|---|
| Què automatitza | Construir i provar cada canvi | Produir un artefacte desplegable sempre | Desplegar a producció sense intervenció |
| Freqüència | Cada push | Cada fusió a main |
Cada fusió a main |
| Hi ha un botó humà? | No s'aplica | Sí: algú decideix quan | No |
| Requisit previ | Proves automàtiques fiables | CI sòlida + entorns reproduïbles | CD + observabilitat + retorn enrere automàtic |
| Risc si falta | Branques que divergeixen setmanes | Desplegaments manuals i fràgils | Cap: és una elecció legítima |
On és la Botiga Aroma. Integració contínua completa, lliurament continu a preproducció de manera automàtica, i desplegament a producció amb un botó. És la configuració adequada per al punt on som, i mereix justificació: el desplegament continu a producció exigeix que el retorn enrere sigui automàtic i que l'observabilitat detecti una degradació en minuts. Tenim el segon des de 04-07; el primer arribarà quan el canary de l'apartat 12 estigui muntat. Adoptar desplegament continu abans de tenir aquestes dues peces no és maduresa, és imprudència.
La paraula que importa als tres termes és contínua: petit i freqüent. Un desplegament de deu canvis petits té deu oportunitats de fallar per separat i cada fallada és trivial de localitzar. Un desplegament trimestral amb dos-cents canvis falla una vegada i ningú no sap quin dels dos-cents va ser.
- Per què la branca principal sempre ha d'estar desplegable
La regla és senzilla d'enunciar i difícil de sostenir: qualsevol commit de main ha de poder desplegar-se a producció en aquest moment. D'ella se'n deriven diverses pràctiques:
- Branques curtes. Una branca de tres setmanes garanteix un conflicte dolorós i una revisió impossible de fer bé. Un o dos dies és el raonable.
- Portes abans de fusionar, no després. Si la canalització s'executa després del merge,
mainestà trencat mentre algú l'arregla, i tot l'equip queda bloquejat. - Funcionalitat incompleta sota bandera. Quan una cosa no està llesta, es fusiona desactivada amb un feature flag en lloc de viure en una branca a part. És l'única alternativa real a les branques llargues.
- Arreglar la canalització és prioritat absoluta. Una canalització vermella que ningú no arregla en una hora deixa de ser un senyal i es converteix en soroll; a partir d'aquí, la gent fusiona en vermell i tot el sistema perd el seu valor.
I una conseqüència menys òbvia: la protecció de la branca ha de ser tècnica, no cultural. A GitHub, això són regles de protecció de branca amb les comprovacions marcades com a obligatòries. Un acord verbal es trenca el dia d'una urgència; una regla configurada, no.
- Empaquetar amb Docker: el
Dockerfile multietapa
Dockerfile multietapaUn contenidor resol el problema més antic de l'ofici: que l'aplicació es comporti igual al teu portàtil, a CI i en producció. Empaqueta el codi, les seves dependències i el runtime en una imatge immutable.
Fitxer nou Dockerfile a l'arrel del projecte:
# syntax=docker/dockerfile:1.7
# ============================================================================
# ETAPA 1 — dependències de construcció
# Instal·la TOTES les dependències (incloses les de desenvolupament) perquè
# better-sqlite3 és un mòdul natiu i s'ha de compilar.
# ============================================================================
FROM node:20-bookworm-slim AS dependencies
# Eines de compilació per als mòduls natius. Només viuen en aquesta
# etapa: no arriben a la imatge final, que és tot el sentit del multietapa.
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 make g++ \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copiem NOMÉS els manifestos abans que el codi font.
# Docker desa a la memòria cau per capes: mentre package*.json no canviï, el npm ci
# de sota es reutilitza encara que hagis tocat cent fitxers de src/.
COPY package.json package-lock.json ./
# npm ci (no npm install): instal·la EXACTAMENT el del lock, és reproduïble
# i falla si el lock i el package.json no concorden. És el que es vol a CI.
RUN npm ci
# ============================================================================
# ETAPA 2 — dependències de producció
# Reinstal·la només el necessari per executar. Redueix molt la imatge final
# i, sobretot, la superfície d'atac: menys codi, menys CVE.
# ============================================================================
FROM dependencies AS dependencies-produccio
RUN npm ci --omit=dev
# ============================================================================
# ETAPA 3 — proves (opcional, s'invoca amb --target proves)
# Permet executar el conjunt de proves dins de la mateixa imatge que es desplegarà,
# eliminant el "a la meva màquina passava".
# ============================================================================
FROM dependencies AS proves
COPY . .
RUN npm run lint && npm test
# ============================================================================
# ETAPA 4 — imatge final d'execució
# ============================================================================
FROM node:20-bookworm-slim AS produccio
# NODE_ENV=production canvia el comportament d'Express (vistes a la memòria cau,
# stack traces fora de les respostes) i de moltes biblioteques. És obligatori.
ENV NODE_ENV=production \
PORT=3000 \
NPM_CONFIG_UPDATE_NOTIFIER=false
# tini és un init mínim: reemet els senyals al procés fill i recull els
# processos zombis. Sense ell, un SIGTERM pot no arribar mai a Node i
# l'aturada ordenada de 03-07 no s'executa.
RUN apt-get update \
&& apt-get install -y --no-install-recommends tini \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Usuari no root: la imatge de Node ja porta l'usuari "node" (uid 1000).
# Executar com a root dins del contenidor és un risc innecessari: si algú
# aconsegueix execució de codi, comença amb tots els privilegis del contenidor.
# --chown evita un "chown -R" posterior, que duplicaria la capa sencera.
COPY --chown=node:node --from=dependencies-produccio /app/node_modules ./node_modules
COPY --chown=node:node package.json ./
COPY --chown=node:node src/ ./src/
COPY --chown=node:node migracions/ ./migracions/
COPY --chown=node:node openapi.yaml ./
USER node
EXPOSE 3000
# HEALTHCHECK fa servir l'endpoint de LIVENESS de 04-07, no el de readiness:
# aquí preguntem "el procés està viu?", no "pot atendre trànsit?".
# Si féssim servir /salut/preparat, una caiguda de Redis reiniciaria el contenidor
# en bucle en lloc de simplement treure'l del balancejador.
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:3000/salut').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
# tini com a PID 1; el procés Node és el seu fill i rep els senyals netament.
ENTRYPOINT ["/usr/bin/tini", "--"]
# Forma "exec" (array), MAI forma shell. Amb `CMD node src/servidor.js` en
# forma shell, el PID 1 seria /bin/sh i SIGTERM no arribaria a Node.
CMD ["node", "src/servidor.js"]Les cinc decisions que més importen d'aquell fitxer:
- Multietapa. Les eines de compilació (
python3,make,g++, uns 300 MB) es queden a l'etapa 1. La imatge final només porta runtime i dependències de producció. npm ci --omit=dev. Forasupertest,eslint,prettier, Spectral,autocannon, Prism i Newman. Menys mida i, sobretot, menys superfície d'atac.- Usuari
node, no root. Combinat ambreadOnlyRootFilesystema l'orquestrador, tanca bona part dels camins d'escalada. tini+ forma exec. És el que fa queSIGTERMarribi a Node i s'executi eltancarOrdenadament()de 03-07. Sense això, cada desplegament talla peticions a mitges i els clients veuen errors de xarxa que no apareixen en cap log.HEALTHCHECKsobre liveness. La distinció de 04-07 té aquí la seva conseqüència pràctica: confondre els dos endpoints provoca reinicis en cascada quan falla una dependència.
Comprovació local:
docker build -t botiga-aroma-api:local .
docker run --rm -p 3000:3000 --env-file .env.local botiga-aroma-api:local
# Verificar l'aturada ordenada: ha de sortir en menys d'un segon,
# no als 10 s del timeout forçat de Docker.
docker stop $(docker ps -q --filter ancestor=botiga-aroma-api:local)Si docker stop triga deu segons, el SIGTERM no està arribant. És la comprovació més útil i la que gairebé ningú no fa.
.dockerignore, capes i mida de la imatge
.dockerignore, capes i mida de la imatgeFitxer nou .dockerignore:
# Mai no entren a la imatge node_modules npm-debug.log* .git .github .env .env.* *.local.json # Artefactes de desenvolupament i proves proves/ cobertura/ informes/ postman/ docs/ *.md !README.md # Base de dades local i temporals dades/ *.sqlite *.sqlite-journal .DS_Store
Tres motius, per ordre d'importància:
- Seguretat. Sense
.dockerignore, unCOPY . .fica el teu.envamb les claus reals dins d'una imatge que potser acaba en un registre compartit. És una de les fuites de credencials més freqüents que existeixen. - Correcció. Copiar el teu
node_moduleslocal amb binaris compilats per a macOS a una imatge Linux produeix fallades incomprensibles. - Velocitat i mida.
.gitpot pesar centenars de megues i s'envia sencer al dimoni de Docker a cada construcció.
Sobre les capes: cada instrucció crea una capa, i Docker les desa a la memòria cau. L'ordre del Dockerfile no és estètic, és una estratègia de memòria cau: el que canvia poc a dalt, el que canvia molt a baix. Copiar package*.json abans que src/ significa que un canvi de codi reutilitza el npm ci, que és la instrucció cara. A l'inrevés, cada construcció reinstal·laria tot.
Referència de mides aproximades per triar base:
| Base | Mida de la imatge final | Notes |
|---|---|---|
node:20 |
~1,1 GB | Debian complet. Només si necessites moltes eines. |
node:20-bookworm-slim |
~250 MB | La nostra. Bon equilibri; glibc, sense sorpreses amb mòduls natius. |
node:20-alpine |
~180 MB | musl en lloc de glibc: pot donar problemes amb mòduls natius com better-sqlite3. |
gcr.io/distroless/nodejs20 |
~170 MB | Sense shell ni gestor de paquets: màxima seguretat, depuració incòmoda. |
La recomanació per a la Botiga Aroma és bookworm-slim: Alpine estalvia 70 MB i et pot costar una tarda compilant better-sqlite3 contra musl. Distroless és una excel·lent elecció quan l'equip té maduresa operativa, però entrar al contenidor a depurar deixa de ser una opció.
I un consell de seguretat que encaixa amb 04-02: escaneja la imatge.
# Vulnerabilitats conegudes a la imatge construïda
docker scout cves botiga-aroma-api:local
# o
trivy image botiga-aroma-api:local --severity HIGH,CRITICAL
- L'entorn complet amb
docker-compose.yml
docker-compose.ymlFitxer nou docker-compose.yml:
# docker-compose.yml — entorn complet de la Botiga Aroma per a desenvolupament i proves
services:
api:
build:
context: .
target: produccio
ports:
- "3000:3000"
environment:
NODE_ENV: development
PORT: 3000
# A Compose els serveis es resolen pel seu nom: "redis" és un host.
REDIS_URL: redis://redis:6379
RUTA_BASE_DADES: /dades/aroma.sqlite
# Els secrets NO van aquí en text: arriben del fitxer .env local,
# que és al .gitignore i al .dockerignore.
JWT_SECRET: ${JWT_SECRET:?falta JWT_SECRET al .env}
ORIGENS_PERMESOS: http://localhost:5173,http://localhost:4173
NIVELL_LOG: debug
volumes:
# Volum amb nom perquè la base sobrevisqui a docker compose down
- dades-api:/dades
depends_on:
redis:
condition: service_healthy # no arrenca fins que Redis respon
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/salut').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
interval: 10s
timeout: 3s
retries: 5
start_period: 15s
restart: unless-stopped
redis:
image: redis:7-alpine
# appendonly sí: el rate limiting de 04-04 i la memòria cau de 04-06 toleren
# perdre dades, però en desenvolupament és còmode que sobrevisquin al reinici.
command: ["redis-server", "--appendonly", "yes", "--maxmemory", "256mb", "--maxmemory-policy", "allkeys-lru"]
ports:
- "6379:6379"
volumes:
- dades-redis:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
# Mock del contracte (05-04): permet a la SPA treballar sense dependre de l'API.
mock:
image: stoplight/prism:5
command: ["mock", "-h", "0.0.0.0", "-p", "4010", "--errors", "/tmp/openapi.yaml"]
ports:
- "4010:4010"
volumes:
- ./openapi.yaml:/tmp/openapi.yaml:ro
profiles: ["desenvolupament"] # només arrenca amb --profile desenvolupament
# PostgreSQL: esmentat aquí perquè és el destí natural quan SQLite
# es queda curt. Migrar exigeix canviar només src/repositoris/*, gràcies al
# patró repositori de 03-05; la resta del projecte no se n'assabenta.
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: aroma
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-clau-local-ficticia}
POSTGRES_DB: aroma
ports:
- "5432:5432"
volumes:
- dades-postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U aroma"]
interval: 5s
retries: 5
profiles: ["postgres"]
volumes:
dades-api:
dades-redis:
dades-postgres:Ús quotidià:
docker compose up -d --wait # aixeca API i Redis, espera que estiguin sans
docker compose --profile desenvolupament up -d # afegeix el mock de Prism
docker compose logs -f api # segueix els logs de pino
docker compose exec api npm run migrar # executa una migració dins del contenidor
docker compose down -v # destrueix tot, volums inclososDos detalls que eviten hores de depuració:
condition: service_healthyadepends_on. Sense ell,depends_onnomés espera que el contenidor arrenqui, no que el servei funcioni, i l'API intenta connectar a un Redis que encara no accepta connexions.${JWT_SECRET:?falta ...}. Aquella sintaxi fa que Compose falli amb un missatge clar si la variable no està definida, en lloc d'arrencar amb un secret buit. Arrencar amb un secret buit és pitjor que no arrencar.
I un fitxer addicional docker-compose.proves.yml, que és el que fan servir les proves d'extrem a extrem de 05-04: igual però amb NODE_ENV=test, base de dades efímera (tmpfs, sense volum persistent) i sense ports exposats tret del de l'API.
- Configuració per entorn i secrets fora de la imatge
El principi, pres dels Twelve-Factor App: una imatge, molts entorns. La mateixa imatge que va passar les proves és la que va a preproducció i després a producció, sense reconstruir. Si reconstrueixes per a cada entorn, no estàs desplegant el que vas provar.
Tot el que varia entre entorns són variables d'entorn:
| Variable | desenvolupament | proves | preproducció | producció |
|---|---|---|---|---|
NODE_ENV |
development |
test |
production |
production |
NIVELL_LOG |
debug |
silent |
info |
info |
RUTA_BASE_DADES |
fitxer local | :memory: |
volum | gestionada |
REDIS_URL |
redis://redis:6379 |
redis://redis:6379 |
interna | gestionada amb TLS |
JWT_SECRET |
fictici al .env |
fictici fix | del gestor de secrets | del gestor de secrets |
ORIGENS_PERMESOS |
localhost:5173 |
localhost |
api-proves.botigaaroma.example |
botigaaroma.example, panel.… |
LIMIT_GLOBAL_PER_MINUT |
10000 |
10000 |
600 |
600 |
DOCS_PUBLIQUES |
true |
true |
true |
false |
MOSTREIG_TRACES |
1.0 |
0 |
1.0 |
0.05 |
Quatre regles sobre secrets que no admeten excepció:
- Cap secret dins de la imatge. Ni al
Dockerfile, ni ambARG—elsARGqueden a l'historial de capes i es veuen ambdocker history—, ni en un fitxer copiat. - Cap secret al repositori.
.enval.gitignore;.env.exampleamb les claus i valors ficticis, versionat, per documentar què cal. - Els secrets s'injecten en temps d'execució per l'orquestrador, des del seu gestor: GitHub Secrets, AWS Secrets Manager, Google Secret Manager, HashiCorp Vault, Kubernetes Secrets.
src/config/entorn.jsvalida en arrencar. Ja el vam escriure a 03-01, i aquí es veu per què importa: si faltaJWT_SECRET, el procés ha de morir immediatament amb un missatge clar, no arrencar i fallar a la primera petició autenticada. Fallar ràpid i sorollosament a l'arrencada és el que fa que un desplegament mal configurat no arribi a rebre trànsit.
- La canalització de CI:
.github/workflows/ci.yml
.github/workflows/ci.ymlFitxer nou .github/workflows/ci.yml:
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
# Cancel·la execucions anteriors de la mateixa branca: si fas tres push seguits,
# només s'executa l'últim. Estalvia minuts de CI i dona retroalimentació abans.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
# Permisos mínims per defecte (apartat 18). Cada treball demana el que necessita.
permissions:
contents: read
env:
NODE_VERSION_PRINCIPAL: '20'
jobs:
# --------------------------------------------------------------------------
# 1. Qualitat estàtica: ràpida i sense dependències externes. Falla en 30 s.
# --------------------------------------------------------------------------
qualitat:
name: Lint i format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION_PRINCIPAL }}
cache: npm # desa ~/.npm fent servir package-lock.json com a clau
- name: Instal·lar dependències
run: npm ci
- name: ESLint
run: npm run lint
- name: Prettier (comprovació, no escriptura)
run: npx prettier --check .
# --------------------------------------------------------------------------
# 2. El contracte: validesa estructural + guia d'estil + canvis trencadors.
# S'executa en paral·lel amb les proves perquè no en depèn.
# --------------------------------------------------------------------------
contracte:
name: Validació del contracte
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # necessitem l'historial per comparar amb main
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION_PRINCIPAL }}
cache: npm
- run: npm ci
- name: És un document OpenAPI vàlid? (05-02)
run: npx swagger-cli validate openapi.yaml
- name: Compleix la guia d'estil? (Spectral, 04-01)
run: npx spectral lint openapi.yaml --fail-severity=error
- name: Instal·lar oasdiff
run: |
curl -fsSL https://raw.githubusercontent.com/oasdiff/oasdiff/main/install.sh | sh
- name: Introdueix canvis trencadors? (05-04)
if: github.event_name == 'pull_request'
run: bash eines/comprovar-contracte.sh origin/main
# --------------------------------------------------------------------------
# 3. Proves: unitàries + integració + contracte, en diverses versions de Node.
# --------------------------------------------------------------------------
proves:
name: Proves (Node ${{ matrix.node }})
runs-on: ubuntu-latest
strategy:
fail-fast: false # que fallin totes les que hagin de fallar
matrix:
node: ['20', '22'] # LTS actual i la següent: detecta trencades aviat
services:
redis:
image: redis:7-alpine
ports: ['6379:6379']
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 3s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- name: Migrar i sembrar la base de proves
run: npm run bd:reiniciar
env:
RUTA_BASE_DADES: ':memory:'
- name: Proves amb cobertura
run: node --test --experimental-test-coverage proves/
env:
NODE_ENV: test
REDIS_URL: redis://localhost:6379
JWT_SECRET: secret-fictici-nomes-per-a-ci
NIVELL_LOG: silent
- name: Publicar l'informe de cobertura
if: matrix.node == '20'
uses: actions/upload-artifact@v4
with:
name: cobertura
path: cobertura/
retention-days: 7
# --------------------------------------------------------------------------
# 4. Seguretat de dependències (04-02).
# --------------------------------------------------------------------------
seguretat:
name: Auditoria de dependències
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION_PRINCIPAL }}
cache: npm
- run: npm ci
# Falla amb vulnerabilitats altes o crítiques. Les moderades es revisen
# però no bloquegen: si bloquegessin, la canalització estaria trencada
# permanentment per dependències transitives i l'equip aprendria a ignorar-la.
- name: npm audit
run: npm audit --audit-level=high
- name: Comprovar que el lock està sincronitzat
run: |
npm ci --dry-run 2>&1 | tee /tmp/sortida
! grep -q "npm warn" /tmp/sortida || echo "Revisar avisos d'npm"
# --------------------------------------------------------------------------
# 5. Imatge: només si tot l'anterior ha passat. Al PR es construeix però no es
# publica; a main es publica etiquetada amb el SHA del commit.
# --------------------------------------------------------------------------
imatge:
name: Construir i publicar la imatge
needs: [qualitat, contracte, proves, seguretat]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # necessari per empènyer al registre
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Autenticar-se al registre
if: github.ref == 'refs/heads/main'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Etiquetes i metadades
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}/api
tags: |
type=sha,format=long # l'etiqueta = el commit: traçabilitat
type=ref,event=branch
type=semver,pattern={{version}}
- name: Construir (i publicar només a main)
uses: docker/build-push-action@v6
with:
context: .
target: produccio
push: ${{ github.ref == 'refs/heads/main' }}
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/trivy-action@master
with:
image-ref: ghcr.io/${{ github.repository }}/api:sha-${{ github.sha }}
severity: 'HIGH,CRITICAL'
exit-code: '1'
ignore-unfixed: true # sense pedaç disponible, no hi ha res a fer
# --------------------------------------------------------------------------
# 6. Desplegament a preproducció i verificació (només a main).
# --------------------------------------------------------------------------
preproduccio:
name: Desplegar a preproducció i verificar
needs: [imatge]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment:
name: preproduccio
url: https://api-proves.botigaaroma.example
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION_PRINCIPAL }}
cache: npm
- run: npm ci
- name: Aplicar migracions (abans del desplegament, apartat 11)
run: npm run migrar
env:
URL_BASE_DADES: ${{ secrets.URL_BASE_DADES_PREPRODUCCIO }}
- name: Desplegar la imatge
run: ./eines/desplegar.sh preproduccio "sha-${{ github.sha }}"
env:
TOKEN_DESPLEGAMENT: ${{ secrets.TOKEN_DESPLEGAMENT_PREPRODUCCIO }}
- name: Esperar que el readiness estigui verd (04-07)
run: |
for intent in $(seq 1 30); do
if curl -fsS https://api-proves.botigaaroma.example/salut/preparat; then
echo "Llest després de ${intent} intents."; exit 0
fi
sleep 5
done
echo "El servei no ha arribat a estar preparat en 150 s."; exit 1
- name: Proves d'extrem a extrem (05-04)
run: node --test proves/e2e/
env:
URL_API: https://api-proves.botigaaroma.example/v1
EMAIL_PROVA: ${{ secrets.EMAIL_PROVA }}
CLAU_PROVA: ${{ secrets.CLAU_PROVA }}
- name: Col·lecció de Postman amb Newman (05-01)
run: |
npx newman run postman/botiga-aroma-v1.postman_collection.json \
-e postman/proves.postman_environment.json \
--env-var "clauClient=${{ secrets.CLAU_PROVA }}" \
--delay-request 100 \
--reporters cli,junit \
--reporter-junit-export informes/newman.xml
- name: Publicar l'informe de Newman
if: always()
uses: actions/upload-artifact@v4
with:
name: informe-newman
path: informes/newman.xml
- Matriu de versions i memòria cau de dependències
La matriu. matrix: node: ['20', '22'] executa les proves dues vegades. El cost és el doble de temps de CI; el benefici, descobrir amb mesos d'antelació que alguna cosa es trenca a la LTS següent, quan encara hi ha temps d'arreglar-ho amb calma en lloc de sota la pressió d'un final de suport.
fail-fast: false és important: per defecte, GitHub cancel·la la resta de la matriu a la primera fallada, i aleshores no saps si el problema és de Node 22 o del teu codi. Amb false veus tots dos resultats.
Quan val la pena la matriu: a les biblioteques, gairebé sempre; en una aplicació desplegada, quan la migració de versió major és previsible i t'hi vols anticipar. A la Botiga Aroma sí, perquè desplegem en Node 20 i la 22 serà la LTS següent.
La memòria cau. cache: npm a setup-node desa el directori ~/.npm fent servir el hash de package-lock.json com a clau. Efecte típic: npm ci baixa de 60-90 segons a 10-15.
Un matís que confon: desa la descàrrega, no node_modules. És deliberat. Desar node_modules és una font clàssica de fallades fantasma —mòduls natius compilats per a una altra versió, restes d'instal·lacions anteriors— i no és reproduïble. npm ci esborra node_modules i reinstal·la des de zero; l'única cosa que s'estalvia és la xarxa.
Per a la imatge, la memòria cau és diferent: cache-from: type=gha desa les capes de Docker a la memòria cau de GitHub Actions, de manera que l'etapa cara —npm ci amb compilació de better-sqlite3— es reutilitza mentre package-lock.json no canviï.
- Què fa fallar la canalització i per què les portes són inflexibles
| Porta | Falla si | Bloqueja el merge? | Per què |
|---|---|---|---|
| ESLint | Qualsevol error | Sí | Els avisos ja estan filtrats; un error és un error |
| Prettier | Un fitxer sense formatar | Sí | Format automàtic: no hi ha excusa ni discussió |
swagger-cli validate |
El contracte no és OpenAPI vàlid | Sí | Trenca la documentació i tots els generadors |
| Spectral | Alguna regla error |
Sí | És la guia d'estil acordada (04-01) |
oasdiff breaking |
Hi ha canvis trencadors | Sí, tret d'etiqueta explícita | Trenca l'Aroma Mòbil, i és permanent |
| Proves | Una sola falla | Sí | Evident |
| Cobertura | Baixa del llindar acordat | Sí, però amb criteri | Vegeu més avall |
npm audit |
Vulnerabilitat alta o crítica | Sí | Amb --audit-level=high: les moderades no bloquegen |
| Trivy | CVE alta o crítica amb pedaç | Sí | ignore-unfixed: sense pedaç no hi ha acció possible |
| E2E a preproducció | Una falla | Sí, no promociona a producció | És l'última xarxa abans dels clients |
Per què la inflexibilitat importa. Una porta que es pot saltar «només aquesta vegada» deixa de ser una porta al tercer «només aquesta vegada». La regla que funciona és: si una porta molesta, es canvia la regla mitjançant un pull request que la modifiqui —discutit i visible— no es salta en una execució concreta. Aquell és exactament el procediment de Spectral de 04-01: una regla entra com a warn, es netegen les infraccions i després puja a error.
L'únic mecanisme d'escapament legítim és explícit, deixa rastre i exigeix justificació. Per a oasdiff:
- name: Introdueix canvis trencadors?
if: >
github.event_name == 'pull_request' &&
!contains(github.event.pull_request.labels.*.name, 'canvi-trencador')
run: bash eines/comprovar-contracte.sh origin/mainPosar l'etiqueta canvi-trencador és un acte deliberat, visible al pull request, que obliga a explicar-se i que a més pot exigir l'aprovació d'algú concret mitjançant CODEOWNERS.
Sobre la cobertura. Un llindar és útil com a xarxa contra el descuit —«no baixem del 80 %»— i pervers com a objectiu: perseguir el 100 % produeix proves que executen codi sense comprovar res. La configuració sensata és exigir que la cobertura no baixi respecte de main, en lloc d'un número absolut.
- Construir i publicar la imatge
El treball imatge construeix sempre i publica només des de main. Dues conseqüències bones: els pull requests verifiquen que el Dockerfile continua funcionant —una fallada de construcció es detecta a la revisió, no en fusionar— i el registre no s'omple d'imatges de branques efímeres.
Les etiquetes que produeix docker/metadata-action:
ghcr.io/botigaaroma/api:sha-3f9a2c1e8b474d2a9e0177c6b5d3a8129e4c1b2d ghcr.io/botigaaroma/api:main ghcr.io/botigaaroma/api:1.7.0 (si el commit porta una etiqueta de versió)
L'etiqueta que es desplega és sempre la del SHA. Mai latest, mai main. Motiu: són etiquetes mòbils. Si desplegues main i demà cal investigar què hi havia en producció el dimarts, la resposta és «no se sap». Amb sha-3f9a2c… la resposta és un commit exacte, amb el seu diff, el seu autor i el seu pull request. És el principi de traçabilitat de l'apartat 16.
cache-from/cache-to: type=gha reutilitza les capes entre execucions. Sense això, cada construcció recompila better-sqlite3 des de zero: dos o tres minuts per execució que es converteixen en vint segons.
- Migracions: expandir, migrar, contraure
Aquí hi ha el problema més subestimat del desplegament continu. Durant un desplegament sense talls conviuen dues versions del codi sobre una sola base de dades. Si la migració no és compatible amb totes dues, hi ha errors garantits.
Escenari concret. Volem reanomenar notes_tast a notes a la taula cafes.
La forma ingènua, que trenca:
-- migracions/013-reanomenar-notes.sql ← NO FACIS AIXÒ
ALTER TABLE cafes RENAME COLUMN notes_tast TO notes;Seqüència dels fets: la migració s'aplica; durant els dos minuts següents, les instàncies amb el codi antic continuen atenent trànsit i executen SELECT notes_tast FROM cafes; aquella columna ja no existeix; cada petició al catàleg retorna 500 fins que acaba el desplegament. I si cal revertir, el codi antic tampoc no funciona: has perdut el retorn enrere.
La forma correcta: expandir → migrar → contraure. Tres desplegaments separats.
-- PAS 1 — EXPANDIR (desplegament 1). Només afegeix. Compatible amb tot.
ALTER TABLE cafes ADD COLUMN notes TEXT;
UPDATE cafes SET notes = notes_tast WHERE notes IS NULL;// Codi del desplegament 1: escriu a TOTES DUES, llegeix de l'antiga.
export function desarCafe(cafe) {
bd.prepare(`
UPDATE cafes SET notes_tast = ?, notes = ? WHERE id = ?
`).run(JSON.stringify(cafe.notesTast), JSON.stringify(cafe.notesTast), cafe.id);
}
export function llegirCafe(id) {
const fila = bd.prepare('SELECT * FROM cafes WHERE id = ?').get(id);
return { ...fila, notesTast: JSON.parse(fila.notes_tast) }; // encara l'antiga
}// PAS 2 — MIGRAR (desplegament 2). Escriu a totes dues, LLEGEIX DE LA NOVA.
// Si alguna cosa va malament, es reverteix al desplegament 1 sense perdre dades,
// perquè totes dues columnes estan poblades i sincronitzades.
export function llegirCafe(id) {
const fila = bd.prepare('SELECT * FROM cafes WHERE id = ?').get(id);
return { ...fila, notesTast: JSON.parse(fila.notes ?? fila.notes_tast) };
}-- PAS 3 — CONTRAURE (desplegament 3, dies o setmanes després).
-- Només quan CAP instància amb codi antic no pugui estar viva
-- i el retorn enrere a aquella versió ja no sigui una opció realista.
ALTER TABLE cafes DROP COLUMN notes_tast;Regles pràctiques de migració:
| Operació | És segura durant un desplegament? | Nota |
|---|---|---|
ADD COLUMN amb valor per defecte o nul |
Sí | La forma segura d'afegir |
ADD COLUMN NOT NULL sense defecte |
No | Les escriptures del codi antic fallen |
DROP COLUMN |
No | Només a la fase de contracció |
RENAME COLUMN |
No | És un DROP disfressat. Expandir/contraure |
CREATE INDEX |
Depèn | A PostgreSQL, CONCURRENTLY; sense això bloqueja la taula |
ALTER TYPE que estreny |
No | Dades existents poden no cabre-hi |
| Afegir taula | Sí | Ningú no la fa servir encara |
Afegir restricció NOT NULL |
No | Expandir: omplir, validar i després restringir |
Quan s'apliquen. Abans de desplegar el codi nou, en un pas propi de la canalització, com al treball preproduccio de l'apartat 7. Mai en arrencar l'aplicació: amb tres instàncies arrencant alhora tindries tres processos migrant en paral·lel sobre la mateixa base. Si el teu sistema de migracions no pren un bloqueig, el resultat és impredictible.
I les migracions s'han de provar. Una prova que aplica totes les migracions sobre una base buida i comprova l'esquema resultant costa poc i evita el pitjor tipus d'incident: el que passa al pas previ al desplegament, quan encara no hi ha res desplegat a revertir.
- Estratègies de desplegament
| Estratègia | Com funciona | Tall | Cost | Risc | Retorn enrere | Quan |
|---|---|---|---|---|---|---|
| Recreate | Atura tot, arrenca el nou | Sí, segons o minuts | Mínim | Alt | Redesplegar | Desenvolupament; sistemes que toleren aturada |
| Rolling | Substitueix instàncies per tandes | No | Mínim | Mitjà | Rolling invers, lent | El cas per defecte |
| Blue-green | Dos entorns complets; es commuta el trànsit | No | Doble infraestructura | Baix | Instantani | Desplegaments crítics |
| Canary | Un 1-5 % del trànsit a la versió nova; es puja si les mètriques aguanten | No | Mitjà | Molt baix | Automàtic per mètriques | Alt volum; canvis arriscats |
graph TD
subgraph Rolling
R1[3 instàncies v1] --> R2[2 v1 + 1 v2] --> R3[1 v1 + 2 v2] --> R4[3 instàncies v2]
end
subgraph BlueGreen
B1[Blau v1 rep trànsit<br/>Verd v2 es desplega i s'escalfa] --> B2[Commutar el balancejador] --> B3[Verd v2 rep trànsit<br/>Blau v1 en espera per revertir]
end
subgraph Canary
C1[100% a v1] --> C2[95% v1 i 5% v2<br/>vigilar errors i latència] --> C3{SLO en verd?}
C3 -->|Sí| C4[50% i 50%, després 100% v2]
C3 -->|No| C5[Tornada a 100% v1<br/>automàtica]
end
Rolling és el mode per defecte de Kubernetes i de gairebé tots els orquestradors, i és una elecció sensata. La seva condició imprescindible és la de l'apartat anterior: durant la substitució conviuen totes dues versions, així que la base de dades i el contracte han de ser compatibles amb les dues.
Blue-green és molt tranquil·litzador —revertir és tornar a commutar el balancejador, segons— i car: durant el desplegament pagues el doble d'infraestructura. I compte: la base de dades no és blue-green. És compartida, així que les migracions continuen havent de ser retrocompatibles.
Canary és l'estratègia que fa viable el desplegament continu real, perquè uneix el desplegament amb l'observabilitat de 04-07: s'envia un percentatge petit de trànsit a la versió nova i es comparen automàticament la seva taxa d'error i la seva latència p99 contra les de la versió estable. Si es degraden, es reverteix sol. Requereix volum suficient perquè les mètriques siguin significatives —amb deu peticions per minut, un 5 % no diu res— i un gateway o malla que sàpiga repartir el trànsit, que és justament el que veurem a 05-06.
La connexió amb /v1 i /v2 de 02-07. Convé no confondre dues coses que s'assemblen:
- Desplegar
v1.7.0sobrev1.6.0és un desplegament: mateix contracte, codi nou. Aquí s'apliquen rolling, blue-green o canary. - Publicar
/v2al costat de/v1és convivència de versions d'API: dos contractes diferents vius durant mesos, ambDeprecationiSunseta/v1. No és una estratègia de desplegament, és una decisió de producte.
Es combinen: /v2 es desplega amb rolling com qualsevol altra versió, i totes dues rutes conviuen al mateix servei (o en serveis separats darrere del gateway, que és més net per poder retirar /v1 apagant alguna cosa).
- El paper de liveness i readiness
Els dos endpoints de 04-07 deixen de ser teoria tan bon punt hi ha un orquestrador al davant.
// src/app.js — recordatori de la distinció, posicions 10 i 11
// LIVENESS: el procés està viu? Res de dependències.
// Si respon malament, l'orquestrador MATA i REINICIA el contenidor.
app.get('/salut', (req, res) => res.json({ estat: 'viu' }));
// READINESS: puc atendre trànsit ARA? Comprova dependències.
// Si respon malament, l'orquestrador TREU la instància del balancejador,
// però NO la reinicia: potser Redis torna en deu segons.
app.get('/salut/preparat', async (req, res) => {
const comprovacions = {
baseDades: await comprovarBaseDades(),
redis: await comprovarRedis(),
migracionsAlDia: await comprovarMigracions(),
};
const llest = Object.values(comprovacions).every(Boolean);
res.status(llest ? 200 : 503).json({ estat: llest ? 'preparat' : 'no_preparat', comprovacions });
});Configuració equivalent a Kubernetes, que mostra com es fan servir:
livenessProbe:
httpGet: { path: /salut, port: 3000 }
initialDelaySeconds: 10
periodSeconds: 30
failureThreshold: 3 # tres fallades seguides → reiniciar
readinessProbe:
httpGet: { path: /salut/preparat, port: 3000 }
initialDelaySeconds: 5
periodSeconds: 5 # més freqüent: reacciona ràpid
failureThreshold: 2
# startupProbe: dona marge a l'arrencada sense relaxar el liveness.
# Fins que no passa, liveness i readiness no s'avaluen.
startupProbe:
httpGet: { path: /salut, port: 3000 }
periodSeconds: 5
failureThreshold: 30 # fins a 150 s per arrencar
lifecycle:
preStop:
# Espera abans del SIGTERM: dona temps que el balancejador s'assabenti
# que aquesta instància surt, evitant peticions encaminades a un
# procés que ja s'està tancant. És la causa núm. 1 d'errors 502
# durant desplegaments "sense talls".
exec: { command: ["sleep", "5"] }
terminationGracePeriodSeconds: 30Els tres errors clàssics, que produeixen incidents molt difícils de diagnosticar:
- Fer servir readiness com a liveness. Redis cau, readiness respon
503, l'orquestrador reinicia tots els contenidors en bucle, i ara tens dos problemes. - Un liveness que consulta la base de dades. Una consulta lenta fa fallar el liveness i reinicia una aplicació perfectament sana, agreujant la càrrega sobre la base.
- Oblidar el
preStop. Sense ell, el balancejador pot continuar enviant peticions durant un o dos segons a un procés que ja ha rebutSIGTERM, i apareixen502esporàdics a cada desplegament que ningú no aconsegueix reproduir.
La cadena completa d'una aturada neta, unint 03-07 amb aquesta lliçó: preStop (5 s de marge) → SIGTERM → tini el reemet a Node → tancarOrdenadament() → servidor.close() deixa d'acceptar connexions noves i acaba les que hi ha en curs → es tanquen base de dades i Redis → process.exit(0). Si alguna cosa s'encalla, terminationGracePeriodSeconds acaba amb un SIGKILL als 30 segons.
- Retorn enrere i feature flags
Revertir el codi és fàcil. És tornar a desplegar l'etiqueta anterior:
Que sigui trivial depèn de tres coses que ja tenim: imatges immutables etiquetades per commit, cap configuració dins de la imatge, i migracions retrocompatibles. La tercera és la que sol faltar.
Revertir les dades és el problema difícil, i convé tenir-ho clar abans de necessitar-ho:
- Si la versió nova va escriure dades amb un format que l'antiga no entén, revertir el codi no arregla res.
- Si la migració va esborrar una columna, revertir el codi el deixa apuntant a una cosa que ja no existeix.
- Restaurar una còpia de seguretat significa perdre tot el que ha passat des de la còpia: comandes, pagaments, ressenyes. Gairebé mai no és acceptable.
D'aquí que la regla d'or sigui: el rollback d'una migració destructiva no existeix; es preveu. Expandir → migrar → contraure no és una cerimònia burocràtica, és precisament el que fa possible revertir.
Feature flags com a alternativa. En lloc de revertir el desplegament, s'apaga la funcionalitat:
// src/config/banderes.js
// Banderes llegides de l'entorn o d'un servei de configuració.
// Canviar-les NO requereix desplegar: és la seva raó de ser.
export const banderes = {
recomanacionsAlCataleg: entorn.llegirBooleana('BANDERA_RECOMANACIONS', false),
nouCalculEnviament: entorn.llegirBooleana('BANDERA_NOU_CALCUL_ENVIAMENT', false),
};// src/serveis/comandes.js
const despesesEnviament = banderes.nouCalculEnviament
? calcularEnviamentPerZones(comanda) // lògica nova, apagable en un segon
: calcularEnviamentPla(comanda); // lògica antiga, intactaAvantatges: separen desplegar d'activar, permeten activar només per a un grup (empleats primer, després el 5 % de clients), i apagar és instantani sense desplegar res. Inconvenients que cal gestionar: cada bandera duplica camins de codi i per tant casos de prova, i les banderes caduquen: una bandera que fa un any que està encesa és deute tècnic. La disciplina que funciona és apuntar la data de retirada al mateix commit que la crea.
- On desplegar
| Opció | Esforç | Control | Cost | Escalat | Encaix amb la Botiga Aroma |
|---|---|---|---|---|---|
| PaaS (Render, Railway, Fly.io) | Molt baix | Baix | Mitjà | Automàtic, limitat | Excel·lent per començar: git push i llest |
| Contenidors gestionats (Cloud Run, ECS Fargate) | Baix | Mitjà | Baix-mitjà | Automàtic, a zero | L'opció sensata: Docker sense operar servidors |
| Kubernetes gestionat (GKE, EKS, AKS) | Alt | Alt | Mitjà-alt | Total | Només amb diversos serveis i algú que l'operi |
| Serverless (Lambda, Cloud Functions) | Baix | Baix | Molt baix sense trànsit | Automàtic | Problemàtic: arrencades en fred i connexions (05-03) |
| VPS propi | Mitjà | Total | Baix | Manual | Vàlid si l'equip sap administrar sistemes |
Recomanació per a la Botiga Aroma: Cloud Run o ECS Fargate. El raonament: ja tenim la imatge Docker, que és l'única cosa que demanen; l'escalat és automàtic; no hi ha servidors per apedaçar; i /salut/preparat s'integra directament com a comprovació. Kubernetes donaria més control del que necessitem avui i exigeix dedicació operativa a temps parcial que un equip petit no té.
Un avís sobre Kubernetes, perquè és la decisió sobredimensionada més comuna del sector: és una eina excel·lent per al seu problema, que és orquestrar molts serveis amb equips que els puguin operar. Adoptar-lo per a una API és canviar el problema «desplegar una aplicació» pel problema «operar Kubernetes», que és molt més gran. La pregunta correcta no és «és bo?», sinó «tenim el seu problema?».
- Versionat d'artefactes i traçabilitat
Davant d'un incident, hi ha tres preguntes que s'han de respondre en menys d'un minut: quina versió està desplegada, què conté i quan va arribar.
Les pràctiques que ho garanteixen:
- Etiqueta d'imatge = commit.
sha-3f9a2c1e…. Sense ambigüitat possible. - L'aplicació exposa la seva versió, a l'arrel o a
/salut:
// src/rutes/salut.js
app.get('/salut', (req, res) => {
res.json({
estat: 'viu',
versio: entorn.versioApi, // 1.7.0, d'info.version del contracte
commit: entorn.commitSha, // injectat com a variable d'entorn
desplegatEl: entorn.dataDesplegament,
});
});- La versió com a etiqueta a les mètriques de 04-07.
aroma_http_peticions_total{versio="1.7.0"}permet veure en un gràfic el moment exacte del desplegament i correlacionar-lo amb un canvi a la taxa d'error. És el senyal que més ràpid resol incidents: «ha començat just en desplegar». - La versió a cada línia de log, que ja surt gratis amb el registrador base de pino.
- Un registre de desplegaments: quina versió, qui, quan, a quin entorn. GitHub Deployments ho fa sol amb
environment:al workflow.
Una nota sobre versions semàntiques: info.version d'openapi.yaml descriu el contracte; el SHA descriu el codi. No coincideixen: vint commits poden compartir contracte 1.7.0. Tots dos són necessaris i responen preguntes diferents.
- Després del desplegament: proves de fum i vigilància
El desplegament no acaba quan la canalització es posa verda. Acaba quan algú ha comprovat que el sistema està bé.
Proves de fum. Un subconjunt mínim, ràpid i no destructiu, executat contra producció just després de desplegar:
#!/usr/bin/env bash
# eines/fum.sh — comprovacions mínimes després del desplegament.
# NO crea dades: en producció, una prova destructiva és inacceptable.
set -euo pipefail
URL="${1:?ús: fum.sh https://api.botigaaroma.example}"
echo "1/5 liveness"
curl -fsS "${URL}/salut" | grep -q '"estat":"viu"'
echo "2/5 readiness (dependències)"
curl -fsS "${URL}/salut/preparat" | grep -q '"estat":"preparat"'
echo "3/5 la versió desplegada és l'esperada"
DESPLEGADA=$(curl -fsS "${URL}/salut" | sed -n 's/.*"commit":"\([^"]*\)".*/\1/p')
[ "${DESPLEGADA}" = "${COMMIT_ESPERAT}" ] || {
echo "Versió desplegada ${DESPLEGADA}, esperada ${COMMIT_ESPERAT}"; exit 1; }
echo "4/5 el catàleg respon i exigeix autenticació"
curl -fsS -o /dev/null -w '%{http_code}' "${URL}/v1/cafes" | grep -q 401
echo "5/5 el contracte està publicat"
curl -fsS "${URL}/docs/openapi.json" | grep -q '"openapi"'
echo "Proves de fum correctes."Cinc comprovacions en dos segons que detecten les fallades de desplegament més freqüents: l'aplicació no arrenca, una dependència no és abastable des de producció, s'ha desplegat la imatge equivocada, o l'autenticació s'ha desconfigurat.
Vigilància durant el desplegament. És on 04-07 dona el seu valor. Els primers quinze minuts, mirant:
| Senyal | Llindar d'alarma | Acció |
|---|---|---|
Taxa d'error 5xx |
Qualsevol pujada sobre la línia base | Revertir i després investigar |
| Latència p99 | +20 % sobre la referència | Investigar; revertir si empitjora |
| Taxa de peticions | Caiguda brusca | Algú no hi arriba: DNS, balancejador, CORS |
| Pressupost d'error de l'SLO | Consum accelerat | Revertir |
| Memòria i event loop | Creixement sostingut | Possible fuita introduïda en aquesta versió |
Mètriques de negoci (aroma_comandes_creades_total) |
Caiguda | El senyal més important: l'impacte real |
L'última fila mereix èmfasi. Un desplegament pot estar tècnicament perfecte —zero errors, latència esplèndida— i haver trencat el negoci, perquè un canvi a la validació fa que cap comanda no es completi. Els 5xx no ho detecten; les comandes per minut, sí. Vigila sempre almenys una mètrica de negoci durant un desplegament.
I la regla cultural més important: primer restaurar el servei, després entendre què ha passat. La temptació d'investigar amb producció trencada és enorme i sempre és un error. Es reverteix, es respira i s'investiga amb el sistema estable.
- Secrets a CI i mínim privilegi
La canalització de CI és un objectiu d'atac de primer ordre: té credencials de desplegament i capacitat d'executar codi arbitrari. Els controls imprescindibles:
1. Permisos mínims per defecte. Ja és al workflow de l'apartat 7:
i cada treball eleva només el seu (packages: write únicament a imatge). Sense aquesta declaració, el GITHUB_TOKEN té permisos amplis d'escriptura sobre el repositori, i qualsevol acció de tercers compromesa els hereta.
2. Mai imprimir secrets. GitHub emmascara els valors coneguts als logs, però és fàcil burlar-ho sense voler: un env | sort o un secret codificat en base64 es veu en clar. Regla simple: res de bolcar l'entorn en un log.
3. Fixar les accions de tercers. uses: algu/accio@v3 segueix una etiqueta mòbil: si aquella etiqueta es mou a codi maliciós, s'executa a la teva canalització amb accés als teus secrets. Ha passat. El segur és fixar el SHA:
4. Entorns protegits. L'environment: produccio de GitHub permet exigir aprovació humana, limitar quines branques poden desplegar i desar secrets accessibles només des d'aquell entorn. Així, el token de producció no està a l'abast d'un workflow de pull request.
5. Res de secrets a workflows de pull requests de forks. Un pull request des d'un fork executa codi que no controles. GitHub no exposa secrets a pull_request de forks per defecte; no ho canviïs amb pull_request_target sense entendre exactament el que implica. És una de les vies d'exfiltració de credencials més explotades.
6. Credencials efímeres en lloc de claus llargues. OIDC entre GitHub Actions i el proveïdor de núvol emet un token de vida curta per execució, en lloc de desar una clau d'accés permanent. És la mateixa idea de 04-03 —tokens de vida curta, abast limitat— aplicada a la infraestructura.
7. Rotació i auditoria. Els secrets caduquen i es roten; els accessos es registren. I si un secret ha aparegut alguna vegada en un log o en un commit, es considera compromès: es rota, no s'esborra el commit i s'hi fa com si res.
Errors Comuns i Consells
COPY . .sense.dockerignore. Fica el teu.envamb les claus reals dins de la imatge. És una de les fuites de credencials més freqüents que existeixen.CMD node src/servidor.jsen forma shell. El PID 1 passa a ser/bin/sh,SIGTERMno arriba a Node i cada desplegament talla peticions a mitges. Forma exec itini.- Fer servir readiness com a liveness. Redis cau i l'orquestrador reinicia tots els contenidors en bucle. Liveness sense dependències; readiness amb elles.
DROP COLUMNen un desplegament sense talls. La versió antiga continua viva i executantSELECTsobre aquella columna. Expandir → migrar → contraure, sempre.- Migrar en arrencar l'aplicació. Amb tres instàncies, tres migracions en paral·lel sobre la mateixa base. Pas propi a la canalització, amb bloqueig.
- Desplegar l'etiqueta
latest. És mòbil: no saps què hi ha en producció ni pots revertir amb precisió. L'etiqueta és el SHA del commit. - Reconstruir la imatge per a cada entorn. Aleshores no desplegues el que vas provar. Una imatge, moltes configuracions.
- Portes de CI que es poden saltar. Al tercer «només aquesta vegada» deixen d'existir. Es canvien amb un pull request, no es salten en una execució.
- Bloquejar amb
npm audita nivell moderat. La canalització estarà trencada permanentment per dependències transitives i l'equip aprendrà a ignorar-la.--audit-level=high. - Un workflow amb
permissions: write-all. Qualsevol acció de tercers compromesa hereta el poder d'escriure al teu repositori. - Consell: comprova
docker stopen local. Si triga deu segons, elSIGTERMno arriba i la teva aturada ordenada tampoc no s'executa en producció. - Consell: exporta la versió desplegada com a etiqueta de mètrica. Veure l'esglaó exacte al gràfic d'errors en el moment del desplegament resol incidents en segons.
- Consell: vigila una mètrica de negoci durant cada desplegament. Un desplegament tècnicament perfecte pot haver trencat la creació de comandes, i els
5xxno t'ho diran. - Consell: escriu un ADR (04-01) amb l'estratègia de desplegament triada. D'aquí a un any algú preguntarà per què no feu canary, i la resposta ha d'estar escrita.
Exercicis
Exercici 1: completar la canalització amb un treball de desplegament a producció
Amplia .github/workflows/ci.yml amb un treball produccio que s'executi després de preproduccio, exigeixi aprovació humana, apliqui migracions, desplegui la mateixa imatge que es va verificar a preproducció, executi les proves de fum i reverteixi automàticament si fallen. Explica per què s'ha de desplegar exactament la mateixa etiqueta d'imatge i no reconstruir-se.
Exercici 2: dissenyar una migració segura
La Botiga Aroma necessita separar el camp origen (avui "Etiòpia") en dos: pais i regio ("Etiòpia" / "Yirgacheffe"), perquè el cercador ha de filtrar per regió. Hi ha 2.400 cafès en producció i el sistema desplega amb rolling sobre tres instàncies.
Dissenya la seqüència completa expandir → migrar → contraure: quin SQL a cada pas, què fa el codi a cada desplegament, quant temps deixaries entre passos i per què, com tractaries el contracte de l'API a cada fase (recorda 02-07), i en quin punt exacte es perd la possibilitat de revertir.
Exercici 3: triar estratègia de desplegament per a tres canvis
Per a cada canvi, tria entre recreate, rolling, blue-green i canary, justifica l'elecció, indica què vigilaries durant el desplegament i descriu el pla de retorn enrere:
A) Correcció d'una fallada al càlcul de l'IVA de les factures. Afecta totes les comandes i hi ha clients pagant ara mateix.
B) Canvi de l'algorisme d'ordenació per defecte del catàleg, que ara prioritza els cafès més ben valorats. No canvia el contracte, però canvia el que veuen tots els usuaris.
C) Migració de la persistència de SQLite a PostgreSQL, amb el mateix contracte d'API i el mateix codi tret de src/repositoris/.
Solucions
Solució 1
# --------------------------------------------------------------------------
# 7. Producció: aprovació humana, desplegament, proves de fum i retorn enrere.
# --------------------------------------------------------------------------
produccio:
name: Desplegar a producció
needs: [preproduccio]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment:
name: produccio # amb "required reviewers" configurat a GitHub:
url: https://api.botigaaroma.example # el treball espera aprovació humana
permissions:
contents: read
deployments: write
steps:
- uses: actions/checkout@v4
- name: Anotar la versió que estava desplegada (per al retorn enrere)
id: anterior
run: |
ACTUAL=$(curl -fsS https://api.botigaaroma.example/salut \
| sed -n 's/.*"commit":"\([^"]*\)".*/\1/p')
echo "sha=${ACTUAL}" >> "$GITHUB_OUTPUT"
echo "Versió actual en producció: ${ACTUAL}"
- name: Aplicar migracions (retrocompatibles, apartat 11)
run: npm ci && npm run migrar
env:
URL_BASE_DADES: ${{ secrets.URL_BASE_DADES_PRODUCCIO }}
- name: Desplegar EXACTAMENT la imatge verificada a preproducció
run: ./eines/desplegar.sh produccio "sha-${{ github.sha }}"
env:
TOKEN_DESPLEGAMENT: ${{ secrets.TOKEN_DESPLEGAMENT_PRODUCCIO }}
- name: Esperar readiness
run: |
for i in $(seq 1 40); do
curl -fsS https://api.botigaaroma.example/salut/preparat && exit 0
sleep 5
done
exit 1
- name: Proves de fum
id: fum
run: ./eines/fum.sh https://api.botigaaroma.example
env:
COMMIT_ESPERAT: ${{ github.sha }}
- name: Vigilar mètriques durant 3 minuts
run: ./eines/vigilar-desplegament.sh --minuts 3 --llindar-error 0.5
- name: RETORN ENRERE automàtic si alguna cosa ha fallat
if: failure()
run: |
echo "::error::Desplegament fallit. Revertint a ${{ steps.anterior.outputs.sha }}"
./eines/desplegar.sh produccio "${{ steps.anterior.outputs.sha }}"
./eines/fum.sh https://api.botigaaroma.example
env:
TOKEN_DESPLEGAMENT: ${{ secrets.TOKEN_DESPLEGAMENT_PRODUCCIO }}
COMMIT_ESPERAT: ${{ steps.anterior.outputs.sha }}
- name: Avisar l'equip
if: always()
run: ./eines/notificar.sh "${{ job.status }}" "sha-${{ github.sha }}"Per què la mateixa imatge i no reconstruir. Cinc raons, de més a menys evident:
- És l'única cosa que s'ha verificat. Les proves d'extrem a extrem i Newman es van executar contra aquella imatge concreta a preproducció. Reconstruir produeix un artefacte que ningú no ha provat, per molt idèntic que sembli el codi.
- Les construccions no són bit a bit reproduïbles.
npm cirespecta el lock per a les teves dependències directes i transitives, però la imatge basenode:20-bookworm-slimés una etiqueta mòbil que s'actualitza; els paquets del sistema instal·lats ambapt-getcanvien de versió; i els mòduls natius es compilen contra el que hi hagi en aquell moment. Dues construccions del mateix commit amb un dia de diferència poden diferir. - Traçabilitat. Una etiqueta, un artefacte, un commit. Si en producció i a preproducció hi ha imatges diferents amb el mateix nom, la depuració es torna un exercici de fe.
- Temps. Reconstruir afegeix minuts al camí crític del desplegament, justament quan pot haver-hi una correcció urgent esperant.
- Superfície d'atac. Cada construcció és una oportunitat perquè entri alguna cosa indeguda a la cadena de subministrament. Menys construccions, menys oportunitats.
Detalls del treball que mereixen atenció: environment: produccio amb revisors obligatoris és el que converteix això en lliurament continu i no en desplegament continu; el pas anterior consulta la versió desplegada abans de tocar res, perquè després ja no es pot saber; i el pas de retorn enrere porta if: failure(), que es dispara si falla qualsevol pas previ del treball, incloses les proves de fum i la vigilància de mètriques.
Una limitació honesta d'aquesta solució: el retorn enrere reverteix el codi, no les dades. Si la migració va ser destructiva, això no salva res, i per això l'apartat 11 no és opcional.
Solució 2
Fase 0 — Preparació (abans de tocar res).
Afegir al contracte els camps nous com a opcionals, sense eliminar origen. Publicar openapi.yaml amb pais i regio documentats i origen encara vigent. oasdiff ho aprova: afegir camps a una resposta no és trencador.
Fase 1 — EXPANDIR (desplegament 1).
-- migracions/014-expandir-origen.sql
ALTER TABLE cafes ADD COLUMN pais TEXT;
ALTER TABLE cafes ADD COLUMN regio TEXT;
-- Ompliment inicial: tot el que hi ha avui a origen es copia a pais.
-- La regió queda nul·la: es completarà manualment o amb un procés a part.
UPDATE cafes SET pais = origen WHERE pais IS NULL;
-- L'índex per al cercador per regió es crea aquí, no després.
CREATE INDEX IF NOT EXISTS idx_cafes_pais_regio ON cafes(pais, regio);// Codi del desplegament 1: escriu a les TRES columnes, llegeix d'`origen`.
export function desarCafe(cafe) {
bd.prepare(`
UPDATE cafes SET origen = ?, pais = ?, regio = ? WHERE id = ?
`).run(
cafe.regio ? `${cafe.pais}, ${cafe.regio}` : cafe.pais, // compon el llegat
cafe.pais,
cafe.regio ?? null,
cafe.id,
);
}
// L'API continua retornant `origen` i ja retorna `pais` i `regio` quan existeixen.
export function cafeARepresentacio(fila) {
return {
...camps,
origen: fila.origen, // continua sent la font per als consumidors
pais: fila.pais,
regio: fila.regio ?? undefined,
};
}Estat del contracte: origen vigent, pais i regio disponibles. Cap consumidor no se n'assabenta. Es pot revertir sense problema: les columnes noves sobren però no molesten.
Fase 1b — Omplir la regió (procés a part, dies).
2.400 cafès la regió dels quals cal deduir o introduir a mà. Un script per lots que actualitza en tandes de 200 amb pauses, per no bloquejar la base ni disparar la latència:
// eines/omplir-regio.js — executable diverses vegades, idempotent
const lot = bd.prepare('SELECT id, origen FROM cafes WHERE regio IS NULL LIMIT 200').all();
for (const fila of lot) {
const [pais, regio] = separarOrigen(fila.origen); // heurística + taula manual
bd.prepare('UPDATE cafes SET pais = ?, regio = ? WHERE id = ?').run(pais, regio, fila.id);
}Aquest pas és el que marca el ritme real, i per això la migració d'esquema i l'ompliment de dades han d'anar separats: barrejar-los produeix migracions que triguen minuts i bloquegen el desplegament.
Fase 2 — MIGRAR (desplegament 2, una o dues setmanes després).
// El codi llegeix de les columnes NOVES i continua escrivint a les tres.
export function cafeARepresentacio(fila) {
return {
...camps,
// `origen` es continua retornant, però ara es COMPON de pais i regio.
// Els consumidors antics veuen exactament el mateix que abans.
origen: fila.regio ? `${fila.pais}, ${fila.regio}` : fila.pais,
pais: fila.pais,
regio: fila.regio ?? undefined,
};
}El filtre ?regio=Yirgacheffe s'activa en aquesta fase. Encara es pot revertir: origen continua poblat i sincronitzat, així que el codi del desplegament 1 funciona perfectament.
Espera d'una o dues setmanes abans de continuar. El motiu: donar temps que apareguin casos rars —cafès amb noms d'origen que l'heurística ha separat malament— amb el retorn enrere encara disponible.
Fase 3 — Deprecar origen al contracte (no toca la base de dades).
origen:
type: string
deprecated: true
description: |
**Obsolet des de la 1.8.0. Es retirarà el 30 de juny de 2027.**
Fes servir `pais` i `regio`. Aquest camp es compon com
`"{pais}, {regio}"` per compatibilitat.Amb Deprecation i Sunset a les respostes, i avís a la SPA, a l'Aroma Mòbil i a CataBox. Com que l'Aroma Mòbil té versions antigues vives, aquest termini ha de ser generós: sis mesos com a mínim.
Fase 4 — CONTRAURE (mesos després, amb /v2 o després del Sunset).
-- migracions/018-contraure-origen.sql
-- Només quan CAP versió del codi que llegeix `origen` no pugui estar viva
-- i el termini de Sunset anunciat hagi vençut.
DROP INDEX IF EXISTS idx_cafes_origen;
ALTER TABLE cafes DROP COLUMN origen;On es perd la possibilitat de revertir: exactament a la fase 4, en executar el DROP COLUMN. A partir d'aquell instant, desplegar qualsevol versió anterior a la fase 2 produeix errors SQL a cada lectura del catàleg, i recuperar la columna significa restaurar una còpia de seguretat i perdre tot el que s'ha escrit des d'aleshores. És la raó per la qual la contracció es fa mesos després, quan revertir a aquella versió ja no és un escenari realista, i preferiblement en un desplegament propi sense cap altre canvi, perquè si alguna cosa falla se sàpiga exactament què ha estat.
Amb tres instàncies en rolling, cada fase individual és segura perquè en cap moment no coexisteixen un esquema i un codi incompatibles: aquella és la propietat que garanteix el patró, i l'única raó per la qual existeix.
Solució 3
A) Correcció de l'IVA a les factures → blue-green (o rolling amb retorn enrere preparat).
| Aspecte | Decisió |
|---|---|
| Estratègia | Blue-green, o rolling si no hi ha infraestructura duplicada |
| Per què no canary | El canary implica que un percentatge de clients rep factures amb el càlcul antic durant la finestra. En un afer fiscal, la incoherència entre factures emeses el mateix dia és pitjor que l'error original: complica la comptabilitat i la correcció posterior. Aquí es vol un tall net amb un instant exacte de canvi. |
| Per què blue-green | Dona un moment de commutació precís —anotable al registre comptable— i retorn enrere en segons. |
| Què vigilar | Taxa d'error de POST /comandes/{id}/pagament i de la generació de factures; imports de les primeres factures emeses, comparats a mà amb el càlcul esperat; aroma_comandes_creades_total per confirmar que es continuen completant compres. |
| Retorn enrere | Recommutar el balancejador a blau. I un pla per a les factures emeses a la finestra: identificar-les per marca de temps i reemetre-les si escau. Això és el veritablement difícil, i cal tenir-ho escrit abans de desplegar. |
| Extra | Desplegar a la franja de menys activitat. Un canvi amb implicacions fiscals no es desplega un divendres a les 18:00. |
B) Nova ordenació per defecte del catàleg → canary.
| Aspecte | Decisió |
|---|---|
| Estratègia | Canary, començant amb un 5 % |
| Per què | És un canvi de producte, no tècnic: no hi ha una resposta «correcta» a verificar, sinó una hipòtesi a mesurar. El canary permet comparar el comportament real dels usuaris entre totes dues versions, que és exactament la pregunta. A més és reversible sense conseqüències: ningú no perd dades si l'ordenació canvia. |
| Què vigilar | Mètriques de negoci per damunt de les tècniques: taxa de conversió de visita a comanda, valor mitjà de la comanda, profunditat de paginació (si la gent pagina menys, l'ordenació encerta més). En allò tècnic, la latència p99 de GET /v1/cafes, perquè ordenar per valoració mitjana pot exigir un JOIN o una agregació que degradi la consulta; convé comprovar que hi ha índex (04-06). |
| Retorn enrere | Feature flag, no desplegament. BANDERA_ORDRE_PER_VALORACIO apagada retorna l'ordre anterior en un segon, sense desplegar res. És el cas de llibre per a una bandera: canvi de comportament visible, reversible i subjecte a mesura. |
| Extra | Si la comparació ha de durar setmanes, això deixa de ser un canary i passa a ser una prova A/B; convé un repartiment estable per client perquè un mateix usuari no vegi el catàleg reordenat a cada visita. |
C) Migració de SQLite a PostgreSQL → blue-green, amb una fase prèvia de doble escriptura.
| Aspecte | Decisió |
|---|---|
| Estratègia | Blue-green, precedida d'una migració de dades per fases. No és un desplegament: és un projecte. |
| Per què no rolling | En rolling coexistirien instàncies escrivint a SQLite i a PostgreSQL simultàniament. Les escriptures es repartirien entre dues bases que divergeixen, i reconciliar-les després és pràcticament impossible. |
| Fases | 1) Desplegar el codi nou escrivint a totes dues i llegint de SQLite. 2) Còpia inicial massiva i verificació que totes dues bases coincideixen, registre a registre. 3) Finestra breu de només lectura o de manteniment per copiar el delta final. 4) Commutar a lectura des de PostgreSQL (blue-green). 5) Vigilar dies. 6) Deixar d'escriure a SQLite. |
| Què vigilar | Latència p99 de tots els endpoints (el rendiment de les consultes canvia del tot amb un altre motor i un altre planificador); errors de restriccions d'integritat que SQLite tolerava i PostgreSQL no; saturació del pool de connexions, que a SQLite no existia i aquí és un límit real; i la coherència de les dades comparant recomptes i sumes entre totes dues bases. |
| Retorn enrere | Recommutar a SQLite, sempre que s'hi hagi continuat escrivint. Aquell és el motiu de la fase de doble escriptura, i el seu cost està justificat: sense ella, la migració és un viatge d'anada. |
| Extra | Gràcies al patró repositori de 03-05, el canvi de codi es limita a src/repositoris/ i a src/config/base-dades.js; la resta del projecte no se n'assabenta. És la millor demostració del valor d'aquella separació, i convé comprovar que les proves d'integració passen contra tots dos motors abans de començar. |
Conclusió
La taula de 05-04 és ara maquinària. L'API de la Botiga Aroma s'empaqueta en un Dockerfile multietapa on les eines de compilació es queden a la primera etapa, la imatge final porta només dependències de producció i s'executa com a usuari node, amb tini i forma exec perquè el SIGTERM arribi de debò a Node i s'executi l'aturada ordenada de 03-07, i amb un HEALTHCHECK sobre liveness —no readiness— per no reiniciar contenidors sans quan falli Redis. El .dockerignore manté fora el teu .env i el teu node_modules, i docker-compose.yml aixeca API, Redis, el mock de Prism i, després d'un perfil, PostgreSQL, amb condition: service_healthy perquè res no arrenqui abans d'hora.
.github/workflows/ci.yml executa les portes en l'ordre correcte: qualitat estàtica primer perquè falla en trenta segons, després el contracte amb swagger-cli validate, Spectral i oasdiff breaking en paral·lel amb les proves sobre Node 20 i 22 amb Redis com a servei i cobertura, npm audit --audit-level=high, i només aleshores la construcció i publicació d'una imatge etiquetada amb el SHA del commit i escanejada amb Trivy; després, migracions, desplegament a preproducció, espera activa a /salut/preparat, proves d'extrem a extrem i la col·lecció de Newman de 05-01. Les portes són inflexibles a propòsit, amb un únic escapament explícit i visible: l'etiqueta canvi-trencador al pull request.
I per damunt de les eines, tres idees que separen un equip que desplega tranquil d'un que desplega amb por. Les migracions retrocompatibles: durant un desplegament sense talls conviuen dues versions sobre una sola base de dades, així que un RENAME COLUMN és un DROP disfressat i el patró expandir → migrar → contraure no és burocràcia, és l'única cosa que preserva la possibilitat de revertir. Les estratègies de desplegament —recreate, rolling, blue-green i canary— amb /salut i /salut/preparat de 04-07 decidint quan entra el trànsit i un preStop que evita els 502 fantasma de cada desplegament. I el retorn enrere: revertir codi és trivial quan les imatges són immutables i la configuració viu fora; revertir dades no existeix, es preveu; i els feature flags separen desplegar d'activar, amb data de caducitat escrita el mateix dia que es creen. Els artefactes nous del projecte són Dockerfile, .dockerignore, docker-compose.yml, docker-compose.proves.yml, .github/workflows/ci.yml, eines/desplegar.sh, eines/fum.sh i src/config/banderes.js.
Queda una última peça del mòdul, i és la que reordena bona part del que hem fet a mà. Molts dels mecanismes del mòdul 4 —el rate limiting amb Redis, la validació de tokens JWT i OAuth, la terminació TLS, CORS, la memòria cau, la compressió, l'encaminament per versió— existeixen resolts una capa per damunt de l'aplicació. A 05-06, API gateways i portals de desenvolupador, tanquem el mòdul amb aquella perspectiva: què és un gateway i quines funcions assumeix, comparades una a una amb les nostres implementacions; el criteri per decidir què es delega i què mai no es delega —l'autorització a nivell de recurs, la lògica de negoci i la validació semàntica es queden sempre a l'aplicació—; una configuració declarativa real de Kong per a la Botiga Aroma amb els límits de 04-04 i la llista blanca de 04-05, i quins middlewares de src/app.js podríem retirar aleshores i quins no; els riscos del gateway com a punt únic de fallada; i els portals de desenvolupador, on l'openapi.yaml de 05-02 es converteix en la porta d'entrada de tercers com CataBox, amb el «temps fins a la primera crida amb èxit» com a mètrica de la qualitat de la teva API.
Curs de REST API: Principis de Disseny i Desenvolupament d'APIs RESTful
Mòdul 1: Introducció a les APIs RESTful
- Què és una API?
- Història i evolució de les APIs
- Fonaments d'HTTP per a APIs
- Principis bàsics de REST
- Model de maduresa de Richardson i HATEOAS
- REST vs. SOAP
- REST davant de GraphQL, gRPC i webhooks
Mòdul 2: Disseny d'APIs RESTful
- Principis de disseny d'APIs RESTful
- Recursos i URIs
- Mètodes HTTP
- Codis d'estat HTTP
- Representacions, capçaleres i negociació de contingut
- Filtratge, ordenació, paginació i cerca
- Versionat d'APIs
- Documentació d'APIs
Mòdul 3: Desenvolupament d'APIs RESTful
- Configuració de l'entorn de desenvolupament
- Creació d'un servidor bàsic
- Gestió de peticions i respostes
- Validació de dades d'entrada
- Persistència i capa d'accés a dades
- Autenticació i autorització
- Gestió d'errors
- Proves i validació
Mòdul 4: Bones Pràctiques i Seguretat
- Bones pràctiques en el disseny d'APIs
- Seguretat en APIs RESTful
- OAuth 2.0 i OpenID Connect a la pràctica
- Rate limiting i throttling
- CORS i polítiques de seguretat
- Memòria cau HTTP i rendiment
- Observabilitat: logs, mètriques i traces
Mòdul 5: Eines i Frameworks
- Postman per a proves d'APIs
- Swagger i OpenAPI per a documentació
- Frameworks populars per a APIs RESTful
- Contractes, mocks i proves automatitzades d'API
- Integració contínua i desplegament
- API gateways i portals de desenvolupador
