Jenkins resolia el problema de "vull automatitzar qualsevol cosa" i deixava tota la resta —repositori, registre, entorns, seguretat— en mans de plugins o d'altres productes. GitLab parteix de la posició contrària: un sol producte que conté el repositori, la revisió de codi, el CI/CD, el registre de contenidors, els entorns, l'anàlisi de seguretat, les incidències i la wiki, amb tot connectat de fàbrica. Aquesta aposta arquitectònica —el que en el seu màrqueting anomenen DevOps platform i aquí anomenarem simplement plataforma integrada— és el que cal entendre per avaluar-la bé, perquè explica tant les seves virtuts més grans com els seus costos. A més, GitLab CI és històricament important: el seu .gitlab-ci.yml (2015) va ser dels primers a normalitzar el pipeline as code integrat al repositori, i el seu model de stages amb jobs a dins és el que molts enginyers porten al cap quan pensen "pipeline". En aquesta lliçó veurem aquest model, la seva evolució d'etapes fixes a graf amb needs, el pipeline de Reservalia traduït per segona vegada, la distinció entre cache i artifacts que més confusió genera, els runners i els seus executors, els entorns i les review apps, els mecanismes de reutilització, i què costa operar una instància pròpia.

Contingut

  1. La plataforma integrada: què és i què implica
  2. El model d'execució: stages, jobs i el pas al graf
  3. El pipeline de Reservalia en .gitlab-ci.yml
  4. cache davant d'artifacts: la distinció que més confon
  5. Paral·lelització: parallel i parallel: matrix
  6. Runners i executors
  7. Variables, secrets i protecció
  8. Entorns, desplegament manual i review apps
  9. Reutilització: extends, include, components i pipelines pare-fill
  10. El registre de contenidors integrat
  11. Auto DevOps, amb criteri
  12. SaaS davant d'autoallotjat, i el cost real
  13. Quan GitLab CI/CD és l'elecció correcta
  14. Errors Comuns i Consells
  15. Exercicis
  16. Conclusió

  1. La plataforma integrada: què és i què implica

En una organització típica amb GitHub, la cadena d'eines es compon: GitHub per al codi, GitHub Actions per al CI, ECR o Docker Hub per al registre, Snyk o Dependabot per a les dependències, Jira per a les incidències, alguna cosa a part per als entorns. A GitLab, tot això són pestanyes del mateix projecte.

El que es guanya, i no és retòrica:

  • Zero integracions per mantenir entre peces. No hi ha tokens creuats entre el CI i el registre, ni webhooks que es trenquen, ni "el bot de Jira ha perdut permisos". El job de CI empeny al registre del mateix projecte amb una variable que ja existeix.
  • Traçabilitat d'extrem a extrem. De la incidència al merge request, al pipeline, a l'artefacte, a l'entorn on està desplegat. La pregunta "quina versió hi ha en producció i quines incidències inclou?" té resposta a la interfície sense construir res.
  • Un sol model de permisos. Qui pot fusionar, pot desplegar a staging però no a producció, i això es configura una sola vegada.
  • Funcionalitats que només existeixen perquè les peces estan juntes: els informes de seguretat que apareixen dins del merge request comparant la branca amb main, els entorns que mostren quin commit està desplegat, les review apps amb el seu enllaç al MR.

El que es paga, amb la mateixa franquesa:

  • Acoblament fort. Sortir de GitLab significa sortir de tot alhora: repositori, CI, registre i historial d'entorns. És el cost de canvi més alt del mòdul, i la 06-07 el quantifica.
  • Les funcionalitats interessants estan esglaonades per nivell de subscripció. Bona part del que s'explica en articles i conferències —aprovacions múltiples, algunes anàlisis de seguretat, certs controls de compliment— no és al nivell gratuït. No convé dissenyar una arquitectura sobre una funcionalitat i descobrir després en quin nivell viu.
  • Si te l'autoallotges, operes una plataforma sencera, no un servidor de CI: base de dades, emmagatzematge d'objectes, Redis, Gitaly, el registre, els runners. Apartat 12.

Una nota de vocabulari útil: a GitLab, merge request (MR) és el que a GitHub és pull request. El concepte és el mateix que la 02-07 va descriure.

  1. El model d'execució: stages, jobs i el pas al graf

El model original de GitLab CI és d'etapes seqüencials amb jobs paral·lels a dins:

flowchart LR
    subgraph S1["stage: preparar"]
      A["preparar"]
    end
    subgraph S2["stage: verificar"]
      B["qualitat"]
      C["test 1/4"]
      D["test 2/4"]
      E["test 3/4"]
      F["test 4/4"]
    end
    subgraph S3["stage: construir"]
      G["build"]
      H["seguretat"]
    end
    subgraph S4["stage: publicar"]
      I["publicar"]
    end
    S1 --> S2 --> S3 --> S4

La regla clàssica: tots els jobs d'una etapa s'executen en paral·lel, i l'etapa següent no comença fins que l'anterior acaba sencera. És un model fàcil de raonar i amb un defecte clar: si test 4/4 triga vuit minuts i els altres dos, build espera vuit minuts encara que només depengués de preparar. És exactament el problema de la barrera d'etapa que la 04-01 va descriure.

La correcció va arribar amb needs, que converteix el pipeline en un DAG: un job amb needs arrenca tan bon punt acaben les seves dependències concretes, sense esperar la seva etapa. Amb needs, les stages passen de ser barreres a ser sobretot agrupació visual.

