La lección anterior cerró con un patrón: lo que sostiene un sistema con muchas versiones vivas es un contrato explícito y una convivencia larga entre ellas. Allí había una app cliente y una API, y la tabla de versiones se vigilaba a mano. Ahora multiplicamos el problema en otra dirección. Reservalia ha crecido: 1.900 negocios de pago, cuatro equipos, y el apps/api que empezó siendo un Fastify con quince rutas se ha partido en cinco servicios —citas, negocios, notificaciones, pagos y disponibilidad— que despliegan por su cuenta. La promesa es conocida: equipos autónomos que entregan sin esperarse. El precio también, y esta lección lo cobra entero. El pipeline se multiplica por cinco y con él cada decisión que antes se tomaba una vez. Cada servicio es a la vez cliente antiguo y proveedor de otro, así que el contrato deja de ser una tabla y pasa a ser una prueba ejecutable con una puerta de despliegue. Aparece un entorno de integración compartido que, si lo dejas, se convierte en el cuello de botella que anula toda la autonomía ganada. Y aparece un coste organizativo del que casi nadie habla hasta que lo paga. Al final de la lección estaremos en condiciones de responder la pregunta que más dinero ahorra de todo el módulo: cuándo no partir el monolito.
Contenido
- Qué tiene este contexto que Reservalia no tenía
- La partición y el grafo de dependencias
- Monorepo o polyrepo, con la ejecución selectiva
- El pipeline plantilla: estandarizar sin ahogar
- Contract testing y la puerta
can-i-deploy - El entorno de integración compartido y por qué se atasca
- Versionado y despliegue independientes
- Compatibilidad en APIs y en eventos
- Despliegue progresivo y GitOps
- Observabilidad distribuida y correlación de despliegues
- El coste organizativo y cuándo no partir
- Ficha del caso
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- Qué tiene este contexto que Reservalia no tenía
| Monorepo de dos apps (módulos 2-4) | Cinco servicios, cuatro equipos | |
|---|---|---|
| Unidades desplegables | 2 | 5, y creciendo |
| Quién decide desplegar | El equipo, todos de acuerdo | Cada equipo, sin avisar |
| Un cambio incompatible | Se ve en el mismo PR | Se ve en producción, si no lo evitas |
| Pruebas de integración | Un docker compose con Postgres |
¿Contra qué versión de los otros cuatro? |
| Rollback | Un digest, 4 min | Uno por servicio, y a veces hay que revertir dos |
| Depuración de un fallo | Un log, un servicio | Una petición que atraviesa cuatro procesos |
| Coste de una decisión de pipeline | Editar un fichero | Editar cinco, o tener un mecanismo |
La última fila es la que engancha con la 04-05 y explica por qué aquel trabajo se hizo. La tercera es el corazón de la lección: en el monorepo, si Diego rompía la firma de una función que usaba apps/web, tsc fallaba en el mismo PR y nadie llegó a enterarse. Con cinco repositorios desplegando por separado, ese mismo error compila, pasa las pruebas, se despliega y rompe a otro equipo. Todo el aparato de contratos del apartado 5 existe para recuperar la señal que el monorepo daba gratis.
Y lo que no cambia, que sigue siendo casi todo: CI y sus seis prácticas (02-01), artefacto inmutable por digest (02-06), OIDC y mínimo privilegio (04-03), IaC (03-03), migraciones con expand and contract (04-06), estrategias de despliegue (03-04) y observabilidad (03-06). Ninguna de esas piezas se sustituye; todas se replican y hay que gobernarlas.
- La partición y el grafo de dependencias
flowchart TD
W["web / movil"] --> GW["API gateway"]
GW --> C["citas"]
GW --> N["negocios"]
GW --> P["pagos"]
C --> D["disponibilidad"]
C -.->|"evento CitaCreada"| NT["notificaciones"]
P -.->|"evento PagoConfirmado"| C
N --> D
C --> BD1[("bd citas")]
N --> BD2[("bd negocios")]
P --> BD3[("bd pagos")]
Dos tipos de arista, y la diferencia decide casi todo lo demás. Las flechas continuas son llamadas síncronas: citas pregunta a disponibilidad y espera respuesta, así que un fallo o una lentitud de disponibilidad se propaga hacia arriba de inmediato. Las discontinuas son eventos: citas publica CitaCreada y sigue; notificaciones lo consume cuando puede. Un consumidor caído no rompe al productor, pero introduce un problema nuevo: los eventos también son contratos, con el agravante de que un evento publicado ya no se puede cambiar y puede consumirse horas después (apartado 8).
Fíjate también en las bases de datos: una por servicio y sin accesos cruzados. Es la regla que hace que el despliegue sea realmente independiente; en el momento en que dos servicios comparten tablas, tienes un monolito distribuido con toda la complejidad de los microservicios y ninguna de sus ventajas.
- Monorepo o polyrepo, con la ejecución selectiva
La primera pregunta que el equipo se hizo, y no tiene respuesta universal:
| Monorepo (los 5 en un repo) | Polyrepo (un repo por servicio) | |
|---|---|---|
| Cambio que toca dos servicios | Un PR atómico, revisión conjunta | Dos PR coordinados a mano |
| Refactor de una librería común | Se actualizan todos los usos a la vez | Publicar versión y esperar adopción |
| Aislamiento entre equipos | Débil: todos ven y tocan todo | Fuerte: permisos por repositorio |
| Pipeline | Uno, con ejecución selectiva obligatoria | Cinco, con plantilla compartida |
| Tiempo de CI | Crece con el repo si no filtras | Naturalmente acotado |
Historial y git blame |
Ruidoso, mezcla cinco productos | Limpio por servicio |
| Herramientas necesarias | Grafo de afectación, paths, caché remota |
Registro de paquetes, gestión de versiones |
| Encaja bien con | Equipos que colaboran mucho | Equipos con autonomía y ritmos distintos |
Reservalia elige monorepo, por una razón concreta: los cinco servicios comparten packages/compartido y todavía hay refactors que cruzan fronteras cada dos por tres. Con polyrepo, cada uno de esos cambios sería publicar un paquete, esperar y abrir cinco PR. La decisión se revisará cuando los equipos dejen de tocarse entre sí, y no es una derrota cambiar de opinión: es el indicador funcionando.
El precio del monorepo es que la ejecución selectiva pasa de optimización a requisito. Ya la vimos en la 04-04 con paths; con cinco servicios y dependencias entre ellos hace falta un grafo de afectación de verdad:
detectar:
runs-on: ubuntu-22.04
outputs:
servicios: ${{ steps.afectados.outputs.lista }} # 1
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 } # 2
- id: afectados
run: |
BASE=$(git merge-base origin/main HEAD)
LISTA=$(npx turbo run build --filter="...[$BASE]" --dry=json \
| jq -c '[.tasks[].package] | unique') # 3
echo "lista=$LISTA" >> "$GITHUB_OUTPUT"
verificar:
needs: [detectar]
if: needs.detectar.outputs.servicios != '[]'
strategy:
matrix:
servicio: ${{ fromJson(needs.detectar.outputs.servicios) }} # 4
fail-fast: false # 5
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/preparar-node
- run: npm run verificar --workspace services/${{ matrix.servicio }}- El job de detección produce una lista que el siguiente consume como matriz. Es el paso de información entre jobs de la 04-01, aplicado a decidir qué se ejecuta.
fetch-depth: 0es imprescindible: sin el historial completo no haymerge-basey el cálculo del diff falla silenciosamente ejecutándolo todo....[$BASE]incluye los dependientes, no solo lo modificado. Si el PR tocapackages/compartido, el filtro devuelve los cinco servicios porque todos dependen de él. Ese es el matiz que separa un grafo de afectación de un simple filtro por carpeta, y es exactamente la trampa del "verde falso" que la 04-04 advirtió: filtrar porpathssin seguir las dependencias deja pasar cambios que rompen a un consumidor.- La matriz se construye en tiempo de ejecución, así que un PR que toca solo
notificacioneslanza un job y no cinco. En Reservalia esto bajó la media de 14 a 4,5 minutos. fail-fast: falsepara que un servicio roto no cancele la información de los demás: quien abre el PR quiere ver todos los rojos de una vez.
- El pipeline plantilla: estandarizar sin ahogar
Aquí se cobra el trabajo de la 04-05. Con un servicio, un workflow duplicado es una molestia; con cinco y creciendo, es la garantía de que la mitad no tendrá escaneo de dependencias ni fijará las acciones por SHA. La forma que toma es un workflow reutilizable al que cada servicio llama con sus parámetros:
# .github/workflows/servicio.yml — la plantilla, mantenida por el equipo de plataforma
on:
workflow_call:
inputs:
servicio: { required: true, type: string }
puerto: { required: false, type: number, default: 3000 }
publicar_pact: { required: false, type: boolean, default: true } # 1
secrets:
ROL_AWS: { required: true }
jobs:
verificar:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/preparar-node
- run: npm run lint --workspace services/${{ inputs.servicio }}
- run: npm test --workspace services/${{ inputs.servicio }}
- if: inputs.publicar_pact
run: npm run pact:publicar --workspace services/${{ inputs.servicio }}
seguridad: # 2 · no es opcional
uses: ./.github/workflows/seguridad.yml
construir-publicar:
needs: [verificar, seguridad]
uses: ./.github/workflows/reusable-build-publicar.yml # 3
with: { servicio: '${{ inputs.servicio }}' }# services/citas/.github/workflows/ci.yml — lo que escribe cada equipo
name: citas
on: { pull_request: {}, push: { branches: [main] } }
jobs:
ci:
uses: reservalia/plataforma/.github/workflows/servicio.yml@v3 # 4
with: { servicio: citas, puerto: 3010 }
secrets: { ROL_AWS: '${{ secrets.ROL_CITAS }}' }- Los parámetros son pocos y con valores por defecto sensatos. Una plantilla con veinte entradas no es una plantilla, es un lenguaje de programación mal diseñado; cada parámetro nuevo es superficie que mantener para siempre.
- Lo que no se puede desactivar: el job de seguridad de la 04-03. Un equipo puede elegir su gestor de pruebas, no si escanea dependencias. Esa lista corta de innegociables es el núcleo de la política.
- La plantilla reutiliza el reusable de la 04-05 en lugar de reimplementarlo: composición, no duplicación.
- La referencia va anclada a un tag (
@v3), nunca amain. Un cambio en la plantilla no debe romper a cinco equipos a la vez sin que ellos lo decidan; publicandov3.1y dejando que cada equipo suba, la migración es progresiva. Es la misma disciplina de fijar las acciones por SHA de la 04-03, con motivación de estabilidad además de seguridad.
La tensión de fondo es real y no se resuelve con YAML. Estandarizar de más produce una plantilla llena de excepciones y equipos que copian el workflow para "arreglarlo"; estandarizar de menos produce cinco pipelines distintos, ninguno con las prácticas del módulo 4 completas. La regla que le funciona a Reservalia:
| Se estandariza (obligatorio) | Se deja a cada equipo |
|---|---|
| Escaneo de seguridad y política de severidades | Framework de pruebas y estilo |
| Firma y publicación del artefacto en ECR | Qué pruebas y cuántas |
| Nomenclatura de imágenes y tags | Estructura interna del servicio |
Publicación de contratos y can-i-deploy |
Cuándo desplegar y con qué estrategia |
| Emisión de eventos de despliegue para DORA | Herramientas de desarrollo local |
- Contract testing y la puerta
can-i-deploy
can-i-deployEl problema, planteado con precisión: citas llama a GET /disponibilidad/:negocioId?fecha=... y espera un array de huecos con el campo inicio. El equipo de disponibilidad decide que ahora se llame desde. Sus pruebas pasan, las de citas pasan —usan un mock escrito hace meses—, ambos despliegan y producción se rompe. El mock era la mentira: verificaba que citas funciona contra lo que citas cree que devuelve el otro, no contra lo que devuelve de verdad.
Los contratos dirigidos por el consumidor invierten la dirección. El consumidor declara qué necesita, esa declaración se publica en un intermediario (un broker), y el proveedor verifica en su propio CI que lo cumple.
// services/citas/test/contratos/disponibilidad.pact.ts (lado consumidor)
describe('citas → disponibilidad', () => {
it('devuelve los huecos de un negocio en una fecha', async () => {
await proveedor.addInteraction({
state: 'el negocio 42 tiene agenda el 25 de octubre', // 1
uponReceiving: 'una consulta de huecos',
withRequest: { method: 'GET', path: '/disponibilidad/42', query: { fecha: '2026-10-25' } },
willRespondWith: {
status: 200,
body: eachLike({ // 2
inicio: iso8601DateTimeWithMillis('2026-10-25T08:30:00.000Z'),
duracionMin: like(30),
}),
},
});
const huecos = await clienteDisponibilidad.consultar(42, '2026-10-25');
expect(huecos[0].inicio).toBeDefined(); // 3
});
});- El
statees una precondición que el proveedor tendrá que montar en su verificación. Es el punto de acuerdo entre los dos equipos y por eso se redacta en lenguaje de negocio. - Se declaran tipos y forma, no valores.
like(30)significa "un número"; fijar el 30 haría fallar el contrato cada vez que cambien los datos de prueba del proveedor, que es la forma más rápida de que el equipo abandone los contratos. - La prueba del consumidor sirve doblemente: valida su cliente contra un servidor simulado por el contrato y, al pasar, publica el contrato en el broker con la versión y las etiquetas de rama.
En el CI de disponibilidad, la verificación:
- name: Verificar contratos de mis consumidores
run: npm run pact:verificar
env:
PACT_BROKER_URL: https://pact.reservalia.internal
PACT_PROVIDER_VERSION: ${{ github.sha }}
PACT_CONSUMER_VERSION_SELECTORS: '[{"deployedOrReleased": true}]' # 1- El selector es la clave y casi nadie lo configura bien: se verifica contra los contratos de las versiones de consumidores realmente desplegadas, no contra todos los contratos históricos. Sin esto, el proveedor arrastra para siempre expectativas de versiones muertas y no puede evolucionar nunca.
Y la puerta, que es lo que convierte todo esto de un informe en un mecanismo:
puede-desplegar:
needs: [construir-publicar]
runs-on: ubuntu-22.04
steps:
- name: can-i-deploy
run: |
pact-broker can-i-deploy \
--pacticipant disponibilidad \
--version "$GITHUB_SHA" \
--to-environment prod \
--retry-while-unknown 30 --retry-interval 10 # 1Devuelve verde solo si todos los consumidores desplegados en prod tienen su contrato verificado contra esta versión concreta del proveedor. Si citas espera inicio y esta versión devuelve desde, el despliegue se detiene antes de salir. --retry-while-unknown cubre el caso de que una verificación esté en curso: espera en vez de fallar. Es la misma clase de puerta que la de calidad de la 04-01, pero con un objeto nuevo: no comprueba tu código, comprueba tu compatibilidad con quien depende de ti.
- El entorno de integración compartido y por qué se atasca
La reacción instintiva de todo equipo que se parte en servicios es montar un entorno donde estén los cinco desplegados y probar allí. Suena razonable y se degrada siempre igual:
| Entorno de integración compartido | Contratos + un entorno por servicio | |
|---|---|---|
| Qué prueba | El sistema completo, versiones reales | La compatibilidad entre pares |
| Cuándo da la señal | Después de desplegar allí | En el CI, antes de publicar |
| Si algo está roto | Bloquea a los cinco equipos | Bloquea solo al que lo rompió |
| Diagnóstico de un fallo | ¿De quién es? Reunión | Del par concreto, con nombre |
| Coste de infraestructura | Cinco servicios siempre encendidos | Uno más simulaciones |
| Escala a 15 servicios | No | Sí |
El mecanismo de degradación es predecible: a partir de tres o cuatro servicios, la probabilidad de que algo esté roto en el entorno compartido en un momento dado se acerca a uno. Entonces las pruebas fallan por motivos ajenos, el equipo aprende a ignorar los rojos, y el entorno deja de dar señal justo cuando más servicios hay. Es la misma dinámica de las pruebas inestables de la 02-04, pero a escala de organización.
Esto no significa que no haya que probar el sistema junto nunca. Significa que el entorno compartido deja de ser una puerta y pasa a ser un detector:
- Las puertas son los contratos y el
can-i-deploy: rápidos, deterministas, con dueño claro, y bloquean el despliegue. - El detector es un conjunto pequeño de recorridos de extremo a extremo ejecutados contra staging después de desplegar, y contra producción como smoke test (03-02). Si falla, es un incidente, no un check rojo de un PR.
- Versionado y despliegue independientes
La regla, en una frase: si desplegar citas obliga a desplegar disponibilidad a la vez, no tienes microservicios. Un "release coordinado de todos los servicios" reintroduce todos los costes de la partición y elimina su única ventaja, porque el lote vuelve a ser grande, el lead time se sincroniza con el servicio más lento y un fallo obliga a revertir cinco cosas.
| Despliegue independiente | Despliegue coordinado | |
|---|---|---|
| Tamaño del lote | Un cambio de un equipo | La suma de cinco equipos |
| Un fallo | Se revierte un servicio | Hay que averiguar cuál de los cinco |
| Cadencia | La de cada equipo | La del más lento |
| Requiere | Compatibilidad hacia atrás siempre | Nada: por eso resulta tentador |
La última fila es la honesta: el despliegue coordinado es tentador porque permite hacer cambios incompatibles. Renunciar a él obliga a la disciplina del apartado siguiente. Y cada servicio versiona por su cuenta, con semantic-release sobre sus propios commits (02-06); no existe "la versión de Reservalia" y no hace falta que exista. Lo que sí existe es un registro de qué versión de cada servicio está en cada entorno, que es precisamente lo que el broker de contratos ya sabe y lo que hace posible el can-i-deploy.
- Compatibilidad en APIs y en eventos
Toda la disciplina de expand and contract (04-06) y de convivencia de versiones (05-02) se aplica aquí, con un añadido propio: los eventos son peores que las APIs. Una API síncrona se consume ahora y sabes quién llama; un evento se consume después, quizá por un servicio nuevo que aún no existe, y los eventos ya publicados están en la cola y no se pueden cambiar.
Reservalia registra los esquemas de sus eventos en un registro central, y el CI valida la compatibilidad antes de dejar publicar uno nuevo:
{
"type": "record",
"name": "CitaCreada",
"fields": [
{ "name": "citaId", "type": "string" },
{ "name": "negocioId", "type": "int" },
{ "name": "inicioUtc", "type": { "type": "long", "logicalType": "timestamp-millis" } },
{ "name": "canal", "type": ["null", "string"], "default": null }
]
} - name: Comprobar compatibilidad del esquema
run: |
curl -sf -X POST "$REGISTRO/compatibility/subjects/CitaCreada/versions/latest" \
-H 'Content-Type: application/json' \
--data-binary @esquemas/CitaCreada.avsc | jq -e '.is_compatible == true' # 1- Falla el CI si el esquema nuevo no es compatible con el anterior según la política configurada. Es una puerta más, del mismo tipo que
can-i-deploy, pero para el canal asíncrono.
Qué es compatible y qué no, con la regla práctica:
| Cambio en el evento | ¿Seguro? | Por qué |
|---|---|---|
| Añadir campo con valor por defecto | ✅ | Los consumidores viejos lo ignoran |
| Añadir campo obligatorio | ❌ | Un consumidor viejo no sabe leerlo… y uno nuevo no puede leer los antiguos |
| Eliminar un campo con default | ✅ (con espera) | Solo si ningún consumidor lo usa; verificar antes |
| Renombrar un campo | ❌ | Es eliminar y añadir a la vez |
| Cambiar el tipo de un campo | ❌ | Salvo ampliaciones muy concretas |
| Cambiar el significado sin cambiar el tipo | ❌❌ | El peor de todos: ninguna herramienta lo detecta |
La última fila merece un aviso. Pasar inicioUtc de hora local a UTC sin cambiar el nombre ni el tipo es compatible para el validador y catastrófico para los consumidores, que seguirán interpretando el número como antes. Cuando cambia el significado, cambia el nombre del campo o la versión del evento; el registro comprueba estructura, no semántica. Y una recomendación que ahorra incidentes: los consumidores deben ser tolerantes —ignorar campos que no conocen en vez de fallar—, porque es lo que permite al productor añadir cosas sin coordinar nada.
- Despliegue progresivo y GitOps
Las estrategias de la 03-04 siguen siendo las mismas y no se re-explican; lo que cambia es que ahora hay cinco despliegues progresivos simultáneos y nadie va a vigilarlos a mano. Con el paso a Kubernetes —cuyo detalle corresponde a la 06-05—, el despliegue progresivo se declara igual que el resto:
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata: { name: disponibilidad }
spec:
strategy:
canary:
steps:
- setWeight: 10 # 1
- pause: { duration: 5m }
- analysis: # 2
templates: [{ templateName: tasa-error-5xx }]
- setWeight: 50
- pause: { duration: 10m }- Los pasos del canary son declarativos: pesos y esperas escritos junto al servicio, no un script en el pipeline. Con cinco servicios, esto es lo que evita cinco implementaciones distintas del mismo canary.
- El análisis automático consulta una métrica y aborta el despliegue si empeora. Es el rollback automático por métricas de la 03-05, ahora como parte del objeto desplegado en lugar de como un job del CD.
Y aquí es donde GitOps aparece por necesidad y no por moda. Con dos apps, el cd.yml empujaba el despliegue desde el pipeline (03-02) y funcionaba. Con cinco servicios y varios entornos, ese modelo obliga a que el CI tenga credenciales de escritura sobre todos los clústeres —justo lo que la 04-03 quiere minimizar— y hace difícil responder a "¿qué hay desplegado ahora mismo en prod?". El modelo pull invierte la dirección:
flowchart LR
CI["CI del servicio<br/>publica imagen por digest"] --> PR["PR automatico al repo<br/>de despliegues"]
PR --> RV["Revision y merge"]
RV --> G["Repositorio de estado deseado<br/>(git)"]
G --> AG["Agente en el cluster<br/>sincroniza"]
AG --> K["Cluster"]
K -.->|"deriva detectada"| AG
Tres consecuencias prácticas que justifican el cambio. El estado deseado está en git, así que la pregunta de qué hay desplegado se responde con git log y no consultando el clúster. El CI ya no necesita credenciales del clúster: solo permiso para abrir un PR, con lo que la superficie de la 04-03 se reduce mucho. Y la deriva se detecta y se corrige sola: si alguien cambia algo a mano en el clúster, el agente lo devuelve al estado declarado, que es la misma promesa que Terraform daba para la infraestructura en la 03-03, ahora para las aplicaciones.
- Observabilidad distribuida y correlación de despliegues
Los tres pilares y las cuatro señales de oro de la 03-06 se mantienen; lo que cambia es que una petición ya no vive en un servicio. Sin trazas distribuidas, la investigación de "la reserva tarda 4 segundos" es una reunión de cuatro equipos diciendo que su parte va bien.
Lo mínimo imprescindible son tres cosas, y las tres se estandarizan en la plantilla del apartado 4 para que no dependan de la buena voluntad de cada equipo:
- Propagar el contexto de traza entre servicios y a través de los eventos. En las llamadas HTTP viaja en la cabecera
traceparent; en los eventos hay que copiarlo dentro del mensaje a mano, y es lo que casi todo el mundo olvida: la traza se corta justo al cruzar la cola, que es donde más falta hace. - Etiquetar todo con
servicioyversion, para poder comparar el comportamiento de dos versiones durante un canary. - Emitir una marca de despliegue por servicio al panel común, con servicio, versión, digest y hora.
La marca es la que resuelve el problema específico de este contexto. El caso típico: a las 11:20, citas empieza a devolver errores 500 y su equipo revisa sus cambios sin encontrar nada, porque su último despliegue fue anteayer. La respuesta está en el panel de despliegues común, donde a las 11:18 aparece una marca de disponibilidad. Sin ese panel, esa correlación tarda una hora de reunión; con él, treinta segundos.
-- ¿Qué se desplegó en los 30 minutos previos a un incidente?
SELECT servicio, version, desplegado_en
FROM despliegues
WHERE desplegado_en BETWEEN '2026-08-14 10:50' AND '2026-08-14 11:20'
ORDER BY desplegado_en DESC;Con esa tabla —la misma que alimentaba las métricas DORA en la 03-06, ahora con una columna servicio— la pregunta "¿qué ha cambiado?" tiene respuesta objetiva, que es la primera pregunta de cualquier incidente y la que más tiempo consume cuando no se puede contestar.
- El coste organizativo y cuándo no partir
Lo que nadie pone en la propuesta inicial: alguien tiene que mantener la plantilla de pipeline, el broker de contratos, el registro de esquemas, el repositorio de despliegues, el clúster, el panel común y las convenciones. En Reservalia eso son dos personas a tiempo completo que no desarrollan producto. Es una decisión legítima con 1.900 negocios y cuatro equipos; con 340 negocios y tres personas habría sido absurda.
La lista de comprobación antes de partir un monolito, en orden de importancia:
| Señal | ¿Justifica partir? |
|---|---|
| Varios equipos se bloquean entre sí al desplegar | ✅ La razón buena, y casi la única |
| Partes con necesidades de escalado muy distintas | ✅ Si la diferencia es de un orden de magnitud |
| Un dominio con requisitos de aislamiento o cumplimiento propios | ✅ Por ejemplo, pagos |
| El pipeline tarda 40 minutos | ❌ Eso se arregla con la 04-04 |
| "El código está hecho un lío" | ❌ Los módulos distribuidos se lían igual, más la red |
| "Es lo que hace todo el mundo" | ❌ Casi nadie tiene tu problema |
| Un equipo de tres personas | ❌❌ Vas a pagar la coordinación sin necesitarla |
Diego: "¿Cuánto nos cuesta esto al mes?" Con números: la infraestructura sube un 35 % —cinco despliegues, más red, más observabilidad—, el CI se multiplica pero la ejecución selectiva lo compensa casi entero, y el coste real son las dos personas de plataforma. A cambio, cuatro equipos despliegan sin esperarse, que era el bloqueo que motivó todo.
Y la alternativa que casi nunca se considera: el monolito modular. Fronteras internas estrictas, módulos con interfaces explícitas, prohibición de accesos cruzados a tablas y un pipeline con ejecución selectiva por módulo. Da la mayor parte del beneficio de organización sin ninguno de los costes de red, y tiene una propiedad valiosísima: es el paso previo natural, porque un monolito ya modularizado se parte después con un esfuerzo muchísimo menor que uno enmarañado. Si dudas, empieza por ahí.
- Ficha del caso
| Contexto | 5 servicios, 4 equipos, 1.900 negocios; llamadas síncronas y eventos; una BD por servicio |
| Qué sigue valiendo tal cual | CI (02), artefacto por digest (02-06), OIDC y mínimo privilegio (04-03), IaC (03-03), estrategias (03-04), expand and contract (04-06), observabilidad (03-06) |
| Decisión 1 | Monorepo con grafo de afectación que incluye dependientes; media de CI de 14 a 4,5 min |
| Decisión 2 | Workflow plantilla anclado a tag, con una lista corta de innegociables |
| Decisión 3 | Contratos dirigidos por consumidor + can-i-deploy como puerta previa al despliegue |
| Decisión 4 | El entorno compartido deja de ser puerta y pasa a ser detector |
| Decisión 5 | Registro de esquemas de eventos con validación de compatibilidad en CI |
| Decisión 6 | GitOps: el CI abre un PR al repositorio de estado; el clúster hace pull |
| Coste | +35 % de infraestructura y 2 personas de plataforma a tiempo completo |
| Efecto en DORA | Frecuencia por equipo ×3; lead time igual o mejor; CFR sube al 5,1 % los tres primeros meses y vuelve al 3,9 % con los contratos maduros |
| Qué te llevas a cualquier proyecto | Lo que el compilador te daba gratis dentro de un proceso, entre servicios hay que comprarlo con contratos ejecutables |
Errores Comunes y Consejos
Error 1: compartir base de datos entre servicios. Es el monolito distribuido: todos los costes, ninguna ventaja, y el despliegue independiente deja de existir. Error 2: filtrar por paths sin seguir el grafo de dependencias, con lo que un cambio en packages/compartido no ejecuta las pruebas de quien lo usa: el verde falso de la 04-04.
Error 3: confiar en mocks escritos a mano como si fueran contratos; verifican lo que tú crees, no lo que el otro devuelve. Error 4: verificar los contratos contra todas las versiones históricas en vez de contra las desplegadas, con lo que el proveedor no puede evolucionar nunca. Error 5: convertir el entorno compartido en la puerta de despliegue, que a partir de cuatro servicios está roto permanentemente y entrena al equipo a ignorar los rojos.
Error 6: releases coordinadas de todos los servicios, que devuelven el lote grande y anulan la razón de haber partido. Error 7: cambiar el significado de un campo sin cambiar su nombre; ningún registro de esquemas lo detecta. Error 8: no propagar el contexto de traza a través de los eventos, cortando la traza justo donde más falta hace. Error 9: partir el monolito por problemas de código y no de organización.
Consejo 1: ancla la plantilla del pipeline a un tag y migra a los equipos uno a uno. Consejo 2: mantén corta la lista de innegociables —seguridad, publicación del artefacto, contratos, eventos DORA— y deja lo demás a cada equipo. Consejo 3: haz que los consumidores sean tolerantes e ignoren campos desconocidos. Consejo 4: si dudas, monolito modular primero; es el paso previo y siempre resulta más barato.
Ejercicios
Ejercicio 1
El equipo de disponibilidad necesita cambiar el campo inicio por desde en la respuesta de GET /disponibilidad/:negocioId, y citas y negocios son consumidores. Diseña la secuencia completa de cambios y despliegues, indicando qué pasa en cada paso con can-i-deploy y en qué momento es seguro eliminar el campo antiguo. Explica además por qué el orden inverso —cambiar primero los consumidores— tampoco funciona sin la fase de convivencia.
Ejercicio 2
Un PR modifica únicamente packages/compartido/src/disponibilidad.ts. El workflow usa paths: ['packages/compartido/**'] y ejecuta un job que compila y prueba ese paquete; sale verde y se fusiona. Dos horas después, citas falla en producción con un error de tipos en tiempo de ejecución. Explica el fallo del diseño del pipeline, propón la corrección concreta y di qué otra puerta del sistema debería haberlo atrapado aunque el filtro hubiera estado bien.
Ejercicio 3
Marta plantea en la reunión de arquitectura: "Reservalia tiene ahora 1.900 negocios y cuatro equipos; ¿deberíamos partir también negocios, que es el servicio más grande, en perfiles, horarios y facturacion?". Construye un análisis con los criterios de la lección y una recomendación argumentada, incluyendo qué datos pedirías antes de decidir.
Soluciones
Solución 1. Es expand and contract (04-06) sobre un contrato HTTP, con la particularidad de que quien tiene que moverse son otros equipos. La secuencia:
Paso 1 — Expandir el proveedor. disponibilidad devuelve los dos campos, inicio y desde, con el mismo valor. Sus propias pruebas se actualizan, pero los contratos publicados por citas y negocios siguen exigiendo inicio, que sigue presente: la verificación pasa y can-i-deploy da verde. Se despliega sin ninguna coordinación. Paso 2 — Migrar los consumidores, cada uno a su ritmo. El equipo de citas cambia su cliente para leer desde y, con ello, su contrato publicado cambia: ahora exige desde. En su CI, antes de fusionar, la verificación se ejecuta contra la versión de disponibilidad desplegada en prod, que ya devuelve ambos campos, así que pasa. Su can-i-deploy da verde y despliega. negocios hace lo mismo la semana siguiente, o al mes: no hay ninguna sincronización necesaria, y esa es exactamente la propiedad que se compró con todo el aparato. Paso 3 — Contraer. disponibilidad elimina inicio. Aquí ocurre lo interesante: si negocios aún no ha migrado, su contrato sigue exigiendo inicio, la verificación falla y can-i-deploy da rojo, deteniendo el despliegue antes de publicarlo. El proveedor no necesita preguntar a nadie ni mantener una hoja de cálculo de quién ha migrado: la puerta lo sabe. Cuando ambos consumidores han migrado y sus versiones desplegadas están registradas, el rojo se vuelve verde solo.
El momento seguro de eliminar es, por tanto, "cuando can-i-deploy lo permita", que es una respuesta operativa y no una fecha en un calendario. Conviene añadir dos cautelas: que los consumidores desplegados en prod sean los que cuentan (el selector del apartado 5), porque un consumidor que migró en su rama pero no ha desplegado todavía no protege nada; y que existan consumidores fuera del broker —un script interno, un cliente grande con acceso directo— que la puerta no ve y hay que buscar a mano.
Por qué el orden inverso no funciona: si citas cambia primero a leer desde, su contrato exige un campo que el proveedor desplegado no tiene, la verificación falla y can-i-deploy le da rojo a él. Aunque desactivaras la puerta, en producción citas leería undefined. La fase de convivencia no es burocracia: es el único estado en el que ambas partes son válidas a la vez, y por eso ninguna de las dos direcciones funciona sin ella.
Solución 2. El fallo tiene dos capas y conviene separarlas. La capa del filtro: paths es una condición sobre qué ficheros cambiaron, no sobre qué se ve afectado. Un cambio en packages/compartido afecta a los cinco servicios que lo importan, pero el filtro solo dispara el job del paquete. Ese job compiló y probó el paquete de forma aislada —donde el cambio es coherente consigo mismo— y salió verde. El "verde falso" de la 04-04 en su forma más pura: el check no dice "el sistema funciona", dice "lo que ejecuté funciona", y lo que ejecutó era una fracción.
La corrección concreta: sustituir el filtro por carpeta por el grafo de afectación del apartado 3, con un filtro que incluya los dependientes (...[BASE] en Turborepo, --affected en Nx, bazel query rdeps en Bazel). Un PR que toca packages/compartido pasa a ejecutar los cinco servicios; uno que toca solo services/notificaciones sigue ejecutando uno. También conviene comprobar dos detalles que hacen fallar el grafo en silencio: que el checkout traiga historial suficiente para calcular el merge-base, y que la dependencia esté declarada de verdad en el package.json del servicio —si se importa por ruta relativa saltándose el workspace, ninguna herramienta la ve—.
Qué otra puerta debería haberlo atrapado: los contratos. Si el cambio alteró la forma de lo que disponibilidad devuelve a citas, la verificación de contratos del proveedor habría fallado, y can-i-deploy habría bloqueado el despliegue aunque el filtro estuviera mal. Que no lo atrapara indica que el cambio afectaba a algo que los contratos no cubren —una función de cálculo interna compartida, no la forma de una respuesta—, y ahí la defensa correcta es la primera. Es una lección general del módulo 4 que aquí se ve muy bien: las puertas se solapan a propósito, y un incidente que atraviesa varias señala qué capa faltaba. Un tercer refuerzo barato: publicar @reservalia/compartido como paquete versionado en lugar de consumirlo por workspace obligaría a una actualización explícita en cada servicio, convirtiendo un cambio invisible en un PR visible por servicio, con el coste de perder el refactor atómico.
Solución 3. El análisis, criterio por criterio. (1) ¿Hay bloqueo entre equipos? Es la pregunta decisiva. Si perfiles, horarios y facturacion los mantiene el mismo equipo, partir no elimina ningún bloqueo y añade tres pipelines, tres despliegues, tres bases de datos y contratos entre piezas que hoy comparten proceso. La respuesta por defecto sería no. Si en cambio hay dos equipos distintos que se pisan en el mismo repositorio y se esperan para desplegar, la conversación cambia. (2) ¿Escalado distinto? horarios probablemente reciba mucho más tráfico de lectura que facturacion, pero la pregunta correcta es si la diferencia es de un orden de magnitud y si hoy está causando un problema real de coste o de saturación. Si el servicio entero cabe en tres tareas de ECS, la respuesta es no. (3) ¿Aislamiento o cumplimiento? facturacion es la única candidata seria: si maneja datos fiscales o de pago con requisitos de auditoría o retención propios, aislarla tiene un valor que no es técnico. (4) ¿Acoplamiento de datos? El punto que suele matar la propuesta: si horarios y facturacion consultan las mismas tablas de negocio, partirlos exige duplicar datos o introducir llamadas síncronas en un camino crítico, y el resultado es más lento y más frágil que el original.
Los datos que pediría antes de decidir, todos obtenibles en una semana: cuántos PR al mes toca cada área y si vienen de personas distintas; cuántas veces en los últimos tres meses un despliegue de negocios se retrasó por esperar a un cambio de otra parte; el reparto de tráfico y de coste entre las tres áreas; el grafo real de importaciones internas y de accesos a tablas dentro del servicio, que es el que dice si las fronteras existen o son un deseo; y el lead time y el change failure rate de negocios comparados con los de los otros cuatro servicios, para saber si hay un problema medible o solo una sensación.
Recomendación: no partir todavía, y en su lugar hacer el monolito modular dentro de negocios —tres módulos con interfaces explícitas, prohibición de accesos cruzados a tablas verificada en CI con una regla de importación, y ejecución selectiva por módulo en el pipeline—. Eso da hoy la mayor parte del beneficio organizativo, no añade ni una llamada de red, y deja el servicio en el estado en el que partirlo mañana es casi mecánico si el criterio (1) llega a cumplirse. La única excepción que consideraría de inmediato es facturacion, y solo si existe un requisito de cumplimiento concreto que hoy no se pueda satisfacer. Es exactamente el consejo 4 de la lección: cuando dudas, la opción reversible gana, y modularizar es reversible mientras que partir no lo es.
Conclusión
Los microservicios han sometido el pipeline a la prueba más dura del módulo, y el resultado no es que haya que tirar nada, sino que todo se multiplica y necesita gobierno. La ejecución selectiva dejó de ser una optimización de la 04-04 para convertirse en un requisito, y con un matiz que separa un pipeline correcto de uno que engaña: hay que seguir el grafo de dependencias, no filtrar por carpeta, o el verde deja de significar nada. El trabajo de pipeline as code de la 04-05 se cobró íntegro en una plantilla anclada a tag, con una lista corta de innegociables y libertad en todo lo demás. Y donde el monolito daba señal gratis —el compilador impidiendo que Diego rompiera a apps/web—, hubo que comprarla con contratos ejecutables: contratos dirigidos por el consumidor, verificados por el proveedor contra las versiones realmente desplegadas, y una puerta can-i-deploy que detiene el despliegue antes de publicarlo. Ese mecanismo es el que permitió degradar el entorno de integración compartido de puerta a detector, evitando el cuello de botella que anula la autonomía a partir de cuatro servicios. Alrededor, la misma disciplina de compatibilidad de siempre, extendida a los eventos con un registro de esquemas y con la advertencia de que ningún validador detecta un cambio de significado; el despliegue progresivo declarado junto al servicio con análisis automático de métricas; GitOps por necesidad, porque el modelo pull reduce las credenciales del CI y responde con git log a qué hay desplegado; y trazas propagadas también a través de las colas, con marcas de despliegue por servicio en un panel común que convierte una hora de reunión en treinta segundos de consulta. El coste, dicho sin adornos: un 35 % más de infraestructura, dos personas de plataforma a tiempo completo, y un change failure rate que subió al 5,1 % durante tres meses antes de volver a bajar. Por eso la lección termina con la lista de cuándo no partir, y con el recordatorio de que el monolito modular da casi todo el beneficio organizativo sin ninguno de los costes de red.
Los tres casos vistos hasta aquí compartían una condición de partida: eran sistemas modernos, con pruebas, con control de versiones sano, con equipos que podían decidir cómo trabajar. La última lección del módulo quita esa red. Gestor Citas 4 es el producto anterior de la empresa —un monolito Java/JSP sobre Tomcat de 2011, sin una sola prueba automatizada, desplegado por FTP los sábados por la noche, con la configuración editada a mano en el servidor y tres clientes grandes que aún pagan por él— y no se puede reescribir. Allí no se trata de elegir entre monorepo y polyrepo ni de afinar un canary: se trata de decidir por dónde se empieza cuando no hay nada, en qué orden se construyen los incrementos para que cada uno aporte valor por sí solo, y hasta dónde merece la pena llegar en un producto que solo está en mantenimiento.
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
