Skip to content

🎯 Meta: entender qué es Kubernetes, cuándo lo necesitas (y cuándo NO), y sus conceptos básicos. No te vamos a convertir en administrador de clústeres; te vamos a dar el mapa mental para que sepas de qué habla la gente y des tus primeros pasos.

Versión: Kubernetes 1.36 (jun-2026; releasea ~3 veces al año, soporta las 3 últimas minor durante ~14 meses — mantén tu clúster dentro de ese margen).


G.1 · ¿Qué problema resuelve? (y el aviso importante)

Docker Compose (cap. 12) corre tus contenedores en una máquina. Pero, ¿y si necesitas:

  • Correr en muchas máquinas y repartir la carga entre ellas.
  • Auto-escalar: más contenedores cuando hay tráfico, menos cuando no.
  • Auto-reparar: si un contenedor (o una máquina) muere, que se reemplace solo.
  • Desplegar sin downtime y hacer rollback automático si algo falla.

Kubernetes (k8s) orquesta contenedores a gran escala haciendo todo eso automáticamente.

⚠️ EL AVISO MÁS IMPORTANTE DE ESTE LIBRO SOBRE INFRAESTRUCTURA: Kubernetes es complejo. Muy complejo. La inmensa mayoría de proyectos no lo necesitan y adoptarlo demasiado pronto es un error clásico que hunde equipos en complejidad accidental. Para casi todo, un VPS con Docker Compose + Nginx (caps. 12-14), o una plataforma gestionada (Coolify, Railway, Cloud Run), es más que suficiente y muchísimo más simple.

Necesitas Kubernetes cuando: tienes muchos servicios, tráfico alto y variable, varios equipos, y requisitos serios de disponibilidad. Aprende que existe y cómo funciona; adóptalo solo cuando el dolor real lo justifique.


G.2 · El modelo mental

Kubernetes es un sistema de control declarativo: tú describes el estado deseado ("quiero 3 copias de mi API corriendo siempre") y k8s trabaja sin parar para que la realidad coincida. Si una copia muere, la recrea. Tú no le dices cómo, le dices qué quieres.

Tú declaras:  "quiero 3 réplicas de la API v2"

┌─────────────────── CLÚSTER ───────────────────┐
│  Control Plane (el cerebro)                     │
│   ├─ API Server   (recibe tus órdenes)          │
│   ├─ Scheduler    (decide dónde va cada pod)    │
│   └─ Controllers  (vigilan que se cumpla)       │
│                                                  │
│  Nodos (las máquinas que ejecutan)              │
│   ├─ Nodo 1: [pod][pod]                          │
│   ├─ Nodo 2: [pod]                               │
│   └─ Nodo 3: [pod][pod]                          │
└──────────────────────────────────────────────┘
   Si un pod muere → k8s crea otro automáticamente

G.3 · Conceptos que debes conocer

ConceptoQué esAnalogía
PodLa unidad mínima: 1+ contenedores juntosUn contenedor "envuelto"
DeploymentGestiona réplicas de pods y actualizaciones"quiero N copias siempre vivas"
ServicePunto de acceso estable a un grupo de podsUn balanceador interno con DNS
IngressEnruta tráfico externo a los services (HTTP/HTTPS)El Nginx del clúster
ConfigMapConfiguración no sensibleVariables de entorno
SecretDatos sensibles (contraseñas, tokens).env gestionado
NamespaceSeparación lógica dentro del clústerCarpetas para organizar
NodeUna máquina (física o virtual) del clústerUn servidor

🧠 La jerarquía: un Deployment mantiene varios Pods (réplicas de tu app); un Service les da una dirección estable (los pods van y vienen, el service no); un Ingress lleva el tráfico de internet a los services. De fuera hacia dentro: Ingress → Service → Pod → contenedor.


G.4 · Tu primer Deployment (YAML)

Kubernetes se configura con YAML declarativo. Un Deployment de tu API:

yaml
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tienda-api
spec:
  replicas: 3                       # quiero 3 copias siempre
  selector:
    matchLabels:
      app: tienda-api
  template:
    metadata:
      labels:
        app: tienda-api
    spec:
      containers:
        - name: api
          image: ghcr.io/miorg/tienda-api:v2   # tu imagen (cap. 12)
          ports:
            - containerPort: 8000
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: db-secret
                  key: url
          resources:                # límites de recursos (importante)
            requests: { cpu: "100m", memory: "128Mi" }
            limits:   { cpu: "500m", memory: "512Mi" }
          livenessProbe:            # ¿está vivo? si no, lo reinicia
            httpGet: { path: /salud, port: 8000 }
            initialDelaySeconds: 10
          readinessProbe:           # ¿listo para recibir tráfico?
            httpGet: { path: /salud, port: 8000 }
          startupProbe:             # da tiempo a apps LENTAS de arrancar antes de que liveness actúe
            httpGet: { path: /salud, port: 8000 }
            failureThreshold: 30    # hasta 30 × periodSeconds antes de rendirse
            periodSeconds: 2

💡 startupProbe evita el ciclo de reinicios de apps lentas al arrancar: sin ella, si tu app tarda 40s en estar lista (migraciones, caché en calentamiento) y el livenessProbe tiene un initialDelaySeconds corto, k8s la mata y reinicia antes de que termine de arrancar — un bucle infinito de CrashLoopBackOff que no es un bug real. Mientras startupProbe no pase, livenessProbe/readinessProbe no se evalúan.

Y el Service que le da acceso estable:

yaml
# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: tienda-api
spec:
  selector:
    app: tienda-api
  ports:
    - port: 80
      targetPort: 8000
bash
kubectl apply -f deployment.yaml -f service.yaml   # aplicar el estado deseado
kubectl get pods                                    # ver los pods
kubectl logs <pod>                                  # logs de un pod
kubectl scale deployment tienda-api --replicas=5    # escalar a 5
kubectl rollout undo deployment tienda-api          # rollback a la versión anterior

💡 livenessProbe y readinessProbe son clave: la liveness reinicia un pod colgado; la readiness evita mandarle tráfico hasta que esté listo (ej. terminó de conectar a la BD). Por eso tu app necesita un endpoint /salud (cap. 14) — Kubernetes lo usa para saber cómo estás.

Despliegues sin downtime (rolling update)

Cuando cambias la imagen (v2v3), el Deployment hace un rolling update: levanta pods nuevos, espera a que su readiness pase, y solo entonces mata los viejos. Si la v3 no arranca, el tráfico sigue en la v2 — por eso las probes no son opcionales.

bash
kubectl set image deployment/tienda-api api=ghcr.io/miorg/tienda-api:v3
kubectl rollout status deployment/tienda-api    # observa el progreso
kubectl rollout history deployment/tienda-api   # versiones anteriores
kubectl rollout undo deployment/tienda-api      # rollback en un comando
yaml
# Control fino del rolling update (dentro de spec:)
strategy:
  type: RollingUpdate
  rollingUpdate:
    maxSurge: 1          # cuántos pods extra puede crear a la vez
    maxUnavailable: 0    # cuántos pueden faltar (0 = siempre a plena capacidad)

G.5 · Configuración: ConfigMaps y Secrets

La config vive fuera de la imagen (misma regla de los 12 factores del cap. 12):

yaml
# configmap.yaml — configuración NO sensible
apiVersion: v1
kind: ConfigMap
metadata:
  name: tienda-config
data:
  LOG_LEVEL: "info"
  CACHE_TTL: "300"
bash
# Secret — datos sensibles (crea mejor por comando, no lo subas a git en claro)
kubectl create secret generic db-secret \
  --from-literal=url='postgres://user:pass@db:5432/tienda'
