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
- CI, delivery i deployment: tres conceptes, dues sigles
- Per què Docker hi encaixa: l'artefacte és la imatge
- Anatomia del pipeline
- Proves dins de contenidors amb Compose
- L'etapa
provesdel Dockerfile i--target - El pipeline d'Aurora Libros: disparadors i permisos
- Etiquetatge automàtic amb
metadata-action - Construcció multiarquitectura amb memòria cau
gha - Escaneig amb Trivy que trenca la build
- Signatura keyless, SBOM i procedència
- El pipeline equivalent a GitLab CI
- Docker-in-Docker davant del socket muntat
- Gestió de credencials: OIDC i tokens efímers
- Versionatge automàtic des de les etiquetes de Git
- 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ó.
- 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.
- 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, astagingi 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.
- 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 | Sí |
| Proves | Regressió, flaky per dependències | 1-3 min | Sí |
| Build | Fallada de compilació, memòria cau freda | 40 s - 4 min | Sí |
| Escaneig | CVE crítica en una dependència | ~30 s | Sí (crítiques) |
| Signatura / SBOM | Permisos OIDC mal configurats | ~15 s | Sí |
| Publicació | Credencials, quota del registre d'imatges | ~30 s | 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.
- 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 -vLes dues banderes són el cor de la qüestió i convé entendre-les bé:
--abort-on-container-exitatura 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 provesfa que el codi de sortida dedocker composesigui el del contenidor de proves, no el de l'operació de Compose. Sense ella, la comanda retorna0encara 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ó.
- L'etapa
proves del Dockerfile i --target
proves del Dockerfile i --targetEl Dockerfile de 06-01 ja tenia l'etapa preparada. Executar-la sola és un --target:
| 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.
- 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 -vSobre 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.
- Etiquetatge automàtic amb
metadata-action
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.
- Construcció multiarquitectura amb memòria cau
gha
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.
- 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 buildL'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.
- 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.comFixa'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).
- 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.
- 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.
- 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.
- 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 publicarmetadata-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.
- 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: 1amb 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 apsdins 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
concurrencyambcancel-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: 1Els 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
# a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0Les 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
- Què és Docker?
- Instal·lant Docker
- Arquitectura de Docker
- Comandes Bàsiques de Docker
- Entenent les Imatges de Docker
- Creant el teu Primer Contenidor Docker
- El Projecte del Curs: la Plataforma Aurora Libros
Mòdul 2: Treballant amb Imatges Docker
- Docker Hub i Repositoris
- Construint Imatges Docker
- Conceptes Bàsics de Dockerfile
- Instruccions Avançades del Dockerfile
- Gestionant Imatges Docker
- Etiquetatge i Publicació d'Imatges
Mòdul 3: Contenidors Docker
- Executant Contenidors
- Cicle de Vida del Contenidor
- Gestionant Contenidors
- Inspecció i Depuració de Contenidors
- Xarxes a Docker
- Persistència de Dades amb Volums
- Límits de Recursos i Polítiques de Reinici
Mòdul 4: Docker Compose
- Introducció a Docker Compose
- Definint Serveis a Docker Compose
- Comandes de Docker Compose
- Aplicacions Multi-Contenidor
- Variables d'Entorn a Docker Compose
- Perfils, Overrides i Múltiples Entorns
- Desenvolupament Local amb Docker Compose
Mòdul 5: Conceptes Avançats de Docker
- Aprofundiment en Xarxes Docker
- Opcions d'Emmagatzematge Docker
- Millors Pràctiques de Seguretat a Docker
- Optimitzant Imatges Docker
- Builds Avançades amb BuildKit i Buildx
- Registre i Monitoratge a Docker
- El Runtime per Dins: Namespaces, Cgroups i Capes
Mòdul 6: Docker en Producció
- Preparar una Imatge per a Producció
- CI/CD amb Docker
- Orquestrant Contenidors amb Docker Swarm
- Introducció a Kubernetes
- Desplegant Contenidors Docker a Kubernetes
- Escalat i Balanceig de Càrrega
- Estratègies de Desplegament i Rollback
