Los cursos de CI/CD suelen fallar en el mismo punto: enseñan pipelines sobre un "hola mundo" que no tiene base de datos, ni migraciones, ni dos aplicaciones que compartan código, ni un equipo con opiniones distintas. Y luego, en un proyecto real, nada encaja. Este curso hace lo contrario: construiremos, paso a paso, el pipeline completo de una aplicación con la complejidad justa para que los problemas de verdad aparezcan. Esa aplicación se llama Reservalia. En esta lección la vamos a conocer a fondo: qué hace el producto, quién forma el equipo, cómo está organizado el repositorio, qué comandos existen ya —porque el pipeline no inventa nada, solo invoca lo que el proyecto ya sabe hacer—, qué entornos hay y a qué infraestructura desplegamos. Terminaremos con la hoja de ruta del curso aplicada a Reservalia y con lo que necesitas instalado para seguirlo. Todavía no escribiremos ningún workflow: la primera pieza real de pipeline se construye en la lección 02-02.

Contenido

  1. Qué es Reservalia
  2. El equipo: Marta, Diego y Nuria
  3. Cómo trabajan hoy (y por qué duele)
  4. La estructura del repositorio
  5. Los package.json y los comandos que el pipeline invocará
  6. El entorno de desarrollo local con Docker Compose
  7. Los tres entornos: dev, staging y prod
  8. La infraestructura AWS de destino
  9. La hoja de ruta del curso aplicada a Reservalia
  10. Cómo seguir el curso si no usas Node.js
  11. Qué necesitas instalado
  12. Errores comunes y consejos
  13. Ejercicios
  14. Conclusión

  1. Qué es Reservalia

Reservalia es una plataforma SaaS de reservas de cita para pequeños negocios de servicios: peluquerías, clínicas dentales, talleres mecánicos, fisioterapeutas, centros de estética. Es una empresa ficticia creada para este curso; cualquier parecido con la realidad es intencionado pero no real.

El producto tiene dos caras:

  • El panel del negocio. El dueño de la peluquería configura sus servicios (corte, color, tratamiento), sus profesionales, los horarios de apertura y sus vacaciones. Consulta la agenda del día, confirma citas y ve estadísticas básicas.
  • La página pública de reserva. El cliente final entra desde un enlace, ve los huecos libres, elige uno y reserva sin registrarse. Recibe una confirmación por correo y un recordatorio 24 horas antes.

Modelo de negocio: suscripción mensual por negocio, con tres planes. Ahora mismo tienen 340 negocios de pago y procesan unas 9.000 citas al mes.

1.1. Por qué Reservalia es un buen proyecto para aprender CI/CD

No es un ejemplo de juguete, y esto importa. Reservalia tiene exactamente los ingredientes que hacen interesante un pipeline:

Ingrediente Por qué complica el pipeline Dónde lo trataremos
Base de datos con esquema evolutivo Hay migraciones que aplicar, y aplicarlas mal rompe producción o pierde datos 04-06
Dos aplicaciones en un mismo repositorio ¿Se prueba todo en cada cambio o solo lo afectado? 04-04
Código compartido entre ellas Un cambio en tipos compartidos afecta a las dos 02-03
Datos sensibles (nombres, teléfonos, correos de clientes finales) Los entornos no pueden compartir datos reales; hay que gestionar secretos 04-03
Horario comercial con picos No se puede desplegar de cualquier manera a las 11:00 de un sábado 03-04
Terceros integrados (correo, pasarela de pago) Hay que decidir qué se simula en pruebas y qué no 02-04
Una aplicación móvil (a partir del módulo 5) El "despliegue" pasa por tiendas de aplicaciones, con sus reglas 05-02

Si consigues automatizar Reservalia, sabrás automatizar tu proyecto.

  1. El equipo: Marta, Diego y Nuria

Tres personas, tres puntos de vista. Los tres aparecerán durante todo el curso, y sus discusiones son las que probablemente tendrás tú con tu equipo.

2.1. Marta — tech lead

Lleva cuatro años en Reservalia y arrastra la responsabilidad de que el producto funcione. Es quien decide qué se despliega y cuándo, y quien recibe la llamada del comercial cuando un cliente se queja.

  • Qué le preocupa: que las incidencias no se repitan y poder justificar ante dirección el tiempo que el equipo dedica a "cosas que no son funcionalidades".
  • Su papel en el curso: aporta el punto de vista de negocio. Es quien pregunta "¿cuánto cuesta esto y qué ganamos?" (la lección 01-02) y quien pulsará el botón de aprobación en la puerta manual del despliegue a producción.
  • Su frase: "Prefiero desplegar diez veces al día y que cada despliegue me dé igual, que desplegar una vez a la semana y no dormir el jueves."

2.2. Diego — desarrollador backend

Tres años en la empresa. Escribe la mayor parte de apps/api y es, hoy, la única persona que sabe desplegar. Sabe perfectamente que el proceso actual es malo; lo que no ha tenido nunca es tiempo para arreglarlo.

  • Qué le preocupa: que el pipeline le ralentice. Le da pánico un CI que tarde 40 minutos en decirle si su cambio está bien.
  • Su papel en el curso: es el escéptico productivo. Cada vez que añadamos un paso al pipeline, Diego preguntará cuánto tiempo suma. Gracias a él, el pipeline final será rápido.
  • Su frase: "Si el CI tarda más que ir a por un café, lo acabaré ignorando."

2.3. Nuria — SRE

Se incorporó hace ocho meses, a media jornada compartida con otro producto. Montó la infraestructura de AWS a mano, desde la consola web, porque había que salir a producción ya. Lleva las guardias.

  • Qué le preocupa: que la infraestructura no esté documentada en ninguna parte salvo en su cabeza, y que un rollback hoy signifique recompilar y volver a subir por SFTP con clientes esperando.
  • Su papel en el curso: aporta la perspectiva de operación. Es quien insiste en la infraestructura como código (03-03), en el rollback (03-05) y en la monitorización (03-06).
  • Su frase: "Un despliegue que no se puede deshacer en cinco minutos no es un despliegue, es una apuesta."

  1. Cómo trabajan hoy (y por qué duele)