Model per stages Model per needs (DAG)
Arrencada d'un job Quan acaba tota l'etapa anterior Quan acaben les seves dependències
Facilitat de lectura Molt alta Mitjana: cal seguir el graf
Temps total Suma dels màxims per etapa Camí crític real
Risc Esperes innecessàries Grafs enredats si ningú els revisa

Vocabulari de GitLab traduït:

GitLab Equivalent al curs
Pipeline Execució de workflow
Stage Etapa (agrupació)
Job Job
Script (una línia de script:) Step
Runner Runner / agent
Executor Com materialitza el runner l'entorn (shell, docker, kubernetes)
Artifacts Artefactes entre jobs i descarregables
Environment Entorn de desplegament amb historial

  1. El pipeline de Reservalia en .gitlab-ci.yml

Segona traducció del mateix pipeline: instal·lar amb memòria cau → lint i test en paral·lel → build d'imatge → publicar per digest.

# .gitlab-ci.yml — Reservalia · CI
stages: [preparar, verificar, construir, publicar, desplegar]   # 1

default:                                                        # 2
  image: node:22-bookworm
  interruptible: true                                           # cancel·la si arriba un push nou
  retry:
    max: 2
    when: [runner_system_failure, stuck_or_timeout_failure]     # 3 · reintentar només fallades d'infraestructura

variables:
  NPM_CONFIG_CACHE: "$CI_PROJECT_DIR/.npm"                      # 4 · memòria cau dins de l'espai de treball
  IMATGE: "$CI_REGISTRY_IMAGE/api"
  FF_USE_FASTZIP: "true"

workflow:                                                       # 5
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - when: never                                               # res més dispara pipeline

# ---------------------------------------------------------------- plantilles
.node:                                                          # 6 · plantilla reutilitzable
  cache:
    key:
      files: [package-lock.json]                                # 7 · clau per lockfile
    paths: [.npm/]
    policy: pull                                                # només llegeix; el job "preparar" l'escriu
  before_script:
    - npm ci --prefer-offline --no-audit

# ---------------------------------------------------------------- jobs
preparar:
  stage: preparar
  extends: .node
  cache:
    key:
      files: [package-lock.json]
    paths: [.npm/]
    policy: pull-push                                           # 8 · aquest sí que actualitza la memòria cau
  script:
    - echo "Dependències instal·lades i memòria cau poblada"

qualitat:
  stage: verificar
  extends: .node
  needs: [preparar]                                             # 9 · DAG
  script:
    - npx prettier --check .
    - npm run lint
    - npm run typecheck

test:
  stage: verificar
  extends: .node
  needs: [preparar]
  parallel: 4                                                   # 10 · sharding
  services:                                                     # 11 · PostgreSQL com a servei
    - name: postgres:16-alpine
      alias: db
  variables:
    POSTGRES_DB: reservalia_test
    POSTGRES_PASSWORD: test
    DATABASE_URL: "postgres://postgres:test@db:5432/reservalia_test"
  script:
    - npm run migrate
    - npm test -- --shard=$((CI_NODE_INDEX))/$CI_NODE_TOTAL
  artifacts:                                                    # 12
    when: always
    expire_in: 1 week
    reports:
      junit: informes/junit-*.xml
      coverage_report:
        coverage_format: cobertura
        path: cobertura/cobertura.xml

build-web:
  stage: construir
  extends: .node
  needs: [preparar]
  script:
    - npm run build --workspace apps/web
  artifacts:
    paths: [apps/web/dist/]                                     # 13 · això sí que és un artefacte
    expire_in: 1 day

build-api:
  stage: construir
  needs: [preparar]
  image: docker:27
  services: [docker:27-dind]                                    # 14 · Docker-in-Docker
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
    - |
      docker buildx build \
        --file apps/api/Dockerfile \
        --cache-from type=registry,ref=$IMATGE:cache \
        --cache-to   type=registry,ref=$IMATGE:cache,mode=max \
        --tag $IMATGE:$CI_COMMIT_SHA \
        --push .
    - docker buildx imagetools inspect $IMATGE:$CI_COMMIT_SHA
        --format '{{.Manifest.Digest}}' > digest.env.tmp
    - echo "DIGEST=$(cat digest.env.tmp)" > build.env
  artifacts:
    reports:
      dotenv: build.env                                         # 15 · outputs entre jobs

seguretat:
  stage: construir
  extends: .node
  needs: [preparar]
  script:
    - npm audit --audit-level=high
    - gitleaks detect --no-git --exit-code 1
  allow_failure: false

publicar:
  stage: publicar
  needs: [qualitat, test, build-api, build-web, seguretat]       # 16 · fan-in
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH                # només a la branca per defecte
  image: docker:27
  services: [docker:27-dind]
  script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
    - docker buildx imagetools create --tag $IMATGE:estable $IMATGE@$DIGEST   # promoció per digest

desplegar-staging:
  stage: desplegar
  needs: [publicar]
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  environment:                                                   # 17
    name: staging
    url: https://staging.reservalia.example
  script:
    - ./scripts/desplegar.sh staging "$IMATGE@$DIGEST"

