Jenkins compite por control y GitLab por amplitud. CircleCI compite por una sola cosa: que el pipeline sea rápido. Es un SaaS especializado —no aloja tu código, no gestiona tus issues, no es tu registro de contenedores— que asume que el repositorio vive en otro sitio y se concentra en ejecutar trabajos deprisa y con buenas herramientas para saber por qué no lo hacen. Esa especialización se nota en tres piezas que ninguna otra herramienta del módulo tiene tan afinadas: una caché explícita donde tú decides la clave, cuándo se guarda y cómo se degrada; los orbs, que son paquetes de configuración versionados y publicables; y 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 y que la 04-04 solo pudo resolver a mano. En esta lección traducimos el pipeline de Reservalia por tercera vez, distinguimos con precisión workspace, caché y artefactos —tres conceptos que aquí son tres mecanismos distintos y explícitos—, y evaluamos el precio de esa velocidad: dependencia de un SaaS, un modelo de crédito que hay que entender antes de firmar, y un ecosistema más pequeño.
Contenido
- La propuesta: un SaaS especializado en velocidad
- Modelo de ejecución: jobs, workflows y executors
- El pipeline de Reservalia en
.circleci/config.yml - Caché explícita: claves,
restore-keysy degradación - Workspace, caché y artefactos: tres cosas distintas
- Paralelización y test splitting por tiempos históricos
- Reutilización:
commands,parametersy orbs - Contexts, secretos y aprobaciones
- Recursos, clases de máquina y el modelo de créditos
- Depurar: SSH into build y ejecución local
- Límites y contrapartidas
- Cuándo elegir CircleCI
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- La propuesta: un SaaS especializado en velocidad
CircleCI nació en 2011, en la misma ola que Travis, con una diferencia de enfoque que resultó decisiva: mientras Travis apostaba por la simplicidad extrema para proyectos de código abierto, CircleCI apostó por equipos que pagan y por el rendimiento del pipeline como producto. De ahí su forma actual.
Lo que la especialización implica, para bien y para mal:
| Consecuencia | |
|---|---|
| No aloja el código | Se conecta a GitHub, GitLab o Bitbucket. Tu identidad y permisos siguen viviendo allí, y CircleCI pide acceso al repositorio |
| No es un registro ni un gestor de issues | Publica en ECR, Docker Hub o donde quieras: nada regalado, pero nada que te ate |
| Toda la inversión está en la ejecución | Clases de recurso finas, arranque rápido, caché controlable, reparto por tiempos, buenas vistas de dónde se va el tiempo |
| Si desaparece la conexión con el SaaS, no hay CI | El plano de control es suyo, incluso con runners propios |
Y una virtud cultural que conviene señalar: CircleCI ha sido durante años de las herramientas que mejor exponen dónde se va el tiempo —tiempo por job, por step, colas, tasas de acierto de caché, distribución de duraciones de test—. Eso importa porque la 04-04 estableció que optimizar sin medir es adivinar; aquí la medición viene de serie.
- Modelo de ejecución: jobs, workflows y executors
El fichero es único: .circleci/config.yml, versionado con el código. Su estructura tiene tres niveles.
flowchart TD
W["workflow<br/>orquesta y ordena"] --> J1["job: preparar"]
J1 --> J2["job: calidad"]
J1 --> J3["job: test<br/>parallelism: 4"]
J1 --> J4["job: build"]
J2 --> J5["job: publicar"]
J3 --> J5
J4 --> J5
J5 --> A{"hold<br/>type: approval"}
A --> J6["job: desplegar-produccion"]
subgraph EX["executor: donde corre cada job"]
E1["docker"]
E2["machine"]
E3["macos"]
E4["arm"]
end
| CircleCI | Equivalente en el curso | Matiz |
|---|---|---|
| Workflow | El grafo de jobs | Puede haber varios en un mismo fichero, con disparadores distintos |
| Job | Job | Unidad que corre en un executor; devuelve éxito o fallo |
| Step | Step | run, checkout, save_cache, store_artifacts… |
| Executor | Tipo de runner | docker, machine, macos, windows, y variantes ARM/GPU |
| Command | Composite action | Secuencia de steps reutilizable, con parámetros |
| Orb | Paquete de plantillas | Publicado y versionado en un registro público o privado |
| Context | Grupo de secretos compartidos | Ámbito de organización, no de proyecto |
| Workspace | Paso de ficheros entre jobs | Distinto de caché y de artefactos |
Dos rasgos característicos del modelo. Primero, el grafo es explícito desde el principio: no hay concepto de "etapa" que actúe como barrera, solo requires. CircleCI nunca tuvo que evolucionar de fases a grafo porque nació con grafo. Segundo, los executors son de primera clase: se declaran arriba, con nombre, y los jobs los referencian; cambiar toda la flota de docker a machine es editar un bloque.
- El pipeline de Reservalia en
.circleci/config.yml
.circleci/config.ymlTercera traducción del mismo pipeline.
version: 2.1
orbs: # 1
aws-cli: circleci/[email protected]
node: circleci/[email protected]
executors: # 2
node-base:
docker:
- image: cimg/node:22.11 # imágenes "convenience" preparadas por CircleCI
auth: { username: $DOCKERHUB_USER, password: $DOCKERHUB_TOKEN }
- image: cimg/postgres:16.2 # 3 · contenedores secundarios del mismo job
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: test
POSTGRES_DB: reservalia_test
resource_class: medium # 4
working_directory: ~/reservalia
docker-builder:
machine:
image: ubuntu-2404:current # VM completa: hay daemon Docker de verdad
docker_layer_caching: true # 5
commands: # 6
preparar-node:
description: Checkout, restauración de caché e instalación reproducible
parameters:
profundidad:
type: integer
default: 1
steps:
- checkout
- restore_cache: # 7
keys:
- npm-v2-{{ checksum "package-lock.json" }}
- npm-v2- # fallback parcial
- run:
name: Instalar dependencias
command: npm ci --prefer-offline --no-audit
jobs:
preparar:
executor: node-base
steps:
- preparar-node
- save_cache: # 8 · solo este job escribe la caché
key: npm-v2-{{ checksum "package-lock.json" }}
paths: [ ~/.npm ]
- persist_to_workspace: # 9
root: ~/reservalia
paths: [ ".", "!node_modules" ]
calidad:
executor: node-base
steps:
- attach_workspace: { at: ~/reservalia }
- run: npx prettier --check .
- run: npm run lint
- run: npm run typecheck
test:
executor: node-base
parallelism: 4 # 10
steps:
- attach_workspace: { at: ~/reservalia }
- run:
name: Esperar a PostgreSQL
command: dockerize -wait tcp://localhost:5432 -timeout 1m
- run: npm run migrate
- run:
name: Ejecutar la partición de tests correspondiente
command: |
FICHEROS=$(circleci tests glob "apps/**/*.test.ts" \
| circleci tests split --split-by=timings) # 11 · reparto por tiempos reales
npm test -- --runTestsByPath $FICHEROS \
--reporters=default --reporters=jest-junit
- store_test_results: { path: informes } # 12 · alimenta el histórico de tiempos
- store_artifacts: { path: cobertura } # 13
build:
executor: docker-builder
steps:
- attach_workspace: { at: ~/reservalia }
- run:
name: Construir imagen
command: |
docker buildx build \
--file apps/api/Dockerfile \
--cache-from type=registry,ref=$ECR/$IMAGEN:cache \
--cache-to type=registry,ref=$ECR/$IMAGEN:cache,mode=max \
--tag $IMAGEN:$CIRCLE_SHA1 --load .
- run:
name: Escanear imagen
command: trivy image --severity HIGH,CRITICAL --exit-code 1 $IMAGEN:$CIRCLE_SHA1
publicar:
executor: docker-builder
steps:
- attach_workspace: { at: ~/reservalia }
- aws-cli/setup: # 14 · OIDC, sin claves de larga vida
role_arn: $AWS_ROLE_ARN
region: eu-west-1
- run:
name: Publicar en ECR por digest
command: |
aws ecr get-login-password --region eu-west-1 \
| docker login --username AWS --password-stdin "$ECR"
docker tag $IMAGEN:$CIRCLE_SHA1 $ECR/$IMAGEN:$CIRCLE_SHA1
docker push $ECR/$IMAGEN:$CIRCLE_SHA1
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' $ECR/$IMAGEN:$CIRCLE_SHA1)
echo "$DIGEST" | tee digest.txt
- store_artifacts: { path: digest.txt }
desplegar:
executor: node-base
parameters:
entorno: { type: enum, enum: [staging, produccion] } # 15
steps:
- attach_workspace: { at: ~/reservalia }
- aws-cli/setup: { role_arn: $AWS_ROLE_ARN, region: eu-west-1 }
- run: ./scripts/desplegar.sh << parameters.entorno >>
workflows:
ci:
jobs:
- preparar
- calidad: { requires: [preparar] } # 16
- test: { requires: [preparar] }
- build: { requires: [preparar] }
- publicar:
requires: [calidad, test, build]
context: [aws-ci] # 17
filters: { branches: { only: main } }
- desplegar:
name: desplegar-staging
entorno: staging
requires: [publicar]
context: [aws-staging]
filters: { branches: { only: main } }
- aprobar-produccion: # 18
type: approval
requires: [desplegar-staging]
filters: { branches: { only: main } }
- desplegar:
name: desplegar-produccion
entorno: produccion
requires: [aprobar-produccion]
context: [aws-produccion]
filters: { branches: { only: main } }- Los orbs se importan arriba y se anclan a versión.
circleci/[email protected]es un paquete publicado; su contenido son commands, jobs y executors ya escritos. Anclar a versión exacta es aquí la misma regla que fijar acciones por SHA de la 04-03: un orb es código de terceros que corre con tus secretos. - Los executors con nombre evitan repetir la configuración de entorno en cada job. Es el rasgo que más limpio deja el fichero.
- Los contenedores secundarios son el equivalente de
servicesen GitLab o GitHub: comparten red con el primario, así que PostgreSQL está enlocalhost:5432. Detalle importante: CircleCI no espera a que estén listos, de ahí el paso condockerize -wait. Es una causa habitual de flaky al empezar. resource_classelige el tamaño de la máquina (small,medium,large,xlarge…). Es la palanca directa de la 04-04: subir de clase acorta el job y multiplica el consumo de créditos por minuto. Se decide midiendo, no por intuición.docker_layer_cachingconserva las capas Docker entre ejecuciones en el executormachine. Acelera mucho los builds de imagen y consume créditos adicionales; con una caché de capas en el registro (--cache-from, 06-05) a veces no compensa. Hay que medir las dos opciones.commandses la composite action de la 04-05: pasos reutilizables con parámetros, dentro del mismo fichero o publicados en un orb.restore_cachecon lista de claves: prueba la primera; si no existe, la segunda como prefijo. Es elrestore-keysde la 04-02, y aquí es completamente explícito.save_cachesolo en un job. Las claves de caché en CircleCI son inmutables: una vez escrita una clave, no se sobrescribe. Eso obliga a versionar la clave (npm-v2-) cuando quieres invalidar, y evita el problema de varios jobs sobreescribiéndose.persist_to_workspaceguarda ficheros para los jobs siguientes de este mismo workflow. Es lo que en Jenkins erastashy en GitHub Actions se hace con artefactos.parallelism: 4lanza cuatro contenedores idénticos del job, conCIRCLE_NODE_INDEXyCIRCLE_NODE_TOTAL.- La pieza estrella:
circleci tests split --split-by=timingsreparte los ficheros entre los cuatro contenedores usando los tiempos reales de ejecuciones anteriores, no el número de ficheros. Apartado 6. store_test_resultsno es cosmético: es lo que alimenta el histórico de tiempos que usa el reparto anterior, además de la vista de tests fallidos y flaky. Sin él,--split-by=timingsdegrada a reparto por nombre.store_artifactssube ficheros descargables desde la interfaz, que es un concepto distinto del workspace (apartado 5).- Identidad federada por OIDC: CircleCI emite un token por job y AWS lo cambia por credenciales temporales, exactamente el diseño de la 03-02. La condición de confianza en AWS se ata al proyecto y opcionalmente al context.
- Jobs parametrizados invocados varias veces con
namedistinto: un solo jobdesplegarsirve para staging y producción, con contexts distintos. Es reutilización sin duplicar definición. requireses todo el mecanismo de orden. No hay etapas.contextaporta los secretos, y se asigna por invocación en el workflow, no dentro del job. Esa separación es una de las mejores ideas de CircleCI: el mismo job corre con credenciales de staging o de producción según quién lo invoque.type: approvales un job especial sin steps: un botón. Su semántica es la puerta de la 03-01; quién puede pulsarlo se controla con las restricciones del context y del proyecto.
- Caché explícita: claves,
restore-keys y degradación
restore-keys y degradaciónFrente a la caché "automática" de setup-node en GitHub Actions o al cache: declarativo de GitLab, aquí todo es manual. Se paga en verbosidad y se gana en control.
- restore_cache:
keys:
# 1 · coincidencia exacta: mismo lockfile, misma versión de Node, mismo SO
- deps-v3-{{ arch }}-{{ checksum "package-lock.json" }}
# 2 · fallback: cualquier caché de esta arquitectura, aunque el lockfile difiera
- deps-v3-{{ arch }}-
# 3 · último recurso
- deps-v3-
- run: npm ci --prefer-offline
- save_cache:
key: deps-v3-{{ arch }}-{{ checksum "package-lock.json" }}
paths: [ ~/.npm ]
when: on_success # no guardes una caché generada por un build rotoTres reglas que evitan los fallos habituales:
- La clave debe incluir todo lo que invalida la caché: el lockfile, la arquitectura y, si tienes varias versiones de runtime, la versión. Una caché de Node 20 restaurada en un job de Node 22 produce errores de módulos nativos que cuestan horas de diagnóstico.
- El fallback parcial es lo que hace útil la caché. Sin él, cualquier cambio en el lockfile obliga a descargar todo desde cero. Con él, se restaura la caché anterior y
npm cisolo baja lo que cambió. Es el mecanismo delrestore-keysde la 04-02. - El prefijo versionado (
v3) es el botón de invalidar. Como las claves son inmutables, la única forma de forzar una caché limpia cuando se corrompe es subir el número. Tenerlo desde el principio ahorra un mal rato.
Y una advertencia de seguridad que aplica a todas las herramientas: la caché es un canal de escritura entre ramas. Un PR de un colaborador que ejecute save_cache puede envenenar una caché que luego restaura un build de main. Cachea solo artefactos derivados de dependencias verificadas por el lockfile, nunca binarios construidos que después se ejecutan con privilegios (04-03).
- Workspace, caché y artefactos: tres cosas distintas
CircleCI es la herramienta que más explícita hace esta distinción, y por eso es el mejor sitio para fijarla.
| Caché | Workspace | Artefactos | |
|---|---|---|---|
| Para qué | Acelerar | Pasar ficheros entre jobs del mismo workflow | Guardar salidas para personas o sistemas externos |
| Alcance | Entre workflows, entre ramas | Un workflow | Persistente, descargable desde la interfaz |
| Cómo | save_cache / restore_cache |
persist_to_workspace / attach_workspace |
store_artifacts |
| Clave | Explícita, inmutable | Rutas; se acumula por job | Ruta y nombre |
| Si desaparece | Más lento | El pipeline falla | Se pierde la evidencia |
| Contenido típico | ~/.npm, ~/.gradle |
dist/, fuentes compiladas, digest.txt |
Informes de cobertura, capturas de Playwright, SBOM |
El error clásico: usar caché para pasar el dist/ entre build y deploy. Funciona casi siempre y falla el día que la caché no está o cuando trae la de otra rama, y entonces se despliega un build antiguo sin que nada avise. Un despliegue con un artefacto equivocado es peor que un fallo, porque no se detecta. Regla, la misma de la 06-02: si perderlo rompe la corrección del resultado, no es caché.
Detalle práctico del workspace: se acumula. Si build persiste dist/ y publicar persiste digest.txt, un job posterior que haga attach_workspace ve los dos. Y como cada persist_to_workspace añade, conviene persistir lo mínimo: persistir node_modules entero es lento y casi nunca necesario si la caché está bien puesta.
- Paralelización y test splitting por tiempos históricos
Este es el argumento técnico más fuerte de CircleCI y la solución más madura al problema que la 02-04 planteó y la 04-04 dejó a medias.
El sharding ingenuo reparte por número de ficheros. Con los tests de Reservalia:
| Contenedor | Ficheros | Tiempo real |
|---|---|---|
| 1 | 34 | 2 min 10 s |
| 2 | 34 | 1 min 40 s |
| 3 | 34 | 7 min 50 s ← lleva los tests de integración con base de datos |
| 4 | 34 | 2 min 05 s |
El job tarda 7:50, no 3:30. Se paga por cuatro contenedores y se aprovecha uno. Con --split-by=timings, CircleCI usa los tiempos por fichero de ejecuciones anteriores —que conoce porque store_test_results los sube— y reparte para igualar duración:
| Contenedor | Ficheros | Tiempo real |
|---|---|---|
| 1 | 51 | 3 min 25 s |
| 2 | 44 | 3 min 30 s |
| 3 | 12 | 3 min 20 s |
| 4 | 29 | 3 min 35 s |
- run:
command: |
# glob → lista de ficheros; split → solo los que tocan a este contenedor
FICHEROS=$(circleci tests glob "apps/**/*.test.ts" | circleci tests split --split-by=timings)
npm test -- --runTestsByPath $FICHEROS --reporters=jest-junit
- store_test_results: { path: informes } # imprescindible: sin esto no hay históricoMatices que hay que conocer:
- La primera ejecución no tiene datos y reparte por nombre de fichero; a partir de la segunda, mejora. Si añades muchos tests nuevos de golpe, el reparto tarda una o dos ejecuciones en reequilibrarse.
- Existen otras estrategias:
--split-by=filesize(útil sin histórico) y--timings-type=classnamepara lenguajes donde la unidad no es el fichero. - No arregla un test lento: reparte mejor, pero un único test de cinco minutos sigue siendo el suelo del job. La 04-04 ya avisaba de que paralelizar no sustituye a arreglar.
- Interacción con los flaky: un test inestable falla en un contenedor y tumba el job entero; el reparto no cambia eso. La cuarentena de la 02-04 sigue siendo necesaria.
Comparado con las demás herramientas: en GitHub Actions y GitLab, el reparto equilibrado por tiempos hay que construirlo (guardando tiempos como artefacto y escribiendo el reparto, o con herramientas de terceros). Aquí es un comando. Es una diferencia de días de trabajo, y es real.
- Reutilización:
commands, parameters y orbs
commands, parameters y orbsTres niveles, del más local al más compartido:
# 1. Command: pasos reutilizables dentro del proyecto (≈ composite action)
commands:
notificar:
parameters:
canal: { type: string, default: "#reservalia-ci" }
estado: { type: enum, enum: [ok, fallo] }
steps:
- run:
when: always
command: ./scripts/notificar.sh "<< parameters.canal >>" "<< parameters.estado >>"
# 2. Job parametrizado: el mismo job invocado con datos distintos
jobs:
desplegar:
parameters:
entorno: { type: enum, enum: [staging, produccion] }
espera: { type: integer, default: 60 }
steps:
- run: ./scripts/desplegar.sh << parameters.entorno >> << parameters.espera >>
# 3. Orb: paquete versionado, publicado y reutilizable entre proyectos
orbs:
reservalia: reservalia/[email protected]
workflows:
ci:
jobs:
- reservalia/build-publicar:
dockerfile: apps/api/Dockerfile
repositorio: reservalia/apiUn orb es un paquete con tres tipos de contenido —commands, jobs y executors— publicado con versionado semántico. Publicar el tuyo:
# src/@orb.yml del orb reservalia/plataforma
version: 2.1
description: Piezas comunes de los pipelines de Reservalia
commands:
preparar-node:
parameters:
version: { type: string, default: "22.11" }
steps:
- checkout
- restore_cache: { keys: ["npm-v2-{{ checksum \"package-lock.json\" }}", "npm-v2-"] }
- run: npm ci --prefer-offline# Publicación: primero una versión de desarrollo, luego la promoción semántica
circleci orb pack src/ > orb.yml
circleci orb validate orb.yml
circleci orb publish orb.yml reservalia/plataforma@dev:pruebas # mutable, para probar
circleci orb publish promote reservalia/plataforma@dev:pruebas patch # inmutable: 2.3.1Esto es exactamente la idea de la 04-05 —repositorio central de plantillas— pero con dos ventajas de diseño: el versionado semántico es obligatorio y las versiones publicadas son inmutables, así que el problema de "alguien cambió main de la librería y rompió a todo el mundo" que vimos en Jenkins y en los include de GitLab no puede ocurrir por accidente. La contrapartida sigue siendo la misma: un orb público de terceros es código que corre con tus secretos, así que las reglas de la 04-03 se aplican igual —revisar qué hace, anclar a versión exacta, preferir orbs certificados o propios para lo que toca credenciales—.
Los orbs pueden ser públicos (catálogo abierto, código visible) o privados dentro de tu organización, que es lo que usarías para reservalia/plataforma.
- Contexts, secretos y aprobaciones
Hay dos sitios donde viven los secretos, y la diferencia importa:
| Variables de proyecto | Context | |
|---|---|---|
| Ámbito | Un proyecto | Toda la organización |
| Se asigna | Siempre, a todos los jobs del proyecto | Por invocación de job en el workflow |
| Restricción de acceso | Por proyecto | Por grupo de seguridad: solo ciertos equipos |
| Uso típico | Configuración del propio proyecto | Credenciales compartidas y, sobre todo, las de producción |
workflows:
cd:
jobs:
- desplegar:
name: desplegar-produccion
context: [aws-produccion, notificaciones] # solo este job las ve
requires: [aprobar-produccion]La práctica correcta: las credenciales de producción viven en un context restringido a un grupo de seguridad, y solo se adjuntan al job de despliegue a producción. Ningún job de test las tiene, así que un PR no puede exfiltrarlas aunque modifique el config.yml —siempre que el context esté restringido, que es lo que hay que verificar—. Es el mismo razonamiento que las variables protegidas de la 06-02 y el mínimo privilegio de la 04-03, con un mecanismo distinto.
Sobre las aprobaciones: type: approval es un job vacío que bloquea el grafo hasta que alguien pulsa. Es más simple que los Environments de GitHub o los entornos protegidos de GitLab: no hay historial de despliegues por entorno ni rollback desde la interfaz, porque CircleCI no modela el entorno como objeto. Si quieres saber qué versión está en producción, lo construyes tú. Es la contrapartida directa de no ser una plataforma integrada.
- Recursos, clases de máquina y el modelo de créditos
jobs:
test:
docker: [ { image: cimg/node:22.11 } ]
resource_class: large # más CPU y memoria por minuto, y más créditos por minutoCircleCI factura por créditos, y esa es la parte del modelo que hay que entender antes de firmar. La idea: cada minuto de ejecución consume créditos, y cuántos depende de la clase de recurso. Una clase grande no cuesta lo mismo por minuto que una pequeña; los executors macos y las máquinas con GPU están en otro orden de magnitud (es exactamente el problema de coste que la 05-02 encontró con los runners macOS). Otras funcionalidades, como docker_layer_caching, también consumen. Además existe un componente por usuario activo.
No doy cifras: cambian, dependen del plan y de la región. Lo que no cambia es la forma de razonar sobre ellas:
- El coste es minutos × clase × concurrencia. Subir de
mediumalargesolo compensa si el tiempo baja más que el multiplicador. Se mide con una ejecución de cada, no se estima. - La paralelización multiplica el coste por el número de contenedores aunque el tiempo de pared baje. Cuatro contenedores durante 3 minutos consumen más que uno durante 8. Se paga velocidad con dinero, que es la decisión explícita que la 04-04 pedía tomar a conciencia.
- Un pipeline que se ejecuta dos veces por push —rama y PR— duplica la factura sin dar información nueva. Los
filtersestán para eso. - La caché mal configurada se paga en cada ejecución de cada rama.
Y una advertencia que vale para todos los SaaS por minuto: el coste crece con el equipo y con la actividad, no con el valor. Un equipo que triplica sus commits triplica la factura. Conviene tener alarmas de consumo y revisar mensualmente los jobs más caros, con la misma disciplina con que se revisa el tiempo de pipeline.
- Depurar: SSH into build y ejecución local
Dos herramientas que resuelven el problema de "falla en CI y no en mi máquina", que es donde más tiempo se pierde.
SSH into build: relanzar un job con SSH habilitado y conectarse a la máquina mientras corre.
# Tras pulsar "Rerun job with SSH" en la interfaz:
ssh -p 54782 [email protected]
# Ya dentro: el workspace está tal cual lo dejó el job
cd ~/reservalia && npm test -- apps/api/reservas.test.ts
env | grep -i databaseEs la forma más rápida de diagnosticar un fallo dependiente del entorno. Dos avisos serios: la sesión mantiene la máquina reservada y consumiendo créditos hasta que la cierras o expira; y el entorno contiene los secretos del job, así que quién puede abrir sesión SSH es una decisión de seguridad, no de comodidad (04-03). En un job con credenciales de producción, restringe esa capacidad.
Ejecución local con la CLI, para iterar sin gastar ejecuciones remotas:
circleci config validate # errores de sintaxis antes de hacer push
circleci config process .circleci/config.yml # expande orbs y parámetros: ver el YAML real
circleci local execute --job calidad # ejecuta un job en Docker localcircleci config process merece atención: muestra el fichero después de expandir orbs, commands y parámetros, y es la mejor forma de entender qué hace realmente un orb de terceros antes de darle tus secretos. circleci local execute tiene límites —no soporta workflows completos, ni workspaces entre jobs, ni contexts—, así que sirve para jobs sueltos, no para validar el pipeline entero. Aun así es una de las mejores respuestas del módulo a la pregunta "¿cómo pruebo el pipeline?" de la 04-05.
- Límites y contrapartidas
Con la misma franqueza que se aplicó a Jenkins y GitLab:
- Acoplamiento a un SaaS. El plano de control es de CircleCI: si el servicio no está disponible, no hay CI, ni siquiera con runners propios. Y hay una lección histórica concreta: en 2023 CircleCI comunicó un incidente de seguridad que obligó a rotar todos los secretos almacenados en el servicio. Es el mismo riesgo estructural que la 06-04 contará con Travis: confiar secretos a un tercero es una decisión con consecuencias, y tener un procedimiento de rotación masiva ensayado no es paranoia.
- No es una plataforma. No hay repositorio, ni registro, ni entornos, ni issues. Se integra con lo que tengas, y todo lo que en GitLab venía regalado aquí hay que montarlo.
- Ecosistema más pequeño. El catálogo de orbs es notablemente menor que el de acciones de GitHub. Para lo común hay orb; para lo raro, lo escribes.
- El coste crece con el equipo, y los executors caros (macOS, GPU) desequilibran una factura rápido.
- Runners autoalojados: existen, y resuelven el caso de "el trabajo debe correr en mi red o en mi hardware". Pero el plano de control sigue siendo SaaS, así que no resuelven la restricción de cumplimiento estricta que en la 06-01 llevaba a Jenkins. Conviene no confundir "el trabajo corre en mi máquina" con "todo el sistema está bajo mi control".
- Mercado. El peso relativo de CircleCI ha disminuido desde que GitHub Actions se integró en el sitio donde ya estaba el código de casi todo el mundo. Eso importa para contratar, para encontrar respuestas y para la longevidad de la herramienta.
- Cuándo elegir CircleCI
Sí, cuando:
- La velocidad del pipeline es un problema medido y caro, con una suite grande y lenta. El reparto por tiempos y las clases de recurso dan mejoras reales sin escribir infraestructura propia.
- Quieres un CI potente sin casarte con la plataforma del repositorio, o tienes proyectos en más de un proveedor de Git.
- Necesitas macOS, ARM o GPU de forma cómoda y sin operar máquinas: la oferta de executors es amplia (relevante para el caso móvil de la 05-02).
- Valoras herramientas de diagnóstico de primera —insights, SSH into build, validación local— y tienes quien las use.
- Ya lo tienes y funciona. Migrar cuesta, y la 06-07 pone números a ese cálculo.
No, cuando:
- Tu código está en GitHub y no tienes un problema de velocidad: GitHub Actions viene integrado y sin una segunda integración de permisos que mantener.
- Necesitas control total o cumplimiento estricto: el plano de control SaaS no lo permite (ahí está Jenkins, 06-01).
- Quieres una plataforma integrada: eso es GitLab (06-02).
- El presupuesto es una restricción dura y el equipo crece: el modelo por créditos y por usuario escala con la actividad.
Errores Comunes y Consejos
No llamar a store_test_results. Sin él, --split-by=timings no tiene histórico y el reparto degrada a alfabético, que es justo el desequilibrio que se quería evitar. Es el error que más veces convierte una buena idea en una decepción.
Claves de caché sin arquitectura ni versión de runtime. Restaurar una caché de otra arquitectura produce fallos de módulos nativos difíciles de leer. Incluye {{ arch }} y la versión.
Sin fallback en restore_cache. Con una sola clave exacta, cualquier cambio del lockfile obliga a descargar todo.
Usar caché para pasar artefactos entre jobs. Es lo que provoca desplegar un build viejo sin que nada falle. Para eso está el workspace.
No esperar a los contenedores secundarios. PostgreSQL tarda en aceptar conexiones; sin dockerize -wait tendrás flaky que dependen de la carga de la máquina.
Subir de resource_class sin medir. Duplicar la clase no siempre reduce el tiempo (un build limitado por E/S o por un test secuencial no mejora) y sí duplica el consumo.
parallelism alto por defecto. Multiplica el coste linealmente. El punto óptimo se busca midiendo tiempo total frente a créditos, y suele estar más bajo de lo que la intuición dice.
Dejar sesiones SSH abiertas. Reservan la máquina y consumen. Ciérralas.
Orbs sin anclar o de origen desconocido. Ejecutan con tus secretos. Ancla a versión exacta, revisa con circleci config process y prefiere orbs certificados o propios para lo que toca credenciales.
Consejo transversal: aunque uses commands y orbs, mantén la lógica real en scripts del repositorio (./scripts/desplegar.sh) y deja el YAML como orquestador. Es lo que hace que el ejercicio 3 de esta lección sea de horas y no de semanas, y es la lección de portabilidad que la 06-07 desarrollará.
Ejercicios
Ejercicio 1. El job test de Reservalia usa parallelism: 6 y tarda 9 minutos. Al mirar los contenedores: cinco terminan entre 1:30 y 2:10, y uno tarda 8:50. En el config.yml aparece circleci tests glob "apps/**/*.test.ts" | circleci tests split y no hay ningún store_test_results. Diagnostica, corrige y calcula qué pasa con el tiempo y con el consumo. ¿Convendría bajar parallelism?
Ejercicio 2. Reservalia tiene seis repositorios en CircleCI y el bloque de preparación y publicación repetido en todos. Diseña el orb reservalia/plataforma: qué contiene, cómo se versiona, cómo se prueba antes de publicar, cómo se despliega a los seis repositorios y qué política se aplica a los orbs de terceros que ya usan.
Ejercicio 3. Diego trae una hoja de cálculo: el consumo de CircleCI ha subido un 240 % en cuatro meses sin que el equipo haya crecido. Los datos: 68 % del consumo es del job e2e con resource_class: xlarge y parallelism: 8; se ejecutan dos pipelines por push (rama y PR); docker_layer_caching está activado en cuatro jobs; y hay 14 ramas con pipelines nocturnos programados que nadie mira. Escribe el plan de reducción, ordenado por relación entre ahorro y riesgo.
Soluciones
Solución 1.
Diagnóstico. circleci tests split sin --split-by=timings reparte por nombre de fichero, y sin store_test_results no hay histórico aunque se pidiera. El contenedor lento concentra los tests que tardan —probablemente los de integración con base de datos—. El job tarda lo que el peor contenedor: 8:50. Se pagan seis contenedores durante casi nueve minutos y se aprovecha uno.
Corrección:
test:
executor: node-base
parallelism: 4
steps:
- attach_workspace: { at: ~/reservalia }
- run: dockerize -wait tcp://localhost:5432 -timeout 1m
- run:
command: |
FICHEROS=$(circleci tests glob "apps/**/*.test.ts" \
| circleci tests split --split-by=timings)
npm test -- --runTestsByPath $FICHEROS --reporters=jest-junit
- store_test_results: { path: informes } # imprescindible
- store_artifacts: { path: cobertura }Efecto. Suma de trabajo real ≈ 5×1:50 + 8:50 ≈ 18 min. Repartido entre 4 contenedores, ~4:30 cada uno más arranque: el job pasa de 8:50 a unos 5 minutos. El consumo baja además por dos vías: menos contenedores (4 en vez de 6) y menos minutos por contenedor.
¿Bajar parallelism? Sí, y el razonamiento importa más que el número. Con 18 minutos de trabajo real:
parallelism |
Tiempo aproximado | Contenedor·minuto |
|---|---|---|
| 1 | 18:00 | 18 |
| 2 | 9:00 | 18 |
| 4 | 4:30 | 18 + arranques |
| 6 | 3:00 | 18 + más arranques |
| 12 | 1:30 | 18 + arranque dominante |
El trabajo total es casi constante, así que el coste por ejecución apenas cambia con el paralelismo —hasta que el arranque del contenedor deja de ser despreciable frente al trabajo asignado—. Por eso la respuesta es "4 está bien y 6 no era el problema": el problema era el reparto, no el número. La regla práctica: sube el paralelismo mientras cada contenedor siga haciendo bastante más trabajo del que cuesta arrancarlo, y arregla el reparto antes de tocar el número.
Solución 2.
Contenido del orb:
# src/@orb.yml
version: 2.1
description: Piezas comunes de CI/CD de Reservalia
executors:
node:
parameters: { version: { type: string, default: "22.11" } }
docker: [ { image: cimg/node:<< parameters.version >> } ]
resource_class: medium
commands:
preparar:
parameters:
version-cache: { type: string, default: "v2" }
steps:
- checkout
- restore_cache:
keys:
- npm-<< parameters.version-cache >>-{{ arch }}-{{ checksum "package-lock.json" }}
- npm-<< parameters.version-cache >>-{{ arch }}-
- run: npm ci --prefer-offline --no-audit
jobs:
build-publicar:
parameters:
dockerfile: { type: string }
repositorio: { type: string }
publicar: { type: boolean, default: false }
machine: { image: ubuntu-2404:current }
steps:
- preparar
- run: ./scripts/build-imagen.sh << parameters.dockerfile >> << parameters.repositorio >> << parameters.publicar >>Obsérvese que el job llama a un script del repositorio, no incrusta veinte líneas de shell en el YAML: eso mantiene la lógica portable y hace que el orb sea un orquestador delgado.
Versionado y publicación: versionado semántico obligatorio; cambios incompatibles suben major. El flujo es publish a @dev:<etiqueta> (mutable, para probar) y publish promote a una versión estable (inmutable). Los repositorios anclan a versión exacta @2.3.0, no a @2, para que ninguna adopción sea automática.
Pruebas antes de publicar: el propio repositorio del orb tiene un pipeline con circleci orb validate, circleci config process sobre un config.yml de ejemplo para comprobar que expande a lo esperado, y un proyecto de integración (reservalia-orb-sandbox) que consume @dev:pruebas y ejerce todos los commands en cada commit. Es el canario de la 04-05.
Despliegue a los seis repositorios: progresivo. Se publica 2.4.0, se adopta en un repositorio de bajo riesgo durante una semana, y solo entonces se abre un PR en los otros cinco. Nunca los seis a la vez, y nunca por adopción automática: el riesgo de radio grande es el mismo que en Jenkins y GitLab.
Política sobre orbs de terceros: lista permitida revisada por el equipo de plataforma; anclados a versión exacta; ningún orb de terceros en jobs que tienen contexts de producción salvo los certificados y revisados; y circleci config process como paso obligatorio de revisión al añadir uno nuevo, para ver qué comandos ejecuta realmente. Es la política de dependencias de la 04-02 aplicada al pipeline.
Solución 3. Ordenado por ahorro dividido por riesgo, que es el orden correcto para este tipo de trabajo:
| # | Acción | Ahorro estimado | Riesgo | Esfuerzo |
|---|---|---|---|---|
| 1 | filters para no ejecutar pipeline de rama y de PR a la vez |
~50 % de todo | Nulo | 1 h |
| 2 | Apagar los 14 pipelines nocturnos que nadie mira | Directo y medible | Bajo: si nadie los mira, no dan señal | 1 h |
| 3 | Arreglar el reparto de e2e (timings) y bajar parallelism de 8 a 4 |
Alto sobre el 68 % | Bajo | 0,5 día |
| 4 | Bajar e2e de xlarge a large y medir |
Medio | Bajo si se mide antes/después | 0,5 día |
| 5 | Quitar docker_layer_caching donde ya hay caché de registro |
Medio | Bajo: se compara el tiempo con y sin | 0,5 día |
| 6 | Ejecutar e2e completo solo en main y un subconjunto en PR |
Alto | Medio: menos señal antes de fusionar | 2 días |
Notas de criterio. El punto 1 es el más rentable del plan y el más invisible: no se manifiesta como lentitud, solo como factura, y por eso lleva meses ahí. Los puntos 3 y 4 hay que hacerlos en este orden y midiendo entre uno y otro, porque si se cambian a la vez no se sabe cuál causó qué; y xlarge puede estar justificado si el e2e usa navegadores reales, así que se comprueba, no se asume. El punto 6 es el único que cambia el contrato de calidad: reduce la señal antes de fusionar y por tanto puede subir el CFR, así que se decide con Marta, se escribe qué subconjunto se ejecuta y por qué, y se vigilan las métricas DORA durante un mes. Si el CFR sube del 3,8 % actual, se revierte.
Y la observación de método que hay que darle a Diego: el 240 % no vino de un cambio, vino de cuatro decisiones pequeñas que nadie revisó. La corrección estructural no es esta lista, es poner una alarma de consumo y una revisión mensual de los jobs más caros, con la misma disciplina con la que se revisa el tiempo de pipeline en la 04-04. Sin eso, en cuatro meses hay otro 240 %.
Conclusión
CircleCI es la herramienta del módulo que mejor responde a una pregunta concreta: cómo hacer que un pipeline grande sea rápido sin construir la infraestructura tú. Sus tres piezas distintivas son de aprendizaje transferible aunque nunca la uses: la caché explícita con claves y fallback te obliga a entender qué invalida qué; la separación nítida entre workspace, caché y artefactos aclara una confusión que en otras herramientas se puede arrastrar durante años; y el reparto de pruebas por tiempos históricos es la mejor solución disponible al problema de sharding, y saber que existe cambia lo que le pides a las demás.
Lo que se paga: es un SaaS y su plano de control no es tuyo —con el precedente de la rotación masiva de secretos de 2023 como recordatorio—, no es una plataforma y todo lo que GitLab regalaba aquí lo montas, el ecosistema es más pequeño, y el modelo por créditos crece con la actividad del equipo, lo que exige una vigilancia del gasto que no hay que dejar para cuando llegue la factura.
La siguiente lección cambia de registro. Travis CI fue el que inventó buena parte de lo que hoy damos por evidente —el fichero de CI en el repositorio, la matriz de versiones, el CI gratuito para código abierto— y hoy es, sobre todo, algo que te encontrarás heredado y una lección de historia con moraleja: sobre modelos de negocio que cambian bajo tus pies, sobre confiar secretos a un tercero, y sobre por qué conviene que tu pipeline no dependa demasiado de la herramienta que lo ejecuta. Veremos su modelo de fases fijas, el pipeline de Reservalia forzado a caber en él, y cómo migrar un .travis.yml a lo que uses hoy.
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
