La imatge aurora-api:2.0.0 és correcta, però la construeixes tu, al teu portàtil, amb la teva memòria cau i el teu git status. Aquesta lliçó treu l'humà del mig: cada git push dispara una cadena que executa les proves, construeix per a dues arquitectures, escaneja, signa, publica i desplega, sense que ningú s'hagi de recordar de res.

Contingut

  1. CI, delivery i deployment: tres conceptes, dues sigles
  2. Per què Docker hi encaixa: l'artefacte és la imatge
  3. Anatomia del pipeline
  4. Proves dins de contenidors amb Compose
  5. L'etapa proves del Dockerfile i --target
  6. El pipeline d'Aurora Libros: disparadors i permisos
  7. Etiquetatge automàtic amb metadata-action
  8. Construcció multiarquitectura amb memòria cau gha
  9. Escaneig amb Trivy que trenca la build
  10. Signatura keyless, SBOM i procedència
  11. El pipeline equivalent a GitLab CI
  12. Docker-in-Docker davant del socket muntat
  13. Gestió de credencials: OIDC i tokens efímers
  14. Versionatge automàtic des de les etiquetes de Git
  15. El desplegament automatitzat

Advertència. Un runner de CI amb accés al teu registre d'imatges i als teus servidors és una peça d'infraestructura crítica: qui controli el pipeline pot publicar i desplegar qualsevol cosa. Els permisos dels runners, l'abast dels tokens i l'accés SSH als entorns s'han de definir i revisar amb el responsable d'infraestructura i de seguretat de la teva organització.

  1. CI, delivery i deployment: tres conceptes, dues sigles

Concepte Què automatitza On acaba Intervenció humana
Integració contínua (CI) Compilar, analitzar i provar cada canvi Un artefacte validat Cap
Lliurament continu (continuous delivery) Tot l'anterior + deixar l'artefacte a punt per desplegar El registre d'imatges, amb la imatge publicada Algú aprova el desplegament
Desplegament continu (continuous deployment) Tot l'anterior + desplegar automàticament Producció Cap

Les dues últimes comparteixen l'abreviatura CD i es confonen constantment. La diferència pràctica és un botó: en delivery, la imatge està a punt i espera un aprovo; en deployment, cada merge a main que passi totes les portes arriba a producció tot sol. Aurora Libros farà delivery cap a producció i deployment cap a staging: és la combinació més habitual i la més assenyada mentre la cobertura de proves no doni per confiar-hi a cegues.

  1. Per què Docker hi encaixa: l'artefacte és la imatge

Abans dels contenidors, l'artefacte de CI era un .jar, un .zip o un tarball, i l'entorn on corria es preparava a part: d'aquí venia el "a la meva màquina funciona", perquè l'artefacte viatjava i l'entorn no. Amb Docker, l'artefacte inclou el seu entorn, i això desbloqueja tres propietats que fan fiable un pipeline:

  • Immutabilitat. El digest identifica un contingut exacte: sha256:a1b2... és la mateixa cosa a CI, a staging i a producció, per sempre.
  • Promoció sense reconstruir. Es promociona reetiquetant. Reconstruir per a producció vol dir desplegar un artefacte que ningú no ha provat.
  • Paritat d'entorns. El contenidor que va passar les proves és, byte a byte, el que atén els clients.

La regla que resumeix el mòdul: construir un cop, desplegar moltes. Un commit produeix una imatge, aquesta imatge té un digest, i aquest digest és el que es promociona a staging i a producció. Si el teu pipeline té un docker build per entorn, té una errada de disseny.

  1. Anatomia del pipeline