desplegar-produccio:
  stage: desplegar
  needs: [desplegar-staging]
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: manual                                               # 18 · aprovació
      allow_failure: false
  environment:
    name: produccio
    url: https://app.reservalia.example
  script:
    - ./scripts/desplegar.sh produccio "$IMATGE@$DIGEST"
  1. stages declara l'ordre. Un job sense stage cau a test per defecte, cosa que sorprèn; declara'l sempre.
  2. default aplica a tots els jobs el que abans es repetia. interruptible: true amb l'opció d'autocancel·lació del projecte és el cancel-in-progress de la 04-04.
  3. retry selectiu. Reintentar qualsevol fallada emmascara les proves flaky de la 02-04 i converteix el pipeline en un generador de verd fals; reintentar només fallades d'infraestructura del runner és legítim. Aquesta distinció és la diferència entre resiliència i autoengany.
  4. La memòria cau ha de ser dins de $CI_PROJECT_DIR: GitLab només pot posar en memòria cau rutes de l'espai de treball. Per això es redirigeix la memòria cau d'npm allà en comptes de deixar-la a ~/.npm. És l'error número u de memòria cau a GitLab.
  5. workflow:rules decideix si es crea pipeline. Sense això, un push a una branca amb MR obert genera dos pipelines (un de branca i un altre de MR): duplica la despesa i confon el senyal.
  6. Una plantilla és un job que comença per punt: GitLab no l'executa i serveix per a extends.
  7. cache:key:files genera la clau a partir del hash del lockfile: canvia el lockfile, canvia la memòria cau. És el hashFiles de la 04-02 amb un altre nom.
  8. policy és la peça que gairebé ningú fa servir i que estalvia molt de temps: pull només descarrega, pull-push descarrega i puja al final. Amb sis jobs pujant la mateixa memòria cau idèntica es malgasten minuts; aquí només l'escriu preparar.
  9. needs converteix el pipeline en graf. Amb needs: [] un job arrenca immediatament, ignorant la seva etapa.
  10. parallel: 4 crea quatre instàncies del job amb CI_NODE_INDEX (1..N) i CI_NODE_TOTAL. És el sharding de la 02-04; el repartiment per temps històrics que veuràs a la 06-03 no ve de sèrie.
  11. services aixeca contenidors auxiliars accessibles pel seu alias, igual que el PostgreSQL de la 02-02.
  12. artifacts:reports són artefactes amb semàntica: GitLab els interpreta i els mostra al MR (resultats de test, cobertura, troballes de seguretat). when: always és imprescindible: els informes de test importen sobretot quan el job falla.
  13. Distinció de fons, que desenvolupa l'apartat 4: dist/ és un artefacte —sortida del pipeline, es passa a altres jobs—; .npm/ és memòria cau —accelera, i perdre-la no trenca res—.
  14. Docker-in-Docker és el patró habitual per construir imatges a GitLab, i té implicacions de seguretat que la 06-05 desenvolupa: el servei dind requereix runners privilegiats.
  15. artifacts:reports:dotenv és el mecanisme d'outputs entre jobs: les variables del fitxer queden disponibles als jobs que depenen d'aquest. És l'equivalent del needs.<job>.outputs de GitHub Actions.
  16. El fan-in de la 04-01: publicar espera les cinc senyals.
  17. environment registra el desplegament: GitLab desa quin commit hi ha a staging, des de quan, i ofereix el botó de tornar a desplegar una versió anterior.
  18. when: manual amb allow_failure: false és la porta de la 03-01: el pipeline s'atura esperant que una persona premi el botó, i qui el pot prémer ho determina la protecció de l'entorn.

Comparat amb el ci.yml original, la traducció és gairebé un a un. Les diferències reals: el sharding és un número en comptes d'una matriu, els outputs entre jobs passen per un fitxer dotenv en comptes de per outputs, la memòria cau exigeix configurar rutes i política a mà, i el registre de contenidors i els entorns són part del mateix producte en lloc de serveis externs amb credencials pròpies.

  1. cache davant d'artifacts: la distinció que més confon

Tots dos desen fitxers i tots dos els restauren en un altre job. No són el mateix i confondre'ls produeix pipelines lents o incorrectes.

cache artifacts
Propòsit Accelerar (dependències, compilacions intermèdies) Transportar resultats entre jobs i fer-los descarregables
Si desapareix El job és més lent, però funciona El pipeline falla o el resultat es perd
On es desa Al runner (o en emmagatzematge compartit, si es configura) Sempre al servidor de GitLab
Abast Compartida entre pipelines i branques, segons key De l'execució concreta del pipeline
Es recupera Per key, best-effort, sense garanties Automàticament des dels jobs que el necessiten
Caducitat Política del runner expire_in, explícit
Contingut típic .npm/, ~/.gradle, vendor/ dist/, .war, informes JUnit, SBOM

Regla mental que resol el 100 % dels casos: si esborrar això trenca el pipeline, és artefacte; si només el fa més lent, és memòria cau.

Dos mecanismes relacionats:

# Descarregar NOMÉS els artefactes que aquest job necessita
publicar:
  needs:
    - job: build-web
      artifacts: true          # descarrega dist/
    - job: seguretat
      artifacts: false         # només la dependència d'ordre, sense transferir fitxers

Sense això, un job descarrega per defecte els artefactes de tots els jobs d'etapes anteriors, i en un pipeline gran això són centenars de megabytes moguts sense raó: una de les causes de pipeline lent més freqüents i menys diagnosticades. dependencies: [] és la manera antiga de dir "no em descarreguis res".

I expire_in no és opcional: els artefactes consumeixen emmagatzematge facturable en SaaS i disc en autoallotjat. La política de retenció de la 02-06 aquí és una línia per job.

  1. Paral·lelització: parallel i parallel: matrix

# Sharding simple: N còpies idèntiques que es reparteixen per índex
test:
  parallel: 4
  script: [ "npm test -- --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL" ]

# Matriu: combinacions de variables, com el matrix de la 02-04
test-compatibilitat:
  parallel:
    matrix:
      - NODE: ["20", "22"]
        POSTGRES: ["15", "16"]      # → 4 jobs
      - NODE: ["22"]                 # els blocs addicionals se sumen
        POSTGRES: ["17"]
        EXPERIMENTAL: "true"
  image: node:$NODE
  services: [ "postgres:$POSTGRES-alpine" ]
  script: [ "npm test" ]