yaml
# En el Deployment: inyectar TODO el ConfigMap como variables de entorno
containers:
  - name: api
    envFrom:
      - configMapRef: { name: tienda-config }
    env:
      - name: DATABASE_URL
        valueFrom:
          secretKeyRef: { name: db-secret, key: url }

⚠️ Los Secrets de k8s van en base64, NO cifrados. Cualquiera con acceso al clúster los lee. Para producción seria: cifrado at rest activado, RBAC restrictivo, y/o un gestor externo (External Secrets Operator + Vault/AWS Secrets Manager). Y jamás secrets en claro en git — si necesitas versionarlos, Sealed Secrets o SOPS.


G.6 · Ingress: la puerta de entrada HTTP

El Service es interno. Para exponer HTTP/HTTPS al mundo con dominios y TLS usas un Ingress (necesita un ingress controller instalado — el más común es ingress-nginx):

yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: tienda
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt   # TLS automático con cert-manager
spec:
  ingressClassName: nginx
  tls:
    - hosts: [api.mitienda.com]
      secretName: tienda-tls
  rules:
    - host: api.mitienda.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: tienda-api
                port: { number: 80 }
Internet ──▶ Ingress (nginx + TLS) ──▶ Service ──▶ Pods
             api.mitienda.com            DNS interno: tienda-api

💡 DNS interno gratis: cada Service tiene nombre DNS dentro del clúster. Tu API se conecta a PostgreSQL con postgres://db:5432 si el Service se llama db — igual que los nombres de servicio en Docker Compose (cap. 12). El futuro de esta pieza es la Gateway API (sucesora del Ingress); conceptualmente es lo mismo con más control.


G.7 · Depurar en Kubernetes (los comandos que usarás de verdad)

bash
kubectl get pods -o wide                  # estado, restarts, nodo, IP
kubectl describe pod <pod>                # eventos: por qué no arranca (¡lee el final!)
kubectl logs <pod> -f                     # logs en vivo
kubectl logs <pod> --previous             # logs del contenedor ANTERIOR (tras un crash)
kubectl exec -it <pod> -- sh              # shell dentro del pod
kubectl port-forward svc/tienda-api 8080:80   # probar el service desde tu máquina
kubectl get events --sort-by=.lastTimestamp   # qué está pasando en el clúster
kubectl top pods                          # CPU/memoria (requiere metrics-server)

Los tres estados de error que verás mil veces:

EstadoSignificaPrimer paso
CrashLoopBackOffEl contenedor arranca y muere en buclekubectl logs <pod> --previous
ImagePullBackOffNo puede descargar la imagen¿nombre/tag correcto? ¿registry privado sin credenciales?
PendingNo hay nodo donde colocarlodescribe pod: ¿recursos insuficientes?

G.8 · Auto-escalado

Kubernetes puede añadir/quitar réplicas según la carga (HPA — Horizontal Pod Autoscaler):

yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: tienda-api
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: tienda-api
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70    # si la CPU media pasa del 70%, añade pods

🔗 El auto-escalado se apoya en métricas (apéndice F). Kubernetes y la observabilidad van de la mano: para escalar bien, primero tienes que medir bien.


G.9 · Otras piezas que te cruzarás

RecursoPara quéEjemplo
StatefulSetApps con estado e identidad estable (BD, colas)PostgreSQL, Kafka
DaemonSetUn pod en cada nodoAgente de logs/métricas
JobTarea que corre hasta terminarMigración de BD
CronJobJob programadoBackup nocturno
PersistentVolumeClaimDisco persistente para un podDatos de PostgreSQL
yaml
# CronJob: el backup del cap. 14, versión k8s
apiVersion: batch/v1
kind: CronJob
metadata: { name: backup-db }
spec:
  schedule: "0 3 * * *"            # cron normal: 03:00 cada día
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: backup
              image: postgres:18-alpine
              command: ["/bin/sh", "-c", "pg_dump $DATABASE_URL | gzip > /backup/$(date +%F).sql.gz"]