flowchart TD
    A[checkout] --> B[lint]
    B --> C[proves en contenidor]
    C --> D[build multiarquitectura]
    D --> E[escaneig de CVE]
    E --> F[signatura + SBOM]
    F --> G[publicació al registre d'imatges]
    G --> H{branca?}
    H -->|main| I[deploy staging]
    H -->|etiqueta v*| J[deploy producció]
Fase Què pot fallar Quant triga Trenca la build?
Checkout + lint Submòduls, estil, console.log oblidat ~25 s
Proves Regressió, flaky per dependències 1-3 min
Build Fallada de compilació, memòria cau freda 40 s - 4 min
Escaneig CVE crítica en una dependència ~30 s Sí (crítiques)
Signatura / SBOM Permisos OIDC mal configurats ~15 s
Publicació Credencials, quota del registre d'imatges ~30 s
Desplegament Xarxa, sondes que no passen 1-5 min Sí, amb rollback

L'ordre no és arbitrari: el que és barat i el que falla més, primer. Un lint de 20 segons que detecta l'error evita gastar quatre minuts de build multiarquitectura. És el mateix principi de la memòria cau de capes de 02-02, aplicat al pipeline.

  1. Proves dins de contenidors amb Compose

Provar contra una base de dades real, i no contra un mock, és el que distingeix una prova útil d'una que aprova codi trencat. Amb Compose és trivial i, sobretot, idèntic al teu portàtil i al runner.

# compose.proves.yaml — pila efímera, sense volums: es mor i no deixa rastre
services:
  proves:
    build: { context: ./api, target: proves }   # l'etapa del Dockerfile de 06-01
    environment:
      DB_HOST: db-proves
      DB_USER: aurora
      DB_PASSWORD: prova-ficticia
      DB_NAME: aurora_llibres_test
      REDIS_HOST: cache-proves
    depends_on:
      db-proves:    { condition: service_healthy }
      cache-proves: { condition: service_healthy }

  db-proves:
    image: postgres:16-alpine
    environment: { POSTGRES_USER: aurora, POSTGRES_PASSWORD: prova-ficticia, POSTGRES_DB: aurora_llibres_test }
    volumes: ["./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro"]
    tmpfs: ["/var/lib/postgresql/data"]     # dades a la RAM: més ràpid i efímer de debò
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U aurora -d aurora_llibres_test"]
      interval: 2s
      retries: 15

  cache-proves:
    image: redis:7-alpine
    healthcheck: { test: ["CMD", "redis-cli", "ping"], interval: 2s, retries: 15 }
docker compose -f compose.proves.yaml up \
  --build --abort-on-container-exit --exit-code-from proves
docker compose -f compose.proves.yaml down -v

Les dues banderes són el cor de la qüestió i convé entendre-les bé:

  • --abort-on-container-exit atura tota la pila tan bon punt un contenidor acaba. Sense ella, les proves s'acaben i PostgreSQL continua viu: la comanda no retorna mai i el job es queda penjat fins al timeout.
  • --exit-code-from proves fa que el codi de sortida de docker compose sigui el del contenidor de proves, no el de l'operació de Compose. Sense ella, la comanda retorna 0 encara que les proves fallin, i el pipeline es posa verd amb el codi trencat. Aquest és, de bon tros, l'error més freqüent d'aquesta lliçó.

El tmpfs sobre el directori de dades de PostgreSQL mereix una nota: en proves no necessites durabilitat, així que posar les dades a la RAM elimina l'escriptura a disc i sol reduir la suite entre un 30 % i un 50 %. Mai, mai de la vida, en producció.

  1. L'etapa proves del Dockerfile i --target

El Dockerfile de 06-01 ja tenia l'etapa preparada. Executar-la sola és un --target:

docker build --target proves -t aurora-api:proves api/
Enfocament Avantatge Inconvenient
Etapa proves amb --target Mateix Dockerfile, mateixes capes a la memòria cau Necessita els serveis a part
compose.proves.yaml Pila completa amb BD i memòria cau reals Un fitxer més per mantenir
Sense contenidor, al runner Rapidíssim El runner deixa de ser reproduïble

Aurora Libros fa servir les dues primeres alhora: compose.proves.yaml construeix amb target: proves. I hi ha un benefici de memòria cau gens menor: l'etapa deps que instal·la node_modules és comuna a les proves i a la imatge final, així que la build de producció reutilitza aquestes capes i no torna a instal·lar res.

  1. El pipeline d'Aurora Libros: disparadors i permisos

# .github/workflows/ci.yaml
name: CI/CD Aurora Libros

on:
  push:
    branches: [main]
    tags: ["v*.*.*"]        # v2.0.0 dispara la publicació de release
  pull_request:
    branches: [main]

env:
  REGISTRE: ghcr.io
  IMATGE: ${{ github.repository_owner }}/aurora-api

# Un push nou sobre la mateixa branca cancel·la el pipeline anterior: estalvia minuts i quota
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  proves:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - name: Lint i proves contra PostgreSQL i Redis reals
        run: |
          docker compose -f compose.proves.yaml up \
            --build --abort-on-container-exit --exit-code-from proves
      - name: Netejar la pila
        if: always()          # també si les proves han fallat
        run: docker compose -f compose.proves.yaml down -v

Sobre els disparadors: els pull requests executen les proves i construeixen sense publicar, perquè una branca de ningú no ha de poder pujar una imatge al registre d'imatges. Els push a main publiquen amb l'etiqueta edge i despleguen a staging. Les etiquetes v*.*.* produeixen la versió SemVer publicable.

  1. Etiquetatge automàtic amb metadata-action

  publicar:
    needs: proves                                   # no es construeix si les proves han fallat
    runs-on: ubuntu-24.04
    permissions:
      contents: read
      packages: write        # publicar a ghcr.io
      id-token: write        # OIDC per a la signatura keyless de Cosign
      security-events: write # pujar l'informe de Trivy a la pestanya Security
    outputs:
      digest: ${{ steps.build.outputs.digest }}
    steps:
      - uses: actions/checkout@v4

      - uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRE }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}     # token efímer, no una contrasenya

      - id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRE }}/${{ env.IMATGE }}
          tags: |
            type=semver,pattern={{version}}         # v2.0.0 -> 2.0.0
            type=semver,pattern={{major}}.{{minor}} # v2.0.0 -> 2.0
            type=semver,pattern={{major}}           # v2.0.0 -> 2
            type=ref,event=branch                   # main   -> main
            type=sha,prefix=sha-,format=short       # sempre -> sha-a1b2c3d
            type=raw,value=latest,enable={{is_default_branch}}
          labels: |
            org.opencontainers.image.title=aurora-api
            org.opencontainers.image.vendor=Aurora Libros S.L.