Diferències amb el matrix de GitHub Actions que convé tenir clares: no existeix exclude —es modela afegint blocs en comptes de restar—, i no hi ha fail-fast global: el control és per job amb allow_failure. A canvi, parallel: matrix sí que permet variar image i services, cosa que dóna força joc.

  1. Runners i executors

Un runner és el procés que executa jobs. Es registra contra una instància i se li assignen jobs segons els seus tags.

Tipus de runner Abast Quan
Compartit (shared) Tota la instància El cas normal en SaaS; minuts consumibles
De grup Tots els projectes d'un grup Runners propis compartits per un departament
Específic de projecte Un projecte Maquinari especial, accés a xarxes concretes

I l'executor determina com materialitza l'entorn:

Executor Com executa Aïllament Quan fer-lo servir
shell Ordres a la màquina del runner Cap Gairebé mai; hereta el problema de l'agent contaminat de la 06-01
docker Un contenidor per job, des d'image: Bo El cas normal
docker+machine Crea una VM per job i la destrueix Molt bo Autoescalat al núvol
kubernetes Un Pod per job Molt bo Si ja tens clúster
ssh, custom Màquines remotes, integracions pròpies Variable Maquinari rar
# config.toml d'un runner autoallotjat amb executor docker
concurrent = 8                                  # jobs simultanis en aquesta màquina
check_interval = 3

[[runners]]
  name = "runner-reservalia-1"
  url = "https://gitlab.example.com/"
  token = "glrt-..."                            # token de registre
  executor = "docker"

  [runners.docker]
    image = "node:22-bookworm"                  # imatge per defecte si el job no la indica
    privileged = false                          # true NOMÉS si cal dind (vegeu 06-05)
    volumes = ["/cache", "/certs/client"]
    memory = "4g"
    cpus = "2"

  [runners.cache]                               # memòria cau compartida entre runners
    Type = "s3"
    Shared = true
    [runners.cache.s3]
      ServerAddress = "s3.eu-west-1.amazonaws.com"
      BucketName = "reservalia-ci-cache"
      AuthenticationType = "iam"                # rol d'instància, sense claus de llarga vida

Tres punts operatius amb conseqüències:

  • privileged = true per a Docker-in-Docker és un forat real: un job amb accés al dimoni privilegiat pot escapar del contenidor i comprometre la màquina, inclosos els secrets d'altres jobs que hi corrin. Alternatives sense privilegis a la 06-05 (Kaniko, Buildah, BuildKit rootless).
  • La memòria cau amb emmagatzematge compartit canvia el rendiment del tot. Amb memòria cau local al runner i diversos runners, cadascun té la seva còpia i la taxa d'encert s'esfondra. Amb S3 compartit, tots comparteixen.
  • concurrent és la palanca de cost: dimensionar-lo malament deixa jobs en cua (cosa que es percep com "el CI és lent", encara que el temps d'execució sigui el mateix) o malbarata màquines.

  1. Variables, secrets i protecció

GitLab té un sol mecanisme —variables de CI/CD— amb atributs que canvien el seu comportament:

Atribut Efecte Quan activar-lo
Protected Només s'exposa en pipelines de branques i tags protegits Sempre, per a qualsevol credencial de staging o producció
Masked El seu valor se substitueix per [MASKED] als logs Sempre, per a secrets
File Es materialitza com a fitxer i la variable conté la ruta Kubeconfig, claus, certificats
Environment scope Valor diferent per entorn (produccio, staging, *) Configuració per entorn de la 03-02
Expanded S'interpolen altres variables a dins Desactivar en secrets amb $

Dos advertiments importants. Masked té restriccions de format —longitud mínima, sense espais ni certs caràcters—; un secret que no compleix simplement no s'emmascara, i GitLab ho diu en un avís que és fàcil passar per alt. I l'emmascarament té la mateixa limitació que a Jenkins: cobreix la coincidència literal, no la transformada. La conclusió de la 04-03 no canvia per eina.

Protected és la protecció de debò, i la que més s'oblida. Sense ella, qualsevol que obri un MR des d'una branca arbitrària pot escriure un .gitlab-ci.yml que imprimeixi les credencials de producció. Amb la variable marcada com a protegida, aquesta variable no existeix en pipelines de branques no protegides. Regla: tota credencial de desplegament, protegida; sense excepcions.

Per a identitat federada, GitLab emet tokens OIDC per job amb id_tokens, exactament el patró sense claus de llarga vida de la 03-02:

desplegar-produccio:
  id_tokens:
    AWS_TOKEN:
      aud: https://gitlab.example.com          # audiència que espera el proveïdor d'identitat
  script:
    - >
      export $(aws sts assume-role-with-web-identity
      --role-arn "$AWS_ROLE_ARN"
      --role-session-name "gitlab-$CI_JOB_ID"
      --web-identity-token "$AWS_TOKEN"
      --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]'
      --output text | awk '{print "AWS_ACCESS_KEY_ID="$1"\nAWS_SECRET_ACCESS_KEY="$2"\nAWS_SESSION_TOKEN="$3}')
    - ./scripts/desplegar.sh produccio

La condició de confiança a AWS es defineix sobre atributs del token (projecte, branca, entorn), de manera que només un pipeline de main desplegant a produccio pot assumir el rol. És el mateix disseny de la 03-02 amb un altre emissor.

  1. Entorns, desplegament manual i review apps

