En 08-02 el sistema de TechCorp quedó completo como código: seis servicios, un gateway, una librería y sus pruebas. Esta lección lo lleva a un clúster y lo opera. La primera mitad responde a "¿cómo levanto todo esto desde cero?": el repositorio techcorp/plataforma completo, el compose.yaml local con los seis servicios y toda la infraestructura, el orden de arranque en un clúster nuevo, el script que lo aplica, los Job de migración y la prueba de humo con un token de Keycloak. La segunda mitad responde a "¿y qué hago cada día?": un despliegue normal de servicio-pedidos de punta a punta, la campaña de Black Friday, un incidente en una DLQ paso a paso, la rotación de un secreto, la actualización de Node y una evolución de contrato aplicada a todo el sistema, más una tabla de operaciones frecuentes y el coste mensual aproximado.

No reexplicamos ningún YAML de 05-02 ni ningún código: cuando aparezca un Deployment, un ServiceMonitor o un ExternalSecret, remitiremos a la lección donde se escribió y mostraremos solo lo que cambia al pasar de un servicio a seis.

Contenido

  1. El repositorio techcorp/plataforma completo
  2. compose.yaml: el sistema entero en un portátil
  3. Orden de arranque en un clúster nuevo
  4. scripts/desplegar-todo.sh y los Job de migración
  5. Verificación y prueba de humo
  6. Operación: un despliegue normal de servicio-pedidos
  7. Operación: la campaña de Black Friday
  8. Operación: un incidente en pagos.stock.dlq, paso a paso
  9. Operación: rotar un secreto, actualizar Node y evolucionar un contrato
  10. Runbook rápido de operaciones frecuentes
  11. Coste mensual aproximado y cómo reducirlo

  1. El repositorio techcorp/plataforma completo

Es el repositorio del equipo de Plataforma y la fuente de verdad de producción (Argo CD lo lee, 05-03 §6). Todo lo que hemos ido dejando en él a lo largo del curso, ordenado:

techcorp/plataforma/
├── local/
│   ├── compose.yaml                     # 05-01, ampliado en el apartado 2
│   ├── kind.yaml                        # 05-02 §4
│   └── keycloak/realm-techcorp.json     # el realm de 07-01 §4, importado al arrancar
├── k8s/
│   ├── namespace.yaml                   # techcorp (labels PSA restricted, 07-04 §3)
│   ├── servicio-catalogo/  servicio-pedidos/  servicio-inventario/  servicio-pagos/
│   ├── servicio-notificaciones/  servicio-clientes/  gateway/  bff-movil/  consumidor-analitica/
│   │   ├── base/         # kustomization, configmap, deployment, service, job-migraciones, serviceaccount, servicemonitor, pdb,
│   │   │                 # externalsecret(s), hpa | scaledobject (donde aplica), networkpolicy (07-04)
│   │   └── overlays/dev|staging|prod/kustomization.yaml         # réplicas, newTag, parches de ConfigMap
│   ├── red/                             # 00-deny-all, 01-permitir-dns, gateway, servicio-*.yaml (07-04 §6)
│   ├── infra/                           # values de Helm y manifiestos de terceros (05-02 §11-12)
│   │   ├── rabbitmq/values-{dev,prod}.yaml, definitions.json (vhost techcorp, usuarios por servicio, colas, 07-02 §8)
│   │   ├── postgres/values-*.yaml, init/01-esquemas-svc.sql     # un usuario svc_* por servicio (02-04)
│   │   ├── mongo/  redis/  keycloak/ (realm import)  ingress-nginx/  cert-manager/ (ClusterIssuer letsencrypt-prod, 07-02)
│   │   ├── external-secrets/ (ClusterSecretStore techcorp-vault, 07-04)  keda/
│   │   └── observabilidad/  kube-prometheus-stack, loki, promtail, otel-collector, jaeger (06-01, 06-02)
│   ├── slos/prometheusrule-slos-techcorp.yaml, alertmanager-config.yaml     # 06-05
│   └── argocd/  app-of-apps.yaml + una Application por servicio y entorno   # 05-03 §6
├── observabilidad/dashboards/red-por-servicio.json, saga-de-pedidos.json, colas.json    # 06-01 §10
├── runbooks/  pedidos/, catalogo/, comun/dlq.md, comun/circuito-abierto.md, plataforma/… # 06-05 §8
├── pruebas/e2e/*.e2e.test.js, pruebas/carga/catalogo.js (k6)               # 04-05, 06-04
├── scripts/desplegar-todo.sh, humo.sh, token-keycloak.sh                    # apartado 4-5
├── .github/workflows/servicio-node-ci.yml (@v1, @v2)                         # 05-03 §11
├── SEGURIDAD.md                                                              # 07-04 §10
└── README.md (mapa de este árbol y "cómo levantar en local en 10 minutos")

Dos decisiones que sostienen el árbol: una base por servicio, un overlay por entorno (05-02 §12: la base de Inventario es la de Pedidos con nombres, puerto 3006 y su ScaledObject en lugar de HPA), y los terceros por Helm con values versionados aquí, nunca instalados a mano. Cuando el equipo de Pagos necesita un Secret nuevo, abre un PR contra este repositorio; nadie ejecuta kubectl create secret en producción.

  1. compose.yaml: el sistema entero en un portátil

El compose.yaml de 05-01 §8 tenía PostgreSQL, MongoDB, RabbitMQ, la semilla, las migraciones de Pedidos, Catálogo, Pedidos, el stub de Clientes y el gateway. Las adiciones para el sistema completo, resumidas (los servicios nuevos siguen exactamente el patrón de servicio-pedidos en 05-01: image + build, variables de la tabla de config.js de 08-02, depends_on con condiciones, stop_grace_period: 15s):

# techcorp/plataforma/local/compose.yaml — SOLO lo que se añade respecto a 05-01 §8
services:
  postgres:                                # ahora crea las cinco bases y los usuarios svc_* al iniciar
    volumes: [pg-datos:/var/lib/postgresql/data, ../k8s/infra/postgres/init:/docker-entrypoint-initdb.d:ro]   # 01-esquemas-svc.sql
  redis: { image: redis:7-alpine, healthcheck: { test: ["CMD", "redis-cli", "ping"] } }
  keycloak:
    image: quay.io/keycloak/keycloak:25.0
    command: ["start-dev", "--import-realm"]           # realm techcorp de 07-01: clientes tienda-web, bff-movil, servicio-*; usuaria ana.ruiz
    volumes: [./keycloak:/opt/keycloak/data/import:ro]
    ports: ["8180:8080"]
    environment: { KEYCLOAK_ADMIN: admin, KEYCLOAK_ADMIN_PASSWORD: admin }        # solo local
  jaeger: { image: jaegertracing/all-in-one:1.60, ports: ["16686:16686"] }        # UI; recibe OTLP en 4317 (06-02)
  prometheus: { image: prom/prometheus:v2.53.0, volumes: [./prometheus.yml:/etc/prometheus/prometheus.yml:ro], ports: ["9090:9090"] }
  grafana: { image: grafana/grafana:11.1.0, ports: ["3000:3000"], volumes: [../observabilidad/dashboards:/var/lib/grafana/dashboards:ro, ./grafana-provisioning:/etc/grafana/provisioning:ro] }
  loki: { image: grafana/loki:3.1.0, ports: ["3100:3100"] }
  # --- migraciones de un solo uso, una por servicio con BD (misma imagen, command distinto; 05-01 §8) ---
  inventario-migraciones: { image: ghcr.io/techcorp/servicio-inventario:local, build: { context: ../../servicio-inventario, secrets: [npmrc] }, command: ["node","scripts/migrar.js"], environment: { INVENTARIO_DB_URL: postgres://svc_inventario:dev-inventario@postgres:5432/inventario }, depends_on: { postgres: { condition: service_healthy } }, restart: "no" }
  pagos-migraciones:          { image: ghcr.io/techcorp/servicio-pagos:local, build: { context: ../../servicio-pagos, secrets: [npmrc] }, command: ["node","scripts/migrar.js"], environment: { PAGOS_DB_URL: postgres://svc_pagos:dev-pagos@postgres:5432/pagos }, depends_on: { postgres: { condition: service_healthy } }, restart: "no" }
  notificaciones-migraciones: { image: ghcr.io/techcorp/servicio-notificaciones:local, build: { context: ../../servicio-notificaciones, secrets: [npmrc] }, command: ["node","scripts/migrar.js"], environment: { NOTIFICACIONES_DB_URL: postgres://svc_notificaciones:dev-notif@postgres:5432/notificaciones }, depends_on: { postgres: { condition: service_healthy } }, restart: "no" }
  clientes-migraciones:       { image: ghcr.io/techcorp/servicio-clientes:local, build: { context: ../../servicio-clientes, secrets: [npmrc] }, command: ["node","scripts/migrar.js"], environment: { CLIENTES_DB_URL: postgres://svc_clientes:dev-clientes@postgres:5432/clientes }, depends_on: { postgres: { condition: service_healthy } }, restart: "no" }
  # --- servicios nuevos (patrón de servicio-pedidos en 05-01; variables de 08-02) ---
  servicio-inventario:     # PUERTO 3006, INVENTARIO_DB_URL, RABBITMQ_URL, RESERVA_TTL_S 900, PEDIDOS_URL, OTEL_*; depends_on postgres, rabbitmq, inventario-migraciones
  servicio-pagos:          # PUERTO 3003, PAGOS_DB_URL, RABBITMQ_URL, PASARELA_URL http://pasarela-falsa:4000, PASARELA_API_KEY dev, PAGO_NUEVO_PROVEEDOR "false"
  servicio-notificaciones: # PUERTO 3005, NOTIFICACIONES_DB_URL, RABBITMQ_URL, CORREO_PROVEEDOR consola
  servicio-clientes:       # SUSTITUYE al stub de 04-04: build ../../servicio-clientes; CLIENTES_DB_URL, RABBITMQ_URL, KEYCLOAK_URL http://keycloak:8080, KEYCLOAK_REALM techcorp, KEYCLOAK_ADMIN_CLIENT_SECRET dev
  pasarela-falsa:          # servicio-pagos/pruebas/dobles/pasarelaFalsa.js: 200 salvo tok_rechazar (402) y tok_503 (503); Idempotency-Key respetada
  gateway:                 # + KEYCLOAK_ISSUER http://keycloak:8080/realms/techcorp, KEYCLOAK_AUDIENCE techcorp-api; sin MONOLITO_URL (retirado en 08-01)
  # todos los servicios: OTEL_EXPORTER_OTLP_ENDPOINT http://jaeger:4317, OTEL_TRACES_SAMPLER_ARG "1.0"

Con esto, docker compose up -d --wait levanta unos veinte contenedores (diecisiete en ejecución más las tareas de un solo uso) en unos 90 segundos en un portátil normal, y las cuatro E2E de 08-02 §9 pasan contra http://localhost:8080 con un token obtenido de http://localhost:8180. Es el entorno con el que el alumno puede reproducir todo el curso (08-04).

  1. Orden de arranque en un clúster nuevo

Sea un clúster kind (plataforma/local/kind.yaml, 05-02 §4) o uno gestionado, el orden importa: cada capa depende de la anterior, y varias piezas (ESO, cert-manager, KEDA, el operador de Prometheus) instalan CRDs que los manifiestos posteriores usan.

Paso Qué Cómo Depende de Lección
1 Namespaces techcorp (PSA restricted), observabilidad, infra, argocd kubectl apply -f k8s/namespace.yaml 05-02, 07-04
2 ingress-nginx, cert-manager (+ ClusterIssuer letsencrypt-prod), External Secrets Operator (+ ClusterSecretStore techcorp-vault), KEDA, kube-prometheus-stack helm upgrade --install con k8s/infra/*/values-<env>.yaml 1 (y CRDs entre sí: ESO antes que cualquier ExternalSecret) 05-02 §11-12, 06-04, 07-02, 07-04
3 RabbitMQ (vhost techcorp, usuarios pedidos, inventario, pagos, notificaciones, clientes, analitica; TLS 5671; definitions.json con permisos por cola), PostgreSQL (init/01-esquemas-svc.sql: cinco BD, cinco usuarios svc_*), MongoDB, Redis Helm (dev) / gestionados (prod), credenciales en el gestor de secretos → ExternalSecret 2 (ESO) 02-04, 05-02, 07-02
4 Keycloak con el realm techcorp importado (clientes, roles, scopes, mapper clienteId) Helm + ConfigMap del realm 3 (PostgreSQL de Keycloak) 07-01
5 Loki + Promtail, otel-collector, Jaeger; PrometheusRule slos-techcorp; Alertmanager por equipos; dashboards kubectl apply -k k8s/infra/observabilidad 2 06-01, 06-02, 06-05
6 NetworkPolicies deny-all + DNS + por servicio kubectl apply -f k8s/red/ 1 07-04 §6
7 ExternalSecret de cada servicio (*-db, *-rabbitmq, pagos-pasarela, notificaciones-correo, clientes-keycloak, gateway-keycloak) Van en la base/ de cada servicio; se sincronizan antes que el Deployment 2, 3 07-04 §4
8 Servicios en el orden de la saga: servicio-clientes y servicio-catalogo (dependencias síncronas de Pedidos), servicio-inventario, servicio-pagos, servicio-notificaciones, servicio-pedidos, consumidor-analitica, bff-movil, gateway kubectl apply -k k8s/<servicio>/overlays/<env> (o Argo CD) 3-7 05-02
9 Ingress api.techcorp.example → gateway, certificado api-techcorp-tls En la base del gateway 2, 8 05-02 §9, 07-02 §3

