Módulo 9 · Despliegue y operaciones

Lección 43 — Kubernetes y servicios gestionados

K8s básico, Cloud Run o ECS: elegir el nivel de complejidad que tu producto necesita.

Publicada
En esta lección
  1. Objetivos
  2. 1. El nivel de complejidad que el producto paga
  3. 2. Los objetos mínimos de TicketFlow en K8s
  4. 3. El HPA y el CronJob: el auto y lo periódico
  5. 4. La base de datos: gestionada fuera, jamás dentro
  6. 5. El despliegue en K8s: los objetos del 41 traducidos
  7. Autoevaluación

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

  1. Leer y escribir los objetos K8s mínimos de TicketFlow: Deployment, Service, ConfigMap/Secret, HPA y CronJob.
  2. Decidir con criterio entre K8s, PaaS de contenedores y compose: la matriz de decisión por tamaño de equipo y producto.
  3. 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:

EscenarioHerramientaPor qué
Proyecto personal / MVP (equipo 1, 1 app)compose / PaaSel 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 dedicadoK8sla plataforma es el producto
Edge/on-prem/regulatorioK8s (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

yaml
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):

yaml
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.

yaml
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

  1. ¿Qué escenario paga K8s y cuál paga PaaS? ¿Dónde está TicketFlow y cuál sería el trigger real de migración?
  2. Deployment/Service/ConfigMap+Secret/readiness+liveness: ¿qué objeto del compose traduce cada uno y por qué las dos probes son distintas?
  3. ¿Por qué el liveness que toca la BD puede causar un reinicio en cascada y qué probe hace qué?
  4. ¿Por qué el HPA no arregla el cuello del checkout y qué pieza del 39 se vuelve crítica con el HPA?
  5. ¿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.