⚠️ ¿Bases de datos DENTRO de k8s? Se puede (StatefulSet + PVC + operadores como CloudNativePG), pero el estado es la parte más delicada de k8s. Si estás empezando: base de datos gestionada fuera del clúster y dentro solo lo stateless. Dormirás mejor.

El patrón Operator: automatizar lo que un DBA haría a mano

Un Operator es software que extiende k8s con conocimiento específico de una aplicación: en vez de que tú ejecutes pg_dump, promuevas una réplica o apliques un parche de seguridad a mano, el Operator observa un recurso personalizado (CRD) y ejecuta esas operaciones él solo.

yaml
# Con CloudNativePG instalado, "pedir" un clúster PostgreSQL de 3 nodos es esto:
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata: { name: tienda-db }
spec:
  instances: 3                        # 1 primario + 2 réplicas, el Operator gestiona el failover
  storage: { size: 20Gi }
  backup:
    barmanObjectStore: { destinationPath: "s3://mis-backups/tienda" }

🧠 La idea central: un Deployment normal solo sabe "mantén N copias de este contenedor". Un Operator para PostgreSQL sabe qué significa un clúster de PostgreSQL sano: cómo promover una réplica a primario si el líder muere, cómo hacer un backup consistente, cómo aplicar un upgrade de versión sin perder datos. Es la diferencia entre k8s genérico y k8s que entiende tu aplicación con estado — por eso StatefulSet + un buen Operator es la forma real de correr BDs en el clúster, nunca un StatefulSet a pelo.


G.10 · Helm y Kustomize: no repitas YAML

Con 3 entornos (dev/staging/prod) no copias YAML 3 veces:

  • Kustomize (integrado en kubectl): una base + parches por entorno. kubectl apply -k overlays/prod
  • Helm: plantillas con variables (values.yaml) + gestor de paquetes para instalar software de terceros:
bash
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install redis bitnami/redis          # Redis completo en el clúster en un comando
helm upgrade tienda ./chart -f values-prod.yaml

💡 Regla práctica: Kustomize para TUS apps (simple, sin plantillas), Helm para instalar software de terceros (ingress-nginx, cert-manager, Prometheus…). GitOps (ArgoCD/Flux) los despliega desde git automáticamente — el CD del cap. 14 llevado al clúster.


G.11 · Herramientas del ecosistema

Necesito...Herramienta
Probar k8s en mi portátilkind, minikube, k3d (clúster local)
Empaquetar apps k8s reutilizablesHelm (el "gestor de paquetes" de k8s)
GitOps (desplegar desde git)ArgoCD, Flux
k8s ligero (edge, IoT, VPS pequeño)k3s
k8s gestionado (sin administrar el control plane)GKE, EKS, AKS, DigitalOcean

💡 Si de verdad vas a usar Kubernetes, usa uno gestionado (GKE/EKS/AKS). Administrar tu propio control plane es un trabajo a tiempo completo. Los gestionados se encargan de la parte difícil y tú solo despliegas.


G.12 · El camino de crecimiento (dónde encaja k8s)

1 servidor, 1 app        →  VPS + systemd o Docker (caps. 12, 14)
1 servidor, varias apps  →  Docker Compose + Nginx (caps. 12, 13)
Quiero simplicidad + CD  →  Coolify / Dokku / Railway / Cloud Run
Escala real, multi-nodo  →  Kubernetes (gestionado)  ← SOLO cuando lo necesitas

🧠 La regla de oro (una vez más): no saltes a Kubernetes por moda o por el CV. Cada escalón añade complejidad; súbelo solo cuando el anterior se te quede pequeño de verdad. Un monolito (cap. 11) en un VPS con Docker ha llevado a muchísimas empresas muy lejos. Conocer k8s te hace mejor ingeniero; usarlo sin necesidad te hace la vida imposible.