environment: és una de les millors peces de GitLab i no té un equivalent exacte de sèrie a totes les eines: converteix un job en un desplegament registrat, amb historial de quin commit està desplegat, enllaç a la URL, i botons per tornar a desplegar o revertir a un desplegament anterior.

review:
  stage: desplegar
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
  environment:
    name: review/$CI_COMMIT_REF_SLUG              # 1 · un entorn per branca
    url: https://$CI_COMMIT_REF_SLUG.review.reservalia.example
    on_stop: parar-review                         # 2
    auto_stop_in: 3 days                          # 3
  script:
    - ./scripts/desplegar-review.sh "$CI_COMMIT_REF_SLUG" "$IMATGE@$DIGEST"

parar-review:
  stage: desplegar
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      when: manual
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    action: stop                                   # 4
  variables:
    GIT_STRATEGY: none                             # no cal el codi per destruir
  script:
    - ./scripts/destruir-review.sh "$CI_COMMIT_REF_SLUG"
  1. Un entorn dinàmic per branca: són les review apps, l'equivalent directe de les previsualitzacions per PR de la 02-07. GitLab publica l'enllaç dins del merge request, així que la Nuria pot provar el canvi sense baixar-se la branca.
  2. on_stop enllaça el job que destrueix l'entorn; s'executa automàticament en tancar o fusionar el MR.
  3. auto_stop_in és la línia que evita la factura sorpresa: sense ella, cada branca abandonada deixa un entorn corrent indefinidament. És el problema que la 05-01 assenyalava amb les previsualitzacions òrfenes, aquí resolt amb una línia.
  4. action: stop marca el job com el que apaga l'entorn.

Sobre aprovacions: when: manual atura el job fins que algú el llança, i qui ho pot fer es controla protegint l'entorn (Deployments → Protected environments), que és l'equivalent dels revisors d'Environments de la 03-02. Als nivells de pagament existeixen aprovacions múltiples i separació de funcions —qui aprova no pot ser qui va escriure el canvi—, que és el que solen exigir les auditories.

  1. Reutilització: extends, include, components i pipelines pare-fill

GitLab ofereix més mecanismes de reutilització que cap altra eina del mòdul, i convé saber quin toca.

# a) Anchors YAML: substitució textual, sense coneixement de GitLab
.base: &base
  image: node:22
  retry: 1
qualitat:
  <<: *base
  script: [ "npm run lint" ]

# b) extends: el mateix però conscient de l'estructura (fusiona mapes en profunditat)
.node:
  image: node:22
  cache: { key: { files: [package-lock.json] }, paths: [.npm/] }
qualitat:
  extends: .node                    # preferible als anchors: fusiona bé i admet herència múltiple
  script: [ "npm run lint" ]

# c) include: portar YAML de fora
include:
  - local: '/ci/plantilles/node.yml'                              # mateix repositori
  - project: 'reservalia/plantilles-ci'                           # un altre projecte de la instància
    ref: 'v3.2.0'                                                 # ancorat a tag, no a main!
    file: '/plantilles/build-publicar.yml'
  - remote: 'https://exemple.com/plantilla.yml'                   # URL: sense control de versió → evitar
  - template: 'Security/SAST.gitlab-ci.yml'                       # plantilla oficial de GitLab
  - component: gitlab.com/reservalia/components/[email protected]       # component amb inputs tipats
    inputs:
      dockerfile: apps/api/Dockerfile
      publicar: true

extends davant d'anchors: els anchors són YAML pur, es resolen abans que GitLab entengui res i no funcionen entre fitxers inclosos; extends fusiona mapes en profunditat, admet cadenes i sí que travessa include. Fes servir extends.

Els components (CI/CD components) són l'evolució més recent i la més semblant al que la 04-05 defensava: unitats amb inputs tipats i versió pròpia, publicades en un catàleg, en comptes de YAML inclòs a cegues. Un include: template no valida res; un component falla en analitzar el fitxer si li passes un booleà on espera una cadena.

I la regla de la 04-03 aplica igual aquí: include d'un altre projecte ancorat a main significa que un commit aliè canvia el teu pipeline sense que tu hagis fusionat res. Ancora a tag o a SHA.

Pipelines pare-fill per a monorepos, que és el problema d'execució selectiva de la 04-04:

# .gitlab-ci.yml del pare
generar-fills:
  stage: preparar
  script:
    - node scripts/generar-pipeline.js > fill.yml     # decideix què construir segons què ha canviat
  artifacts: { paths: [fill.yml] }

executar-api:
  stage: verificar
  rules:
    - changes: [ "apps/api/**/*", "packages/compartit/**/*" ]   # només si ha canviat això
  trigger:
    include:
      - artifact: fill.yml                             # pipeline generat dinàmicament
        job: generar-fills
    strategy: depend                                   # el pare espera i reflecteix el resultat

I trigger:project per disparar el pipeline d'un altre projecte —el cas multiprojecte de la 05-03—, amb strategy: depend si el pare ha d'esperar el fill.

Mecanisme Què reutilitza Quan
Anchors YAML Fragments, mateix fitxer Gairebé mai: fes servir extends
extends Configuració de job Repetició dins d'un projecte
include: local Fitxers del repositori Partir un .gitlab-ci.yml gran
include: project Plantilles entre projectes Estandarditzar a l'organització
Component Unitat versionada amb inputs El recomanable avui per a plantilles compartides
Pipeline fill Un pipeline sencer Monorepo, pipelines dinàmics
trigger: project Pipeline d'un altre projecte Multiprojecte, microserveis

  1. El registre de contenidors integrat

