La lección anterior terminó con una cuenta: seis servicios, tres almacenes, Kafka, Redis, MinIO, Kong, Keycloak, Vault, Prometheus, Loki, Tempo, Patroni, etcd. Decenas de contenedores, cientos de parámetros, y un docker-compose.yml que ya no cabe en una pantalla. Mientras todo eso se despliegue como el monolito de 01-06 (los martes y jueves, con un ssh, un git pull y una lista de pasos en un documento que alguien sigue a mano), la plataforma tiene dos problemas que ningún patrón de resiliencia arregla: no es reproducible (nadie sabe exactamente qué hay en inventario-2 ni por qué difiere de inventario-1) y depende de que una persona no se equivoque un jueves a las 18:00. Esta lección trata de eliminar a la persona del camino crítico: primero describiendo la infraestructura como código (Ansible para configurar máquinas; imágenes inmutables para los servicios), y después delegando en un orquestador, Kubernetes, las decisiones que hoy toma alguien a mano: dónde corre cada réplica, cuántas hacen falta, qué hacer cuando una muere, y cómo llevar una versión nueva a producción sin que Ana lo note. Las pruebas que dan confianza para automatizar son la lección 07-06; los servicios gestionados en nube, 08-03.

Contenido

  1. Por qué automatizar
  2. Infraestructura como código: configuración frente a aprovisionamiento
  3. Ansible: inventario, playbooks, roles e idempotencia
  4. Inmutabilidad: imágenes, registro y etiquetas por commit
  5. Kubernetes: por qué un orquestador y cómo está hecho
  6. Objetos esenciales
  7. Manifiestos de Kilómetro Cero: k8s/
  8. Estrategias de despliegue: rolling, blue/green, canary
  9. Namespaces, RBAC, service mesh y operadores
  10. GitOps y CI/CD
  11. docker-compose frente a Kubernetes
  12. Errores comunes y consejos
  13. Ejercicios y soluciones
  14. Conclusión

  1. Por qué automatizar

Tres razones, y las tres se han visto ya en el curso sin nombrarlas:

  • Reproducibilidad. En 07-03 se dijo que la redundancia solo protege contra fallos independientes, y que tres réplicas "iguales" configuradas a mano nunca lo son del todo. Si inventario-2 se creó copiando inventario-1 y editando "lo necesario", hay diferencias que nadie recuerda, y una de ellas será la causa de un incidente. Automatizar es que la descripción sea la única fuente: se puede destruir un nodo y recrearlo idéntico.
  • Escala. Con 3 réplicas se puede hacer a mano; con 30, durante la Semana de la Vendimia, no. Y "a mano" incluye decidir cuántas hacen falta: el autoescalado es automatización de una decisión, no solo de una tarea.
  • Errores humanos. El DELETE de 07-03, el despliegue del sábado de 07-01 con el 8 % de errores, el certificado que caducó un domingo: los tres tienen en común que alguien hizo algo a mano o dejó de hacerlo. La automatización no elimina el error (un script equivocado se equivoca 30 veces en 30 nodos), pero lo hace revisable antes (código en un pull request), probable (07-06) y reversible (rollout undo).

El monolito de 01-06 se desplegaba con una ventana de mantenimiento; el objetivo aquí es que un despliegue de pedidos sea un evento tan rutinario que ocurra varias veces al día sin que nadie se dé cuenta, incluida Ana.

  1. Infraestructura como código: configuración frente a aprovisionamiento

Infraestructura como código (IaC) es tratar servidores, redes, configuraciones y despliegues como ficheros en un repositorio: versionados, revisados y aplicados por una herramienta. Dos familias que conviene no confundir:

Gestión de configuración Aprovisionamiento
Pregunta "Dada esta máquina, ¿qué debe tener instalado y configurado?" "¿Qué máquinas, redes, discos y balanceadores deben existir?"
Herramientas Ansible, Puppet, Chef, Salt Terraform, OpenTofu, Pulumi, CloudFormation
Modelo Procedimental-idempotente: tareas que llevan la máquina al estado deseado Declarativo: describe el estado final y calcula la diferencia
En Kilómetro Cero Instalar y configurar los nodos de Kafka, Cassandra, Patroni; preparar los nodos del clúster de Kubernetes Crear las máquinas virtuales, la red y el balanceador en el proveedor (08-03)

Ansible es la que toca esta lección, porque es la que configura lo que ya existe (las máquinas físicas o virtuales de Kilómetro Cero) y porque su concepto central, la idempotencia de tareas, es el mismo que gobierna los reintentos de 02-05 y 07-04. Terraform se ve en 08-03, cuando la infraestructura pase a ser una API de nube.

  1. Ansible: inventario, playbooks, roles e idempotencia

Ansible no necesita agente: se conecta por SSH a cada máquina del inventario, ejecuta módulos (pequeños programas Python que saben instalar paquetes, escribir ficheros, gestionar servicios) y devuelve si cambió algo. Un playbook es un YAML que dice qué tareas aplicar a qué grupo de máquinas; un rol es un playbook empaquetado y reutilizable (tareas, plantillas, variables por defecto, handlers).

# km0/ansible/inventario.ini
[kafka]
kafka-1 ansible_host=10.10.1.11 kafka_id=1 zona=bcn
kafka-2 ansible_host=10.10.2.11 kafka_id=2 zona=vlc
kafka-3 ansible_host=10.10.3.11 kafka_id=3 zona=gir

[cassandra]
cass-1 ansible_host=10.10.1.21 zona=bcn
cass-2 ansible_host=10.10.2.21 zona=vlc
cass-3 ansible_host=10.10.3.21 zona=gir

[patroni]
inv-bcn ansible_host=10.10.1.31
inv-vlc ansible_host=10.10.2.31
inv-gir ansible_host=10.10.3.31

[k8s_control]
k8s-cp-1 ansible_host=10.10.1.41
[k8s_workers]
k8s-w-[1:6] ansible_host=10.10.1.5[1:6]

[all:vars]
ansible_user=km0ops
ansible_ssh_private_key_file=~/.ssh/km0ops

El playbook de Kafka delega en un rol y pasa las variables del clúster:

# km0/ansible/kafka.yml
- name: Configurar los brokers de Kafka de Kilómetro Cero
  hosts: kafka
  become: true                     # sudo para instalar y tocar /etc
  serial: 1                        # UN broker cada vez: nunca dos brokers reiniciándose a la vez (ISR, 07-03)
  vars:
    kafka_version: "3.8.0"
    kafka_cluster_id: "km0-kafka-prod"
    kafka_controller_quorum: "1@kafka-1:9093,2@kafka-2:9093,3@kafka-3:9093"
    kafka_default_replication_factor: 3
    kafka_min_insync_replicas: 2
    kafka_log_retention_hours: 168
  roles:
    - kafka

Y el rol, con tareas cuidadosamente idempotentes:

# km0/ansible/roles/kafka/tasks/main.yml
- name: Usuario de sistema para Kafka
  ansible.builtin.user:
    name: kafka
    system: true
    shell: /usr/sbin/nologin
  # Módulo 'user': si el usuario existe con esos atributos, no hace nada (changed=false)

- name: Java 17
  ansible.builtin.apt:
    name: openjdk-17-jre-headless
    state: present
    update_cache: true
    cache_valid_time: 3600

- name: Descargar y desempaquetar Kafka {{ kafka_version }}
  ansible.builtin.unarchive:
    src: "https://archive.apache.org/dist/kafka/{{ kafka_version }}/kafka_2.13-{{ kafka_version }}.tgz"
    dest: /opt
    remote_src: true
    creates: "/opt/kafka_2.13-{{ kafka_version }}"     # si ya existe, no descarga: idempotente
    owner: kafka
    group: kafka

- name: Enlace /opt/kafka a la versión activa
  ansible.builtin.file:
    src: "/opt/kafka_2.13-{{ kafka_version }}"
    dest: /opt/kafka
    state: link

- name: Directorio de datos en el disco dedicado
  ansible.builtin.file:
    path: /var/lib/kafka
    state: directory
    owner: kafka
    group: kafka
    mode: "0750"

- name: Configuración del broker (KRaft)
  ansible.builtin.template:
    src: server.properties.j2
    dest: /opt/kafka/config/kraft/server.properties
    owner: kafka
    group: kafka
    mode: "0640"
  notify: reiniciar kafka          # solo si el fichero CAMBIÓ se dispara el handler

- name: Certificado y clave del broker desde Vault (06-04)
  community.hashi_vault.vault_pki_generate_certificate:
    engine_mount_point: pki_int
    role_name: kafka-broker
    common_name: "{{ inventory_hostname }}.km0.internal"
    ttl: 720h
  register: cert
  changed_when: false               # generar un cert no es "cambiar la máquina"; el fichero de abajo sí
  no_log: true                      # nunca imprimir la clave privada en la salida

- name: Escribir el keystore del broker
  ansible.builtin.copy:
    content: "{{ cert.data.data.certificate }}\n{{ cert.data.data.private_key }}"
    dest: /etc/kafka/broker.pem
    owner: kafka
    mode: "0600"
  no_log: true
  notify: reiniciar kafka

- name: Formatear el almacenamiento KRaft (solo la primera vez)
  ansible.builtin.command:
    cmd: /opt/kafka/bin/kafka-storage.sh format -t {{ kafka_cluster_id }} -c /opt/kafka/config/kraft/server.properties
    creates: /var/lib/kafka/meta.properties     # si existe, ya está formateado: no se repite JAMÁS
  become_user: kafka

- name: Unidad systemd
  ansible.builtin.template:
    src: kafka.service.j2
    dest: /etc/systemd/system/kafka.service
  notify: reiniciar kafka

- name: Kafka habilitado y arrancado
  ansible.builtin.systemd:
    name: kafka
    state: started
    enabled: true
    daemon_reload: true

- name: Esperar a que el broker acepte conexiones antes de pasar al siguiente
  ansible.builtin.wait_for:
    port: 9092
    host: "{{ ansible_host }}"
    timeout: 120
