Jenkins resolvía el problema de "quiero automatizar cualquier cosa" y dejaba todo lo demás —repositorio, registro, entornos, seguridad— en manos de plugins o de otros productos. GitLab parte de la posición contraria: un solo producto que contiene el repositorio, la revisión de código, el CI/CD, el registro de contenedores, los entornos, el análisis de seguridad, los issues y la wiki, con todo conectado de fábrica. Esa apuesta arquitectónica —lo que en su marketing llaman DevOps platform y aquí llamaremos simplemente plataforma integrada— es lo que hay que entender para evaluarla bien, porque explica tanto sus mayores virtudes como sus costes. Además, GitLab CI es históricamente importante: su .gitlab-ci.yml (2015) fue de los primeros en normalizar el pipeline as code integrado en el repositorio, y su modelo de stages con jobs dentro es el que muchos ingenieros llevan en la cabeza cuando piensan "pipeline". En esta lección veremos ese modelo, su evolución de etapas fijas a grafo con needs, el pipeline de Reservalia traducido por segunda vez, la distinción entre cache y artifacts que más confusión genera, los runners y sus executors, los entornos y review apps, los mecanismos de reutilización, y qué cuesta operar una instancia propia.
Contenido
- La plataforma integrada: qué es y qué implica
- El modelo de ejecución: stages, jobs y el paso al grafo
- El pipeline de Reservalia en
.gitlab-ci.yml cachefrente aartifacts: la distinción que más confunde- Paralelización:
parallelyparallel: matrix - Runners y executors
- Variables, secretos y protección
- Entornos, despliegue manual y review apps
- Reutilización:
extends,include, componentes y pipelines padre-hijo - El registro de contenedores integrado
- Auto DevOps, con criterio
- SaaS frente a autoalojado, y el coste real
- Cuándo GitLab CI/CD es la elección correcta
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- La plataforma integrada: qué es y qué implica
En una organización típica con GitHub, la cadena de herramientas se compone: GitHub para el código, GitHub Actions para el CI, ECR o Docker Hub para el registro, Snyk o Dependabot para las dependencias, Jira para los issues, algo aparte para los entornos. En GitLab, todo eso son pestañas del mismo proyecto.
Lo que se gana, y no es retórica:
- Cero integraciones que mantener entre piezas. No hay tokens cruzados entre el CI y el registro, ni webhooks que se rompen, ni "el bot de Jira perdió permisos". El job de CI empuja al registro del mismo proyecto con una variable que ya existe.
- Trazabilidad de extremo a extremo. Del issue al merge request, al pipeline, al artefacto, al entorno donde está desplegado. La pregunta "¿qué versión está en producción y qué issues incluye?" tiene respuesta en la interfaz sin construir nada.
- Un solo modelo de permisos. Quien puede fusionar, puede desplegar a staging pero no a producción, y eso se configura una vez.
- Funcionalidades que solo existen porque las piezas están juntas: los informes de seguridad que aparecen dentro del merge request comparando la rama con
main, los entornos que muestran qué commit está desplegado, las review apps con su enlace en el MR.
Lo que se paga, con la misma franqueza:
- Acoplamiento fuerte. Salir de GitLab significa salir de todo a la vez: repositorio, CI, registro e historial de entornos. Es el coste de cambio más alto del módulo, y la 06-07 lo cuantifica.
- Las funcionalidades interesantes están escalonadas por nivel de suscripción. Buena parte de lo que se cuenta en artículos y conferencias —aprobaciones múltiples, algunos análisis de seguridad, ciertos controles de cumplimiento— no está en el nivel gratuito. No conviene diseñar una arquitectura sobre una funcionalidad y descubrir después en qué nivel vive.
- Si te lo autoalojas, operas una plataforma entera, no un servidor de CI: base de datos, almacenamiento de objetos, Redis, Gitaly, el registro, los runners. Apartado 12.
Una nota de vocabulario útil: en GitLab, merge request (MR) es lo que en GitHub es pull request. El concepto es el mismo que la 02-07 describió.
- El modelo de ejecución: stages, jobs y el paso al grafo
El modelo original de GitLab CI es de etapas secuenciales con jobs paralelos dentro:
flowchart LR
subgraph S1["stage: preparar"]
A["preparar"]
end
subgraph S2["stage: verificar"]
B["calidad"]
C["test 1/4"]
D["test 2/4"]
E["test 3/4"]
F["test 4/4"]
end
subgraph S3["stage: construir"]
G["build"]
H["seguridad"]
end
subgraph S4["stage: publicar"]
I["publicar"]
end
S1 --> S2 --> S3 --> S4
La regla clásica: todos los jobs de una etapa corren en paralelo, y la etapa siguiente no empieza hasta que la anterior termina entera. Es un modelo fácil de razonar y con un defecto claro: si test 4/4 tarda ocho minutos y los demás dos, build espera ocho minutos aunque solo dependiera de preparar. Es exactamente el problema de la barrera de etapa que la 04-01 describió.
La corrección llegó con needs, que convierte el pipeline en un DAG: un job con needs arranca en cuanto sus dependencias concretas terminan, sin esperar a su etapa. Con needs, las stages pasan de ser barreras a ser sobre todo agrupación visual.
Modelo por stages |
Modelo por needs (DAG) |
|
|---|---|---|
| Arranque de un job | Cuando termina toda la etapa anterior | Cuando terminan sus dependencias |
| Facilidad de lectura | Muy alta | Media: hay que seguir el grafo |
| Tiempo total | Suma de los máximos por etapa | Camino crítico real |
| Riesgo | Esperas innecesarias | Grafos enmarañados si nadie los revisa |
Vocabulario de GitLab traducido:
| GitLab | Equivalente en el curso |
|---|---|
| Pipeline | Ejecución de workflow |
| Stage | Etapa (agrupación) |
| Job | Job |
Script (una línea de script:) |
Step |
| Runner | Runner / agente |
| Executor | Cómo materializa el runner el entorno (shell, docker, kubernetes) |
| Artifacts | Artefactos entre jobs y descargables |
| Environment | Entorno de despliegue con historial |
- El pipeline de Reservalia en
.gitlab-ci.yml
.gitlab-ci.ymlSegunda traducción del mismo pipeline: instalar con caché → lint y test en paralelo → build de imagen → publicar por digest.
# .gitlab-ci.yml — Reservalia · CI
stages: [preparar, verificar, construir, publicar, desplegar] # 1
default: # 2
image: node:22-bookworm
interruptible: true # cancela si llega un push nuevo
retry:
max: 2
when: [runner_system_failure, stuck_or_timeout_failure] # 3 · reintentar solo fallos de infraestructura
variables:
NPM_CONFIG_CACHE: "$CI_PROJECT_DIR/.npm" # 4 · caché dentro del workspace
IMAGEN: "$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 # nada más dispara pipeline
# ---------------------------------------------------------------- plantillas
.node: # 6 · plantilla reutilizable
cache:
key:
files: [package-lock.json] # 7 · clave por lockfile
paths: [.npm/]
policy: pull # solo lee; el job "preparar" la escribe
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 · este sí actualiza la caché
script:
- echo "Dependencias instaladas y caché poblada"
calidad:
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 como servicio
- 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 · esto sí es un artefacto
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=$IMAGEN:cache \
--cache-to type=registry,ref=$IMAGEN:cache,mode=max \
--tag $IMAGEN:$CI_COMMIT_SHA \
--push .
- docker buildx imagetools inspect $IMAGEN:$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
seguridad:
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: [calidad, test, build-api, build-web, seguridad] # 16 · fan-in
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # solo en la rama por defecto
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 $IMAGEN:estable $IMAGEN@$DIGEST # promoción por 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 "$IMAGEN@$DIGEST"
desplegar-produccion:
stage: desplegar
needs: [desplegar-staging]
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual # 18 · aprobación
allow_failure: false
environment:
name: produccion
url: https://app.reservalia.example
script:
- ./scripts/desplegar.sh produccion "$IMAGEN@$DIGEST"stagesdeclara el orden. Un job sinstagecae entestpor defecto, lo cual sorprende; decláralo siempre.defaultaplica a todos los jobs lo que antes se repetía.interruptible: truecon la opción de auto-cancelación del proyecto es elcancel-in-progressde la 04-04.retryselectivo. Reintentar cualquier fallo enmascara los flaky de la 02-04 y convierte el pipeline en un generador de verde falso; reintentar solo fallos de infraestructura del runner es legítimo. Esa distinción es la diferencia entre resiliencia y autoengaño.- La caché tiene que estar dentro de
$CI_PROJECT_DIR: GitLab solo puede cachear rutas del workspace. Por eso se redirige la caché de npm ahí en vez de dejarla en~/.npm. Es el error número uno de caché en GitLab. workflow:rulesdecide si se crea pipeline. Sin esto, un push a una rama con MR abierto genera dos pipelines (uno de rama y otro de MR): duplica el gasto y confunde la señal.- Una plantilla es un job que empieza por punto: GitLab no lo ejecuta y sirve para
extends. cache:key:filesgenera la clave a partir del hash del lockfile: cambia el lockfile, cambia la caché. Es elhashFilesde la 04-02 con otro nombre.policyes la pieza que casi nadie usa y que ahorra mucho tiempo:pullsolo descarga,pull-pushdescarga y sube al final. Con seis jobs subiendo la misma caché idéntica se malgastan minutos; aquí solo la escribepreparar.needsconvierte el pipeline en grafo. Conneeds: []un job arranca de inmediato, ignorando su etapa.parallel: 4crea cuatro instancias del job conCI_NODE_INDEX(1..N) yCI_NODE_TOTAL. Es el sharding de la 02-04; el reparto por tiempos históricos que verás en la 06-03 no viene de serie.serviceslevanta contenedores auxiliares accesibles por sualias, igual que el PostgreSQL de la 02-02.artifacts:reportsson artefactos con semántica: GitLab los interpreta y los muestra en el MR (resultados de test, cobertura, hallazgos de seguridad).when: alwayses imprescindible: los informes de test importan sobre todo cuando el job falla.- Distinción de fondo, que desarrolla el apartado 4:
dist/es un artefacto —salida del pipeline, se pasa a otros jobs—;.npm/es caché —acelera, y perderla no rompe nada—. - Docker-in-Docker es el patrón habitual para construir imágenes en GitLab, y tiene implicaciones de seguridad que la 06-05 desarrolla: el servicio
dindrequiere runners privilegiados. artifacts:reports:dotenves el mecanismo de outputs entre jobs: las variables del fichero quedan disponibles en los jobs que dependen de este. Es el equivalente delneeds.<job>.outputsde GitHub Actions.- El fan-in de la 04-01:
publicarespera a las cinco señales. environmentregistra el despliegue: GitLab guarda qué commit está enstaging, desde cuándo, y ofrece el botón de volver a desplegar una versión anterior.when: manualconallow_failure: falsees la puerta de la 03-01: el pipeline se detiene esperando a que una persona pulse el botón, y quién puede pulsarlo lo determina la protección del entorno.
Comparado con el ci.yml original, la traducción es casi uno a uno. Las diferencias reales: el sharding es un número en vez de una matriz, los outputs entre jobs pasan por un fichero dotenv en vez de por outputs, la caché exige configurar rutas y política a mano, y el registro de contenedores y los entornos son parte del mismo producto en lugar de servicios externos con credenciales propias.
cache frente a artifacts: la distinción que más confunde
cache frente a artifacts: la distinción que más confundeLos dos guardan ficheros y los dos los restauran en otro job. No son lo mismo y confundirlos produce pipelines lentos o incorrectos.
cache |
artifacts |
|
|---|---|---|
| Propósito | Acelerar (dependencias, compilaciones intermedias) | Transportar resultados entre jobs y hacerlos descargables |
| Si desaparece | El job es más lento, pero funciona | El pipeline falla o el resultado se pierde |
| Dónde se guarda | En el runner (o en almacenamiento compartido, si se configura) | Siempre en el servidor de GitLab |
| Alcance | Compartida entre pipelines y ramas, según key |
De la ejecución concreta del pipeline |
| Se recupera | Por key, best-effort, sin garantías |
Automáticamente desde los jobs que lo necesitan |
| Caducidad | Política del runner | expire_in, explícito |
| Contenido típico | .npm/, ~/.gradle, vendor/ |
dist/, .war, informes JUnit, SBOM |
Regla mental que resuelve el 100 % de los casos: si borrar eso rompe el pipeline, es artefacto; si solo lo hace más lento, es caché.
Dos mecanismos relacionados:
# Descargar SOLO los artefactos que este job necesita
publicar:
needs:
- job: build-web
artifacts: true # descarga dist/
- job: seguridad
artifacts: false # solo la dependencia de orden, sin transferir ficherosSin esto, un job descarga por defecto los artefactos de todos los jobs de etapas anteriores, y en un pipeline grande eso son cientos de megabytes movidos sin razón: una de las causas de pipeline lento más frecuentes y menos diagnosticadas. dependencies: [] es la forma antigua de decir "no me descargues nada".
Y expire_in no es opcional: los artefactos consumen almacenamiento facturable en SaaS y disco en autoalojado. La política de retención de la 02-06 aquí es una línea por job.
- Paralelización:
parallel y parallel: matrix
parallel y parallel: matrix# Sharding simple: N copias idénticas que se reparten por índice
test:
parallel: 4
script: [ "npm test -- --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL" ]
# Matriz: combinaciones de variables, como el matrix de la 02-04
test-compatibilidad:
parallel:
matrix:
- NODE: ["20", "22"]
POSTGRES: ["15", "16"] # → 4 jobs
- NODE: ["22"] # bloques adicionales se suman
POSTGRES: ["17"]
EXPERIMENTAL: "true"
image: node:$NODE
services: [ "postgres:$POSTGRES-alpine" ]
script: [ "npm test" ]Diferencias con el matrix de GitHub Actions que conviene tener claras: no existe exclude —se modela añadiendo bloques en vez de restar—, y no hay fail-fast global: el control es por job con allow_failure. A cambio, parallel: matrix sí permite variar image y services, lo que da bastante juego.
- Runners y executors
Un runner es el proceso que ejecuta jobs. Se registra contra una instancia y se le asignan jobs según sus tags.
| Tipo de runner | Alcance | Cuándo |
|---|---|---|
| Compartido (shared) | Toda la instancia | El caso normal en SaaS; minutos consumibles |
| De grupo | Todos los proyectos de un grupo | Runners propios compartidos por un departamento |
| Específico de proyecto | Un proyecto | Hardware especial, acceso a redes concretas |
Y el executor determina cómo materializa el entorno:
| Executor | Cómo ejecuta | Aislamiento | Cuándo usarlo |
|---|---|---|---|
shell |
Comandos en la máquina del runner | Ninguno | Casi nunca; hereda el problema del agente contaminado de la 06-01 |
docker |
Un contenedor por job, desde image: |
Bueno | El caso normal |
docker+machine |
Crea una VM por job y la destruye | Muy bueno | Autoescalado en la nube |
kubernetes |
Un Pod por job | Muy bueno | Si ya tienes clúster |
ssh, custom |
Máquinas remotas, integraciones propias | Variable | Hardware raro |
# config.toml de un runner autoalojado con executor docker
concurrent = 8 # jobs simultáneos en esta máquina
check_interval = 3
[[runners]]
name = "runner-reservalia-1"
url = "https://gitlab.example.com/"
token = "glrt-..." # token de registro
executor = "docker"
[runners.docker]
image = "node:22-bookworm" # imagen por defecto si el job no la indica
privileged = false # true SOLO si hace falta dind (ver 06-05)
volumes = ["/cache", "/certs/client"]
memory = "4g"
cpus = "2"
[runners.cache] # caché compartida entre runners
Type = "s3"
Shared = true
[runners.cache.s3]
ServerAddress = "s3.eu-west-1.amazonaws.com"
BucketName = "reservalia-ci-cache"
AuthenticationType = "iam" # rol de instancia, sin claves de larga vidaTres puntos operativos con consecuencias:
privileged = truepara Docker-in-Docker es un agujero real: un job con acceso al daemon privilegiado puede escapar del contenedor y comprometer la máquina, incluidos los secretos de otros jobs que corran ahí. Alternativas sin privilegios en la 06-05 (Kaniko, Buildah, BuildKit rootless).- La caché con almacenamiento compartido cambia el rendimiento por completo. Con caché local en el runner y varios runners, cada uno tiene su copia y la tasa de acierto se desploma. Con S3 compartido, todos comparten.
concurrentes la palanca de coste: dimensionar mal deja jobs en cola (que se percibe como "el CI es lento", aunque el tiempo de ejecución sea el mismo) o desperdicia máquinas.
- Variables, secretos y protección
GitLab tiene un solo mecanismo —variables de CI/CD— con atributos que cambian su comportamiento:
| Atributo | Efecto | Cuándo activarlo |
|---|---|---|
| Protected | Solo se expone en pipelines de ramas y tags protegidos | Siempre, para cualquier credencial de staging o producción |
| Masked | Su valor se sustituye por [MASKED] en los logs |
Siempre, para secretos |
| File | Se materializa como fichero y la variable contiene la ruta | Kubeconfig, claves, certificados |
| Environment scope | Valor distinto por entorno (produccion, staging, *) |
Configuración por entorno de la 03-02 |
| Expanded | Se interpolan otras variables dentro | Desactivar en secretos con $ |
Dos advertencias importantes. Masked tiene restricciones de formato —longitud mínima, sin espacios ni ciertos caracteres—; un secreto que no cumple simplemente no se enmascara, y GitLab lo dice en un aviso que es fácil pasar por alto. Y el enmascarado tiene la misma limitación que en Jenkins: cubre la coincidencia literal, no la transformada. La conclusión de la 04-03 no cambia por herramienta.
Protected es la protección de verdad, y la que más se olvida. Sin ella, cualquiera que abra un MR desde una rama arbitraria puede escribir un .gitlab-ci.yml que imprima las credenciales de producción. Con la variable marcada como protegida, esa variable no existe en pipelines de ramas no protegidas. Regla: toda credencial de despliegue, protegida; sin excepciones.
Para identidad federada, GitLab emite tokens OIDC por job con id_tokens, exactamente el patrón sin claves de larga vida de la 03-02:
desplegar-produccion:
id_tokens:
AWS_TOKEN:
aud: https://gitlab.example.com # audiencia que espera el proveedor de identidad
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 produccionLa condición de confianza en AWS se define sobre atributos del token (proyecto, rama, entorno), de modo que solo un pipeline de main desplegando en produccion puede asumir el rol. Es el mismo diseño de la 03-02 con otro emisor.
- Entornos, despliegue manual y review apps
environment: es una de las mejores piezas de GitLab y no tiene un equivalente exacto de serie en todas las herramientas: convierte un job en un despliegue registrado, con historial de qué commit está desplegado, enlace a la URL, y botones para volver a desplegar o revertir a un despliegue anterior.
review:
stage: desplegar
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
environment:
name: review/$CI_COMMIT_REF_SLUG # 1 · un entorno por rama
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" "$IMAGEN@$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 hace falta el código para destruir
script:
- ./scripts/destruir-review.sh "$CI_COMMIT_REF_SLUG"- Un entorno dinámico por rama: son las review apps, el equivalente directo de las previsualizaciones por PR de la 02-07. GitLab publica el enlace dentro del merge request, así que Nuria puede probar el cambio sin bajarse la rama.
on_stopenlaza el job que destruye el entorno; se ejecuta automáticamente al cerrar o fusionar el MR.auto_stop_ines la línea que evita la factura sorpresa: sin ella, cada rama abandonada deja un entorno corriendo indefinidamente. Es el problema que la 05-01 señalaba con las previsualizaciones huérfanas, aquí resuelto con una línea.action: stopmarca el job como el que apaga el entorno.
Sobre aprobaciones: when: manual detiene el job hasta que alguien lo lance, y quién puede hacerlo se controla protegiendo el entorno (Deployments → Protected environments), que es el equivalente de los revisores de Environments de la 03-02. En los niveles de pago existen aprobaciones múltiples y separación de funciones —quien aprueba no puede ser quien escribió el cambio—, que es lo que suelen exigir las auditorías.
- Reutilización:
extends, include, componentes y pipelines padre-hijo
extends, include, componentes y pipelines padre-hijoGitLab ofrece más mecanismos de reutilización que ninguna otra herramienta del módulo, y conviene saber cuál toca.
# a) Anchors YAML: sustitución textual, sin conocimiento de GitLab
.base: &base
image: node:22
retry: 1
calidad:
<<: *base
script: [ "npm run lint" ]
# b) extends: lo mismo pero consciente de la estructura (fusiona mapas en profundidad)
.node:
image: node:22
cache: { key: { files: [package-lock.json] }, paths: [.npm/] }
calidad:
extends: .node # preferible a los anchors: fusiona bien y admite herencia múltiple
script: [ "npm run lint" ]
# c) include: traer YAML de fuera
include:
- local: '/ci/plantillas/node.yml' # mismo repositorio
- project: 'reservalia/plantillas-ci' # otro proyecto de la instancia
ref: 'v3.2.0' # ¡anclado a tag, no a main!
file: '/plantillas/build-publicar.yml'
- remote: 'https://ejemplo.com/plantilla.yml' # URL: sin control de versión → evitar
- template: 'Security/SAST.gitlab-ci.yml' # plantilla oficial de GitLab
- component: gitlab.com/reservalia/componentes/[email protected] # componente con inputs tipados
inputs:
dockerfile: apps/api/Dockerfile
publicar: trueextends frente a anchors: los anchors son YAML puro, se resuelven antes de que GitLab entienda nada y no funcionan entre ficheros incluidos; extends fusiona mapas en profundidad, admite cadenas y sí atraviesa include. Usa extends.
Los componentes (CI/CD components) son la evolución más reciente y la más parecida a lo que la 04-05 defendía: unidades con inputs tipados y versión propia, publicadas en un catálogo, en vez de YAML incluido a ciegas. Un include: template no valida nada; un componente falla al analizar el fichero si le pasas un booleano donde espera una cadena.
Y la regla de la 04-03 aplica igual aquí: include de otro proyecto anclado a main significa que un commit ajeno cambia tu pipeline sin que tú hayas fusionado nada. Ancla a tag o a SHA.
Pipelines padre-hijo para monorepos, que es el problema de ejecución selectiva de la 04-04:
# .gitlab-ci.yml del padre
generar-hijos:
stage: preparar
script:
- node scripts/generar-pipeline.js > hijo.yml # decide qué construir según lo que cambió
artifacts: { paths: [hijo.yml] }
ejecutar-api:
stage: verificar
rules:
- changes: [ "apps/api/**/*", "packages/compartido/**/*" ] # solo si cambió esto
trigger:
include:
- artifact: hijo.yml # pipeline generado dinámicamente
job: generar-hijos
strategy: depend # el padre espera y refleja el resultadoY trigger:project para disparar el pipeline de otro proyecto —el caso multiproyecto de la 05-03—, con strategy: depend si el padre debe esperar al hijo.
| Mecanismo | Qué reutiliza | Cuándo |
|---|---|---|
| Anchors YAML | Fragmentos, mismo fichero | Casi nunca: usa extends |
extends |
Configuración de job | Repetición dentro de un proyecto |
include: local |
Ficheros del repositorio | Partir un .gitlab-ci.yml grande |
include: project |
Plantillas entre proyectos | Estandarizar en la organización |
| Componente | Unidad versionada con inputs | Lo recomendable hoy para plantillas compartidas |
| Pipeline hijo | Un pipeline entero | Monorepo, pipelines dinámicos |
trigger: project |
Pipeline de otro proyecto | Multiproyecto, microservicios |
- El registro de contenedores integrado
Cada proyecto de GitLab lleva su propio registro de contenedores, y eso elimina el trámite de credenciales que en el ci.yml de Reservalia ocupa un job entero:
publicar:
image: docker:27
services: [docker:27-dind]
script:
# Estas tres variables existen sin configurarlas: las inyecta GitLab por 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 es un token efímero con permiso solo sobre el registro de ese proyecto y con la vida del job. Es mínimo privilegio (04-03) sin haber configurado nada, y es un buen ejemplo de lo que la integración regala.
El registro incluye políticas de limpieza por edad y por patrón de tag, que es la retención de la 02-06 como formulario. Sin ellas, el almacenamiento crece sin freno: un pipeline que publica por commit genera decenas de imágenes al día. Configúralas el primer día y conserva siempre lo referenciado por un entorno activo, porque borrar la imagen que está desplegada convierte el rollback por digest de la 03-05 en imposible.
También hay registro de paquetes (npm, Maven, PyPI…) y registro de dependencias proxy, que resuelve el caso de los registros privados de la 04-02 sin montar Nexus o Artifactory aparte.
- Auto DevOps, con criterio
Auto DevOps es un pipeline completo preconfigurado: detecta el lenguaje, construye con buildpacks, ejecuta tests, análisis SAST, dependencias, licencias, contenedor, DAST, y despliega en Kubernetes con revisión, staging y producción con canary. Se activa con una casilla.
Valoración honesta: es una demostración excelente y una base de producción discutible. A favor: para un proyecto nuevo y estándar, en diez minutos tienes un pipeline con más puertas de calidad que muchas empresas después de un año. En contra: el pipeline es una caja negra grande que hay que entender para modificar, asume Kubernetes con una topología concreta, la construcción por buildpacks produce imágenes que no controlas —y la 06-05 explicará por qué querrías controlarlas—, y en cuanto necesitas algo particular acabas sobrescribiendo tantas variables que habría salido más barato escribir el pipeline.
Uso recomendado: como catálogo de ideas y como punto de partida a desactivar pronto. Actívalo, mira qué jobs genera, copia los que te sirvan a tu propio .gitlab-ci.yml y desactívalo. Lo que sí merece la pena adoptar por separado son las plantillas de seguridad (include: template: Security/...), que son piezas sueltas, entendibles y ajustables.
- SaaS frente a autoalojado, y el coste real
| GitLab.com (SaaS) | Autoalojado | |
|---|---|---|
| Quién opera | GitLab | Tú |
| Modelo de coste | Por usuario y mes, con minutos de cómputo incluidos y consumibles adicionales | Licencia por usuario (según nivel) más infraestructura más personas |
| Actualizaciones | Continuas | Tuyas, con ventana y ensayo |
| Datos | Fuera de tu red | Donde tú decidas |
| Runners | Compartidos, o los tuyos | Los tuyos |
| Punto de fallo | El proveedor | Tu instancia |
Autoalojar GitLab no es autoalojar un servidor de CI: es operar una plataforma con PostgreSQL, Redis, Gitaly (el servicio de Git), almacenamiento de objetos, el registro y los runners. Las tareas recurrentes reales: actualizaciones —GitLab publica a ritmo alto y saltarse versiones intermedias no siempre está soportado—, copias de seguridad con restauración ensayada, crecimiento del almacenamiento (los artefactos y el registro son los que se disparan), y dimensionado de runners. En la práctica es al menos media persona a tiempo parcial en una organización mediana, y no es un coste que desaparezca con el tiempo.
Sobre las cifras: los precios por usuario, los minutos incluidos y qué funcionalidad está en qué nivel cambian con frecuencia y por región. Lo que importa aquí es el modelo: pagas por usuario, no por proyecto; el cómputo se factura por minuto con multiplicadores según el tipo de máquina; el almacenamiento de artefactos y registro también cuenta. Consulta las tarifas vigentes antes de decidir y, sobre todo, verifica en qué nivel está la funcionalidad concreta sobre la que quieres construir antes de diseñar nada.
- Cuándo GitLab CI/CD es la elección correcta
Sí, con bastante claridad, cuando:
- Tu código ya está en GitLab. Es el factor que decide la mayoría de los casos y la 06-07 lo pone el primero por una razón: usar otro CI contra un repositorio de GitLab significa sincronizar identidades, webhooks y permisos para obtener menos.
- Quieres una plataforma en vez de una cadena de herramientas, y valoras no mantener integraciones ni conciliar permisos entre cinco productos.
- Necesitas autoalojar todo por normativa, y quieres una experiencia integrada que Jenkins no da. Es probablemente el mejor punto del cuadrante autoalojado.
- Los entornos y las review apps te importan: el modelo de
environmentcon historial,on_stopyauto_stop_ines maduro y ahorra código propio. - Trabajas en monorepo y necesitas pipelines dinámicos: padre-hijo con generación de YAML es una solución de primera para el problema de la 04-04.
No, o piénsalo dos veces, cuando:
- Tu código está en GitHub. Espejar repositorios para usar GitLab CI es una fuente permanente de fricción.
- Tu equipo es pequeño y no quiere operar nada: SaaS resuelve, pero el coste por usuario con muchos colaboradores ocasionales pesa.
- Dependes de funcionalidad de niveles altos: comprueba el nivel antes de diseñar, no después.
- Necesitas mucho macOS o hardware exótico: es viable con runners propios, pero el ecosistema móvil está más rodado en otras herramientas (06-03, 05-02).
Errores Comunes y Consejos
Cachear rutas fuera de $CI_PROJECT_DIR. La caché no se guarda y no hay error visible, solo lentitud. Redirige NPM_CONFIG_CACHE, GRADLE_USER_HOME o equivalentes al workspace.
Todos los jobs con policy: pull-push. Suben una y otra vez la misma caché. Solo el job que la genera necesita escribirla.
Pipelines duplicados en los MR. Un push a una rama con MR abierto crea dos pipelines. workflow:rules lo arregla y ahorra la mitad del cómputo.
Confundir cache con artifacts. Poner dist/ en caché produce despliegues con un build de otra rama; poner node_modules en artifacts sube cientos de megabytes por job al servidor. La regla: si perderlo rompe el pipeline, es artefacto.
No poner expire_in. El almacenamiento crece hasta que alguien recibe la factura o se llena el disco.
Descargar artefactos que no necesitas. Por defecto llegan los de todas las etapas anteriores. Usa needs con artifacts: false donde solo hace falta orden.
Variables de despliegue sin marcar como protegidas. Cualquier rama puede leerlas desde un .gitlab-ci.yml modificado en un MR. Es la vía más directa de exfiltración de credenciales en GitLab.
Review apps sin on_stop ni auto_stop_in. Entornos huérfanos acumulándose y facturando.
retry sin when. Reintentar todo esconde los flaky y produce verde falso (02-04).
include de otro proyecto apuntando a main. Un cambio ajeno modifica tu pipeline sin revisión. Ancla a tag.
Runners privilegiados por costumbre. privileged = true solo donde de verdad hace falta dind, y evalúa alternativas sin daemon (06-05).
Ejercicios
Ejercicio 1. El pipeline de Reservalia en GitLab tarda 19 minutos. Observas: la etapa verificar termina en 6 min pero construir no empieza hasta el minuto 9; cada uno de los seis jobs sube 400 MB de caché; el job publicar descarga 1,2 GB de artefactos y solo usa un fichero digest; y un push a una rama con MR abierto lanza dos pipelines. Escribe las correcciones concretas y estima el efecto de cada una.
Ejercicio 2. Nuria quiere review apps para apps/web: un entorno por MR con URL propia, comentario automático en el MR, destrucción al cerrarlo y caducidad automática a los tres días. Escribe los jobs completos y explica qué protege cada línea. Añade qué hacer con los secretos, sabiendo que un MR puede venir de una rama de cualquier miembro del equipo.
Ejercicio 3. La empresa que compró Gestor Citas 4 (05-04) tiene su código en GitLab autoalojado 14.x, con pipelines que usan only/except, sin needs y con un runner shell único que ejecuta todo en la máquina del servidor. Escribe el plan de modernización en fases, con qué se gana en cada una y qué riesgos hay.
Soluciones
Solución 1.
| Problema | Causa | Corrección | Efecto estimado |
|---|---|---|---|
construir espera 3 min |
Barrera de etapa: espera a que termine el job más lento de verificar |
needs: [preparar] en build-web y build-api |
−3 min de camino crítico |
| 6 × 400 MB de subida de caché | Todos con policy: pull-push |
Solo preparar con pull-push; el resto pull |
−2 GB de tráfico; ~1 min por job |
publicar descarga 1,2 GB |
Descarga implícita de artefactos de etapas previas | needs explícito con artifacts: false salvo el dotenv del build |
−1 a 2 min |
| Pipelines duplicados | Falta workflow:rules |
Reglas de MR y rama por defecto, when: never en el resto |
−50 % del gasto de cómputo |
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 # solo lectura
preparar:
extends: .node
cache:
key: { files: [package-lock.json] }
paths: [.npm/]
policy: pull-push # el único que escribe
script: [ "npm ci --prefer-offline" ]
build-api:
needs: [preparar] # no espera a la etapa verificar
# ...
publicar:
needs:
- job: build-api
artifacts: true # solo el dotenv con el digest
- job: build-web
artifacts: true # dist/ sí hace falta
- job: calidad
artifacts: false # solo orden
- job: test
artifacts: false
- job: seguridad
artifacts: falseEstimación total: de 19 min a alrededor de 12-13 en el camino crítico, y aproximadamente la mitad del cómputo facturado por el arreglo de los pipelines duplicados. Ese último punto es el más rentable de los cuatro y el que más se pasa por alto, porque no se manifiesta como lentitud sino como factura: es exactamente la lección de la 04-04 de que optimizar empieza por medir dónde se va el tiempo y el dinero, no por adivinar.
Solución 2.
review:
stage: desplegar
needs: [build-web]
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes: [ "apps/web/**/*", "packages/compartido/**/*" ] # solo si afecta a 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=Previsualización lista: $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/" --recursiveQué protege cada línea: changes evita levantar entornos por cambios que no tocan la web; auto_stop_in acota el coste de las ramas abandonadas; on_stop garantiza limpieza al cerrar el MR; GIT_STRATEGY: none acelera la destrucción y reduce superficie; needs: [] hace que el job de parada no dependa de nada y funcione aunque el pipeline original fallara.
Sobre secretos: el entorno review/* debe usar credenciales propias y limitadas —un rol que solo puede escribir en el prefijo reservalia-review/ de un bucket sin datos reales—, nunca las de staging o producción. Las de producción van marcadas como protegidas y su environment scope es produccion, así que ni existen en este job. TOKEN_BOT_MR es un token de proyecto con permiso mínimo de comentar. Y la regla general que aplica igual que en la 04-03: un entorno de previsualización se construye asumiendo que su contenido es público, porque su URL es adivinable y no lleva autenticación real.
Solución 3. Cuatro fases, cada una con valor propio —el enfoque de la 05-04—:
Fase 0, inventario y quitar riesgo inmediato (1-2 semanas). El runner shell en la máquina del servidor es el problema urgente: cualquier job ejecuta comandos como el usuario del runner en el mismo host que la instancia, así que un .gitlab-ci.yml en un MR puede leer la base de datos de GitLab. Se cambia primero, antes que ninguna mejora de rendimiento: runner nuevo con executor docker en una máquina distinta, y el shell se apaga. En paralelo, inventario de variables sin marcar como protegidas —rotar las que hayan podido ser expuestas— y de proyectos con .gitlab-ci.yml.
Fase 1, actualizar GitLab (2-4 semanas de calendario). 14.x está muy atrás: hay que subir por las versiones de actualización obligatorias, no de un salto, y ensayar la restauración de la copia en una instancia de pruebas antes de tocar la real. Se gana: seguridad, componentes de CI/CD, mejoras de rules y de entornos. Riesgo principal: ventana de indisponibilidad y migraciones de base de datos largas; se mitiga ensayando sobre una copia y anunciando la ventana.
Fase 2, only/except → rules (1 semana, proyecto a proyecto). only/except sigue funcionando pero no se puede combinar con rules en el mismo job y no expresa condiciones compuestas. La traducción es mecánica:
| Antiguo | Moderno |
|---|---|
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 |
Se hace un proyecto de prueba primero y se compara que el conjunto de pipelines disparados es el mismo antes y después, que es la única verificación que de verdad demuestra la equivalencia.
Fase 3, needs y caché compartida (1-2 semanas). Añadir needs para pasar de etapas a grafo, y configurar caché en S3 compartida entre runners. Se gana tiempo de pipeline. Va la última a propósito: es la fase más visible y la menos importante, y ponerla antes que la fase 0 sería optimizar la velocidad de un sistema con un agujero de seguridad abierto. Ese orden —riesgo, luego mantenibilidad, luego rendimiento— es el mismo que gobernaba los incrementos de la 05-04.
Conclusión
GitLab CI/CD es lo que ocurre cuando el CI se diseña dentro de la plataforma en vez de junto a ella. El .gitlab-ci.yml nació siendo pipeline as code, sin la herencia de la interfaz que arrastra Jenkins; el modelo evolucionó de etapas secuenciales a grafo con needs sin romper lo anterior; y la integración regala cosas que en otras cadenas cuestan trabajo: credenciales de registro por job, informes de test y seguridad dentro del merge request, entornos con historial y rollback, y review apps con caducidad en una línea. A cambio se paga acoplamiento —salir es salir de todo—, funcionalidad escalonada por nivel de suscripción, y una operación considerable si te lo autoalojas.
De lo visto, tres cosas se llevan a cualquier otra herramienta: la distinción caché frente a artefacto —si perderlo rompe el pipeline, es artefacto— es universal aunque el nombre cambie; marcar como protegidas las credenciales de despliegue es la defensa concreta contra el MR malicioso, y su ausencia es un agujero real en muchas instalaciones; y anclar los include a una versión es la misma regla que las Shared Libraries de Jenkins y las acciones fijadas por SHA de la 04-03.
La siguiente herramienta ataca una dimensión distinta. Donde GitLab compite por amplitud, CircleCI compite por profundidad en una sola cosa: la velocidad del pipeline. Trae la caché más explícita y controlable del módulo, un sistema de reutilización empaquetado —los orbs— y, sobre todo, el reparto de pruebas por tiempos históricos, que es la respuesta más madura al problema de sharding que arrastramos desde la 02-04. Veremos el pipeline de Reservalia por tercera vez y qué se paga por esa velocidad.
Curso de CI/CD: Integración y Despliegue Continuo
Módulo 1: Introducción a CI/CD
- Conceptos Básicos de CI/CD
- Beneficios de CI/CD
- Herramientas Populares de CI/CD
- El Proyecto del Curso: la Aplicación que Vamos a Automatizar
- Métricas DORA: Cómo se Mide la Entrega de Software
Módulo 2: Integración Continua (CI)
- Introducción a la Integración Continua
- Configuración de un Entorno de CI
- Automatización de la Construcción
- Pruebas Automatizadas
- Calidad de Código y Análisis Estático
- Artefactos, Versionado y Promoción
- Integración con Control de Versiones
Módulo 3: Despliegue Continuo (CD)
- Introducción al Despliegue Continuo
- Automatización del Despliegue
- Infraestructura como Código y Entornos Reproducibles
- Estrategias de Despliegue
- Feature Flags, Rollback y Recuperación ante Fallos
- Monitoreo y Retroalimentación
Módulo 4: Prácticas Avanzadas de CI/CD
- Pipelines de CI/CD
- Gestión de Dependencias
- Seguridad en CI/CD
- Escalabilidad y Rendimiento
- Pipeline as Code: Plantillas, Reutilización y Pruebas del Pipeline
- Bases de Datos en el Pipeline: Migraciones Seguras
Módulo 5: Implementación de CI/CD en Proyectos Reales
- Caso de Estudio: Proyecto Web
- Caso de Estudio: Aplicación Móvil
- Caso de Estudio: Microservicios
- Caso de Estudio: Modernizar un Proyecto Legacy
Módulo 6: Herramientas y Tecnologías
- Jenkins
- GitLab CI/CD
- CircleCI
- Travis CI
- Docker y Kubernetes
- GitHub Actions a Fondo
- Comparativa y Criterios para Elegir Herramienta
Módulo 7: Ejercicios Prácticos
- Ejercicio 1: Configuración de un Pipeline Básico
- Ejercicio 2: Integración de Pruebas Automatizadas
- Ejercicio 3: Despliegue en un Entorno de Producción
- Ejercicio 4: Monitoreo y Retroalimentación
- Ejercicio 5: Endurecer el Pipeline con Seguridad y Secretos
- Proyecto Final: Pipeline Completo de Extremo a Extremo
