Stack: K8s básico / Cloud Run / ECS · Proyecto: TicketFlow Estado: Publicada — elegir el nivel de complejidad que tu producto necesita Prerrequisito: Lección 42 — Nube
Objetivos
- Leer y escribir los objetos K8s mínimos de TicketFlow: Deployment, Service, ConfigMap/Secret, HPA y CronJob.
- Decidir con criterio entre K8s, PaaS de contenedores y compose: la matriz de decisión por tamaño de equipo y producto.
- Aplicar las reglas de Postgres gestionado + K8s (la pareja correcta) y saber por qué la BD dentro de K8s es el error clásico.
1. El nivel de complejidad que el producto paga
Kubernetes no es un premio de madurez: es un costo operativo permanente (actualizaciones del cluster, networking, RBAC, el YAML infinito). La matriz honesta:
| Escenario | Herramienta | Por qué |
|---|---|---|
| Proyecto personal / MVP (equipo 1, 1 app) | compose / PaaS | el costo de K8s > su beneficio |
| SaaS en crecimiento (equipo 1-5, 2-10 servicios) | PaaS de contenedores (Cloud Run/Fargate) | escala y parcheo gratis, 0 nodos |
| Multi-servicio con redes complejas, multi-tenant estricto, o equipo platform dedicado | K8s | la plataforma es el producto |
| Edge/on-prem/regulatorio | K8s (o VMs) | cuando la nube no llega |
TicketFlow hoy: PaaS (la 42). Esta lección enseña K8s igual — porque el K8s-literacy es la alfabetización del backend (todos los despliegues grandes lo hablan) y porque la migración llega cuando el producto lo pida: saber leer un Deployment deja de ser opcional.
2. Los objetos mínimos de TicketFlow en K8s
apiVersion: apps/v1
kind: Deployment
metadata: {name: ticketflow-web}
spec:
replicas: 3
selector: {matchLabels: {app: ticketflow-web}}
template:
metadata: {labels: {app: ticketflow-web}}
spec:
containers:
- name: web
image: ghcr.io/ticketflow/app:git-a1b2c3d # el digest/SHA del 41, jamás :latest
ports: [{containerPort: 8000}]
envFrom:
- configMapRef: {name: ticketflow-config} # la config no-secreta (27)
- secretRef: {name: ticketflow-secrets} # los secretos (27/42)
readinessProbe: {httpGet: {path: /healthz/, port: 8000}, initialDelaySeconds: 5}
livenessProbe: {httpGet: {path: /healthz/, port: 8000}, periodSeconds: 10}
resources:
requests: {cpu: 250m, memory: 512Mi}
limits: {cpu: "1", memory: 1Gi}
---
apiVersion: v1
kind: Service
metadata: {name: ticketflow-web}
spec: {selector: {app: ticketflow-web}, ports: [{port: 80, targetPort: 8000}]}El diccionario del 40 traducido: el compose service → Deployment (las réplicas son explícitas o del HPA), el puerto → Service, el .env → ConfigMap+Secret, el healthcheck → readiness (¿puede recibir tráfico?) + liveness (¿está vivo y hay que reiniciarlo?). Las dos probes SON distintas: el liveness que toca la BD puede matar el pod en cascada (la BD se cae → todos los pods "no vivos" → reinicio masivo → la BD vuelve → el thundering herd) — el /healthz del liveness es barato y local; el /ready de la BD va en readiness (el pod sale del balanceo sin morir).
3. El HPA y el CronJob: el auto y lo periódico
El autoscaling por CPU/memoria (la respuesta de K8s al escalado del 55):
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: {name: web-hpa}
spec:
scaleTargetRef: {apiVersion: apps/v1, kind: Deployment, name: ticketflow-web}
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource: {name: cpu, target: {type: Utilization, averageUtilization: 70}}El matiz del 36: el HPA reacciona a CPU, pero el cuello de TicketFlow era la BD (el lock del evento) — escalar réplicas web no arregla la contención del UPDATE: el HPA escala el tier que escala (web), y el cuello real se ataca con las decisiones del 36/55 (partición, turnos de preventa). Y el CronJob (el beat del 31 en idioma K8s): el beat de Celery queda como singleton (Deployment replicas=1) O se migra a CronJob de K8s por tarea — el lock de corrida (31) hace seguras ambas: el patrón "lógica estable, transporte sustituible" de la 28.
apiVersion: batch/v1
kind: CronJob
metadata: {name: expirar-reservas}
spec:
schedule: "*/1 * * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: expirar
image: ghcr.io/ticketflow/app:git-a1b2c3d
command: ["python", "manage.py", "expirar_reservas"]4. La base de datos: gestionada fuera, jamás dentro
El error clásico del K8s-entusiasta: StatefulSet de Postgres dentro del cluster. Las razones del NO: el almacenamiento del pod (PersistentVolume) es el recurso más frágil del ecosistema (el volume que no se monta tras un fallo de zona = la BD que no arranca), el failover es artesanal comparado con el del gestionado (42), los backups del proveedor (con PITR) desaparecen, y el drain/mantenimiento del nodo puede patear la BD en producción. La pareja correcta: K8s (o PaaS) para todo lo stateless del app + Postgres/Redis gestionados fuera del cluster, hablándose por la VPC (42). El corolario: la aritmética de conexiones del 39 se vuelve crítica (cada pod × pool × HPA replicas: el HPA que escala a 10 pods puede tumbal el max_connections del gestionado — el presupuesto del 39 incluye el maxReplicas del HPA).
5. El despliegue en K8s: los objetos del 41 traducidos
El rolling update reemplaza al blue-green artesanal (41): el Deployment con maxSurge: 1, maxUnavailable: 0 sube pods nuevos (con el readiness decidido), drena los viejos: el blue-green del 41 sigue siendo mejor para el rollback atómico, pero el rolling + kubectl rollout undo es el default del mundo K8s. El secreto/config sigue siendo del 27 (el ConfigMap/Secret se versiona en el repo por IaC del 44, no se edita con kubectl a mano — el "kubectl edit" de las 3 a.m. es la config que nadie encontrará mañana). Y las migraciones (41): un Job pre-deploy en el pipeline (el job de migrate corre ANTES del rollout, desde la misma imagen): el orden canónico del release es idéntico en cualquier plataforma — eso es lo que significa dockerizar bien (40).
Autoevaluación
- ¿Qué escenario paga K8s y cuál paga PaaS? ¿Dónde está TicketFlow y cuál sería el trigger real de migración?
- Deployment/Service/ConfigMap+Secret/readiness+liveness: ¿qué objeto del compose traduce cada uno y por qué las dos probes son distintas?
- ¿Por qué el liveness que toca la BD puede causar un reinicio en cascada y qué probe hace qué?
- ¿Por qué el HPA no arregla el cuello del checkout y qué pieza del 39 se vuelve crítica con el HPA?
- ¿Qué tres razones matan el Postgres StatefulSet dentro del cluster y cómo se conecta el app al gestionado?
Continúa con los ejercicios. Las solutions.md solo tras intentarlo.