Pipelines más rápidos en GitLab CI con needs y parallel
Tus jobs son eficientes pero tu pipeline es lento, porque los stages obligan a cada job a esperar a jobs de los que no depende. Cómo reemplazar las barreras de stage por un grafo con needs, y partir el job más lento entre varios runners con parallel.
- gitlab
- ci-cd
- devops
Después de las partes 2 y 3, ningún job del pipeline repite el trabajo de otro. Y sin embargo el pipeline sigue lento. La razón ya no está dentro de los jobs sino entre ellos: los stages son barreras, y cada job espera al stage anterior completo, dependa de esos jobs o no. En un pipeline de cinco jobs nadie lo nota. Cuando el archivo crece, la espera se convierte, sin hacer ruido, en el rubro más caro de tu tiempo total.
Los stages hacen que todo espere a todo
Toma un pipeline que compila y despliega una app junto con su sitio de documentación:
stages: [setup, build, deploy]
install-deps: # setup ~60s
build-app: # build ~180s
build-docs: # build ~30s
deploy-app: # deploy ~60s
deploy-docs: # deploy ~20sSigue el camino de la documentación. build-docs termina a los 30 segundos de arrancar el stage de build, y a partir de ahí deploy-docs (que no necesita nada más que build-docs) se queda 150 segundos de brazos cruzados esperando a que build-app libere la barrera. La documentación estaba lista para desplegarse en el segundo 110 (60 + 30 + 20) y termina desplegándose alrededor del segundo 260. Más de la mitad de ese camino se va en esperar a un job que no le importa.
needs: declara el grafo real
Con needs, un job lista sus dependencias reales y arranca en cuanto terminan, ignorando la barrera de stage:
build-docs:
stage: build
needs: [install-deps]
script: pnpm --filter docs build
deploy-docs:
stage: deploy
needs: [build-docs]
script: ./scripts/deploy-docs.shMismos stages, mismos jobs, pero ahora deploy-docs corre en el segundo 110 en lugar del 260. El camino de la app no cambia (install-deps → build-app → deploy-app era el camino crítico de todas formas), pero todos los caminos que no son el crítico dejan de pagarle renta. Mientras más cadenas independientes tenga tu pipeline, más se nota; la documentación de GitLab llama a esto un pipeline de grafo acíclico dirigido, y la vista de pipeline incluso te dibuja el grafo.
Dos efectos secundarios que conviene conocer:
- Un job con
needsdescarga artifacts únicamente de los jobs que lista. Eso recorta el comportamiento por defecto de la parte 3 (descargar todo lo de los stages anteriores), que además casi siempre es lo que querías. needs: []hace que el job arranque de inmediato al crearse el pipeline, ignorando los stages por completo. Útil para jobs sin dependencias reales: un lint que solo lee código fuente, un job de notificación.
Una advertencia que viene de producción: needs saca la información de dependencias de una sola lista stages: y la reparte en cada job, y mantenerla honesta corre por tu cuenta. Si olvidas listar un job del que sí dependes, acabas de fabricar una race condition: el job arranca antes de tiempo, a veces antes de que exista su input, y falla de forma intermitente. Cuando un pipeline con needs se pone inestable, revisa el grafo antes de culpar al runner.
parallel: parte un job lento entre varios runners
El grafo es tan rápido como su nodo más lento, y en la mayoría de pipelines frontend ese nodo es la suite de tests. parallel: N clona el job N veces y le entrega a cada copia dos variables, CI_NODE_INDEX y CI_NODE_TOTAL, que calzan directo con los flags de sharding de los test runners:
e2e:
stage: quality
parallel: 4
script: pnpm playwright test --shard=$CI_NODE_INDEX/$CI_NODE_TOTALCuatro runners, un cuarto de la suite cada uno. Una suite de 20 minutos pasa a durar 5 y pico, al precio de tener tres runners extra ocupados. Las cuentas cierran solas: los minutos de runner son baratos; los ingenieros esperando para hacer merge, no.
La variante parallel:matrix corre el job una vez por cada combinación de variables, que es como un solo job de build cubre varias apps sin triplicar el YAML:
build-app:
stage: build
parallel:
matrix:
- APP: [storefront, admin, docs]
script: pnpm --filter $APP buildCada entrada de la matriz aparece como un job separado en la vista del pipeline (build-app: [storefront], etcétera), y cada una se puede listar en needs por separado: un job de deploy puede depender exactamente del build que va a desplegar.
Con qué te quedas
Cuatro partes después, el pipeline hace puro trabajo honesto: instala una vez, compila una vez, corre los tests en pedazos, y cada job arranca en cuanto sus dependencias reales están listas.
Lo que nada de esto controla todavía es cuándo corre todo esto. Haces push a una rama de feature y los jobs de deploy se cuelan igual. Abres un merge request y puedes terminar con dos pipelines para el mismo commit. Creas un tag de release y todo el stage de quality vuelve a correr sobre código que ya pasó. Decidir qué jobs existen en qué situaciones empieza en la parte 5 con las rules a nivel de job; la mitad a nivel de pipeline (workflow y la elección del tipo de pipeline, que es la decisión más importante de todo el archivo) viene en la parte 6.