Aquesta acció substitueix el guió de sed i git describe que tothom acaba escrivint. De l'etiqueta v2.0.0 en dedueix tota sola les quatre etiquetes mòbils i genera a més les etiquetes OCI de 06-01 amb la data, el commit i la URL del repositori, sense que hagis de passar-les com a --build-arg.

El type=sha és el més important de la llista tot i que sembli el més avorrit: cada build produeix una etiqueta única i irrepetible, així que sempre pots referir-te a una construcció concreta encara que latest i 2.0 s'hagin mogut deu vegades.

  1. Construcció multiarquitectura amb memòria cau gha

      - uses: docker/setup-qemu-action@v3            # emulació per a arm64 (05-05)
      - uses: docker/setup-buildx-action@v3          # builder amb driver docker-container

      - id: build
        uses: docker/build-push-action@v5
        with:
          context: ./api
          target: runtime
          platforms: linux/amd64,linux/arm64
          push: ${{ github.event_name != 'pull_request' }}   # els PR construeixen, no publiquen
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha,scope=aurora-api
          cache-to: type=gha,scope=aurora-api,mode=max
          provenance: mode=max                       # attestation de procedència SLSA
          sbom: true                                 # SBOM adjunta a l'índex de la imatge
          build-args: |
            REVISION=${{ github.sha }}

Els dos paràmetres de memòria cau són la diferència entre un pipeline usable i un d'insuportable. Sense ells, cada execució arrenca amb una memòria cau buida —el runner és una màquina nova— i npm ci s'executa sencer dues vegades, una per arquitectura. Amb type=gha, BuildKit desa les capes a la memòria cau de GitHub Actions i les recupera a la build següent.

Configuració Build en fred Build amb memòria cau Amb canvi només a src/
Sense memòria cau 4 min 10 s 4 min 10 s 4 min 10 s
type=gha,mode=min 4 min 20 s 1 min 05 s 55 s
type=gha,mode=max 4 min 35 s 48 s 41 s