Recordemos el punto de partida, ahora con el detalle completo del proceso:

# El ritual del viernes en Reservalia. Duración: ~3 horas.
# Ejecutor: Diego, siempre Diego.

# 1) Se baja lo último de main (sin saber exactamente qué entra)
git checkout main && git pull

# 2) Compila en su portátil, con SU versión de Node y SUS node_modules
cd apps/api  && npm install && npm run build
cd ../web    && npm install && npm run build

# 3) Sube los ficheros por SFTP al único servidor de producción
sftp diego@reservalia-prod
#   > put -r apps/api/dist/*  /var/www/api/
#   > put -r apps/web/dist/*  /var/www/web/

# 4) Reinicia el proceso a mano
ssh diego@reservalia-prod 'pm2 restart api'

# 5) Aplica las migraciones pegando SQL en psql
psql -h reservalia-prod-db -U admin -d reservalia
#   > ALTER TABLE citas ADD COLUMN recordatorio_enviado boolean DEFAULT false;

# 6) Comprueba a ojo que la web carga y crea una cita de prueba

# 7) Escribe en el canal del equipo: "desplegado ✅"

Y los problemas de este ritual, enumerados sin piedad:

# Problema Consecuencia real que ya han sufrido
1 Build en el portátil de Diego La build no es reproducible; nadie puede recrear lo que hay en producción
2 npm install en vez de npm ci Una versión menor de una dependencia entró en producción sin que nadie la hubiera probado
3 Despliegue incremental por SFTP Ficheros de versiones antiguas siguen vivos en el servidor; el estado real es desconocido
4 Ventana de despliegue sin servicio Hay unos segundos en los que la API responde a medias
5 Migraciones a mano Nadie sabe con certeza qué migraciones se han aplicado ni en qué orden
6 Sin registro de qué se desplegó Ante un fallo, hay que adivinar qué cambió
7 Verificación visual Se dio por bueno un despliegue en el que el envío de correos estaba roto
8 Bus factor de 1 En las vacaciones de Diego en agosto, no se desplegó nada durante tres semanas
9 Rollback ≈ 1 hora En la última incidencia, el servicio estuvo degradado 40 minutos
10 Viernes por la tarde Dos fines de semana estropeados el último trimestre

Cada línea de esta tabla desaparecerá en algún momento del curso. Guárdala: en la lección final (07-06) la repasaremos entera.

  1. La estructura del repositorio

Reservalia tiene un único repositorio en github.com/reservalia/reservalia, organizado como monorepo con workspaces de npm. Esta es su estructura tal como está hoy, antes de empezar:

reservalia/
├── .github/
│   └── workflows/              ← vacío hoy; aquí vivirán nuestros pipelines
├── apps/
│   ├── api/                    ← Node.js 20 + TypeScript + Express + PostgreSQL
│   │   ├── src/
│   │   │   ├── index.ts              punto de entrada del servidor
│   │   │   ├── rutas/
│   │   │   │   ├── citas.ts          crear, listar y cancelar citas
│   │   │   │   ├── negocios.ts       alta y configuración de negocios
│   │   │   │   └── disponibilidad.ts cálculo de huecos libres
│   │   │   ├── dominio/
│   │   │   │   ├── cita.ts           reglas de negocio de una cita
│   │   │   │   └── agenda.ts         solapamientos, horarios, festivos
│   │   │   └── db/
│   │   │       ├── cliente.ts        conexión a PostgreSQL
│   │   │       └── migraciones/
│   │   │           ├── 0001_crear_negocios.sql
│   │   │           ├── 0002_crear_citas.sql
│   │   │           └── 0003_añadir_recordatorios.sql
│   │   ├── tests/
│   │   │   ├── unidad/               rápidos, sin base de datos
│   │   │   └── integracion/          contra PostgreSQL real
│   │   ├── Dockerfile          ← lo escribiremos en 02-03
│   │   ├── package.json
│   │   └── tsconfig.json
│   │
│   └── web/                    ← React + Vite + TypeScript
│       ├── src/
│       │   ├── main.tsx
│       │   ├── paginas/
│       │   │   ├── PanelNegocio.tsx
│       │   │   └── ReservaPublica.tsx
│       │   └── componentes/
│       ├── tests/
│       ├── Dockerfile          ← lo escribiremos en 02-03
│       ├── package.json
│       └── vite.config.ts
│
├── packages/
│   └── tipos-compartidos/      ← tipos TypeScript usados por api y web
│       ├── src/index.ts
│       └── package.json
│
├── infra/                      ← infraestructura como código (módulo 3)
│   ├── terraform/
│   │   ├── modulos/
│   │   └── entornos/
│   │       ├── staging/
│   │       └── prod/
│   └── README.md
│
├── docker-compose.yml          ← entorno de desarrollo local
├── package.json                ← raíz del monorepo, con workspaces
├── package-lock.json           ← ¡uno solo para todo el monorepo!
├── .nvmrc                      ← fija la versión de Node
└── README.md

Tres decisiones de esta estructura merecen explicación, porque condicionan todo el pipeline:

Monorepo con un único package-lock.json. Al usar workspaces de npm, hay un solo fichero de bloqueo en la raíz que gobierna las dependencias de api, web y tipos-compartidos. Ventaja: un único npm ci instala todo de forma coherente y reproducible. Consecuencia para el pipeline: la instalación se hace una vez en la raíz, no una por aplicación.

packages/tipos-compartidos. Aquí viven los tipos que la API y la web comparten (por ejemplo, la forma de una Cita). Es lo que garantiza que si Diego cambia el modelo de datos, la web deje de compilar en CI en lugar de romperse en producción. También significa que un cambio en ese paquete obliga a probar las dos aplicaciones: es la complicación que hace interesante el "ejecutar solo lo afectado" de la lección 04-04.