Cada projecte de GitLab porta el seu propi registre de contenidors, i això elimina el tràmit de credencials que al ci.yml de Reservalia ocupa un job sencer:

publicar:
  image: docker:27
  services: [docker:27-dind]
  script:
    # Aquestes tres variables existeixen sense configurar-les: les injecta GitLab per job
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
    - docker build -t "$CI_REGISTRY_IMAGE/api:$CI_COMMIT_SHA" -f apps/api/Dockerfile .
    - docker push "$CI_REGISTRY_IMAGE/api:$CI_COMMIT_SHA"

CI_REGISTRY_PASSWORD és un token efímer amb permís només sobre el registre d'aquell projecte i amb la vida del job. És mínim privilegi (04-03) sense haver configurat res, i és un bon exemple del que regala la integració.

El registre inclou polítiques de neteja per edat i per patró d'etiqueta, que és la retenció de la 02-06 com a formulari. Sense elles, l'emmagatzematge creix sense fre: un pipeline que publica per commit genera desenes d'imatges al dia. Configura-les el primer dia i conserva sempre el que està referenciat per un entorn actiu, perquè esborrar la imatge que està desplegada converteix el rollback per digest de la 03-05 en impossible.

També hi ha registre de paquets (npm, Maven, PyPI…) i registre de dependències proxy, que resol el cas dels registres privats de la 04-02 sense muntar Nexus o Artifactory a part.

  1. Auto DevOps, amb criteri

Auto DevOps és un pipeline complet preconfigurat: detecta el llenguatge, construeix amb buildpacks, executa tests, anàlisi SAST, dependències, llicències, contenidor, DAST, i desplega a Kubernetes amb revisió, staging i producció amb canari. S'activa amb una casella.

Valoració honesta: és una demostració excel·lent i una base de producció discutible. A favor: per a un projecte nou i estàndard, en deu minuts tens un pipeline amb més portes de qualitat que moltes empreses després d'un any. En contra: el pipeline és una caixa negra gran que cal entendre per modificar, assumeix Kubernetes amb una topologia concreta, la construcció per buildpacks produeix imatges que no controles —i la 06-05 explicarà per què voldries controlar-les—, i tan bon punt necessites alguna cosa particular acabes sobreescrivint tantes variables que hauria sortit més barat escriure el pipeline.

Ús recomanat: com a catàleg d'idees i com a punt de partida a desactivar aviat. Activa'l, mira quins jobs genera, copia els que et serveixin al teu propi .gitlab-ci.yml i desactiva'l. El que sí que val la pena adoptar per separat són les plantilles de seguretat (include: template: Security/...), que són peces soltes, comprensibles i ajustables.

  1. SaaS davant d'autoallotjat, i el cost real

GitLab.com (SaaS) Autoallotjat
Qui opera GitLab Tu
Model de cost Per usuari i mes, amb minuts de còmput inclosos i consumibles addicionals Llicència per usuari (segons nivell) més infraestructura més persones
Actualitzacions Contínues Teves, amb finestra i assaig
Dades Fora de la teva xarxa On tu decideixis
Runners Compartits, o els teus Els teus
Punt de fallada El proveïdor La teva instància

Autoallotjar GitLab no és autoallotjar un servidor de CI: és operar una plataforma amb PostgreSQL, Redis, Gitaly (el servei de Git), emmagatzematge d'objectes, el registre i els runners. Les tasques recurrents reals: actualitzacions —GitLab publica a ritme alt i saltar-se versions intermèdies no sempre està suportat—, còpies de seguretat amb restauració assajada, creixement de l'emmagatzematge (els artefactes i el registre són els que es disparen), i dimensionament de runners. A la pràctica és com a mínim mitja persona a temps parcial en una organització mitjana, i no és un cost que desaparegui amb el temps.

Sobre les xifres: els preus per usuari, els minuts inclosos i quina funcionalitat és a quin nivell canvien amb freqüència i per regió. El que importa aquí és el model: pagues per usuari, no per projecte; el còmput es factura per minut amb multiplicadors segons el tipus de màquina; l'emmagatzematge d'artefactes i registre també compta. Consulta les tarifes vigents abans de decidir i, sobretot, verifica a quin nivell és la funcionalitat concreta sobre la qual vols construir abans de dissenyar res.

  1. Quan GitLab CI/CD és l'elecció correcta

Sí, amb força claredat, quan:

  • El teu codi ja és a GitLab. És el factor que decideix la majoria dels casos i la 06-07 el posa el primer per una raó: fer servir un altre CI contra un repositori de GitLab significa sincronitzar identitats, webhooks i permisos per obtenir menys.
  • Vols una plataforma en comptes d'una cadena d'eines, i valores no mantenir integracions ni conciliar permisos entre cinc productes.
  • Necessites autoallotjar-ho tot per normativa, i vols una experiència integrada que Jenkins no dóna. És probablement el millor punt del quadrant autoallotjat.
  • Els entorns i les review apps t'importen: el model d'environment amb historial, on_stop i auto_stop_in és madur i estalvia codi propi.
  • Treballes en monorepo i necessites pipelines dinàmics: pare-fill amb generació de YAML és una solució de primera per al problema de la 04-04.

No, o pensa-t'ho dues vegades, quan:

  • El teu codi és a GitHub. Emmirallar repositoris per fer servir GitLab CI és una font permanent de fricció.
  • El teu equip és petit i no vol operar res: SaaS ho resol, però el cost per usuari amb molts col·laboradors ocasionals pesa.
  • Depens de funcionalitat de nivells alts: comprova el nivell abans de dissenyar, no després.
  • Necessites molt macOS o maquinari exòtic: és viable amb runners propis, però l'ecosistema mòbil està més rodat en altres eines (06-03, 05-02).