G.13 · NetworkPolicy y PodDisruptionBudget: aislar y proteger

Por defecto, en Kubernetes cualquier pod puede hablar con cualquier otro pod del clúster. Un NetworkPolicy restringe eso — el firewall del cap. 13, pero entre pods:

yaml
# 1. Denegar TODO el tráfico entrante por defecto en el namespace (política base)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: negar-todo }
spec:
  podSelector: {}              # aplica a TODOS los pods del namespace
  policyTypes: [Ingress]
---
# 2. Permitir SOLO que la API hable con la base de datos (nada más)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: permitir-api-a-db }
spec:
  podSelector:
    matchLabels: { app: postgres }
  policyTypes: [Ingress]
  ingress:
    - from:
        - podSelector: { matchLabels: { app: tienda-api } }
      ports:
        - port: 5432

🧠 "Denegar todo" primero, luego permitir explícitamente es la misma filosofía de whitelist del apéndice C (validación de entrada): en vez de listar lo prohibido, declaras lo permitido y todo lo demás queda bloqueado por defecto. Sin NetworkPolicy, si un atacante compromete un pod cualquiera (una dependencia vulnerable, ap. C.10), puede escanear y tocar cualquier otro servicio del clúster, incluida tu base de datos.

PodDisruptionBudget (PDB): protege tus réplicas durante mantenimiento planificado del clúster (actualizar un nodo, vaciarlo con kubectl drain) — distinto de un crash (G.4):

yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: { name: tienda-api-pdb }
spec:
  minAvailable: 2          # nunca menos de 2 pods vivos durante un drenado planificado
  selector:
    matchLabels: { app: tienda-api }

⚠️ Sin PDB, un kubectl drain (mantenimiento normal de un nodo) puede tumbar TODAS tus réplicas a la vez si por mala suerte están todas en el mismo nodo — el PDB obliga al proceso de drenado a esperar y desalojar de a poco, respetando el mínimo. Para una base de datos con quorum (3 réplicas de Postgres, StatefulSet de G.9), usa maxUnavailable: 1: nunca más de una fuera a la vez, o pierdes el quorum.

💡 Regla práctica: define un PDB para todo servicio de producción, incluso los que no necesitan alta disponibilidad real — en ese caso, maxUnavailable: 100% documenta explícitamente "este servicio tolera caerse entero durante mantenimiento", en vez de dejarlo como un olvido. Es más barato declararlo que descubrir el problema en el peor momento.


✅ Ejercicio del apéndice

1. Instala kind o minikube y levanta un clúster local.
2. Dockeriza una de tus APIs (cap. 12) y súbela a un registry.
3. Escribe un Deployment (3 réplicas) + Service + liveness/readiness probes.
   Usa tu endpoint /salud.
4. `kubectl apply`, comprueba los 3 pods, y mata uno a mano (`kubectl delete pod`).
   Observa cómo k8s crea otro solo.
5. Añade un ConfigMap (LOG_LEVEL) y un Secret (DATABASE_URL) e inyéctalos como env.
6. Escala a 5 réplicas y luego configura un HPA por CPU.
7. Haz un despliegue de una v2, observa el rolling update con `rollout status`,
   despliega una v3 rota a propósito (imagen inexistente) y arréglalo con `rollout undo`.
8. Provoca un CrashLoopBackOff (comando que falle) y diagnostícalo con
   describe + logs --previous.
9. Instala ingress-nginx con Helm y expón tu API con un Ingress en local.
10. Añade un NetworkPolicy que solo permita a tu API hablar con tu base de datos
    (deniega todo lo demás) y un PodDisruptionBudget con minAvailable acorde a
    tus réplicas. Provoca un `kubectl drain` del nodo y observa cómo el PDB lo frena.