.nvmrc. Un fichero de una línea que dice qué versión de Node usa el proyecto. Es la pieza más barata de reproducibilidad que existe:

20.11.0

Con eso, el portátil de Diego, el de Marta y el runner de CI usan exactamente la misma versión. En la lección 01-01 vimos que la versión no fijada es uno de los enemigos clásicos de la build reproducible; este fichero lo resuelve.

  1. Los package.json y los comandos que el pipeline invocará

Aquí está una idea central del curso, y conviene subrayarla:

El pipeline no inventa nada. Solo ejecuta, en una máquina limpia, los mismos comandos que tú ejecutas en tu portátil.

Por eso, antes de escribir ningún workflow, el proyecto debe tener sus comandos bien definidos. Si npm test no funciona en tu máquina, no funcionará en CI. Si funciona en tu máquina pero solo porque tienes una variable de entorno que nadie más tiene, fallará en CI. Un pipeline es, sobre todo, un revelador de suposiciones ocultas.

5.1. package.json de la raíz

{
  "name": "reservalia",
  "private": true,
  "workspaces": [
    "apps/*",
    "packages/*"
  ],
  "engines": {
    "node": ">=20.11.0 <21"
  },
  "scripts": {
    "build":     "npm run build --workspaces --if-present",
    "test":      "npm run test --workspaces --if-present",
    "lint":      "npm run lint --workspaces --if-present",
    "typecheck": "npm run typecheck --workspaces --if-present",
    "dev":       "docker compose up -d && npm run dev --workspace apps/api"
  }
}

Punto por punto, porque cada línea tiene un motivo:

  • "private": true evita que el paquete raíz se publique por accidente en el registro público de npm. En un monorepo es obligatorio.
  • "workspaces" declara los subproyectos. Al ejecutar npm ci en la raíz, npm instala las dependencias de todos ellos y crea los enlaces entre tipos-compartidos y las aplicaciones que lo usan.
  • "engines" documenta la versión de Node soportada. Combinado con .nvmrc, deja constancia explícita del requisito.
  • --workspaces --if-present ejecuta el script en cada subproyecto que lo tenga definido, y no falla en los que no lo tengan. Es lo que permite que un solo npm test en la raíz pruebe la API, la web y el paquete compartido.

Con esto, el pipeline entero podría reducirse a cuatro comandos. Compruébalo:

npm ci             # instalación reproducible de todo el monorepo
npm run lint       # estilo y errores evidentes
npm run typecheck  # coherencia de tipos entre api, web y tipos-compartidos
npm run test       # pruebas de todos los subproyectos
npm run build      # artefactos compilados

Estos cinco comandos son, literalmente, el esqueleto del pipeline que construiremos en el módulo 2.

5.2. package.json de apps/api

{
  "name": "@reservalia/api",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "dev":             "tsx watch src/index.ts",
    "build":           "tsc --project tsconfig.build.json",
    "start":           "node dist/index.js",
    "test":            "vitest run",
    "test:unidad":     "vitest run tests/unidad",
    "test:integracion":"vitest run tests/integracion",
    "lint":            "eslint src tests --max-warnings 0",
    "typecheck":       "tsc --noEmit",
    "migrate":         "node dist/db/migrar.js",
    "migrate:status":  "node dist/db/migrar.js --status"
  },
  "dependencies": {
    "@reservalia/tipos-compartidos": "*",
    "express": "4.19.2",
    "pg": "8.11.5",
    "zod": "3.23.8"
  },
  "devDependencies": {
    "@types/express": "4.17.21",
    "eslint": "8.57.0",
    "tsx": "4.7.1",
    "typescript": "5.4.5",
    "vitest": "1.6.0"
  }
}

Detalles importantes para el pipeline:

  • test:unidad y test:integracion están separados. Las unitarias son rápidas y no necesitan base de datos; las de integración necesitan un PostgreSQL levantado. Esta separación es la que nos permitirá, en 02-04, dar feedback rápido a Diego: primero las rápidas, y solo si pasan, las lentas.
  • --max-warnings 0 en el lint. Sin esto, ESLint termina con código de salida 0 aunque haya avisos, y el pipeline se pondría verde con problemas dentro. Recuerda de 01-03: el pipeline solo entiende de códigos de salida.
  • typecheck separado de build. tsc --noEmit comprueba los tipos sin generar ficheros. Es rápido y puede correr en paralelo con los tests.
  • migrate existe como script. Hoy Diego pega SQL a mano; el proyecto ya tiene el comando, solo que nadie lo usa. En la lección 04-06 lo integraremos en el despliegue.
  • Versiones exactas, sin ^. "express": "4.19.2" y no "^4.19.2". Junto con npm ci, garantiza que dos instalaciones den lo mismo. La estrategia completa de dependencias es materia de 04-02.

5.3. package.json de apps/web

{
  "name": "@reservalia/web",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "dev":       "vite",
    "build":     "vite build",
    "preview":   "vite preview",
    "test":      "vitest run",
    "lint":      "eslint src tests --max-warnings 0",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "@reservalia/tipos-compartidos": "*",
    "react": "18.3.1",
    "react-dom": "18.3.1"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "4.2.1",
    "typescript": "5.4.5",
    "vite": "5.2.11",
    "vitest": "1.6.0"
  }
}

Fíjate en que los nombres de los scripts coinciden con los de la API: build, test, lint, typecheck. Esta convención no es cosmética: es lo que hace que npm run test --workspaces funcione y lo que permitirá que el pipeline trate ambas aplicaciones con la misma lógica.

Consejo transferible: unificar los nombres de los scripts entre subproyectos es una de las inversiones más rentables antes de montar un pipeline. Si en un proyecto el comando es npm test y en otro npm run test:ci, tu YAML se llenará de casos especiales.

  1. El entorno de desarrollo local con Docker Compose

Para que las pruebas de integración funcionen —tanto en el portátil como después en CI— hace falta un PostgreSQL. Reservalia lo levanta con Docker Compose:

# docker-compose.yml — entorno de desarrollo local de Reservalia
services:
  db:
    image: postgres:16.3          # versión FIJADA, igual que en RDS
    container_name: reservalia-db
    environment:
      POSTGRES_USER: reservalia
      POSTGRES_PASSWORD: desarrollo    # ⚠️ solo local, jamás en otro entorno
      POSTGRES_DB: reservalia
    ports:
      - "5432:5432"               # accesible desde el portátil
    volumes:
      - datos-db:/var/lib/postgresql/data
    healthcheck:                  # ¿está la base de datos LISTA, no solo arrancada?
      test: ["CMD-SHELL", "pg_isready -U reservalia -d reservalia"]
      interval: 5s
      timeout: 3s
      retries: 10

  mailpit:
    image: axllent/mailpit:v1.18   # captura los correos en local
    ports:
      - "1025:1025"                # el SMTP al que apunta la API
      - "8025:8025"                # interfaz web para leerlos

volumes:
  datos-db:

Cuatro cosas que explicar, porque todas reaparecerán en el pipeline:

La versión está fijada: postgres:16.3. No postgres:latest. Si en local desarrollas contra PostgreSQL 16 y producción corre 15, tarde o temprano una consulta funcionará en tu máquina y fallará en producción. La versión de la base de datos es parte de la reproducibilidad.

El healthcheck es la línea más importante del fichero. Un contenedor de PostgreSQL "arrancado" no significa "listo para aceptar conexiones": hay unos segundos de inicialización. Sin healthcheck, las pruebas de integración fallan de forma intermitente porque la base de datos aún no acepta conexiones. Y eso es exactamente un test flaky, el veneno del que hablábamos en 01-02. La causa número uno de flakiness en CI es no esperar a que los servicios estén realmente listos.

Mailpit sustituye al proveedor de correo real. Reservalia envía confirmaciones y recordatorios. En local y en pruebas, esos correos no deben salir a internet: Mailpit los captura y los muestra en una web. La regla general que aplicaremos en 02-04: en pruebas, ningún servicio externo real.

La contraseña desarrollo está escrita en claro, y está bien. Porque es una base de datos local, efímera y sin datos reales. En staging y prod jamás haremos esto: allí las credenciales vienen de un gestor de secretos. Distinguir cuándo un valor es un secreto y cuándo no es parte de la materia de 04-03.

El flujo de trabajo diario de Diego, en cuatro comandos:

git clone https://github.com/reservalia/reservalia.git
cd reservalia

nvm use              # lee .nvmrc → Node 20.11.0
npm ci               # instala todo el monorepo de forma reproducible
docker compose up -d # levanta PostgreSQL y Mailpit
npm run dev          # arranca la API en modo desarrollo

Cuando en el módulo 2 configuremos el CI, verás que el pipeline hace exactamente lo mismo: preparar Node, instalar dependencias, levantar los servicios necesarios y ejecutar comandos. No hay magia.

  1. Los tres entornos: dev, staging y prod

Un entorno (definido en 01-01) es una instancia desplegada y ejecutable del sistema, con su propia configuración y sus propios datos. Reservalia tendrá tres:

Aspecto dev staging prod
Para qué sirve Que el equipo pruebe cambios en un entorno compartido y real Ensayo general: la última verificación antes de producción Los clientes de verdad
Quién lo usa Marta, Diego, Nuria El equipo, antes de aprobar un despliegue 340 negocios y sus clientes
Qué se despliega Cada merge a main, automáticamente Cada merge a main, tras pasar dev Solo tras aprobación manual de Marta
Datos Datos ficticios generados, se pueden borrar Datos ficticios con volumen realista Datos reales de clientes
Base de datos RDS db.t4g.micro RDS db.t4g.micro RDS db.t4g.medium, multi-AZ y con copias de seguridad
Instancias de la API 1 1 2 como mínimo, con escalado automático
Correo Capturado, no sale Capturado, no sale Proveedor real
Pasarela de pago Modo simulado Modo simulado Modo real
Quién puede acceder Solo el equipo Solo el equipo Público
Si se cae No pasa nada No pasa nada Incidente

Dos principios que rigen los tres entornos y que conviene interiorizar ya:

Principio 1: el mismo artefacto en los tres. Es la regla de promoción de 01-01. La imagen reservalia/api:a3f9c21 que se prueba en dev es exactamente la misma que llega a staging y a prod. Lo único que cambia entre entornos es la configuración inyectada por fuera:

# dev
DATABASE_URL=postgres://[email protected]:5432/reservalia
LOG_LEVEL=debug
PASARELA_PAGO_MODO=simulado
SMTP_HOST=mailpit.interno

# prod
DATABASE_URL=postgres://[email protected]:5432/reservalia
LOG_LEVEL=info
PASARELA_PAGO_MODO=real
SMTP_HOST=smtp.proveedor-correo.com

Principio 2: los datos de producción nunca salen de producción. Copiar la base de datos de prod a staging "para probar con datos reales" es una práctica frecuente y una muy mala idea: Reservalia guarda nombres, teléfonos y correos de clientes finales. staging usa datos generados con volumen realista, no datos reales.

  1. La infraestructura AWS de destino

Nuria montó esto a mano desde la consola de AWS. En el módulo 3 lo pasaremos a Terraform para que sea reproducible; de momento, esta es la foto de a dónde despliega el pipeline:

flowchart TB
    U["👥 Usuarios<br/>negocios y sus clientes"] --> CF["CloudFront + S3<br/>apps/web (estático)"]
    U --> ALB["Application Load Balancer<br/>api.reservalia.com"]

    subgraph AWS["AWS · región eu-west-1"]
        ALB --> ECS

        subgraph ECS["ECS Fargate · servicio reservalia-api"]
            T1["Tarea 1<br/>contenedor api:a3f9c21"]
            T2["Tarea 2<br/>contenedor api:a3f9c21"]
        end

        ECS --> RDS[("RDS PostgreSQL 16<br/>multi-AZ en prod")]
        ECS --> SM["Secrets Manager<br/>credenciales de BD y APIs"]
        ECS --> CW["CloudWatch Logs"]

        ECR[("ECR<br/>registro de imágenes")] -.->|"descarga la imagen<br/>al desplegar"| ECS
    end

    GHA["⚙️ GitHub Actions"] -->|"1· publica la imagen"| ECR
    GHA -->|"2· actualiza el servicio"| ECS
    GHA -->|"3· sube los estáticos"| CF

    style ECR fill:#d9f2d9
    style GHA fill:#cfe8ff

Qué hace cada pieza y por qué está ahí:

Pieza Función Por qué esta y no otra
ECR (Elastic Container Registry) Guarda las imágenes Docker etiquetadas con el SHA del commit Es el registro de artefactos: la pieza que hace posible construir una vez y promocionar
ECS Fargate Ejecuta los contenedores de la API sin administrar servidores Orquestación sin la complejidad de Kubernetes: proporcional a un equipo de tres personas (ver 01-03)
ALB (Application Load Balancer) Reparte el tráfico entre las tareas y comprueba su salud Permite despliegues sin corte: retira una tarea vieja solo cuando la nueva responde correctamente
RDS PostgreSQL La base de datos gestionada Copias de seguridad, parches y alta disponibilidad sin trabajo manual
S3 + CloudFront Sirven la web compilada como ficheros estáticos La web de Vite es HTML, CSS y JS: no necesita servidor
Secrets Manager Guarda credenciales y las inyecta en tiempo de ejecución Los secretos no viajan en la imagen ni en el repositorio (04-03)
CloudWatch Logs Recoge los registros de las tareas Base para la monitorización de 03-06

Y el recorrido completo de un cambio, del portátil de Diego al cliente final:

sequenceDiagram
    participant D as Diego
    participant GH as GitHub
    participant GA as GitHub Actions
    participant ECR as ECR
    participant ECS as ECS Fargate
    participant U as Usuario

    D->>GH: push a rama + pull request
    GH->>GA: trigger del pipeline
    GA->>GA: npm ci · lint · typecheck · test
    GA-->>GH: ✅ verde, se puede fusionar
    D->>GH: merge a main
    GH->>GA: trigger del pipeline de despliegue
    GA->>GA: docker build → imagen etiquetada con el SHA
    GA->>ECR: push de reservalia/api:a3f9c21
    GA->>ECS: desplegar en dev y staging
    GA->>GA: pruebas de humo en staging
    GA-->>GH: ⏸️ esperando aprobación de Marta
    Note over GA: puerta manual = Entrega Continua
    GA->>ECS: promocionar el MISMO artefacto a prod
    ECS->>U: nueva versión sirviendo tráfico

Este diagrama es el objetivo del curso. Al terminar el módulo 3, Reservalia tendrá exactamente este flujo funcionando.

  1. La hoja de ruta del curso aplicada a Reservalia

Qué habrá cambiado en Reservalia al final de cada módulo:

Módulo Qué habremos automatizado Estado del ritual del viernes
1. Introducción (estás aquí) Nada aún: vocabulario, criterios, proyecto y métricas de partida Intacto: 3 h, manual
2. Integración Continua Cada PR ejecuta npm ci, lint, typecheck, tests unitarios y de integración contra PostgreSQL. Se construyen imágenes Docker etiquetadas con el SHA y se publican en ECR. Se protege main para que nada entre en rojo El despliegue sigue siendo manual, pero ya nada llega roto a main. Diego deja de compilar a ciegas
3. Despliegue Continuo Despliegue automático a dev y staging; puerta manual para prod; infraestructura en Terraform; despliegue progresivo; feature flags; rollback automático; monitorización El ritual del viernes desaparece. Se despliega cuando hace falta, en minutos, y se puede deshacer
4. Prácticas avanzadas Pipeline reutilizable y probado; gestión de dependencias y actualizaciones; escaneo de seguridad y gestión de secretos; optimización de tiempo y coste; migraciones de base de datos seguras y reversibles Diego deja de tocar psql para siempre. El pipeline baja de 14 a menos de 8 minutos
5. Proyectos reales Se incorpora apps/mobile (React Native) con su propio flujo hacia las tiendas; se estudia la separación en microservicios y cómo modernizar un legacy El pipeline soporta tres aplicaciones con necesidades distintas
6. Herramientas El mismo pipeline reexpresado en Jenkins, GitLab CI, CircleCI y Travis CI; contenedores y Kubernetes; GitHub Actions a fondo El conocimiento del equipo deja de depender de una herramienta
7. Ejercicios Reconstrucción práctica de todo, de principio a fin, por tu cuenta Lo sabes hacer tú, no solo leerlo
8. Recursos Rutas de aprendizaje, comunidades, certificaciones

  1. Cómo seguir el curso si no usas Node.js

Reservalia usa Node.js y TypeScript, pero el curso no trata sobre Node.js. Trata sobre pipelines. Si tu día a día es Python, Java, Go, PHP, Ruby o .NET, todo lo que aprendas se aplica igual: lo único que cambia son los comandos concretos de cada paso.

Aquí está la tabla de traducción. Guárdala:

Paso del pipeline Node.js (el curso) Python Java (Maven) Go PHP .NET
Fijar la versión .nvmrc .python-version pom.xml + toolchain go.mod composer.json global.json
Instalar dependencias npm ci pip install -r requirements.txt mvn dependency:go-offline go mod download composer install --no-dev dotnet restore
Fichero de bloqueo package-lock.json requirements.txt fijado / poetry.lock pom.xml con versiones exactas go.sum composer.lock packages.lock.json
Análisis estático eslint ruff / flake8 checkstyle / spotbugs go vet phpstan dotnet format
Comprobación de tipos tsc --noEmit mypy (el compilador) (el compilador) phpstan (el compilador)
Ejecutar pruebas npm test (vitest) pytest mvn test go test ./... phpunit dotnet test
Construir npm run build (empaquetado o imagen) mvn package go build (imagen) dotnet publish
Artefacto resultante imagen Docker imagen Docker / wheel .jar o imagen binario o imagen imagen imagen
Migraciones de BD script migrate alembic upgrade head flyway migrate migrate up doctrine:migrations:migrate dotnet ef database update