Errors Comuns i Consells

Posar en memòria cau rutes fora de $CI_PROJECT_DIR. La memòria cau no es desa i no hi ha error visible, només lentitud. Redirigeix NPM_CONFIG_CACHE, GRADLE_USER_HOME o equivalents a l'espai de treball.

Tots els jobs amb policy: pull-push. Pugen una vegada i una altra la mateixa memòria cau. Només el job que la genera necessita escriure-la.

Pipelines duplicats als MR. Un push a una branca amb MR obert crea dos pipelines. workflow:rules ho arregla i estalvia la meitat del còmput.

Confondre cache amb artifacts. Posar dist/ en memòria cau produeix desplegaments amb un build d'una altra branca; posar node_modules a artifacts puja centenars de megabytes per job al servidor. La regla: si perdre-ho trenca el pipeline, és artefacte.

No posar expire_in. L'emmagatzematge creix fins que algú rep la factura o s'omple el disc.

Descarregar artefactes que no necessites. Per defecte arriben els de totes les etapes anteriors. Fes servir needs amb artifacts: false on només cal ordre.

Variables de desplegament sense marcar com a protegides. Qualsevol branca les pot llegir des d'un .gitlab-ci.yml modificat en un MR. És la via més directa d'exfiltració de credencials a GitLab.

Review apps sense on_stop ni auto_stop_in. Entorns orfes acumulant-se i facturant.

retry sense when. Reintentar-ho tot amaga les proves flaky i produeix verd fals (02-04).

include d'un altre projecte apuntant a main. Un canvi aliè modifica el teu pipeline sense revisió. Ancora a tag.

Runners privilegiats per costum. privileged = true només on de debò cal dind, i avalua alternatives sense dimoni (06-05).

Exercicis

Exercici 1. El pipeline de Reservalia a GitLab triga 19 minuts. Observes: l'etapa verificar acaba en 6 min però construir no comença fins al minut 9; cadascun dels sis jobs puja 400 MB de memòria cau; el job publicar descarrega 1,2 GB d'artefactes i només fa servir un fitxer digest; i un push a una branca amb MR obert llança dos pipelines. Escriu les correccions concretes i estima l'efecte de cadascuna.

Exercici 2. La Nuria vol review apps per a apps/web: un entorn per MR amb URL pròpia, comentari automàtic al MR, destrucció en tancar-lo i caducitat automàtica als tres dies. Escriu els jobs complets i explica què protegeix cada línia. Afegeix què fer amb els secrets, sabent que un MR pot venir d'una branca de qualsevol membre de l'equip.

Exercici 3. L'empresa que va comprar Gestor Citas 4 (05-04) té el seu codi a GitLab autoallotjat 14.x, amb pipelines que fan servir only/except, sense needs i amb un runner shell únic que ho executa tot a la màquina del servidor. Escriu el pla de modernització en fases, amb què es guanya a cadascuna i quins riscos hi ha.

Solucions

Solució 1.

Problema Causa Correcció Efecte estimat
construir espera 3 min Barrera d'etapa: espera que acabi el job més lent de verificar needs: [preparar] a build-web i build-api −3 min de camí crític
6 × 400 MB de pujada de memòria cau Tots amb policy: pull-push Només preparar amb pull-push; la resta pull −2 GB de trànsit; ~1 min per job
publicar descarrega 1,2 GB Descàrrega implícita d'artefactes d'etapes prèvies needs explícit amb artifacts: false llevat del dotenv del build −1 a 2 min
Pipelines duplicats Falta workflow:rules Regles de MR i branca per defecte, when: never a la resta −50 % de la despesa de còmput
workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - when: never

.node:
  cache:
    key: { files: [package-lock.json] }
    paths: [.npm/]
    policy: pull                      # només lectura

preparar:
  extends: .node
  cache:
    key: { files: [package-lock.json] }
    paths: [.npm/]
    policy: pull-push                 # l'únic que escriu
  script: [ "npm ci --prefer-offline" ]

build-api:
  needs: [preparar]                   # no espera l'etapa verificar
  # ...

publicar:
  needs:
    - job: build-api
      artifacts: true                 # només el dotenv amb el digest
    - job: build-web
      artifacts: true                 # dist/ sí que cal
    - job: qualitat
      artifacts: false                # només ordre
    - job: test
      artifacts: false
    - job: seguretat
      artifacts: false

Estimació total: de 19 min a al voltant de 12-13 al camí crític, i aproximadament la meitat del còmput facturat per l'arranjament dels pipelines duplicats. Aquest últim punt és el més rendible dels quatre i el que més es passa per alt, perquè no es manifesta com a lentitud sinó com a factura: és exactament la lliçó de la 04-04 que optimitzar comença per mesurar on se'n va el temps i els diners, no per endevinar.

Solució 2.

review:
  stage: desplegar
  needs: [build-web]
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      changes: [ "apps/web/**/*", "packages/compartit/**/*" ]   # només si afecta la web
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    url: https://$CI_COMMIT_REF_SLUG.review.reservalia.example
    on_stop: parar-review
    auto_stop_in: 3 days
  script:
    - aws s3 sync apps/web/dist/ "s3://reservalia-review/$CI_COMMIT_REF_SLUG/" --delete
    - |
      curl -sS --request POST \
        --header "PRIVATE-TOKEN: $TOKEN_BOT_MR" \
        --data-urlencode "body=Previsualització llesta: $CI_ENVIRONMENT_URL (commit $CI_COMMIT_SHORT_SHA)" \
        "$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes"