11. Reflexiona honestamente: ¿tu proyecto real necesita esto, o Docker Compose basta?
💡 Pistas de la solución
  • Si kind no arranca por falta de recursos, prueba k3d — el mismo modelo mental con menos consumo de RAM en portátiles modestos.
  • El readinessProbe es la pieza que suele fallar primero: si tu endpoint /salud tarda en responder mientras la app aún conecta a la BD, k8s le mandará tráfico antes de tiempo. Sube initialDelaySeconds o haz que /salud devuelva 503 hasta que la conexión esté lista.
  • Para el CrashLoopBackOff provocado a propósito, el truco más simple es un command que haga exit 1 — verás el ciclo de reinicios y aprenderás a leer describe pod bajo presión.
  • El Ingress con ingress-nginx en local necesita que tu clúster (kind) tenga los puertos 80/443 mapeados al host — revisa la documentación de kind sobre extraPortMappings.

🧠 Autoevaluación

Sin límites, un pod puede consumir toda la CPU/memoria del nodo y afectar a otros pods que comparten esa máquina ("noisy neighbor"). Los requests/limits (G.4) le dicen al scheduler cuánto reservar y evitan que un pod descontrolado tumbe a los demás.

Que el tag de la imagen no existe en el registry (typo, o el build de CI aún no terminó), o que el registry es privado y el clúster no tiene las credenciales (imagePullSecrets) configuradas. kubectl describe pod muestra el mensaje exacto del intento de pull fallido.

Con maxUnavailable: 0, k8s nunca reduce la capacidad actual: solo añade pods nuevos (maxSurge) y espera a que pasen la readiness antes de matar uno viejo. Es más lento porque no puede avanzar en paralelo tan agresivamente, pero garantiza que el servicio nunca sirve con menos réplicas de las declaradas.

Base64 es codificación, no cifrado — se decodifica con un comando de una línea (echo ... | base64 -d). Cualquiera con permiso de lectura sobre el Secret (o acceso al etcd del clúster) ve el valor real. La seguridad real viene de RBAC restrictivo, cifrado at rest, y gestores externos como Vault.

Para empezar, fuera y gestionada (RDS, Cloud SQL, DigitalOcean Managed Databases). El estado es la parte más delicada de operar en k8s — StatefulSet + PersistentVolumeClaim funciona, pero exige entender backups, failover y operadores especializados (CloudNativePG) antes de confiar en ello en producción.

Por defecto, cualquier pod del clúster puede conectarse a cualquier otro — el atacante puede escanear y tocar directamente tu base de datos, tu Redis o cualquier servicio interno desde ese pod comprometido, sin pasar por ningún control adicional. Un NetworkPolicy de "denegar todo por defecto, permitir explícitamente" (G.13) limita el movimiento lateral exactamente igual que un firewall entre servidores tradicionales.

Si las 3 réplicas (o varias) terminan programadas en el mismo nodo por casualidad, el drenado puede desalojarlas todas a la vez y dejar el servicio caído momentáneamente, aunque el Deployment las vuelva a crear después en otro nodo. Un PDB con minAvailable/maxUnavailable obliga al proceso de drenado a respetar un mínimo de réplicas vivas en todo momento, incluso durante mantenimiento planificado.

Un StatefulSet solo mantiene pods con identidad y almacenamiento estables — no sabe qué es un primario, una réplica o un backup consistente. Un Operator (como CloudNativePG) entiende la semántica específica de PostgreSQL: promueve una réplica si el líder cae, orquesta backups y aplica upgrades sin perder datos. Es la pieza que falta entre "contenedores con disco" y "una base de datos gestionada de verdad".

startupProbe: mientras no pase, livenessProbe no se evalúa, así que k8s no mata el pod por tardar en arrancar. Sin ella, un initialDelaySeconds corto en el livenessProbe reinicia el pod antes de que termine de inicializarse, entrando en un bucle de reinicios que no refleja ningún fallo real.


Volver al: README.md · Relacionado: 12-docker.md, F-observabilidad.md, 11-arquitectura.md