# km0/ansible/roles/kafka/handlers/main.yml
- name: reiniciar kafka
  ansible.builtin.systemd:
    name: kafka
    state: restarted
{# km0/ansible/roles/kafka/templates/server.properties.j2 #}
process.roles=broker,controller
node.id={{ kafka_id }}
controller.quorum.voters={{ kafka_controller_quorum }}
listeners=SSL://:9092,CONTROLLER://:9093
broker.rack={{ zona }}
log.dirs=/var/lib/kafka
default.replication.factor={{ kafka_default_replication_factor }}
min.insync.replicas={{ kafka_min_insync_replicas }}
unclean.leader.election.enable=false
log.retention.hours={{ kafka_log_retention_hours }}
ssl.keystore.type=PEM
ssl.keystore.location=/etc/kafka/broker.pem
ssl.truststore.location=/etc/kafka/ca.pem
ssl.client.auth=required

La idempotencia está en cada detalle: creates: evita repetir descargas y, sobre todo, evita volver a formatear un broker con datos (un kafka-storage.sh format sobre un broker en producción lo destruye); template solo notifica al handler si el contenido cambió, así que ejecutar el playbook diez veces seguidas reinicia Kafka cero veces; serial: 1 con wait_for al final hace que un despliegue de configuración recorra los brokers de uno en uno respetando las ISR de 07-03. Ejecutar ansible-playbook -i inventario.ini kafka.yml --check --diff muestra qué cambiaría sin cambiar nada: es la revisión previa que el ssh de los martes nunca tuvo. La misma estructura (rol cassandra con nodetool drain antes de reiniciar, rol patroni con patronictl switchover si el nodo es líder) se aplica al resto de máquinas con estado.

  1. Inmutabilidad: imágenes, registro y etiquetas por commit

Ansible configura máquinas que cambian. Para los servicios de km0/ se sigue el camino contrario: la infraestructura inmutable. Un servicio no se "actualiza": se construye una imagen de contenedor nueva con todo dentro (intérprete, dependencias fijadas, código, configuración por defecto), se sube a un registro y se despliega sustituyendo los contenedores viejos por nuevos. Nunca se hace ssh a un contenedor para arreglar algo; si algo está mal, se construye otra imagen.

# km0/servicios/pedidos/Dockerfile
FROM python:3.12-slim AS base
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
RUN useradd --system --uid 10001 km0

FROM base AS deps
COPY servicios/pedidos/requirements.lock .        # versiones exactas (06-05: dependencias fijadas)
RUN pip install --no-cache-dir -r requirements.lock

FROM deps AS final
COPY servicios/comun /app/servicios/comun
COPY servicios/pedidos /app/servicios/pedidos
COPY contratos /app/contratos
USER km0                                           # nunca root dentro del contenedor
EXPOSE 8000
ENTRYPOINT ["uvicorn", "servicios.pedidos.app:app", "--host", "0.0.0.0", "--port", "8000"]

La etiqueta de la imagen es la clave de la reproducibilidad: registro.km0.internal/km0/pedidos:1.14.2-3f9a1c7 (versión semántica + hash corto del commit). Nunca latest: latest es un puntero móvil, y "qué versión hay en producción" debe tener una respuesta exacta. La misma imagen recorre pruebas, staging y producción; lo que cambia entre entornos es la configuración inyectada (variables de entorno, ConfigMap, Secret), no la imagen. Ese es el principio "build once, deploy many".

  1. Kubernetes: por qué un orquestador y cómo está hecho

Con imágenes inmutables, queda decidir dónde corre cada contenedor, cuántos hay, qué pasa cuando uno muere o un nodo cae, cómo se encuentran entre sí y cómo se sustituyen por la versión nueva. En docker-compose todo eso lo decide una persona, en un solo nodo. Un orquestador lo decide continuamente, en un clúster:

Necesidad Lo que hacía la persona Lo que hace Kubernetes
Planificación "pedidos-3 en el nodo 2, que tiene sitio" El scheduler coloca cada Pod según recursos, afinidades y zonas
Autoescalado "Vendimia: subo a 6 réplicas el viernes" HPA ajusta réplicas según CPU o una métrica de Prometheus (lag de Kafka)
Autorreparación Alerta ObjetivoCaido, ssh, docker restart Reinicia por liveness; recrea Pods en otro nodo si el nodo cae
Despliegue progresivo Ventana de mantenimiento Rolling update con readiness; canary; rollout undo
Descubrimiento y balanceo Editar prometheus.yml y la config de Kong con cada IP Service con DNS estable y balanceo entre Pods listos
Configuración y secretos Ficheros copiados a mano ConfigMap, Secret, integración con Vault
flowchart TB
    subgraph cp["Plano de control"]
        API[kube-apiserver<br/>única puerta de entrada]
        ETCD[(etcd<br/>estado deseado y actual)]
        SCH[kube-scheduler<br/>elige nodo para cada Pod]
        CM[controller-manager<br/>bucles: Deployment, ReplicaSet, HPA...]
        API <--> ETCD
        SCH --> API
        CM --> API
    end
    subgraph w1["Nodo trabajador 1 (zona bcn)"]
        K1[kubelet] --> P1[Pod pedidos-7d9f-abc<br/>+ sidecar Envoy]
        K1 --> P2[Pod catalogo-5c1e-xyz]
        KP1[kube-proxy]
    end
    subgraph w2["Nodo trabajador 2 (zona vlc)"]
        K2[kubelet] --> P3[Pod pedidos-7d9f-def]
        K2 --> P4[Pod inventario-0<br/>StatefulSet]
        KP2[kube-proxy]
    end
    API --> K1
    API --> K2
    OPS[kubectl apply -f k8s/<br/>Argo CD] --> API
    PROM[Prometheus<br/>service discovery] --> API

La idea que lo explica todo es el bucle de reconciliación: el usuario declara en la API el estado deseado ("3 réplicas de pedidos:1.14.2"), etcd lo guarda (el mismo etcd de 03-03: Kubernetes es, en el fondo, un conjunto de controladores sobre un almacén de consenso), y cada controlador compara continuamente el estado deseado con el real y actúa para acercarlos: si hay 2 Pods y se desean 3, crea uno; si el nodo 1 desaparece, sus Pods se recrean en otro. No hay "comando de desplegar"; hay "cambiar el estado deseado" y esperar a que los controladores converjan. Es la misma filosofía de la idempotencia de Ansible, pero ejecutándose sin parar.

  1. Objetos esenciales

Objeto Qué es En Kilómetro Cero
Pod La unidad mínima: uno o más contenedores que comparten red y almacenamiento; efímero, con IP propia Un contenedor de pedidos + el sidecar Envoy del mesh
ReplicaSet Mantiene N Pods idénticos vivos Lo crea el Deployment; no se escribe a mano
Deployment Gestiona ReplicaSets para servicios sin estado: réplicas, plantilla del Pod, estrategia de actualización, historial para undo pedidos, catalogo, pagos, reparto, analitica, Kong
StatefulSet Pods con identidad estable (inventario-0, -1, -2), disco propio persistente y arranque ordenado Cassandra, Kafka, PostgreSQL (aunque en producción se prefieren operadores, sección 9)
Service Nombre DNS e IP estables que balancean hacia los Pods listos de un selector pedidos.km0.svc.cluster.local:8000
Ingress Regla de entrada HTTP/HTTPS desde fuera del clúster hacia Services Expone Kong; Kong hace el resto (06-05)
ConfigMap Configuración no secreta (ficheros, variables) prometheus.yml, KM0_TRAZAS_RATIO
Secret Datos sensibles codificados en base64 (y cifrados en etcd si se configura) Credenciales de Vault (el resto lo da Vault dinámicamente, 06-04)
Job / CronJob Un Pod que corre hasta terminar; CronJob lo programa reconciliacion_nocturna.py (07-03), base_backup.sh, prueba de restauración
HPA Horizontal Pod Autoscaler: ajusta las réplicas de un Deployment según métricas pedidos por CPU; analitica por lag de Kafka
PersistentVolumeClaim Petición de disco que sobrevive al Pod Los datos de cada nodo de Cassandra
Namespace Partición lógica del clúster: nombres, cuotas, permisos km0-prod, km0-staging, observabilidad

  1. Manifiestos de Kilómetro Cero: k8s/

k8s/pedidos-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: pedidos
  namespace: km0-prod
  labels: {app: pedidos, equipo: comercio}
spec:
  replicas: 3
  revisionHistoryLimit: 10              # cuántos ReplicaSets antiguos conservar para `rollout undo`
  selector:
    matchLabels: {app: pedidos}
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1                       # como mucho 1 Pod extra durante la actualización (4 en total)
      maxUnavailable: 0                 # nunca menos de 3 listos: la capacidad no baja
  template:
    metadata:
      labels: {app: pedidos, version: "1.14.2"}
      annotations:
        prometheus.io/scrape: "true"    # Prometheus (07-01) descubre el Pod por esta anotación
        prometheus.io/port: "8000"
        prometheus.io/path: /metrics
    spec:
      serviceAccountName: pedidos       # identidad del Pod: para RBAC y para autenticarse en Vault
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
      topologySpreadConstraints:        # repartir las réplicas entre zonas (07-03: dominios de fallo)
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector: {matchLabels: {app: pedidos}}
      containers:
        - name: pedidos
          image: registro.km0.internal/km0/pedidos:1.14.2-3f9a1c7
          ports: [{containerPort: 8000, name: http}]
          env:
            - name: KM0_SERVICIO
              value: pedidos
            - name: OTEL_EXPORTER_OTLP_ENDPOINT
              value: otel-collector.observabilidad.svc:4317
          envFrom:
            - configMapRef: {name: pedidos-config}      # KM0_LOG_NIVEL, KM0_TRAZAS_RATIO...
            - secretRef: {name: pedidos-vault}          # VAULT_ROLE_ID / VAULT_SECRET_ID (AppRole, 06-04)
          resources:
            requests: {cpu: "250m", memory: "256Mi"}    # lo que el scheduler reserva: base para el HPA
            limits: {cpu: "1", memory: "512Mi"}         # tope: por encima, throttling de CPU / OOMKilled
          startupProbe:                                 # 07-03: mientras arranca, no aplicar liveness
            httpGet: {path: /salud/vivo, port: http}
            failureThreshold: 30
            periodSeconds: 2                            # hasta 60 s para cargar certificados de Vault
          livenessProbe:
            httpGet: {path: /salud/vivo, port: http}
            periodSeconds: 10
            failureThreshold: 3                         # 30 s sin responder: reinicio
          readinessProbe:
            httpGet: {path: /salud/listo, port: http}
            periodSeconds: 5
            failureThreshold: 2                         # 10 s con PostgreSQL/Kafka caídos: fuera del Service
            successThreshold: 1
          lifecycle:
            preStop:
              exec: {command: ["sleep", "5"]}           # dar tiempo a que el Service deje de enviar tráfico
      terminationGracePeriodSeconds: 30                 # SIGTERM, y 30 s para terminar peticiones en curso

Cada bloque responde a algo visto antes: las sondas son las de 07-03 con sus parámetros; requests alimenta al scheduler y al HPA; limits es un bulkhead de recursos (07-04) por Pod; maxUnavailable: 0 garantiza que un despliegue no reduce la capacidad; topologySpreadConstraints reparte por zonas; preStop + terminationGracePeriodSeconds son el apagado ordenado (una petición de 400 ms no se corta a la mitad). Y envFrom: secretRef solo da al Pod lo mínimo para presentarse ante Vault, que es quien entrega credenciales dinámicas y certificados.

k8s/pedidos-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: pedidos
  namespace: km0-prod
spec:
  selector: {app: pedidos}          # cualquier Pod con esta etiqueta y readiness OK recibe tráfico
  ports:
    - name: http
      port: 8000
      targetPort: http
  type: ClusterIP                   # solo alcanzable dentro del clúster: Kong es la única entrada

Kong (dentro del clúster) enruta /api/v1/pedidos a http://pedidos.km0-prod.svc:8000. Cuando un Pod falla la readiness, desaparece de los endpoints del Service en segundos; cuando se despliega una versión nueva, los Pods nuevos entran solo cuando están listos. Nadie edita una IP.

k8s/pedidos-hpa.yaml

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: pedidos
  namespace: km0-prod
spec:
  scaleTargetRef: {apiVersion: apps/v1, kind: Deployment, name: pedidos}
  minReplicas: 3
  maxReplicas: 12
  metrics:
    - type: Resource
      resource:
        name: cpu
        target: {type: Utilization, averageUtilization: 60}   # 60 % de los `requests` de CPU
    - type: External                                            # métrica de Prometheus vía prometheus-adapter
      external:
        metric:
          name: kafka_consumergroup_lag_sum
          selector: {matchLabels: {consumergroup: pedidos-saga, topic: pedidos.eventos}}
        target: {type: AverageValue, averageValue: "2000"}     # 2 000 mensajes de lag por réplica
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 30
      policies: [{type: Pods, value: 3, periodSeconds: 60}]    # como mucho +3 Pods por minuto
    scaleDown:
      stabilizationWindowSeconds: 300                          # esperar 5 min antes de reducir: evitar oscilar
      policies: [{type: Percent, value: 25, periodSeconds: 60}]

El HPA toma el máximo de lo que pidan las métricas: si la CPU dice 5 réplicas y el lag dice 8, escala a 8. La métrica externa viene del kafka_exporter de 07-01 a través de prometheus-adapter, que la publica en la API de métricas de Kubernetes. Las ventanas de estabilización son el equivalente del for: de las alertas: escalar rápido, reducir despacio.

k8s/inventario-statefulset.yaml (reducido)

apiVersion: apps/v1
kind: StatefulSet
metadata: {name: inventario-db, namespace: km0-prod}
spec:
  serviceName: inventario-db-headless      # DNS por Pod: inventario-db-0.inventario-db-headless
  replicas: 3
  selector: {matchLabels: {app: inventario-db}}
  template:
    metadata: {labels: {app: inventario-db}}
    spec:
      containers:
        - name: postgres
          image: ghcr.io/zalando/spilo-16:3.3-p1       # PostgreSQL + Patroni (07-03)
          env:
            - name: SCOPE
              value: km0-inventario
            - name: KUBERNETES_USE_CONFIGMAPS          # Patroni usa la API de Kubernetes como almacén de consenso
              value: "true"
          volumeMounts: [{name: datos, mountPath: /home/postgres/pgdata}]
  volumeClaimTemplates:                     # cada Pod recibe SU disco; sobrevive a reinicios y reprogramaciones
    - metadata: {name: datos}
      spec:
        accessModes: [ReadWriteOnce]
        storageClassName: ssd-replicado
        resources: {requests: {storage: 200Gi}}

Un StatefulSet da lo que una base de datos necesita y un Deployment no: nombres estables (inventario-db-0 es siempre el mismo, con el mismo disco), arranque y parada ordenados, y un volumen por réplica. En producción, no obstante, la práctica habitual es delegar en un operador (sección 9).

k8s/ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: km0-borde
  namespace: km0-prod
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod   # certificado público ACME renovado automáticamente
spec:
  ingressClassName: nginx
  tls:
    - hosts: [api.km0.example]
      secretName: api-km0-tls
  rules:
    - host: api.km0.example
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service: {name: kong-proxy, port: {number: 443}}

El Ingress solo lleva el tráfico hasta Kong; todo lo de 06-05 (JWT, rate limiting, OpenAPI, X-Request-Id) sigue ocurriendo en Kong. Con cert-manager, el certificado que caducaba un domingo se renueva solo y la alerta CertificadoCaducaPronto de 07-01 vigila que así sea.

Operar con kubectl

# Probar en local: un clúster de un nodo en Docker (kind) o una VM (minikube)
kind create cluster --name km0 --config k8s/local/kind.yaml
kubectl config use-context kind-km0

# Aplicar el estado deseado (idempotente: repetirlo no cambia nada si ya está así)
kubectl apply -f k8s/ -n km0-prod
kubectl get pods -n km0-prod -w                   # -w: observar cómo cambian los Pods en tiempo real
# NAME                       READY   STATUS    RESTARTS   AGE
# pedidos-7d9f6c4b8-abcde    2/2     Running   0          40s
# pedidos-7d9f6c4b8-fghij    2/2     Running   0          40s
# pedidos-7d9f6c4b8-klmno    1/2     Running   0          12s     <- sidecar listo, app aún en startupProbe

# Desplegar una versión nueva = cambiar la imagen en el manifiesto y aplicar (o, para probar, en línea)
kubectl set image deployment/pedidos pedidos=registro.km0.internal/km0/pedidos:1.15.0-8b2d4e1 -n km0-prod
kubectl rollout status deployment/pedidos -n km0-prod
# Waiting for deployment "pedidos" rollout to finish: 1 out of 3 new replicas have been updated...
# deployment "pedidos" successfully rolled out

# El sábado de 07-01 (8 % de errores tras desplegar): volver atrás en segundos
kubectl rollout undo deployment/pedidos -n km0-prod
kubectl rollout history deployment/pedidos -n km0-prod

kubectl describe pod pedidos-7d9f6c4b8-klmno -n km0-prod    # eventos: por qué no arranca, sondas fallidas
kubectl logs -f deployment/pedidos -c pedidos -n km0-prod   # (en producción, Loki; esto es para local)
kubectl scale deployment/pedidos --replicas=6 -n km0-prod   # manual; el HPA lo sobrescribirá

  1. Estrategias de despliegue: rolling, blue/green, canary

Estrategia Cómo Capacidad extra Riesgo de exposición Rollback Cuándo
Rolling update Sustituir Pods de uno en uno (o de maxSurge en maxSurge), esperando readiness maxSurge Todo el tráfico ve la nueva versión progresivamente; si el bug es sutil, llega al 100 % rollout undo (segundos, pero ya ha afectado) Por defecto para cambios pequeños con buenas pruebas
Blue/green Dos entornos completos; el Service apunta a uno; se cambia el selector de golpe 100 % (dos copias) Cero hasta el cambio; 100 % después Cambiar el selector de vuelta (instantáneo) Cambios grandes que deben ser atómicos (esquema + código)
Canary Versión nueva con una fracción del tráfico (1 %, 10 %, 50 %); se observan los SLI (07-01); se avanza o se aborta Pequeña Solo la fracción canary Poner el peso a 0 Cambios con riesgo desconocido; lo habitual en servicios críticos

Un canary con Kubernetes puro se hace con dos Deployments bajo el mismo Service; el reparto es proporcional al número de Pods (1 canary de 10 = 10 %):

# k8s/pedidos-canary.yaml: Deployment paralelo con la versión nueva
apiVersion: apps/v1
kind: Deployment
metadata: {name: pedidos-canary, namespace: km0-prod}
spec:
  replicas: 1                                # 1 de 4 Pods con etiqueta app=pedidos => ~25 % del tráfico
  selector: {matchLabels: {app: pedidos, track: canary}}
  template:
    metadata:
      labels: {app: pedidos, track: canary, version: "1.15.0"}   # app=pedidos: el Service lo incluye
    spec:
      # ... idéntico al Deployment estable salvo la imagen
      containers:
        - name: pedidos
          image: registro.km0.internal/km0/pedidos:1.15.0-8b2d4e1

Para pesos finos (1 %) independientes del número de Pods, se usan las capacidades del mesh o del ingress (Istio VirtualService con weight: 1/99; Kong con upstreams ponderados) y herramientas como Argo Rollouts o Flagger que automatizan el análisis: consultan a Prometheus la tasa de error y el p99 de la versión canary (etiqueta version en las métricas de 07-01) y avanzan o revierten solos según el SLO. El sábado de 07-01, con un canary al 10 % analizado automáticamente, el 8 % de errores habría afectado a menos del 1 % de los pedidos durante dos minutos.

  1. Namespaces, RBAC, service mesh y operadores

Namespaces separan entornos y equipos en el mismo clúster: km0-prod, km0-staging, observabilidad, mesh. Cada uno con ResourceQuota (CPU y memoria totales) y LimitRange (valores por defecto para Pods sin resources), y con RBAC de Kubernetes (distinto del RBAC de la aplicación de 06-01, mismo modelo): quién puede hacer qué sobre qué objetos.

# k8s/rbac-operadores.yaml: Jordi y Marta pueden ver todo y reiniciar Deployments en prod, no borrar Secrets
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: {name: operador, namespace: km0-prod}
rules:
  - apiGroups: ["", "apps"]
    resources: [pods, pods/log, deployments, replicasets, services, configmaps]
    verbs: [get, list, watch]
  - apiGroups: ["apps"]
    resources: [deployments]
    verbs: [patch]                     # rollout restart / undo
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: {name: operadores, namespace: km0-prod}
subjects:
  - {kind: Group, name: km0-operadores, apiGroup: rbac.authorization.k8s.io}   # grupo del OIDC de Keycloak (06-03)
roleRef: {kind: Role, name: operador, apiGroup: rbac.authorization.k8s.io}

Los Pods también tienen identidad (ServiceAccount), y con ella se autentican en Vault (06-04) y ante la API si lo necesitan; el principio es el mismo de mínimo privilegio.

Service mesh. En 07-04 se vio Envoy como sidecar con timeouts, reintentos y outlier detection; en 06-04, mTLS entre servicios con certificados de Vault gestionados por cada servicio. Un service mesh (Istio, Linkerd) instala ese sidecar automáticamente en cada Pod del namespace y lo configura desde objetos de Kubernetes: mTLS obligatorio y con rotación automática de certificados (PeerAuthentication: STRICT), políticas de autorización servicio-a-servicio, timeouts y reintentos por ruta (VirtualService, DestinationRule con outlierDetection), y telemetría RED uniforme para Prometheus sin instrumentar nada. Es la manera de obtener 06-04 y 07-04 sin código para servicios que no son Python (o para no depender de que cada equipo lo haga bien), a cambio de 1-2 ms de latencia, memoria por sidecar y otra pieza que operar. La decisión arquitectónica de cuándo merece la pena se retoma en 08-01.

Operadores. Un operador es un controlador que conoce un sistema concreto: sabe hacer un failover de PostgreSQL, un nodetool drain antes de reiniciar un nodo de Cassandra, o reasignar particiones de Kafka al añadir un broker. Se instala en el clúster y se le habla con objetos propios (kind: Kafka, kind: PostgresCluster). Para Kilómetro Cero: Strimzi para Kafka, K8ssandra para Cassandra, CloudNativePG o el operador de Zalando (Patroni) para PostgreSQL. Con ellos, el StatefulSet de la sección 7 se sustituye por una declaración de diez líneas y el operador se ocupa de lo que 07-03 hizo a mano (y de los backups a MinIO). Aquí basta con saber que existen y que son la forma recomendada de correr sistemas con estado en Kubernetes.

  1. GitOps y CI/CD

Con manifiestos en k8s/, la última pieza es quién los aplica y cuándo. GitOps es la respuesta: el repositorio Git es la única fuente de verdad del estado deseado; nadie ejecuta kubectl apply a mano en producción; un agente en el clúster (Argo CD o Flux) observa el repositorio y reconcilia continuamente el clúster con lo que hay en la rama main. Desplegar es hacer merge de un pull request que cambia la etiqueta de imagen; revertir es revertir el commit; auditar quién desplegó qué es git log. Y un cambio manual en el clúster ("drift") se detecta y se deshace.

flowchart LR
    DEV[Desarrollador<br/>push a rama] --> CI
    subgraph CI["CI (GitHub Actions / GitLab CI)"]
        T[pytest + contratos<br/>07-06] --> B[docker build<br/>pedidos:1.15.0-8b2d4e1]
        B --> S[escaneo de imagen<br/>y firma cosign]
        S --> PUSH[push al registro]
    end
    PUSH --> PR[PR automático en repo de despliegue:<br/>k8s/pedidos-deployment.yaml<br/>image: ...:1.15.0-8b2d4e1]
    PR --> REV[Revisión y merge]
    REV --> ARGO[Argo CD detecta el cambio<br/>y sincroniza km0-prod]
    ARGO --> ROLL[Argo Rollouts: canary 10 %<br/>análisis con Prometheus]
    ROLL -- SLO OK --> FULL[100 %]
    ROLL -- error rate > SLO --> ABORT[rollback automático<br/>+ alerta a #km0-operaciones]
# .github/workflows/pedidos.yml (fragmento)
name: pedidos
on:
  push:
    paths: ["servicios/pedidos/**", "servicios/comun/**", "contratos/**"]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pip install -r servicios/pedidos/requirements.lock -r requirements-test.txt
      - run: pytest tests/unit tests/integracion -q          # Testcontainers levanta PostgreSQL y Kafka (07-06)
      - run: python tests/contrato/verificar.py pedidos      # contratos consumidor-productor (07-06)
  build:
    needs: test
    runs-on: ubuntu-latest
    outputs:
      tag: ${{ steps.meta.outputs.tag }}
    steps:
      - uses: actions/checkout@v4
      - id: meta
        run: echo "tag=$(cat servicios/pedidos/VERSION)-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
      - run: docker build -f servicios/pedidos/Dockerfile -t registro.km0.internal/km0/pedidos:${{ steps.meta.outputs.tag }} .
      - run: trivy image --exit-code 1 --severity CRITICAL registro.km0.internal/km0/pedidos:${{ steps.meta.outputs.tag }}
      - run: docker push registro.km0.internal/km0/pedidos:${{ steps.meta.outputs.tag }}
      - run: cosign sign --key env://COSIGN_KEY registro.km0.internal/km0/pedidos:${{ steps.meta.outputs.tag }}
  promover:
    needs: build
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: {repository: km0/despliegue, token: "${{ secrets.DESPLIEGUE_TOKEN }}"}
      - run: |
          sed -i "s|km0/pedidos:.*|km0/pedidos:${{ needs.build.outputs.tag }}|" k8s/pedidos-deployment.yaml
          git commit -am "pedidos ${{ needs.build.outputs.tag }}" && git push origin HEAD:refs/heads/pedidos-${{ needs.build.outputs.tag }}
          gh pr create --fill --base main       # el merge lo aprueba una persona (o se automatiza en staging)

Dos repositorios: el del código (que produce imágenes) y el del despliegue (k8s/, que Argo CD observa). Así, "qué versión hay en producción" es una línea en un YAML con historial de Git, y el pipeline puede promover a staging automáticamente y a prod con una aprobación. La firma de imágenes (cosign) y una política de admisión que solo permite imágenes firmadas cierran el círculo con 06-05: nada corre en producción que no haya pasado por el pipeline.

  1. docker-compose frente a Kubernetes

Aspecto docker-compose Kubernetes
Alcance Un nodo Un clúster de N nodos
Estado deseado Se aplica una vez (up) Reconciliado continuamente
Autorreparación restart: always (reinicio local) Reinicio, reprogramación en otro nodo, sustitución
Escalado --scale manual, un nodo HPA por métricas, entre nodos
Despliegue Parar y arrancar (corte) Rolling, canary, blue/green sin corte
Descubrimiento DNS de compose (por nombre de servicio) Service + DNS + balanceo por readiness
Configuración/secretos .env, ficheros, secrets: ConfigMap, Secret, RBAC, Vault
Curva y coste operativo Minutos; casi nulo Semanas; una plataforma que operar (o pagar gestionada, 08-03)
Cuándo Desarrollo local; demos; el laboratorio de este curso; producción pequeña en un solo nodo con tolerancia a caídas Producción con más de un nodo, escalado, despliegues frecuentes, varios equipos

La regla práctica: compose para desarrollar (el docker-compose.yml de km0/ sigue siendo la forma de levantar la plataforma en el portátil), Kubernetes para producción a partir del momento en que se necesita más de un nodo o desplegar sin corte. Saltar a Kubernetes con un equipo de dos personas y un servicio es pagar la complejidad sin cobrar el beneficio; quedarse en compose con seis servicios, tres zonas y despliegues diarios es lo de los martes y jueves con otro nombre.

Errores Comunes y Consejos

  • Tareas de Ansible que no son idempotentes. Un command: sin creates: que formatea un broker en cada ejecución. Usa módulos declarativos; para command/shell, siempre creates:/removes: o changed_when:; prueba con --check --diff.
  • Reiniciar todos los nodos con estado a la vez. serial: 1 y una espera de salud entre nodos; para Kafka y Cassandra, es la diferencia entre mantenimiento e incidente.
  • image: pedidos:latest. Nadie sabe qué corre en producción y un rollout undo puede "volver" a la misma imagen. Versión + hash de commit, siempre.
  • Deployments sin resources. Sin requests el scheduler coloca a ciegas y el HPA por CPU no funciona; sin limits un Pod con fuga de memoria se lleva el nodo. Los dos, medidos con las métricas de 07-01.
  • Liveness que comprueba dependencias. Ya en 07-03: reinicios en cascada. Liveness mínima, readiness con dependencias, startup generosa.
  • maxUnavailable alto "para desplegar rápido". Reduce la capacidad durante el despliegue, justo cuando la versión nueva puede ser más lenta. maxUnavailable: 0, maxSurge: 1.
  • kubectl apply a mano en producción. Se pierde el historial y aparece drift. GitOps: todo pasa por un PR; Argo CD/Flux aplican.
  • Reducir réplicas tan rápido como se suben. El HPA oscila y cada bajada corta capacidad en un pico que vuelve. Ventana de estabilización larga en scaleDown.
  • Secretos en ConfigMaps o en el repositorio. Un Secret de Kubernetes es solo base64; lo sensible de verdad lo entrega Vault en tiempo de ejecución (06-04); en Git, nunca.
  • Kubernetes porque sí. Un clúster para un servicio con dos personas es una carga sin beneficio. Compose hasta que las razones (nodos, escalado, despliegues sin corte) sean reales.

Ejercicios

Ejercicio 1. Jordi ejecuta ansible-playbook -i inventario.ini kafka.yml por segunda vez sin haber cambiado nada, y ve changed=2 en kafka-2: las tareas "Certificado y clave del broker desde Vault" y "Escribir el keystore del broker" aparecen como cambiadas, y Kafka se ha reiniciado en kafka-2. (a) ¿Qué está pasando y por qué no ocurrió en kafka-1 ni kafka-3? Pista: mira changed_when y lo que hace copy cuando el contenido difiere. (b) ¿Es un problema de idempotencia del rol o del diseño de certificados de 06-04? Propón un cambio en el rol que evite reiniciar el broker en cada ejecución sin dejar de renovar el certificado cuando corresponda. (c) ¿Qué habría pasado sin serial: 1 y con los tres brokers en la misma situación?

Ejercicio 2. Durante la Semana de la Vendimia, pedidos sube a 12 réplicas (el máximo del HPA) y sigue con p99 de 700 ms. Marta mira: la CPU de los Pods está al 35 %, el lag de pedidos-saga es bajo, km0_bulkhead_en_uso{dependencia="inventario"} está en 24 en todos los Pods y km0_circuit_estado en 0. (a) ¿Por qué el HPA no ayuda y qué indica el bulkhead lleno con CPU baja? (b) ¿Qué habría que escalar, y qué objeto de Kubernetes gobierna a ese componente? ¿Sirve un HPA ahí? (c) Diseña una métrica de HPA para pedidos que refleje mejor la saturación real que la CPU, usando algo de 07-01.

Ejercicio 3. Se va a desplegar pedidos 1.15.0, que cambia el formato del evento pedido.confirmado en pedidos.eventos añadiendo un campo obligatorio que analitica 2.3 aún no entiende. (a) ¿Por qué un rolling update de pedidos es peligroso aquí aunque las sondas estén bien, y qué estrategia de la sección 8 lo mitiga solo en parte? (b) Describe un plan de despliegue en pasos (con los comandos o PRs de GitOps que correspondan) que no rompa analitica en ningún momento, apoyándote en la compatibilidad de esquemas de 02-05. (c) ¿Qué comprobación automática del pipeline de CI habría bloqueado el PR antes de llegar a esta situación? (Se desarrolla en 07-06; basta con nombrarla y decir en qué job iría.)

Soluciones

Ejercicio 1.

(a) La tarea de Vault genera un certificado nuevo en cada ejecución (con changed_when: false no se marca como cambio, pero el contenido registrado en cert es distinto cada vez); la tarea copy compara el contenido nuevo con el fichero existente, ve que difiere (otro certificado, otra clave) y lo escribe: changed=true y notify: reiniciar kafka. En kafka-1 y kafka-3 no ocurrió porque... sí ocurrió, o habría ocurrido: si no se vio es porque el playbook con serial: 1 se interrumpió o porque en esos nodos la ejecución anterior falló antes de escribir el keystore; en un rol así, todos los brokers se reiniciarían en cada ejecución. (b) Es un defecto de idempotencia del rol, no del diseño de certificados: los certificados de corta duración de 06-04 son correctos, pero el rol debe generar uno solo cuando el actual está a punto de caducar: añadir una tarea previa que lea la fecha de caducidad del certificado existente (community.crypto.x509_certificate_info) y ejecutar la generación y la copia con when: cert_actual.expired or (cert_actual.not_after | to_datetime - now()) < 7 days; o, mejor, sacar la renovación del playbook y delegarla al agente de Vault en cada nodo (Vault Agent con plantillas, que renueva y recarga sin reiniciar el broker mediante kafka-configs dinámico). (c) Los tres brokers reiniciándose a la vez dejan sin ISR a todas las particiones: con min.insync.replicas=2 y acks=all, pedidos.eventos rechaza escrituras (NotEnoughReplicas) durante el reinicio, el outbox de pedidos se acumula y KafkaLagReparto y las alertas de 07-01 saltan; sin unclean.leader.election.enable=false podría incluso haber pérdida de mensajes. serial: 1 con wait_for convierte eso en tres reinicios sucesivos sin pérdida de disponibilidad.

Ejercicio 2.

(a) El HPA escala pedidos por CPU, y pedidos no está limitado por CPU: está esperando. km0_bulkhead_en_uso en 24 (el máximo) con CPU al 35 % y circuito cerrado significa que todas las llamadas a inventario tienen éxito pero tardan: los 24 permisos están ocupados por peticiones que esperan a inventario, y las siguientes se rechazan (BulkheadLleno, fallback a "pendiente de confirmar") o esperan un hilo. Añadir réplicas de pedidos multiplica los bulkheads (12 × 24 = 288 llamadas concurrentes) y empeora la carga sobre inventario. (b) Hay que escalar inventario (o su base de datos): inventario es un Deployment (sin estado, la base es aparte) y sí admite HPA, con una métrica de saturación propia (peticiones en vuelo o p99 de ReservarStock); si el cuello es km0_inventario (contención de bloqueos sobre vino-crianza, como en la traza de 07-02), añadir réplicas de inventario no ayuda: el StatefulSet/operador de PostgreSQL no se escala horizontalmente para escrituras, y la solución es de diseño (04-01: particionar el stock por mercado, o reservas por lotes). (c) Una métrica de Prometheus vía prometheus-adapter: km0_peticiones_en_vuelo{servicio="pedidos"} promedio por Pod con objetivo, p. ej., 20 (con 32 hilos, 20 en vuelo de media indica cola incipiente); o directamente la latencia: histogram_quantile(0.99, ...) de POST /pedidos con objetivo 0,4 s. La primera es mejor para el HPA porque responde de forma casi lineal al número de réplicas; la segunda sirve más como alerta. En ambos casos, con scaleDown lento.

Ejercicio 3.

(a) El rolling update sustituye Pods de pedidos sin corte, pero en cuanto el primer Pod nuevo publica un pedido.confirmado con el formato nuevo, analitica 2.3 falla al deserializarlo: las sondas de pedidos están perfectas; el daño está en otro servicio, a través de Kafka, y se manifiesta como mensajes en la DLQ de analitica (02-05) y lag. Un canary reduce la fracción de eventos con formato nuevo, pero un solo mensaje ya rompe al consumidor (o lo manda a la DLQ): mitiga el volumen, no el problema. (b) Compatibilidad hacia adelante y atrás: 1) PR en analitica (2.4) que tolera el campo nuevo (lo ignora si no lo entiende, lo usa si está) y desplegarlo primero (kubectl rollout status deployment/analitica), con el esquema registrado como compatible; 2) desplegar pedidos 1.15.0 con canary al 10 % (PR en el repo de despliegue con pedidos-canary, o Argo Rollouts), observar la DLQ y el lag de analitica y la tasa de error de pedidos; 3) promover a 100 %; 4) solo cuando ningún productor emite el formato viejo (y los mensajes viejos han salido de la retención del tópico, 7 días), un PR en analitica que hace obligatorio el campo. Además, hacer el campo opcional en el esquema en lugar de obligatorio evita el paso 4. En ningún momento hay un consumidor que no entienda lo que hay en el tópico. (c) Una prueba de contrato entre el productor pedidos y el consumidor analitica sobre el esquema de pedido.confirmado (verificación de compatibilidad del esquema contra los registrados, y del contrato consumidor-productor): iría en el job test del pipeline de pedidos (python tests/contrato/verificar.py pedidos), y habría fallado al detectar un campo obligatorio nuevo que un consumidor registrado no acepta. Se desarrolla en 07-06.