mode=max desa també les capes intermèdies de les etapes prèvies —incloses deps i deps-dev—, així que ocupa més memòria cau però encerta moltíssim més. L'scope evita que dos workflows diferents es trepitgin les entrades. I un avís operatiu: la memòria cau de GitHub Actions té un límit de 10 GB per repositori i expulsa per antiguitat, de manera que un mode=max amb moltes branques pot desallotjar entrades útils.

  1. Escaneig amb Trivy que trenca la build

      - name: Escaneig de vulnerabilitats
        uses: aquasecurity/[email protected]
        with:
          image-ref: ${{ env.REGISTRE }}/${{ env.IMATGE }}@${{ steps.build.outputs.digest }}
          format: sarif
          output: trivy.sarif
          severity: CRITICAL,HIGH
          ignore-unfixed: true       # sense pedaç disponible, trencar la build no arregla res
          exit-code: "0"             # aquest pas només informa...

      - uses: github/codeql-action/upload-sarif@v3
        with: { sarif_file: trivy.sarif }

      - name: "Porta de qualitat: cap crítica corregible"
        uses: aquasecurity/[email protected]
        with:
          image-ref: ${{ env.REGISTRE }}/${{ env.IMATGE }}@${{ steps.build.outputs.digest }}
          severity: CRITICAL
          ignore-unfixed: true
          exit-code: "1"             # ...i aquest trenca la build

L'escaneig es fa per digest, no per etiqueta: garanteix que analitzes exactament la imatge que acabes de construir i no una altra que algú hagi publicat amb el mateix nom mentrestant.

L'estructura en dos passos és deliberada. El primer ho recull tot (crítiques i altes) i ho puja a la pestanya de seguretat per tenir-ne visibilitat; el segon, molt més estricte, és la porta: només trenca davant de crítiques amb pedaç disponible. Aquest ignore-unfixed no és relaxació, és pragmatisme: bloquejar els desplegaments per una CVE que ningú no ha corregit encara no millora la teva seguretat, només impedeix que publiquis la correcció d'una altra fallada. Quin és el llindar acceptable per a la teva organització ho decideix la seva política de seguretat, no aquest curs.

  1. Signatura keyless, SBOM i procedència

      - uses: sigstore/cosign-installer@v3

      - name: Signar la imatge (keyless, sense gestionar claus)
        run: |
          cosign sign --yes \
            ${{ env.REGISTRE }}/${{ env.IMATGE }}@${{ steps.build.outputs.digest }}
        env:
          COSIGN_EXPERIMENTAL: "1"

La signatura keyless de 05-03 hi encaixa com un guant. No hi ha cap clau privada per guardar, rotar ni filtrar: Cosign demana un token OIDC de vida curta a GitHub, obté un certificat efímer de Fulcio i registra la signatura al log públic de transparència Rekor. El certificat caduca en minuts; el que queda és la prova, verificable per qualsevol:

cosign verify ghcr.io/auroralibros/aurora-api:2.0.0 \
  --certificate-identity-regexp '^https://github.com/auroralibros/aurora-libros/.github/workflows/ci.yaml@' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

Fixa't en què verifica aquesta identitat: no diu "està signada", diu "la va construir aquell workflow, d'aquell repositori". Una imatge signada des del portàtil d'algú no passa aquesta comprovació. Juntament amb el provenance: mode=max i el sbom: true del pas anterior, tens les tres peces de la cadena de subministrament: què porta a dins (SBOM), qui i com la va construir (procedència) i que ningú no l'ha tocada des de llavors (signatura).

  1. El pipeline equivalent a GitLab CI

# .gitlab-ci.yml
stages: [proves, build, deploy]

variables:
  IMATGE: $CI_REGISTRY_IMAGE/aurora-api

.docker: &docker                       # àncora YAML reutilitzada pels dos jobs
  image: docker:27-cli
  services: ["docker:27-dind"]         # Docker-in-Docker com a servei del job
  variables: { DOCKER_TLS_CERTDIR: "/certs" }

proves:
  <<: *docker
  stage: proves
  script:
    - docker compose -f compose.proves.yaml up --abort-on-container-exit --exit-code-from proves