Sobre el paso 8: el orden "por la saga" no es estrictamente necesario —los servicios arrancan aunque falte un colaborador (/health/ready no comprueba a Catálogo ni a Clientes, 04-04 §9) y las colas duraderas guardan mensajes— pero desplegar primero los consumidores y al final Pedidos y el gateway evita que los primeros pedidos de la prueba de humo esperen a que alguien declare inventario.pedidos.

  1. scripts/desplegar-todo.sh y los Job de migración

En dev y en un clúster de pruebas se aplica todo con un script; en producción los pasos 8-9 los hace Argo CD (app-of-apps.yaml), y el script solo se usa para la infraestructura de los pasos 1-7 (o Terraform/Helmfile, si Plataforma prefiere).

#!/usr/bin/env bash
# techcorp/plataforma/scripts/desplegar-todo.sh <entorno: dev|staging>  — idempotente: se puede relanzar entero
set -euo pipefail
ENV="${1:?entorno}"; NS=techcorp
cd "$(dirname "$0")/.."

echo "== 1. namespaces";        kubectl apply -f k8s/namespace.yaml
echo "== 2. operadores y CRDs"                                                # helm upgrade --install es idempotente
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx >/dev/null; helm repo add jetstack https://charts.jetstack.io >/dev/null
helm repo add external-secrets https://charts.external-secrets.io >/dev/null; helm repo add kedacore https://kedacore.github.io/charts >/dev/null
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts >/dev/null; helm repo add bitnami https://charts.bitnami.com/bitnami >/dev/null; helm repo update >/dev/null
helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx -n infra -f k8s/infra/ingress-nginx/values-$ENV.yaml --wait
helm upgrade --install cert-manager jetstack/cert-manager -n infra --set crds.enabled=true --wait && kubectl apply -f k8s/infra/cert-manager/
helm upgrade --install external-secrets external-secrets/external-secrets -n infra --wait && kubectl apply -f k8s/infra/external-secrets/   # ClusterSecretStore
helm upgrade --install keda kedacore/keda -n infra --wait
helm upgrade --install kube-prometheus-stack prometheus-community/kube-prometheus-stack -n observabilidad -f k8s/infra/observabilidad/prometheus-values-$ENV.yaml --wait
echo "== 3. datos y mensajería"                                                # en prod: gestionados; el script solo aplica los ExternalSecret
helm upgrade --install rabbitmq bitnami/rabbitmq -n infra -f k8s/infra/rabbitmq/values-$ENV.yaml --set-file loadDefinition.definitions=k8s/infra/rabbitmq/definitions.json --wait
helm upgrade --install postgres bitnami/postgresql -n infra -f k8s/infra/postgres/values-$ENV.yaml --set-file primary.initdb.scripts."01-esquemas-svc\.sql"=k8s/infra/postgres/init/01-esquemas-svc.sql --wait
helm upgrade --install mongo bitnami/mongodb -n infra -f k8s/infra/mongo/values-$ENV.yaml --wait
helm upgrade --install redis bitnami/redis -n infra -f k8s/infra/redis/values-$ENV.yaml --wait
echo "== 4. identidad";         helm upgrade --install keycloak bitnami/keycloak -n infra -f k8s/infra/keycloak/values-$ENV.yaml --wait   # importa realm-techcorp.json
echo "== 5. observabilidad";    kubectl apply -k k8s/infra/observabilidad/ && kubectl apply -f k8s/slos/
echo "== 6. red";               kubectl apply -f k8s/red/
echo "== 7-8. servicios en el orden de la saga"
for s in servicio-clientes servicio-catalogo servicio-inventario servicio-pagos servicio-notificaciones servicio-pedidos consumidor-analitica bff-movil gateway; do
  echo "   -> $s"
  kubectl -n $NS delete job "$s-migraciones" --ignore-not-found                # un Job es inmutable: se borra y se recrea (05-03 §5)
  kubectl apply -k "k8s/$s/overlays/$ENV"                                       # ExternalSecret, ConfigMap, Job, Deployment, Service, PDB, HPA/ScaledObject, ServiceMonitor, NetworkPolicy
  if kubectl -n $NS get job "$s-migraciones" >/dev/null 2>&1; then              # solo los servicios con BD tienen Job
    kubectl -n $NS wait --for=condition=complete "job/$s-migraciones" --timeout=180s
  fi
  kubectl -n $NS rollout status "deploy/$s" --timeout=180s                     # no seguir hasta que esté Ready
done
echo "== 9. ingress";           kubectl -n $NS get ingress,certificate
echo "OK: $(kubectl -n $NS get pods --no-headers | grep -c Running) pods Running en $NS"

Cada servicio con base de datos lleva su Job de migraciones en la base/ (05-02 §6) con argocd.argoproj.io/hook: PreSync para producción (05-03 §6): servicio-pedidos-migraciones, servicio-inventario-migraciones, servicio-pagos-migraciones, servicio-notificaciones-migraciones, servicio-clientes-migraciones (Catálogo tiene servicio-catalogo-semilla solo en dev; Notificaciones sí tiene BD para envios). Todos ejecutan node scripts/migrar.js desde la imagen del servicio y son idempotentes, por eso el delete + apply es seguro. Y todos son expand-only (05-04 §3): el script nunca necesita "esperar a que ningún pod viejo quede" para aplicar una migración.

  1. Verificación y prueba de humo

kubectl -n techcorp get pods                                        # todos Running y READY 1/1 (o 2/2 con otel sidecar, si lo hubiera); Jobs Completed
kubectl -n techcorp get externalsecret                              # SecretSynced / READY True en todos
kubectl -n techcorp get hpa,scaledobject,pdb                        # catalogo 2/20; inventario 2/10; PDB minAvailable 1 en todos
kubectl -n techcorp get networkpolicy | wc -l                       # 11: deny-all, dns, gateway, 6 servicios, bff, analitica
for s in clientes catalogo inventario pagos notificaciones pedidos; do
  kubectl -n techcorp exec deploy/servicio-$s -- wget -qO- http://localhost:$(kubectl -n techcorp get svc servicio-$s -o jsonpath='{.spec.ports[0].port}')/health/ready
done                                                                # {"estado":"ok","comprobaciones":{"postgres":"ok","rabbitmq":"ok"}} en cada uno

