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
- Por qué automatizar
- Infraestructura como código: configuración frente a aprovisionamiento
- Ansible: inventario, playbooks, roles e idempotencia
- Inmutabilidad: imágenes, registro y etiquetas por commit
- Kubernetes: por qué un orquestador y cómo está hecho
- Objetos esenciales
- Manifiestos de Kilómetro Cero:
k8s/ - Estrategias de despliegue: rolling, blue/green, canary
- Namespaces, RBAC, service mesh y operadores
- GitOps y CI/CD
- docker-compose frente a Kubernetes
- Errores comunes y consejos
- Ejercicios y soluciones
- Conclusión
- 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-2se creó copiandoinventario-1y 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
DELETEde 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.
- 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.
- 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/km0opsEl 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:
- kafkaY 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=requiredLa 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.
- 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".
- 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.
- 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 |
- Manifiestos de Kilómetro Cero:
k8s/
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 cursoCada 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 entradaKong (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á
- 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-8b2d4e1Para 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.
- 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.
- 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.
- 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:sincreates:que formatea un broker en cada ejecución. Usa módulos declarativos; paracommand/shell, siemprecreates:/removes:ochanged_when:; prueba con--check --diff. - Reiniciar todos los nodos con estado a la vez.
serial: 1y 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 unrollout undopuede "volver" a la misma imagen. Versión + hash de commit, siempre.- Deployments sin
resources. Sinrequestsel scheduler coloca a ciegas y el HPA por CPU no funciona; sinlimitsun 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.
maxUnavailablealto "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 applya 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
Secretde 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
- Conceptos Básicos de Sistemas Distribuidos
- Modelos de Sistemas Distribuidos
- Ventajas y Desafíos de los Sistemas Distribuidos
- Las Falacias de la Computación Distribuida
- Tiempo, Relojes y Ordenación de Eventos
- Del Monolito a la Plataforma Distribuida: el Caso Kilómetro Cero
Módulo 2: Comunicación en Sistemas Distribuidos
- Protocolos de Comunicación
- RPC y RMI
- gRPC y Serialización de Datos
- Mensajería y Colas de Mensajes
- Patrones de Comunicación Asíncrona
Módulo 3: Consistencia y Replicación
- Modelos de Consistencia
- El Teorema CAP y PACELC
- Algoritmos de Consenso
- Replicación de Datos
- Transacciones Distribuidas y Sagas
Módulo 4: Almacenamiento Distribuido
- Particionado de Datos y Hashing Consistente
- Sistemas de Archivos Distribuidos
- Almacenamiento de Objetos
- Bases de Datos Distribuidas
- Cachés Distribuidos
Módulo 5: Computación Distribuida
- Modelos de Computación Distribuida
- MapReduce y Hadoop
- Spark y Computación en Memoria
- Procesamiento de Flujos de Datos
- Planificación de Trabajos y Pipelines de Datos
Módulo 6: Seguridad en Sistemas Distribuidos
- Autenticación y Autorización
- Cifrado y Protección de Datos
- Gestión de Identidades
- Seguridad entre Servicios: mTLS y Gestión de Secretos
- Puertas de Enlace, Limitación de Tasa y Auditoría
Módulo 7: Monitoreo y Mantenimiento
- Monitoreo de Sistemas Distribuidos
- Logs Centralizados y Trazabilidad Distribuida
- Gestión de Fallos y Recuperación
- Patrones de Resiliencia: Timeouts, Reintentos y Circuit Breaker
- Automatización y Orquestación
- Pruebas en Sistemas Distribuidos e Ingeniería del Caos