Lo que es idéntico en todos los lenguajes —y es la inmensa mayoría del curso:

  • Los conceptos: trigger, job, etapa, runner, artefacto, entorno, promoción.
  • El criterio de éxito o fracaso: el código de salida de cada comando.
  • La estructura del pipeline: instalar → verificar → construir → publicar → desplegar.
  • La estrategia de entornos y la regla de construir una vez y promocionar.
  • Los contenedores: un Dockerfile de Python y uno de Node se parecen muchísimo.
  • Las estrategias de despliegue, el rollback, los feature flags y la monitorización.
  • Las métricas DORA y todo lo relativo a seguridad y secretos.

Sugerencia práctica: si quieres sacar el máximo partido, adapta cada ejercicio a tu propio proyecto. Cuando en 02-02 configuremos npm ci && npm test, monta en paralelo el equivalente con pip install && pytest en un repositorio tuyo. Aprenderás el doble.

  1. Qué necesitas instalado

Para leer el curso y entender los ejemplos: nada. Todos los ficheros están completos en las lecciones.

Para reproducir los ejemplos, que es muy recomendable:

Herramienta Para qué Cómo comprobar que la tienes
Git Clonar, ramificar, hacer commits git --version
Cuenta de GitHub Alojar tu repositorio y ejecutar Actions (gratis en repositorios públicos)
Node.js 20 Ejecutar el proyecto de ejemplo node --version
Docker + Docker Compose PostgreSQL local y construcción de imágenes docker --version y docker compose version
Un editor con soporte YAML Escribir workflows sin pelearte con la indentación

Opcionales, y solo a partir del módulo 3:

Herramienta Para qué Nota
Cuenta de AWS Desplegar de verdad en ECS y RDS Tiene coste. Puedes seguir el módulo 3 sin ella: todos los ficheros son legibles y aplicables después
AWS CLI Interactuar con AWS desde la terminal aws --version
Terraform u OpenTofu Infraestructura como código terraform --version

Comprobación rápida de tu entorno:

# Ejecuta esto y comprueba que no falta nada esencial.
echo "--- Imprescindible ---"
git --version            || echo "❌ falta Git"
node --version           || echo "❌ falta Node.js"
docker --version         || echo "❌ falta Docker"
docker compose version   || echo "❌ falta Docker Compose"

echo "--- Opcional (módulo 3 en adelante) ---"
aws --version            || echo "ℹ️  AWS CLI no instalado (opcional)"
terraform --version      || echo "ℹ️  Terraform no instalado (opcional)"

Si no puedes instalar nada (por ejemplo, en un ordenador corporativo restringido): puedes seguir prácticamente todo el curso creando un repositorio en GitHub desde el navegador y editando los workflows desde la interfaz web. Los runners de GitHub ejecutan en la nube. Es menos cómodo, pero funciona.

Errores Comunes y Consejos

Error 1: querer automatizar antes de que los comandos funcionen en local. Si npm test no funciona en tu portátil, no funcionará en CI. El pipeline es un revelador de suposiciones ocultas: variables de entorno que solo tú tienes, servicios que están arrancados en tu máquina desde hace semanas, ficheros que nunca subiste al repositorio. Primero haz que funcione en una máquina limpia; luego automatízalo.

Error 2: no fijar las versiones de las herramientas. .nvmrc, engines, postgres:16.3 en lugar de postgres:latest, dependencias sin ^. Cada versión no fijada es una build que algún día dejará de funcionar sin que nadie haya tocado nada, y ese es el tipo de fallo más caro de diagnosticar.

Error 3: nombres de scripts distintos en cada subproyecto. Si apps/api usa npm test y apps/web usa npm run test:ci, tu pipeline se llenará de casos especiales. Unifica los nombres antes de escribir el primer workflow: es media hora de trabajo que ahorra días.

Error 4: copiar la base de datos de producción a staging. Es tentador ("probamos con datos reales") y es una fuga de datos personales esperando a ocurrir. Reservalia guarda teléfonos y correos de clientes finales. Genera datos ficticios con volumen realista.

Error 5: olvidar el healthcheck de los servicios auxiliares. "Contenedor arrancado" no es "servicio listo". Es la causa número uno de tests intermitentes en CI, y como vimos en 01-02, los tests flaky destruyen la confianza en el pipeline entero.

Consejo 1: si tienes un proyecto propio, úsalo en paralelo. Aplica cada lección a Reservalia y también a tu proyecto. La transferencia de conocimiento es mucho mayor cuando te enfrentas a las particularidades de tu propio código.

Consejo 2: escribe hoy tu propia tabla del ritual del viernes. Enumera todos los pasos manuales de tu proceso de despliegue actual, con su duración. Es una lista incómoda de leer y es exactamente el mapa de lo que vas a automatizar.

Consejo 3: un README.md que funcione es el primer paso de CI. Si un desarrollador nuevo puede clonar el repositorio y tener el proyecto funcionando siguiendo el README sin preguntar a nadie, tu proyecto está listo para automatizarse. Si no, arregla eso primero: el pipeline es exactamente ese README ejecutado por una máquina.

Ejercicios

Ejercicio 1: deducir el pipeline a partir del proyecto

Sin escribir nada de YAML —todavía no toca—, y usando únicamente lo que sabes del repositorio de Reservalia, responde:

  1. ¿En qué orden ejecutarías npm ci, lint, typecheck, test:unidad, test:integracion y build? Justifica el orden.
  2. ¿Cuáles de esos pasos pueden ejecutarse en paralelo y cuáles obligatoriamente en serie?
  3. ¿Qué paso necesita que haya un PostgreSQL levantado, y qué implica eso para el runner?
  4. Un cambio toca solo apps/web/src/paginas/PanelNegocio.tsx. ¿Sería correcto ejecutar únicamente las pruebas de apps/web? ¿Y si el cambio tocara packages/tipos-compartidos/src/index.ts?