La prueba de humo es la E2E de 04-05 hecha a mano a través del Ingress, con un token de verdad. scripts/token-keycloak.sh obtiene uno para la usuaria de pruebas con password grant (activado solo en el cliente pruebas-e2e del realm de dev/staging; en producción no existe ese cliente, 07-01 ej. 1):

TOKEN=$(scripts/token-keycloak.sh https://auth.techcorp.example techcorp pruebas-e2e ana.ruiz 'clave-de-pruebas')   # JWT con clienteId=c-1024, rol cliente
API=https://api.techcorp.example/api/v1

curl -sf "$API/productos?ids=p-501,p-777" | jq -r '.datos[].nombre'                     # público: Auriculares BT X200 / Cable USB-C 2 m
PED=$(curl -sf -X POST "$API/pedidos" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -H "Idempotency-Key: humo-$(date +%s)" \
      -d '{"clienteId":"c-1024","lineas":[{"productoId":"p-501","cantidad":1},{"productoId":"p-777","cantidad":2}],"direccionEnvio":{"calle":"Gran Vía 12","codigoPostal":"28013","ciudad":"Madrid","pais":"ES"}}' | jq -r .id)
echo "pedido $PED"                                                                       # ped-… (202)
for i in $(seq 1 15); do E=$(curl -sf "$API/pedidos/$PED" -H "Authorization: Bearer $TOKEN" | jq -r .estado); echo "$i: $E"; [ "$E" = CONFIRMADO ] && break; sleep 1; done
# 1: PENDIENTE  2: STOCK_RESERVADO  3: STOCK_RESERVADO  4: CONFIRMADO   ← la saga completa con los seis servicios reales
curl -s "$API/pedidos/ped-0000" -H "Authorization: Bearer $TOKEN" -o /dev/null -w '%{http_code}\n'   # 404 (no existe: mismo código que "ajeno", 07-01)
curl -s "$API/pedidos/$PED" -o /dev/null -w '%{http_code}\n'                                          # 401 sin token

Y las tres comprobaciones de observabilidad, que son la razón de haber montado los módulos 6 y 7: (1) en Jaeger (kubectl -n observabilidad port-forward svc/jaeger-query 16686), buscar servicio=gateway, etiqueta pedidoId=$PED: una traza con los spans del gateway, Pedidos, Clientes, Catálogo, PostgreSQL, y —enlazados por links desde el traceparent del outbox (06-02 §6)— Inventario, Pagos (con el span de la pasarela) y Notificaciones; (2) en Grafana, panel "Saga de pedidos": pedidos_creados_total +1, saga_duracion_segundos con una observación de ~3 s, outbox_pendientes a 0 en los cuatro servicios con outbox; (3) en Loki, {namespace="techcorp"} | json | pedidoId="ped-…" devuelve las líneas de los seis servicios ordenadas por tiempo, con el mismo requestId en la parte síncrona.

Si el estado se queda en STOCK_RESERVADO, la tabla del apartado 10 dice dónde mirar (casi siempre: pagos.stock sin consumidor, o PASARELA_URL mal en el ConfigMap de Pagos).

  1. Operación: un despliegue normal de servicio-pedidos

Luis fusiona un PR que añade la cancelación por el cliente (ejercicio 1 de 08-02). Lo que ocurre, con cada pieza en su lección:

flowchart LR
    PR[PR en servicio-pedidos] --> CI[ci.yml → servicio-node-ci.yml@v1<br/>lint, unit, componente, integración Testcontainers,<br/>pactos consumidor publicados]
    CI --> IMG[imagen ghcr.io/…/servicio-pedidos:sha-4b7e9c1<br/>Trivy, cosign, SBOM]
    IMG --> STG[cd.yml: staging<br/>Job migraciones 007, rollout, E2E, record-deployment]
    STG --> CID[can-i-deploy servicio-pedidos sha-4b7e9c1 --to-environment prod<br/>¿verifican Catálogo, Clientes, Inventario, Pagos, Notificaciones?]
    CID --> TAG[git tag v1.6.0 → misma imagen etiquetada 1.6.0]
    TAG --> PRP[PR en plataforma: overlays/prod newTag 1.6.0<br/>revisión humana: es Pedidos]
    PRP --> ARGO[Argo CD sync<br/>PreSync: Job migraciones]
    ARGO --> CAN[Ingress pedidos-canary weight 10 %<br/>+ X-Canary para el equipo]
    CAN --> OBS[30 min: burn rate PedidosErrorBudget,<br/>RED canary vs estable, saga_duracion]
    OBS -->|bien| FULL[weight 100 % → Deployment estable 1.6.0, canary a 0]
    OBS -->|mal| BACK[canary-weight 0 + revert del commit]
Paso Herramienta Tiempo típico Lección
CI completo con Testcontainers y publicación de pactos GitHub Actions, servicio-node-ci.yml@v1 6 min 04-05, 05-03 §3, §11
Imagen, Trivy (bloquea CRITICAL/HIGH), cosign, SBOM docker/build-push-action, aquasecurity/trivy-action, sigstore/cosign 3 min 05-01 §5, 07-04 §2, §9
Staging: kustomize edit set image, Job 007, rollout status, E2E, record-deployment cd.yml 4 min 05-03 §5
can-i-deploy a prod: los cinco consumidores/proveedores han verificado esta versión Pact Broker segundos 05-03 §4, 08-02 §9
Etiqueta v1.6.0 (misma imagen); PR de promoción en plataforma; revisión humana (Pedidos y Pagos la exigen) peter-evans/create-pull-request, revisión de Luis o de un par 10-60 min (persona) 05-03 §7-8
Argo CD aplica overlays/prod: PreSync con servicio-pedidos-migraciones (007-cancelacion-cliente-expand.sql: columna cancelado_por, DEFAULT NULL), después el Deployment canary Argo CD 2 min 05-03 §6, 05-04 §3
Canary al 10 % durante 30 min mirando slo:pedidos_error_ratio por version (etiqueta service.version de 06-02) y la latencia del canary frente a la estable Ingress NGINX canary-weight, Grafana 30 min 05-04 §5, 06-05 §5
100 %: newTag de la estable a 1.6.0, canary-weight: 0 PR (o Argo Rollouts, cuando llegue) 3 min 05-04

Total: unos 50 minutos de reloj, de los cuales unos 12 son máquina; el resto es la revisión humana y la observación del canary, y son deliberados. Frente al jueves noche de 01-05 (dos horas de despliegue, cuatro reversiones de doce), esto ocurre a las 11 de la mañana de un martes, once veces al día en el conjunto de servicios, y si el burn rate sube durante el canary, canary-weight: 0 deja el 100 % en la estable en segundos: MTTR de minutos sin que nadie haya escrito un kubectl a mano.

  1. Operación: la campaña de Black Friday

La campaña se prepara con una lista que Plataforma y los cuatro equipos repasan tres semanas antes (la primera versión se escribió para el Black Friday de 2025, 08-01 §3; hoy es un issue con plantilla en plataforma):

Cuándo Qué Cómo Lección
T−3 semanas Prueba de carga k6 sobre staging al ×20 de navegación y ×4 de pedidos; se corrigen requests, pools e índices pruebas/carga/catalogo.js y pedidos.js 06-04 §5
T−1 semana Overlay temporal overlays/prod-bf/: Catálogo minReplicas: 6 (HPA hasta 20), Inventario minReplicaCount: 4 (KEDA hasta 10), Pedidos y Pagos 4 réplicas fijas, gateway 4; PDB revisados; Cluster Autoscaler con nodos de reserva PR en plataforma, Argo CD 06-04 §2-4
T−1 semana Presupuesto de error: se comprueba que todos los SLOs tienen > 50 % restante; si Pedidos está por debajo, se dedica la semana a fiabilidad, no a funcionalidad Panel de SLOs 06-05 §11
T−48 h Congelación de despliegues salvo correcciones (Argo CD sigue sincronizando, pero no se fusionan PRs de promoción); guardia reforzada con secundario por equipo Regla de equipo, #estado-plataforma 06-05 §8
T−24 h Caché de Catálogo precalentada; TTL de Redis subido de 30 s a 120 s por flag; rate limit del gateway ajustado por ruta ConfigMap + rollout restart de Catálogo 06-04 §6, 03-04
Día D Sala de campaña: paneles "RED por servicio", "Saga de pedidos", "Colas"; alertas de SLO con umbrales normales (no se relajan: si arden, es real) Grafana, Alertmanager 06-01, 06-05
D+3 Se retira el overlay prod-bf, se descongela, se revisan métricas y coste, breve postmortem aunque no haya habido incidente PR, reunión de 30 min 06-05 §10

Lo que se ve el día D en un buen año (como 2025): Catálogo entre 6 y 16 réplicas siguiendo la curva de navegación; Inventario a 4-7 por longitud de inventario.pedidos; saga_duracion_segundos p95 en 4 s (la pasarela también va más lenta); presupuesto de error consumido en el día: 6 % del mensual. Y lo que la lista impide: un kubectl scale manual que Argo CD revierta a los tres minutos (05-03 §6, selfHeal: true), por eso el escalado de campaña también va por PR.

  1. Operación: un incidente en pagos.stock.dlq, paso a paso

Un martes de septiembre de 2026, 15:20. Distinto de INC-2031 (mensaje venenoso por un bug): esta vez la causa es externa y las herramientas de 06-03/06-05 ya existen. Sigue la cronología como si fueras la persona de guardia:

  1. 15:31 — Alerta DlqConMensajes{cola="pagos.stock.dlq"} (warning: 10 min con mensajes, 06-05 §12) llega al canal del equipo de Pagos; no despierta a nadie (no es critical), pero es horario laboral y la guardia la reconoce y abre #inc-2047-pagos-dlq.
  2. 15:33 — Runbook comun/dlq: "¿cuántos, desde cuándo, qué tipo?". kubectl -n infra exec rabbitmq-0 -- rabbitmqctl list_queues name messages | grep dlqpagos.stock.dlq 14. En la consola de RabbitMQ, los 14 son stock.reservado con x-intentos: 5: agotaron los reintentos, no son venenosos.
  3. 15:36 — Loki: {app="servicio-pagos"} | json | nivel="error" | line_format "{{.eventoId}} {{.err.codigo}} {{.mensaje}}"DEPENDENCIA_NO_DISPONIBLE pasarela: timeout desde las 15:12; CircuitoAbierto{dependencia="pasarela"} está también en warning desde las 15:15 (06-03 §4). Jaeger: el span POST pasarela/cobros con error=true y 5.000 ms exactos: la pasarela no responde, no rechaza.
  4. 15:38 — Impacto: panel "Saga de pedidos": 41 pedidos en STOCK_RESERVADO con más de 5 minutos; el vigilante empezará a cancelarlos por TIMEOUT_PAGO a los 10 (06-03 §9). Severidad SEV2 (parte del flujo roto, sin workaround para el cliente); se avisa en #estado-plataforma: "desde las 15:12 los cobros no se completan por un problema del proveedor de pagos; los pedidos se están reteniendo; siguiente actualización 16:00".
  5. 15:40 — Estabilizar antes que entender: la página de estado de la pasarela confirma incidente. Decisión de Pagos: subir temporalmente el límite del vigilante de 10 a 30 minutos (ConfigMap VIGILANTE_LIMITE_MIN de Pedidos + rollout restart; PR exprés en plataforma con la etiqueta incidente, para que Argo no lo revierta) y así no cancelar 41 pedidos con stock reservado por una caída de 20 minutos ajena. Las reservas caducan a los 15 min por expira_en… salvo que se suba también RESERVA_TTL_S; se sube a 2.400 s del mismo modo. Dos cambios de configuración, cero código.
  6. 15:52 — La pasarela se recupera. El breaker pasa a semiabierto y cierra; los mensajes de pagos.stock.reintento (los que aún tenían intentos) se cobran solos en 2 minutos. Quedan los 14 de la DLQ.
  7. 15:55 — Reprocesar: kubectl -n techcorp run reprocesar --rm -it --image=ghcr.io/techcorp/servicio-pagos:1.4.2 --env-from=secret/pagos-rabbitmq -- node scripts/reprocesarDlq.js --cola pagos.stock --max 50 (06-03 §8): 14 mensajes vuelven a la cola con x-intentos: 0 y x-reproceso; cobrarPedido los encuentra EN_CURSO (paso 1 de 08-02 §4) y consulta por clave de idempotencia antes de cobrar: 3 ya se habían cobrado en el primer intento (la pasarela cobró y el timeout llegó antes de la respuesta) y se marcan CAPTURADO sin cobrar dos veces; 11 se cobran ahora. pago.confirmado ×14, sagas cerradas.
  8. 16:02 — Panel: 0 pedidos en STOCK_RESERVADO antiguos; se revierten los dos cambios de configuración (PR de vuelta); se cierra el incidente. Duración: 50 min desde el primer fallo, 31 desde la alerta; 0 pedidos cancelados, 0 cobros duplicados.
  9. Postmortem breve (SEV2, 06-05 §10): causa externa; funcionó todo lo diseñado (reintentos, breaker, DLQ, alerta, runbook, idempotencia de la pasarela); acciones: (a) la alerta DlqConMensajes tardó 10 min por diseño, pero CircuitoAbierto saltó a los 3: añadir al runbook de CircuitoAbierto{pasarela} el paso "subir el vigilante"; (b) hacer VIGILANTE_LIMITE_MIN y RESERVA_TTL_S flags recargables en caliente (04-03 §7) para no reiniciar; (c) preguntar a la pasarela por su SLA. Y la pregunta fija: "¿qué alerta lo habría detectado antes?" —CircuitoAbierto ya lo hizo; lo que faltaba era el enlace entre esa alerta y la acción.

Compara con INC-2031 (40 min, 61 pedidos afectados, 38 cancelados): mismo síntoma ("la saga no avanza"), la mitad de impacto y ninguna acción destructiva, porque el sistema tenía ya reintentos diferidos, DLQ, alertas por síntoma, runbook y un script. Es exactamente para lo que se escribió 06-05.

  1. Operación: rotar un secreto, actualizar Node y evolucionar un contrato

Rotar pedidos-db. Es el procedimiento de 07-04 §4, ejecutado el primer lunes de cada semestre por Plataforma con el equipo dueño: (1) nueva contraseña de svc_pedidos en Vault (techcorp/prod/pedidos/db), con el usuario svc_pedidos_b para tener las dos válidas; (2) kubectl -n techcorp annotate externalsecret pedidos-db force-sync=$(date +%s) → el Secret cambia; (3) kubectl -n techcorp rollout restart deploy/servicio-pedidos (rolling maxUnavailable: 0, 05-04) y rollout status; (4) pedidos-migraciones no hace falta; (5) revocar la antigua y vigilar password authentication failed en Loki durante una hora. Quince minutos, sin ventana de mantenimiento; se repite para *-db y *-rabbitmq de los seis servicios en la misma mañana con un bucle en scripts/rotar-secretos.sh. La auditoria de quién rotó qué es el git log del PR y el audit log de Vault.

Node 20 → 22. Node 20 termina su mantenimiento en abril de 2026; la actualización se hace una vez en la plantilla y se propaga por la regla de Luis: (1) plantilla-servicio-node: FROM node:22-alpine en las dos etapas del Dockerfile (05-01 §3), engines.node: ">=22", @types/lint; (2) servicio-node-ci.yml@v2: actions/setup-node con node-version: 22 y una matriz temporal [20, 22] para que cada servicio compruebe ambas antes de cambiar; (3) @techcorp/comun-http se publica y prueba en 22 (05-03 §10); (4) cada equipo, cuando quiere dentro de un plazo (un mes), abre un PR en su servicio con dos líneas: uses: …/servicio-node-ci.yml@v2 y el FROM; CI, Testcontainers, pactos y staging validan; producción con la estrategia habitual (canary en Pedidos y Pagos, rolling en el resto); (5) al mes, @v1 se retira. Seis PRs de dos líneas en lugar de una migración coordinada; y si Notificaciones se retrasa una semana, nadie más espera.

v2 de productos con precio: { importe, moneda }. Es el cambio incompatible de 03-06 §10, ahora ejecutado de punta a punta:

Semana Catálogo (proveedor) Consumidores Técnica
0 OpenAPI de /v2/productos; precio objeto, imagenUrl obligatorio; revisión con Pedidos, BFF y socios 03-06 §10
1-2 Sirve /v1/ y /v2/ desde el mismo código; el modelo interno ya es el nuevo y una capa traduce hacia atrás para v1 (precio: importe, moneda); Deprecation y Sunset (+6 meses) en /v1/; pactos: los de v1 siguen verificando 03-06, 04-05
2 Migración MongoDB expand: precio numérico coexiste con precioDetallado hasta que todo el código lea el nuevo (un script idempotente rellena; 05-04 §3 aplicado a documentos) 05-04 §3
3-4 Pedidos: nuevo pacto contra /v2/productos (precio.importe, precio.moneda); traductorProducto (ACL de 04-04) mapea {importe, moneda}precioUnitario + moneda; lineas_pedido.moneda ya existía desde 005-lineas-moneda-expand.sql y 006-…-contract.sql (05-04); pedido.creado pasa a version: 2 con precioUnitario: {importe, moneda} y un upcaster en los consumidores (03-06 §6) para tolerar v1 en la DLQ; BFF: Precio en el esquema GraphQL, moneda @deprecated (03-06) canary de Pedidos, can-i-deploy
5-6 Inventario, Pagos, Notificaciones: aceptan pedido.creado v1 y v2 (eachLike en sus pactos de mensajes, 08-02 §9); Pagos usa moneda del evento en lugar de 'EUR' fijo rolling
+6 meses Retira /v1/ (la cabecera Sunset lo anunció); métrica http_requests_total{ruta="/v1/productos"} a 0 durante un mes antes Ninguno queda 03-06 §7

Ningún paso corta nada: en cada momento conviven dos versiones del contrato, del evento y del esquema, y cada equipo despliega cuando quiere dentro del plazo. Es la misma disciplina que en el apartado 6, aplicada a un cambio que en el monolito habría sido "buscar precio en todo el repositorio y cruzar los dedos el jueves".

  1. Runbook rápido de operaciones frecuentes

Necesito… Comando / recurso Lección
Ver estado general kubectl -n techcorp get pods,hpa,scaledobject,externalsecret · panel "RED por servicio" 05-02, 06-01
Logs de un pedido en todos los servicios Loki: {namespace="techcorp"} | json | pedidoId="ped-…" 06-01 §5
Traza de un pedido Jaeger: servicio=gateway, tag pedidoId 06-02 §8
Desplegar a producción PR en plataforma/k8s/<svc>/overlays/prod (newTag); Argo CD sincroniza 05-03 §6
Revertir un despliegue argocd app rollback <app> <id> (o revertir el commit); emergencia: kubectl rollout undo deploy/<svc> 05-04 §2, 05-03
Cortar un canary kubectl -n techcorp annotate ingress <svc>-canary nginx.ingress.kubernetes.io/canary-weight=0 --overwrite 05-04 §5
Cambiar configuración PR al ConfigMap del overlay + rollout restart (o flag en caliente si la hay) 04-03, 05-02 §5
Relanzar migraciones kubectl -n techcorp delete job <svc>-migraciones && kubectl apply -k … (idempotentes) 05-02 §6, 05-03 §5
Ver colas y DLQ rabbitmqctl list_queues name messages consumers · consola 15672 · panel "Colas" 03-02, 06-03 §8
Reprocesar una DLQ kubectl run … -- node scripts/reprocesarDlq.js --cola <cola> --max N [--descartar] (auditado) 06-03 §8, 07-03 §9
Pedidos atascados en la saga Panel "Saga de pedidos"; vigilante TIMEOUT_PAGO; CronJob reconciliar-reservas (kubectl create job --from=cronjob/reconciliar-reservas ahora) 06-03 §9
Escalar a mano (dev) / campaña (prod) kubectl scale (dev) · overlay prod-bf por PR (prod) 06-04, §7
Rotar un secreto Vault → annotate externalsecret force-syncrollout restart 07-04 §4, §9
Añadir un servicio Copiar k8s/servicio-pedidos/, cambiar nombre/puerto/ConfigMap; su NetworkPolicy en k8s/red/; usuario RabbitMQ y svc_*; Application de Argo 05-02 §12, 07-04
Ver presupuesto de error Panel SLOs; slo:*_presupuesto_restante:ratio 06-05 §3, §11
Silenciar una alerta en mantenimiento Alertmanager: amtool silence add alertname=… --duration=2h --comment=… 06-05 §7
Comprobar seguridad de la plataforma SEGURIDAD.md; kubectl -n techcorp get networkpolicy; Trivy en CI; cosign verify 07-04

  1. Coste mensual aproximado y cómo reducirlo

Cifras ficticias, orientativas, para un clúster gestionado de producción con 3.000 pedidos/día (sin campaña), redondeadas al alza. Sirven para tener orden de magnitud y para el argumento de FinOps de 08-04:

Componente Dimensión €/mes aprox.
Nodos del clúster (plano de control gestionado incluido) 6 nodos de 4 vCPU/16 GB + autoescalado a 12 en campaña 900
PostgreSQL gestionado (5 BD en 2 instancias: pedidos+pagos, resto) con réplica y copias 2 × (2 vCPU/8 GB) 420
MongoDB gestionado (catálogo, réplica de 3) M20-equivalente 180
RabbitMQ gestionado (3 nodos, TLS) pequeño 160
Redis gestionado (caché de catálogo) 1 GB con réplica 60
Keycloak (en el clúster) + su PostgreSQL 2 pods, BD compartida con "resto" 40
Observabilidad: Prometheus/Grafana/Loki/Jaeger en el clúster + almacenamiento de objetos (métricas 15 d, logs 30 d, trazas 7 d al 10 %) ~1,5 nodos + 400 GB 260
Balanceador, IPs, tráfico de salida, certificados 90
Registro de contenedores, Pact Broker, gestor de secretos, GitHub Actions (minutos extra) SaaS 150
Staging (todo lo anterior a escala 1/3, apagado de noche) 500
Total ≈ 2.760 €/mes (≈ 4.100 en el mes de Black Friday)

Frente al monolito (tres servidores grandes + PostgreSQL + diez servidores durante un mes al año ≈ 1.900 €/mes de media), es un 45 % más caro en infraestructura pura… y un 30 % más barato en campaña, y no incluye lo que se ahorra en horas de despliegue e incidentes (08-01 §10). Cómo reducirlo, por orden de retorno: (1) requests reales medidos con Prometheus (06-04 §4) y right-sizing de nodos: la mayoría de servicios de TechCorp piden 250 m de CPU y usan 60 m; (2) apagar staging fuera de horario (ya se hace) y usar minReplicaCount: 0 de KEDA para consumidores esporádicos como analítica; (3) muestreo de trazas al 10 % y retención de logs de 30 días (ya), sample_limit y revisión trimestral de cardinalidad (08-01 §11); (4) reservar capacidad de nodos base a un año (−30 %); (5) revisar si Redis, con la carga real, compensa frente a la caché HTTP del gateway; (6) no ahorrar en copias de seguridad, TLS ni réplicas de las bases de datos de Pedidos y Pagos.

Errores Comunes y Consejos

  • Instalar la infraestructura a mano y los servicios por GitOps. A los tres meses nadie sabe qué versión de RabbitMQ hay ni con qué values. Todo en k8s/infra/ y aplicado por script o por Argo, aunque sea Helm.
  • Saltarse el orden de CRDs. Un ExternalSecret aplicado antes que ESO o un ServiceMonitor antes que el operador fallan con "no matches for kind": el script espera con --wait a cada operador antes de seguir.
  • Prueba de humo sin token o con el token de admin. Debe usar un usuario con rol cliente y su clienteId: es la única forma de comprobar la regla del propietario y el 404 del pedido ajeno.
  • Escalar a mano en producción con Argo CD en selfHeal. Lo revierte en minutos y, peor, en mitad de una campaña. Por PR, siempre.
  • Reprocesar la DLQ antes de entender la causa. Si el mensaje es venenoso, vuelve a la DLQ y consume cinco reintentos; si la dependencia sigue caída, igual. Primero Loki/Jaeger, después el script.
  • Rotar un secreto sin rollout restart y creer que ha funcionado porque kubectl get secret muestra el nuevo. Los pods viejos siguen con la contraseña antigua hasta que reinician (o hasta que se lee como fichero).
  • Consejo: ejecuta desplegar-todo.sh dev en un clúster kind limpio una vez al mes. Si tarda más de 15 minutos o falla en algún paso, algo del árbol de plataforma se ha desactualizado; mejor descubrirlo un martes que en la recuperación ante desastre.

Ejercicios

Ejercicio 1: Recuperación ante desastre

El clúster de producción se pierde por completo (región caída). Con el repositorio plataforma, las copias de las bases de datos gestionadas y las imágenes en ghcr.io, escribe la secuencia de recuperación en un clúster nuevo en otra región, indicando qué pasos del apartado 3 cambian, qué hay que restaurar antes de qué, qué datos podrían perderse (piensa en el outbox y las colas) y una estimación de tiempo. ¿Qué habría que tener preparado de antemano para que fuera de una hora en vez de un día?

Ejercicio 2: El canary que miente

Durante el canary al 10 % de servicio-pedidos 1.6.0 el burn rate está perfecto, pero al pasar al 100 % saga_duracion_segundos p95 sube de 3 a 40 s. Explica qué tipo de fallo no detecta un canary por peso de tráfico HTTP en un servicio que además consume eventos, cómo lo habrías detectado durante el canary (métricas y etiquetas concretas), y qué cambiarías en el procedimiento del apartado 6.

Ejercicio 3: Reducir la factura

Marta pide bajar la factura mensual un 25 % sin tocar la disponibilidad de Pedidos ni de Pagos. Con la tabla del apartado 11, propón una lista concreta de medidas con su ahorro estimado y su riesgo, y di cuáles no aceptarías aunque ahorren.

Soluciones

Ejercicio 1. Secuencia: pasos 1-2 iguales (namespaces, operadores) en el clúster nuevo; paso 3 cambia: restaurar las bases de datos gestionadas desde copia (PostgreSQL de Pedidos y Pagos primero, con point-in-time recovery al instante más cercano; MongoDB del catálogo; RabbitMQ no se restaura: se crea vacío con definitions.json, y los mensajes en vuelo se pierden); paso 4 Keycloak desde su BD restaurada; 5-6 iguales; 7 los ExternalSecret apuntan al mismo Vault (que debe estar fuera de la región o replicado); 8-9 iguales, cambiando DNS de api.techcorp.example al nuevo balanceador y esperando el certificado. Datos en riesgo: los eventos que estaban en RabbitMQ sin consumir (colas duraderas de un clúster perdido) y los segundos entre la última copia y la caída. El outbox es la salvación parcial: al arrancar, los relays de los cuatro servicios republican todo lo que tenga publicado_en IS NULL, así que los eventos generados y no publicados se recuperan; los ya publicados y no consumidos se pierden → los pedidos en STOCK_RESERVADO o PENDIENTE sin avanzar los cierra el vigilante (TIMEOUT_PAGO) y la reconciliación de reservas (06-03 §9), y cobrarPedido consulta a la pasarela antes de cobrar (EN_CURSO), así que no hay cobros dobles. Tiempo: con todo a mano y probado, 2-4 horas; sin práctica, un día. Para una hora: clúster secundario "caliente" con la infraestructura de los pasos 1-7 ya aplicada y Argo CD apuntando al mismo repositorio (solo cambiar el destino), réplicas de las BD gestionadas en la otra región, Vault multirregión, y un ejercicio de recuperación trimestral (el consejo del apartado anterior).

Ejercicio 2. El canary por peso del Ingress reparte peticiones HTTP, pero las réplicas canary también consumen pedidos.saga en igualdad de condiciones con las estables (colas competidoras, 06-04 §9): al 10 % de tráfico HTTP, el canary ya procesa quizá el 33 % de los eventos (1 réplica de 3), y si su consumidor es lento (una consulta sin índice sobre pedidos añadida en 1.6.0), el efecto se diluye entre las estables y aparece "de golpe" al 100 %. Detección durante el canary: mirar métricas etiquetadas por version (la etiqueta service.version de 06-02, que las métricas de comun-http incluyen): histogram_quantile(0.95, sum by (le, version) (rate(saga_duracion_segundos_bucket[5m]))), la duración de procesarUnaVez por versión, pg_stat_statements o la latencia de las consultas del consumidor, y x-intentos/mensajes en pedidos.saga sin ack por pod. Cambio de procedimiento: añadir al apartado 6 un panel "canary vs estable" que compare también las métricas de consumidores y de base de datos por version, no solo RED HTTP; y, para servicios con consumidores, alargar el canary o hacer canary por réplica con una cola de prueba (o mesh, cuando llegue) —es una de las señales que 02-05 fijó para replantear la orquestación de la saga y que 05-05 dejó como argumento a favor del mesh.

Ejercicio 3. Medidas: (1) requests reales y nodos de 6 a 4 fuera de campaña (autoescalado sigue): −250 €, riesgo bajo si los PDB y el HPA están bien; (2) staging a 1/4 y solo en horario laboral con arranque bajo demanda: −200 €, riesgo bajo (la E2E de cd.yml tarda 5 min más si está apagado); (3) capacidad reservada un año para 4 nodos base: −180 €, riesgo de compromiso; (4) trazas al 5 % con muestreo por cola de errores al 100 %, logs de Catálogo a info (hoy debug en un overlay olvidado), 15 días de logs: −80 €, riesgo medio en depuración; (5) MongoDB a la instancia inferior con la caché de Redis (que ya absorbe el 85 %): −60 €, riesgo bajo, medir con k6; (6) KEDA a cero para analítica y bff-movil de noche: −30 €. Total ≈ −800 € (29 %). No aceptaría: quitar la réplica o las copias de PostgreSQL de Pedidos/Pagos, pasar RabbitMQ a un nodo, quitar TLS interno de RabbitMQ/BD, o reducir minReplicas de Pedidos/Pagos a 1 (un solo pod rompe el rolling sin cortes de 05-04 y el PDB): ahorran 100-200 € y ponen en juego el SLO del 99,9 % y la seguridad de 07-02.

Conclusión

El sistema de TechCorp ya no es solo código: vive en un clúster y se opera con procedimientos escritos. Hemos recorrido el repositorio techcorp/plataforma completo (bases y overlays por servicio, k8s/red/, k8s/infra/ con los values de Helm de RabbitMQ, PostgreSQL, MongoDB, Redis, Keycloak y la observabilidad, SLOs, Argo CD, dashboards, runbooks, pruebas E2E y de carga, el workflow reutilizable y SEGURIDAD.md), el compose.yaml que levanta los seis servicios con Keycloak, Jaeger, Prometheus, Grafana y Loki en un portátil, el orden de arranque de un clúster nuevo (namespaces → operadores y CRDs → datos y mensajería → identidad → observabilidad → red → secretos → servicios en el orden de la saga → Ingress) plasmado en desplegar-todo.sh con sus Job de migración idempotentes, y la verificación con una prueba de humo real —un token de Keycloak, un pedido a través del Ingress que llega a CONFIRMADO, su traza en Jaeger, su marca en el panel "Saga de pedidos" y sus logs en Loki—. Y hemos operado: un despliegue de servicio-pedidos de PR a 100 % pasando por CI, pactos, imagen firmada, staging, can-i-deploy, revisión humana, Argo CD y canary observado; la lista de Black Friday con overlay de campaña, KEDA, presupuesto de error y congelación; el incidente INC-2047 en pagos.stock.dlq resuelto con alerta, runbook, Loki, Jaeger, dos cambios de configuración y reprocesarDlq.js sin un cobro duplicado; la rotación de pedidos-db con ESO, la actualización a Node 22 propagada por plantilla y workflow reutilizable, y la v2 de productos con precio {importe, moneda} aplicada de punta a punta con expand/contract; más el runbook rápido y una factura mensual con sus palancas.

Con esto, la historia de TechCorp está completa: monolito, migración, implementación, despliegue y operación. Queda lo más valioso de un caso de estudio, que es destilarlo: qué hicimos, qué salió mal y qué haríamos distinto en cada dimensión, qué antipatrones aprendimos a reconocer, una lista consolidada de buenas prácticas con su lección de referencia, qué haría TechCorp en su fase 2 y cómo puedes aplicar todo esto a tu propio contexto. Es la última lección del curso.

Curso de Microservicios

Módulo 1: Introducción a los Microservicios

Módulo 2: Diseño de Microservicios

Módulo 3: Comunicación entre Microservicios

Módulo 4: Implementación de Microservicios

Módulo 5: Despliegue y Orquestación

Módulo 6: Monitoreo y Mantenimiento

Módulo 7: Seguridad en Microservicios

Módulo 8: Casos de Estudio y Ejemplos Prácticos

© Copyright 2026. Todos los derechos reservados