build:
  <<: *docker
  stage: build
  before_script:
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
    - docker buildx create --use --driver docker-container
  script:
    - |
      docker buildx build --platform linux/amd64,linux/arm64 \
        --cache-from type=registry,ref=$IMATGE:cache \
        --cache-to   type=registry,ref=$IMATGE:cache,mode=max \
        --tag $IMATGE:$CI_COMMIT_REF_SLUG --tag $IMATGE:sha-$CI_COMMIT_SHORT_SHA --push ./api
  rules: [{ if: '$CI_COMMIT_BRANCH == "main"' }, { if: $CI_COMMIT_TAG }]
Concepte GitHub Actions GitLab CI
Unitat executable job amb steps job amb script
Agrupació / ordre needs: entre jobs stages: seqüencials
Fitxer .github/workflows/*.yaml .gitlab-ci.yml
Reutilització uses: (accions del marketplace) include:, extends:, àncores YAML
Contenidor del job container: (opcional) image: (habitual)
Memòria cau de build type=gha type=registry
Registre d'imatges integrat ghcr.io $CI_REGISTRY_IMAGE
Condicions if: / on: rules: / only:
Secrets Repository secrets CI/CD variables (protegides/emmascarades)
Identitat sense contrasenya OIDC natiu OIDC (id_tokens:)

La memòria cau és la diferència pràctica més visible: GitHub té un backend propi, mentre que a GitLab l'habitual és desar la memòria cau al mateix registre d'imatges amb type=registry, que a més funciona amb qualsevol proveïdor.

  1. Docker-in-Docker davant del socket muntat

Per construir imatges, el runner necessita accés a un daemon. Hi ha tres maneres, i no són equivalents en seguretat.

Opció Com Risc Rendiment
DinD (docker:dind) Daemon niat dins del job Requereix --privileged: escapada a l'amfitrió si alguna cosa falla Memòria cau freda cada vegada
Socket muntat -v /var/run/docker.sock:... Grup docker = root a l'amfitrió (05-03) Memòria cau calenta compartida
BuildKit sense daemon buildkitd rootless o Kaniko El menor: sense privilegis Bo, amb memòria cau remota

Les dues primeres files diuen el mateix amb paraules diferents: qualsevol job pot prendre el control del runner. Amb DinD, perquè el contenidor privilegiat té totes les capacitats. Amb el socket muntat, perquè qui parla amb el daemon pot llançar docker run -v /:/host --privileged i llegir o modificar el disc sencer de l'amfitrió, incloses les credencials dels altres pipelines.

En un repositori privat amb col·laboradors de confiança, qualsevol de les dues és la pràctica comuna. Tan bon punt acceptis pull requests de tercers, el socket muntat és inacceptable: un PR maliciós que modifiqui el workflow s'endú els teus secrets. Les alternatives són runners efímers d'un sol ús (el que fan els runners allotjats de GitHub) o construir sense daemon amb BuildKit rootless.

  1. Gestió de credencials: OIDC i tokens efímers

Pràctica Per què A Aurora Libros
Mai al repositori L'historial de Git és per sempre Tot en secrets del proveïdor
Token efímer, no contrasenya Caduca sol; robar-lo serveix de poc GITHUB_TOKEN per execució
Abast mínim Un token de publicació no desplega permissions: per job
OIDC en comptes de claus No hi ha secret que rotar ni filtrar id-token: write per a Cosign
Rotació i auditoria Detectar l'ús indegut Registre d'accessos del registre d'imatges

El GITHUB_TOKEN no és un secret que hagis creat tu: GitHub el genera en començar l'execució, amb exactament els permisos del bloc permissions:, i l'invalida en acabar. Robat mitja hora després, no val res.

La regla que no es trenca mai és la de no imprimir cap secret. Els proveïdors emmascaren els valors coneguts i mostren ***, però l'emmascarament se salta amb facilitat: echo $CLAU | base64 surt en clar, i un set -x en un script bash imprimeix cada comanda amb els seus arguments. Per això les credencials es passen sempre per stdin (--password-stdin) i mai com a argument de línia de comandes, que a més queda visible a la llista de processos del runner.

  1. Versionatge automàtic des de les etiquetes de Git

git tag -a v2.0.0 -m "Sondes separades, aturada ordenada i validacio de configuracio"
git push origin v2.0.0     # això és, a la pràctica, el botó de publicar

metadata-action tradueix aquesta etiqueta a les quatre de la imatge sense intervenció:

Etiqueta de Git Etiquetes de la imatge Es mou
v2.0.0 2.0.0 Mai
v2.0.0 2.0 A cada pedaç
v2.0.0 2 A cada versió menor
v2.0.0 latest A cada versió
(qualsevol build) sha-a1b2c3d Mai

Les dues files que no es mouen mai són les que serveixen per a producció; les mòbils són còmodes per a desenvolupament i perilloses en desplegaments. El compose.prod.yaml d'Aurora Libros fixa el digest, amb la qual cosa ni tan sols depèn que 2.0.0 continuï apuntant al mateix.

  1. El desplegament automatitzat

  desplegar-staging:
    needs: publicar
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-24.04
    environment: staging          # permet exigir aprovació i restringir secrets
    steps:
      - uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.STAGING_HOST }}
          username: desplegament
          key: ${{ secrets.STAGING_SSH_KEY }}
          script: |
            set -euo pipefail
            cd /opt/aurora-libros
            export AURORA_API_DIGEST="${{ needs.publicar.outputs.digest }}"
            docker compose -f compose.prod.yaml pull
            docker compose -f compose.prod.yaml up -d --wait --wait-timeout 120
            docker compose -f compose.prod.yaml ps --format '{{.Name}} {{.Status}}'

Quatre detalls que fan que això sigui un desplegament i no una ruleta. El --wait (que ja feies servir a l'onboarding) espera que els healthcheck passin i retorna error si no ho fan, així que un desplegament trencat posa el job en vermell en comptes de deixar-te un servei caigut en silenci. El set -euo pipefail talla a la primera línia que falli. L'usuari desplegament no és root i només pot tocar aquell directori. I es desplega per digest, no per etiqueta: l'up -d arrenca exactament la imatge que acaba de passar les proves.

Tot i així, això és un desplegament d'una sola màquina: durant uns segons el servei es recrea i no hi ha ningú servint. A partir de 06-03 el pas final deixa de ser un docker compose up remot i passa a ser una instrucció a l'orquestrador, que substitueix les rèpliques d'una en una sense tallar el servei.

Errors Habituals i Consells

  • Oblidar --exit-code-from. El pipeline es posa verd amb les proves en vermell, cosa que és pitjor que no tenir proves: dona confiança falsa. Comprova-ho expressament trencant una prova.
  • Reconstruir la imatge al job de desplegament. Trenca la traçabilitat i desplega una cosa que ningú no ha provat. Es promociona per digest.
  • Construir sense memòria cau remota. Un pipeline de quatre minuts per canvi fa que la gent deixi de fer push petits, i això empitjora tota la resta.
  • Escanejar per etiqueta en comptes de per digest. Analitzes una imatge que pot no ser la teva. Fes servir sempre imatge@sha256:....
  • exit-code: 1 amb totes les severitats. El pipeline es trenca cada matí per una CVE baixa sense pedaç, i la gent aprèn a saltar-se la porta. Sigues estricte on importa.
  • Publicar des de pull requests. Qualsevol que obri un PR pot pujar una imatge al teu registre d'imatges: condiciona push: a l'esdeveniment. I mai no passis secrets com a arguments, que apareixen a ps dins del runner i a molts logs; sempre --password-stdin.
  • Consell: fes el pipeline reproduïble en local. Si docker compose -f compose.proves.yaml up és el mateix que executa CI, depurar una fallada del pipeline no requereix vint commits de prova.
  • Consell: fes servir concurrency amb cancel-in-progress. Deu pushes seguits no han de llançar deu builds multiarquitectura; només importa l'últim.

Exercicis

Exercici 1. Demostra el perill de --exit-code-from: trenca deliberadament una prova d'aurora-api i executa la pila de proves amb i sense aquesta bandera, comparant els codis de sortida. Explica què hauria fet el pipeline en cada cas.

Exercici 2. Mesura l'efecte de la memòria cau remota: executa el pipeline tres vegades (fred, amb memòria cau i amb memòria cau després d'un canvi només a src/) i construeix la taula de temps. Explica per què el tercer cas és el més ràpid.

Exercici 3. Verifica la cadena de subministrament completa d'una imatge publicada: comprova la signatura exigint que provingui del teu workflow, extreu-ne l'SBOM i localitza el commit exacte que la va generar.

Solucions

Solució 1.

# Es trenca una asserció expressament a api/test/llibres.test.js
docker compose -f compose.proves.yaml up --build --abort-on-container-exit
echo "sense --exit-code-from: $?"
docker compose -f compose.proves.yaml up --build --abort-on-container-exit --exit-code-from proves
echo "amb --exit-code-from: $?"
proves-1  | FAIL  test/llibres.test.js > retorna els 9 titols del cataleg
proves-1  | AssertionError: expected 9 to equal 8
proves-1 exited with code 1
sense --exit-code-from: 0
amb --exit-code-from: 1

Els dos codis de sortida, davant d'exactament la mateixa prova fallida, resumeixen l'exercici:

Execució Codi Què faria CI
Sense --exit-code-from 0 Continua: construeix, signa i publica el codi trencat
Amb --exit-code-from proves 1 S'atura al job de proves; no es publica res

El que té de pervers el primer cas és que la fallada sí que apareix al log, amb el seu AssertionError i tot, però ningú no la llegeix: la marca verda diu que tot va bé. Sense la bandera, docker compose up informa de si ell va poder aixecar la pila —i va poder—, no de si el contenidor de proves va acabar satisfet.

D'aquí en surt una pràctica que val la pena adoptar: la primera vegada que muntes un pipeline, trenca'l expressament. Un pipeline que no has vist mai fallar no és un pipeline verd, és un pipeline sense comprovar.

Solució 2.

gh run list --workflow ci.yaml --limit 3 \
  --json displayTitle,conclusion,createdAt,updatedAt \
  --jq '.[] | "\(.displayTitle): \((.updatedAt|fromdate) - (.createdAt|fromdate))s"'
fix: missatge d error mes clar (nomes src/):  41s
chore: pujar versio de pino (package.json):  108s
ci: activar cache gha (primera execucio):    275s
Execució Què va canviar Temps del job de build Capes reutilitzades
1a (freda) Tot 4 min 35 s 0
2a package.json 1 min 48 s Base i sistema
3a Només src/ 41 s Base, sistema i npm ci

El tercer cas és el més ràpid per la mateixa raó que vas estudiar a 02-02, ara aplicada a una màquina diferent cada vegada. El Dockerfile de 06-01 copia primer package.json i package-lock.json, executa npm ci, i només després copia src/. Si únicament canvia el codi font, la capa de npm ci continua sent vàlida i BuildKit la porta des de la memòria cau de GitHub en comptes de tornar a instal·lar 180 paquets.

El que fa possible que un runner nou aprofiti la feina de l'anterior és cache-from: type=gha: el runner és efímer, però la memòria cau no. Sense ella, les tres columnes de la taula donarien 4 min 35 s.

Un matís important per no enganyar-te amb el número: els 41 segons són de dues arquitectures. La construcció arm64 corre sota emulació QEMU i és unes tres vegades més lenta que la nativa; sense memòria cau, aquest sol fet hi afegiria més de dos minuts.

Solució 3.

IMG=ghcr.io/auroralibros/aurora-api
DIG=$(docker buildx imagetools inspect $IMG:2.0.0 --format '{{.Manifest.Digest}}')

cosign verify $IMG@$DIG \
  --certificate-identity-regexp '^https://github.com/auroralibros/aurora-libros/.github/workflows/ci.yaml@' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com | jq '.[0].optional'
{ "Issuer": "https://token.actions.githubusercontent.com",
  "Subject": "https://github.com/auroralibros/aurora-libros/.github/workflows/ci.yaml@refs/tags/v2.0.0",
  "githubWorkflowSha": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0",
  "Bundle": { "Payload": { "logIndex": 148392017 } } }
cosign download sbom $IMG@$DIG 2>/dev/null | jq -r '.packages[] | "\(.name) \(.versionInfo)"' | head -3
docker buildx imagetools inspect $IMG@$DIG \
  --format '{{index .Image.Config.Labels "org.opencontainers.image.revision"}}'
# express 4.21.2
# pg 8.13.1
# ioredis 5.4.1
# a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0

Les comprovacions responen a preguntes diferents, i juntes tanquen el cercle:

Pregunta Mecanisme Evidència
L'ha tocada algú? Signatura Cosign Verificació correcta contra Rekor
Qui la va construir? Identitat del certificat El workflow ci.yaml a refs/tags/v2.0.0
Què porta a dins? SBOM 182 paquets amb nom i versió
De quin codi surt? Etiqueta OCI revision El commit a1b2c3d…

La dada decisiva és el Subject, i convé aturar-s'hi: no diu "algú va signar aquesta imatge", diu quin workflow, de quin repositori i des de quina referència de Git la va construir. Si un atacant aconseguís publicar una imatge al teu registre d'imatges amb l'etiqueta 2.0.0, la signatura no verificaria contra aquesta identitat i el desplegament l'hauria de rebutjar. Aquesta comprovació és la que s'automatitza al clúster amb una política d'admissió.

I la parella SBOM + revision és el que converteix una alerta de seguretat en feina de deu minuts: quan es publiqui la propera CVE crítica d'una llibreria, cosign download sbom et diu en segons si la teva imatge la inclou i en quina versió, i l'etiqueta revision et porta al commit exacte sobre el qual aplicar la correcció.

Conclusió

El camí del commit a la imatge publicada ja no el recorres tu. Distingeixes la integració contínua del lliurament i del desplegament continus —tres conceptes i dues sigles— i saps per què Aurora Libros fa deployment a staging i delivery a producció. Has interioritzat la regla que sosté tot el mòdul: construir un cop i desplegar moltes, perquè l'artefacte és la imatge i el seu digest és el mateix objecte a tots els entorns; un docker build per entorn és una errada de disseny, no una comoditat.

Les proves corren contra un PostgreSQL i un Redis reals en una pila efímera amb les dades a tmpfs, i saps que --abort-on-container-exit sense --exit-code-from produeix el pitjor resultat possible: un pipeline verd sobre proves vermelles, que has provocat expressament per veure-ho. El workflow complet de GitHub Actions encadena lint i proves, metadata-action traduint v2.0.0 a quatre etiquetes més el sha- irrepetible, build-push-action construint per a amd64 i arm64 amb cache-to: type=gha,mode=max —que va portar la build de 4 min 35 s a 41 s quan només canvia src/—, Trivy informant de tot i trencant només davant de crítiques amb pedaç disponible, i la signatura keyless de Cosign que no obliga a custodiar cap clau privada. Has verificat la cadena sencera des de fora: signatura vàlida, Subject que anomena el workflow i l'etiqueta de Git que la va produir, SBOM amb les versions de cada paquet i l'etiqueta OCI revision amb el commit exacte.

Coneixes també l'equivalent a GitLab CI amb la seva taula de correspondències, el compromís real entre Docker-in-Docker i el socket muntat —on totes dues opcions signifiquen, dit sense embuts, que un job pot prendre el control del runner—, i les regles de credencials: tokens efímers amb permisos per job, OIDC en lloc de contrasenyes estàtiques i res de secrets per línia de comandes. El pas final, de moment, és un docker compose pull && up -d --wait remot per SSH desplegant per digest.

I aquí hi ha el límit. Aquest desplegament té un buit de segons en què no serveix ningú, corre en una sola màquina i, si aquesta màquina s'apaga, Aurora Libros desapareix d'internet. A la lliçó següent, Orquestrant Contenidors amb Docker Swarm, muntaràs el teu primer clúster: diversos nodes amb managers i workers, xarxes overlay que connecten contenidors de màquines diferents, serveis que es reprogramen sols quan un node cau, i el compose.yaml que ja coneixes desplegat com a stack amb docker stack deploy.

Docker: De Principiant a Avançat

Mòdul 1: Introducció a Docker

Mòdul 2: Treballant amb Imatges Docker

Mòdul 3: Contenidors Docker

Mòdul 4: Docker Compose

Mòdul 5: Conceptes Avançats de Docker

Mòdul 6: Docker en Producció

Mòdul 7: Ecosistema i Eines de Docker

© Copyright 2026. Tots els drets reservats