parar-review:
  stage: desplegar
  needs: []
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      when: manual
  environment: { name: review/$CI_COMMIT_REF_SLUG, action: stop }
  variables: { GIT_STRATEGY: none }
  script:
    - aws s3 rm "s3://reservalia-review/$CI_COMMIT_REF_SLUG/" --recursive

Què protegeix cada línia: changes evita aixecar entorns per canvis que no toquen la web; auto_stop_in acota el cost de les branques abandonades; on_stop garanteix neteja en tancar el MR; GIT_STRATEGY: none accelera la destrucció i redueix superfície; needs: [] fa que el job d'aturada no depengui de res i funcioni encara que el pipeline original hagués fallat.

Sobre secrets: l'entorn review/* ha de fer servir credencials pròpies i limitades —un rol que només pot escriure al prefix reservalia-review/ d'un bucket sense dades reals—, mai les de staging o producció. Les de producció van marcades com a protegides i el seu environment scope és produccio, així que ni existeixen en aquest job. TOKEN_BOT_MR és un token de projecte amb permís mínim de comentar. I la regla general que aplica igual que a la 04-03: un entorn de previsualització es construeix assumint que el seu contingut és públic, perquè la seva URL és endevinable i no porta autenticació real.

Solució 3. Quatre fases, cadascuna amb valor propi —l'enfocament de la 05-04—:

Fase 0, inventari i treure risc immediat (1-2 setmanes). El runner shell a la màquina del servidor és el problema urgent: qualsevol job executa ordres com a usuari del runner al mateix amfitrió que la instància, així que un .gitlab-ci.yml en un MR pot llegir la base de dades de GitLab. Es canvia primer, abans que cap millora de rendiment: runner nou amb executor docker en una màquina diferent, i el shell s'apaga. En paral·lel, inventari de variables sense marcar com a protegides —rotar les que hagin pogut ser exposades— i de projectes amb .gitlab-ci.yml.

Fase 1, actualitzar GitLab (2-4 setmanes de calendari). 14.x està molt endarrerit: cal pujar per les versions d'actualització obligatòries, no d'un salt, i assajar la restauració de la còpia en una instància de proves abans de tocar la real. Es guanya: seguretat, components de CI/CD, millores de rules i d'entorns. Risc principal: finestra d'indisponibilitat i migracions de base de dades llargues; es mitiga assajant sobre una còpia i anunciant la finestra.

Fase 2, only/exceptrules (1 setmana, projecte a projecte). only/except continua funcionant però no es pot combinar amb rules al mateix job i no expressa condicions compostes. La traducció és mecànica:

Antic Modern
only: [main] rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
only: [merge_requests] rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event"
only: changes: [src/**] rules: - changes: [src/**]
except: [tags] rules: - if: $CI_COMMIT_TAG when: never + regla positiva

Es fa un projecte de prova primer i es compara que el conjunt de pipelines disparats és el mateix abans i després, que és l'única verificació que de debò demostra l'equivalència.

Fase 3, needs i memòria cau compartida (1-2 setmanes). Afegir needs per passar d'etapes a graf, i configurar memòria cau a S3 compartida entre runners. Es guanya temps de pipeline. Va l'última a propòsit: és la fase més visible i la menys important, i posar-la abans que la fase 0 seria optimitzar la velocitat d'un sistema amb un forat de seguretat obert. Aquest ordre —risc, després mantenibilitat, després rendiment— és el mateix que governava els increments de la 05-04.

Conclusió

GitLab CI/CD és el que passa quan el CI es dissenya dins de la plataforma en comptes de al costat. El .gitlab-ci.yml va néixer sent pipeline as code, sense l'herència de la interfície que arrossega Jenkins; el model va evolucionar d'etapes seqüencials a graf amb needs sense trencar el que hi havia; i la integració regala coses que en altres cadenes costen feina: credencials de registre per job, informes de test i seguretat dins del merge request, entorns amb historial i rollback, i review apps amb caducitat en una línia. A canvi es paga acoblament —sortir és sortir de tot—, funcionalitat esglaonada per nivell de subscripció, i una operació considerable si te l'autoallotges.

Del que hem vist, tres coses s'enduen a qualsevol altra eina: la distinció memòria cau davant d'artefacte —si perdre-ho trenca el pipeline, és artefacte— és universal encara que el nom canviï; marcar com a protegides les credencials de desplegament és la defensa concreta contra el MR maliciós, i la seva absència és un forat real en moltes instal·lacions; i ancorar els include a una versió és la mateixa regla que les Shared Libraries de Jenkins i les accions fixades per SHA de la 04-03.

L'eina següent ataca una dimensió diferent. Allà on GitLab competeix per amplitud, CircleCI competeix per profunditat en una sola cosa: la velocitat del pipeline. Porta la memòria cau més explícita i controlable del mòdul, un sistema de reutilització empaquetat —els orbs— i, sobretot, el repartiment de proves per temps històrics, que és la resposta més madura al problema de sharding que arrosseguem des de la 02-04. Veurem el pipeline de Reservalia per tercera vegada i què es paga per aquesta velocitat.

Curs de CI/CD: Integració i Desplegament Continu

Mòdul 1: Introducció al CI/CD

Mòdul 2: Integració Contínua (CI)

Mòdul 3: Desplegament Continu (CD)

Mòdul 4: Pràctiques Avançades de CI/CD

Mòdul 5: Implementació de CI/CD en Projectes Reals

Mòdul 6: Eines i Tecnologies

Mòdul 7: Exercicis Pràctics

Mòdul 8: Recursos Addicionals

© Copyright 2026. Tots els drets reservats