Ejercicio 2: diseñar la matriz de configuración por entorno

Reservalia necesita decidir, para cada valor de configuración, si es un secreto (va al gestor de secretos), una variable de entorno normal (puede estar en el repositorio) o algo que no debe existir en ese entorno. Completa la tabla y justifica los tres casos que te parezcan menos evidentes:

Valor dev staging prod
DATABASE_URL
Contraseña de la base de datos
LOG_LEVEL
Clave de API de la pasarela de pago
SMTP_HOST
Nombre del repositorio de ECR
Clave de firma de los tokens de sesión

Ejercicio 3: traducir Reservalia a tu tecnología

Imagina que Reservalia estuviera escrita en Python con FastAPI en vez de Node.js con Express. Escribe:

  1. La estructura equivalente de apps/api/ (nombres de ficheros de configuración y de gestión de dependencias).
  2. El equivalente de los cinco comandos del pipeline (npm ci, lint, typecheck, test, build).
  3. Qué partes del docker-compose.yml cambiarían y cuáles no.
  4. Qué partes del curso dejarían de aplicar. (Pista: pocas.)

Soluciones

Solución al Ejercicio 1

1. Orden y justificación

1. npm ci               ← imprescindible primero: sin dependencias no hay nada
2. lint  ·  typecheck   ← rápidos, detectan errores evidentes, sin infraestructura
3. test:unidad          ← rápidos, sin base de datos
4. test:integracion     ← lentos, requieren PostgreSQL
5. build                ← solo tiene sentido si todo lo anterior está verde

El principio que gobierna este orden se llama fail fast: coloca primero lo más rápido y lo que más probablemente falle. Si Diego se ha dejado un console.log o un tipo incompatible, quieres que lo sepa en 40 segundos, no después de esperar seis minutos a las pruebas de integración. Ordenar el pipeline al revés funciona igual de bien técnicamente, pero desperdicia el tiempo del equipo en cada fallo.

2. Paralelo frente a serie

  • npm ci debe ir primero y solo: todo depende de él.
  • lint, typecheck y test:unidad pueden ir en paralelo entre sí: son independientes, no comparten estado y ninguno necesita el resultado del otro.
  • test:integracion puede ir en paralelo con los anteriores si el runner puede levantar PostgreSQL a la vez, aunque suele ejecutarse después para no pagar el coste de levantar la base de datos cuando algo trivial ya ha fallado. Es una decisión de compromiso entre velocidad y coste.
  • build va al final: construir un artefacto de código que no pasa las pruebas es tiempo y dinero tirados.

Un matiz sobre typecheck en este monorepo concreto: como apps/web depende de packages/tipos-compartidos, la comprobación de tipos de la web puede requerir que el paquete compartido esté compilado. En proyectos con referencias de proyecto de TypeScript esto se resuelve solo; en otros, obliga a un build parcial previo. Es el tipo de detalle que descubres al montar el pipeline y que resolveremos en 02-03.

3. PostgreSQL en el runner

test:integracion lo necesita. Implicaciones para el runner:

  • El runner debe poder ejecutar contenedores (los runners alojados de GitHub pueden).
  • Hay que esperar a que la base de datos esté lista, no solo arrancada: healthcheck o espera activa. Sin esto, tests flaky garantizados.
  • Hay que aplicar las migraciones antes de ejecutar las pruebas, para que el esquema exista.
  • Cada ejecución debe partir de una base de datos limpia, o el estado de una prueba contaminará a la siguiente.

4. Ejecución selectiva

  • Cambio solo en apps/web/src/paginas/PanelNegocio.tsx: sí, sería razonable ejecutar únicamente las pruebas de apps/web. La API no puede verse afectada por un cambio en un componente de React. Esto es ejecución selectiva y es una de las principales palancas de optimización (04-04).
  • Cambio en packages/tipos-compartidos/src/index.ts: no. Ese paquete es una dependencia de las dos aplicaciones, así que hay que probar ambas. Este es precisamente el caso que hace peligrosa la ejecución selectiva mal implementada: si tu regla es "solo pruebo la carpeta que cambió", un cambio en el paquete compartido no probaría nada y podría romper las dos aplicaciones a la vez. La ejecución selectiva debe basarse en el grafo de dependencias, no en la ruta del fichero.

Solución al Ejercicio 2

Valor dev staging prod
DATABASE_URL (sin contraseña) Variable normal Variable normal Variable normal
Contraseña de la base de datos Secreto Secreto Secreto
LOG_LEVEL Variable normal (debug) Variable normal (debug) Variable normal (info)
Clave de API de la pasarela de pago Secreto (clave de pruebas) Secreto (clave de pruebas) Secreto (clave real)
SMTP_HOST Variable normal Variable normal Variable normal
Nombre del repositorio de ECR Variable normal Variable normal Variable normal
Clave de firma de tokens de sesión Secreto Secreto Secreto, distinta de las otras dos

Los tres casos menos evidentes:

DATABASE_URL. La clave está en separar la cadena de conexión de la contraseña. postgres://[email protected]:5432/reservalia no contiene ningún secreto: es un nombre de host interno que, sin credenciales y sin acceso de red, no sirve de nada. La contraseña se inyecta aparte. Meter la contraseña dentro de la URL es cómodo y convierte un valor público en un secreto, con todo lo que eso implica: no se puede loguear, no se puede poner en el repositorio, no se puede mostrar en un mensaje de error.

Clave de la pasarela de pago en dev y staging. Aunque sean claves de entorno de pruebas y no muevan dinero real, siguen siendo secretos: permiten hacer llamadas en nombre de Reservalia, consultar datos y agotar cuotas. Una clave de pruebas filtrada en un repositorio público es un incidente de seguridad menor, pero es un incidente. Regla práctica: si el proveedor la llama "clave secreta", es un secreto.