Conclusión

Automatizar es sacar a la persona del camino crítico y dejar en su lugar una descripción que se puede revisar, probar y aplicar tantas veces como haga falta. Para las máquinas con estado, Ansible: un inventario, playbooks y roles cuyas tareas son idempotentes (creates:, template con handlers, --check --diff), y que recorren los brokers de Kafka de uno en uno respetando las ISR. Para los servicios, imágenes inmutables etiquetadas por versión y commit, construidas una vez y desplegadas en todos los entornos con configuración inyectada. Y sobre ellas, Kubernetes: un almacén de consenso con controladores que reconcilian sin parar el estado deseado con el real; Deployments con sondas, resources, reparto por zonas y apagado ordenado; Services que balancean solo hacia Pods listos; HPA por CPU y por lag de Kafka; StatefulSets u operadores para Cassandra, Kafka y PostgreSQL; Ingress hacia Kong; rolling, blue/green y canary con análisis automático contra los SLO; namespaces y RBAC; el service mesh que da mTLS y políticas de resiliencia sin código; y GitOps con un pipeline en el que desplegar es hacer merge y revertir es revertir un commit. Compose queda para el portátil.

Pero todo este engranaje descansa sobre una suposición que aún no se ha examinado: que la versión 1.15.0 que el pipeline construye, firma y despliega funciona. El pipeline ejecuta pytest y verifica contratos, y el canary observa los SLO, pero ¿qué prueba demuestra que la saga compensa bien cuando inventario cae a mitad de camino? ¿Cómo se sabe que el circuit breaker de 07-04 se abre a tiempo, o que Patroni conmuta en menos de 30 segundos, antes de que ocurra en producción un sábado? Probar un sistema distribuido es más difícil que probar un programa: el no determinismo, los fallos parciales y el tiempo hacen que las pruebas unitarias no basten. La última lección del módulo trata las pruebas en sistemas distribuidos (integración con dependencias reales, contratos, carga simulando la Semana de la Vendimia) y la ingeniería del caos: provocar los fallos de 07-03 y 07-04 a propósito, con hipótesis y radio de explosión controlado, para comprobar que la plataforma responde como se diseñó.

Curso de Arquitecturas Distribuidas

Módulo 1: Introducción a los Sistemas Distribuidos

Módulo 2: Comunicación en Sistemas Distribuidos

Módulo 3: Consistencia y Replicación

Módulo 4: Almacenamiento Distribuido

Módulo 5: Computación Distribuida

Módulo 6: Seguridad en Sistemas Distribuidos

Módulo 7: Monitoreo y Mantenimiento

Módulo 8: Casos de Estudio y Aplicaciones

© Copyright 2026. Todos los derechos reservados