Clave de firma de tokens: distinta en cada entorno. Este es el caso más sutil y el más importante. Si staging y prod compartieran la clave de firma, un token emitido en staging —donde el equipo tiene acceso total y puede crear el usuario que quiera— sería válido en producción. Es una escalada de privilegios de manual. Los secretos criptográficos jamás se comparten entre entornos, ni siquiera "temporalmente para probar".

Un criterio general para clasificar: si al filtrarse permite a alguien hacer algo que no debería, es un secreto. Si solo revela cómo se llaman las cosas, es configuración. Volveremos sobre ello en 04-03.

Solución al Ejercicio 3

1. Estructura equivalente en Python/FastAPI

apps/api/
├── src/
│   ├── main.py                punto de entrada (en vez de index.ts)
│   ├── rutas/
│   ├── dominio/
│   └── db/
│       └── migraciones/       gestionadas por Alembic
├── tests/
│   ├── unidad/
│   └── integracion/
├── pyproject.toml             ← equivalente a package.json
├── poetry.lock                ← equivalente a package-lock.json
├── .python-version            ← equivalente a .nvmrc
├── alembic.ini                configuración de migraciones
└── Dockerfile

2. Los cinco comandos

Paso Node.js Python/FastAPI
Instalar npm ci poetry install --sync (respeta el lock, como npm ci)
Lint npm run lint ruff check src tests
Tipos npm run typecheck mypy src
Pruebas npm test pytest
Construir npm run build No hay compilación: el "build" es directamente docker build
Migraciones npm run migrate alembic upgrade head

Una observación interesante: Python no tiene paso de compilación, así que el artefacto se produce directamente al construir la imagen Docker. El pipeline tiene un paso menos, pero el resto de la estructura es idéntica.

3. Cambios en docker-compose.yml

  • No cambia nada del servicio db: PostgreSQL 16.3, su healthcheck, su volumen y sus variables son exactamente iguales. La base de datos no sabe en qué lenguaje está escrita la aplicación.
  • No cambia nada del servicio mailpit: captura SMTP, sea quien sea el emisor.
  • Cambiaría únicamente el servicio de la aplicación, si se añadiera: la imagen base (python:3.12-slim en vez de node:20-slim) y el comando de arranque (uvicorn src.main:app --reload).

Es un buen indicador de cuánto de este curso es independiente del lenguaje: la infraestructura de desarrollo es prácticamente la misma.

4. Qué partes del curso dejarían de aplicar

Casi ninguna. Concretamente:

  • Deja de aplicar: los comandos exactos de npm, la sintaxis de package.json y los detalles de las herramientas concretas de JavaScript (vitest, ESLint, tsc).
  • Sigue aplicando íntegro: todo el vocabulario de 01-01; los beneficios y costes de 01-02; el mapa de herramientas de 01-03; las métricas DORA de 01-05; todo el módulo 3 (estrategias de despliegue, IaC, feature flags, rollback, monitorización); todo el módulo 4 salvo los ejemplos de sintaxis; el módulo 6 completo; y la estructura, el orden y la lógica de cada pipeline que construyamos.

Estimación honesta: más del 85 % del curso es independiente del lenguaje. Lo que cambia son las cadenas de texto dentro de los pasos run:.

Conclusión

Ya conoces el proyecto sobre el que trabajaremos durante todo el curso:

  • Reservalia es una plataforma SaaS de reservas de cita con 340 negocios de pago, dos aplicaciones (apps/api en Node.js + Express + PostgreSQL y apps/web en React + Vite), un paquete de tipos compartidos y un monorepo con workspaces de npm.
  • El equipo son Marta (tech lead, mira el negocio y el riesgo), Diego (backend, quiere un CI rápido y es el único que sabe desplegar) y Nuria (SRE, quiere infraestructura reproducible y rollback fiable). Sus tensiones son las que tendrás en tu equipo.
  • El proyecto ya tiene sus comandos definidos: npm ci, lint, typecheck, test, build y migrate. Esta es la idea clave de la lección: el pipeline no inventa nada, solo ejecuta en una máquina limpia lo que el proyecto ya sabe hacer. Un pipeline es un revelador de suposiciones ocultas.
  • Hay tres entornosdev, staging y prod— gobernados por dos principios: el mismo artefacto se promociona a los tres, y los datos de producción nunca salen de producción.
  • La infraestructura de destino es AWS: imágenes en ECR, ejecución en ECS Fargate detrás de un balanceador, datos en RDS PostgreSQL, secretos en Secrets Manager y registros en CloudWatch.
  • Y sabes cómo seguir el curso con cualquier tecnología: más del 85 % es independiente del lenguaje; solo cambian los comandos dentro de cada paso.

Nos queda una última cosa antes de empezar a construir. Marta va a pedir tiempo del equipo para montar todo esto, y dentro de seis meses alguien preguntará si ha servido de algo. Para responder a eso hace falta haber medido el punto de partida antes de tocar nada. En la siguiente lección, Métricas DORA: Cómo se Mide la Entrega de Software, veremos las cuatro métricas estándar del sector —frecuencia de despliegue, lead time for changes, change failure rate y time to restore service—, cómo calcularlas a partir de datos que el propio pipeline genera, y estableceremos el cuadro de mando inicial de Reservalia: dónde están hoy y a dónde quieren llegar al final del curso.

Curso de CI/CD: Integración y Despliegue Continuo

Módulo 1: Introducción a CI/CD

Módulo 2: Integración Continua (CI)

Módulo 3: Despliegue Continuo (CD)

Módulo 4: Prácticas Avanzadas de CI/CD

Módulo 5: Implementación de CI/CD en Proyectos Reales

Módulo 6: Herramientas y Tecnologías

Módulo 7: Ejercicios Prácticos

Módulo 8: Recursos Adicionales

© Copyright 2026. Todos los derechos reservados