Swarm te dio un clúster con las ideas justas y una curva de aprendizaje amable. Kubernetes te da el estándar de la industria a cambio de un vocabulario nuevo. La buena noticia es que los conceptos ya los tienes: estado deseado, reconciliación, réplicas, red entre nodos y secretos montados como ficheros. Esta lección pone nombres nuevos a ideas conocidas y añade las que faltan, sin desplegar todavía Aurora Libros entera.

Contenido

  1. Por qué existe Kubernetes y qué añade
  2. El modelo declarativo y los controladores
  3. Arquitectura del clúster
  4. El runtime tras dockershim: CRI y OCI
  5. Los objetos fundamentales de un vistazo
  6. Pod: la unidad de despliegue
  7. ReplicaSet y Deployment
  8. Service, sus tipos e Ingress
  9. ConfigMap y Secret
  10. Namespace
  11. PersistentVolume, PersistentVolumeClaim y StorageClass
  12. Job, CronJob, StatefulSet y DaemonSet
  13. Anatomía de un manifiesto: etiquetas y selectores
  14. kubectl esencial, traducido desde Docker
  15. Un clúster local para practicar
  16. Glosario Docker → Kubernetes

  1. Por qué existe Kubernetes y qué añade

Kubernetes nació en Google a partir de la experiencia de Borg y se donó a la CNCF en 2015. Su objetivo no era mejorar Swarm, sino resolver la orquestación a escala de miles de nodos y de equipos, y por eso su diseño es más ambicioso y también más complejo.

Capacidad Swarm Kubernetes
Unidad mínima Un contenedor por tarea Pod: varios contenedores acoplados
Extensibilidad Fija CRD y operadores: objetos propios
Autoescalado No HPA, VPA y autoescalado de nodos (06-06)
Almacenamiento Volúmenes locales o plugins CSI, PVC y aprovisionamiento dinámico
Control de acceso Roles: manager/worker RBAC por usuario, recurso y verbo
Enrutado HTTP Nginx que pones tú Ingress y Gateway API nativos
Cargas con estado Sin soporte específico StatefulSet con identidad estable
Curva de aprendizaje Días Semanas
Ecosistema Reducido Enorme: Helm, Argo, Prometheus, mallas

La última fila es la que decide en la mayoría de las organizaciones. La primera es la que más cambia tu forma de pensar, y es la que veremos a fondo.

  1. El modelo declarativo y los controladores

El corazón conceptual es idéntico al de Swarm: describes el estado deseado y el sistema lo mantiene. La diferencia es que Kubernetes lo generaliza: todo es un objeto en una base de datos (etcd), y por cada tipo de objeto hay un controlador ejecutando el mismo bucle.

observar el estado real → compararlo con el deseado → actuar para acercarlos → repetir

Ese patrón se llama reconciliation loop, y explica comportamientos que al principio desconciertan:

  • Borras un Pod creado por un Deployment y reaparece: su controlador ve que faltan réplicas.
  • Editas un Pod a mano y tu cambio desaparece en el siguiente despliegue: la fuente de verdad es la plantilla del Deployment.
  • Aplicas un manifiesto dos veces y no pasa nada: no describes acciones, describes destinos. La operación es idempotente.

  1. Arquitectura del clúster

flowchart TB
    U["kubectl / CI"] -->|API REST| API
    subgraph CP["Plano de control"]
        API[kube-apiserver] <--> ETCD[(etcd)]
        SCH[kube-scheduler] --> API
        CM[kube-controller-manager] --> API
    end
    API --> K1[kubelet · nodo-1]
    API --> K2[kubelet · nodo-2]
    K1 --> R1["containerd (CRI) → runc"]
    K2 --> R2["containerd (CRI) → runc"]
    style API fill:#e8f0fe,stroke:#3367d6
Componente Dónde Responsabilidad
kube-apiserver Plano de control Única puerta de entrada; valida, autentica y escribe en etcd
etcd Plano de control Base de datos clave-valor con todo el estado. Su copia de seguridad es la del clúster
kube-scheduler Plano de control Decide en qué nodo va cada Pod nuevo según recursos, afinidades y taints
kube-controller-manager Plano de control Ejecuta los controladores (Deployment, ReplicaSet, Node...)
kubelet Cada nodo Habla con el runtime para que los Pods asignados existan y reporta su estado
kube-proxy Cada nodo Programa las reglas de red que hacen funcionar los Services
Runtime (CRI) Cada nodo Ejecuta los contenedores: containerd, CRI-O

Fíjate en la propiedad que hace robusto el diseño: nadie habla con nadie salvo con el kube-apiserver. El scheduler no llama al kubelet; escribe en la API que un Pod va a cierto nodo, y el kubelet de ese nodo, que está observando la API, actúa. Todo pasa por el mismo punto de autenticación y auditoría.

  1. El runtime tras dockershim: CRI y OCI

En 2022, Kubernetes 1.24 eliminó dockershim, el adaptador que le permitía usar Docker Engine como runtime. Aquello generó titulares del tipo "Kubernetes elimina Docker" que confundieron a todo el mundo.

Lo que ocurrió en realidad es mucho menos dramático:

  • Kubernetes habla con los runtimes por una interfaz llamada CRI (Container Runtime Interface).
  • Docker Engine no implementa CRI, así que hacía falta un adaptador (dockershim) mantenido por el propio proyecto.
  • Ese adaptador se retiró, y los nodos usan containerd o CRI-O directamente, que sí hablan CRI. Y containerd es, precisamente, el mismo componente que usa Docker Engine por dentro desde 01-03.

Tus imágenes siguen valiendo exactamente igual. Lo que construyes con docker build es una imagen OCI, un formato estándar, no un formato "de Docker". La ghcr.io/auroralibros/aurora-api:2.0.0 que produjo tu pipeline se ejecuta en containerd sin ninguna modificación. Lo único que cambió es qué programa la arranca en el nodo.

  1. Los objetos fundamentales de un vistazo

Objeto Para qué ¿Lo creas a mano?
Pod Una o varias contenedores que comparten red y almacenamiento Casi nunca
ReplicaSet Mantiene N Pods idénticos No: lo crea el Deployment
Deployment Gestiona ReplicaSets y las actualizaciones progresivas
Service IP y nombre DNS estables ante un conjunto de Pods
Ingress Enrutado HTTP/HTTPS de entrada por host y ruta
ConfigMap Configuración no sensible
Secret Configuración sensible (codificada en base64)
Namespace Espacio de nombres lógico dentro del clúster
PersistentVolumeClaim Petición de almacenamiento persistente
StorageClass Cómo se aprovisiona ese almacenamiento La da el clúster
Job / CronJob Tarea que termina / tarea periódica
StatefulSet Pods con identidad y disco estables
DaemonSet Un Pod por nodo

  1. Pod: la unidad de despliegue

Un Pod es un grupo de contenedores que comparten namespace de red (misma IP, se hablan por localhost), namespace IPC y, opcionalmente, volúmenes. Es la unidad que el scheduler coloca y que se escala: en Kubernetes no se replican contenedores, se replican Pods.

apiVersion: v1
kind: Pod
metadata:
  name: aurora-cache
  labels: { app: aurora-cache }
spec:
  containers:
    - name: redis
      image: redis:7-alpine
      args: ["--maxmemory", "200mb", "--maxmemory-policy", "allkeys-lru"]
      ports: [{ containerPort: 6379 }]
      resources:
        requests: { memory: 64Mi, cpu: 50m }
        limits:   { memory: 256Mi, cpu: 500m }

Casi nunca se crea un Pod a mano, por una razón contundente: un Pod no se recupera. Si el nodo muere, el Pod muere con él y nadie lo recrea; es el equivalente exacto de un docker run sin --restart. Lo que se crea es un Deployment, que a través de su ReplicaSet garantiza que siempre haya tantos Pods como pediste.

La mayoría de los Pods tienen un solo contenedor. El caso multicontenedor es el patrón sidecar: un contenedor auxiliar que acompaña al principal —un recolector de logs, un proxy de malla de servicios, un sincronizador de ficheros— y se beneficia de compartir red y volúmenes.

  1. ReplicaSet y Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: aurora-cache
spec:
  replicas: 1
  selector:
    matchLabels: { app: aurora-cache }     # qué Pods me pertenecen
  template:                                # la plantilla del Pod (sin apiVersion ni kind)
    metadata:
      labels: { app: aurora-cache }        # DEBE casar con el selector
    spec:
      containers:
        - name: redis
          image: redis:7-alpine
          ports: [{ containerPort: 6379 }]

La cadena de responsabilidad tiene tres eslabones, y entenderla evita mucha confusión:

Deployment  →  ReplicaSet  →  Pods
(versiones)    (cuántos)      (procesos)

El ReplicaSet solo sabe contar: mantiene N Pods que casen con su selector. El Deployment gestiona ReplicaSets: al cambiar la imagen crea uno nuevo, lo va llenando mientras vacía el antiguo, y conserva el viejo con cero réplicas para poder deshacer. Ese ReplicaSet vacío que ves en kubectl get rs no es basura: es tu historial de rollback (06-07).

Un aviso: el selector de un Deployment es inmutable. Si lo cambias, hay que borrar y recrear el objeto. Conviene elegir bien las etiquetas la primera vez.

  1. Service, sus tipos e Ingress

Los Pods son ganado, no mascotas: nacen y mueren con IPs distintas. Un Service aporta una IP virtual y un nombre DNS estables delante de un conjunto cambiante de Pods, seleccionados —de nuevo— por etiquetas.

apiVersion: v1
kind: Service
metadata:
  name: aurora-cache
spec:
  type: ClusterIP
  selector: { app: aurora-cache }    # cualquier Pod con esta etiqueta entra al balanceo
  ports:
    - port: 6379          # el puerto del Service
      targetPort: 6379    # el puerto del contenedor

Los Pods llegan por DNS con aurora-cache dentro del namespace, o aurora-cache.aurora.svc.cluster.local desde fuera de él: es el equivalente del DNS por nombre de servicio de Compose y Swarm.

Tipo Qué expone Alcance Uso en Aurora Libros
ClusterIP (defecto) IP virtual interna Solo dentro del clúster aurora-db, aurora-cache, aurora-api
NodePort Un puerto (30000-32767) en todos los nodos Externo, tosco Pruebas locales
LoadBalancer Un balanceador del proveedor cloud Externo, uno por servicio Solo aurora-web
ExternalName Un CNAME a un dominio externo Migrar a una BD gestionada
Headless (clusterIP: None) Sin IP virtual: DNS a cada Pod Interno aurora-db en StatefulSet

Un LoadBalancer por servicio se vuelve caro enseguida: cada uno es un balanceador facturado del proveedor. La solución estándar es un Ingress: un único punto de entrada que enruta por host y por ruta hacia varios Services.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: aurora
  annotations: { nginx.ingress.kubernetes.io/proxy-body-size: "8m" }
spec:
  ingressClassName: nginx            # qué controlador lo atiende
  rules:
    - host: libros.aurora.example
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend: { service: { name: aurora-api, port: { number: 3000 } } }
          - path: /
            pathType: Prefix
            backend: { service: { name: aurora-web, port: { number: 80 } } }

Detalle que causa desconcierto la primera vez: el objeto Ingress no hace nada por sí solo. Es una declaración de intenciones que necesita un ingress controller instalado en el clúster (ingress-nginx, Traefik, HAProxy...) que la lea y configure el proxy real. Sin controlador, el objeto existe, no da error y el tráfico no llega a ninguna parte.

  1. ConfigMap y Secret

apiVersion: v1
kind: ConfigMap
metadata: { name: aurora-config }
data:
  DB_HOST: aurora-db
  DB_NAME: aurora_libros
  CACHE_TTL: "60"
  PLAZO_APAGADO_MS: "15000"
---
apiVersion: v1
kind: Secret
metadata: { name: aurora-secretos }
type: Opaque
stringData:                          # stringData acepta texto plano; Kubernetes lo codifica
  DB_PASSWORD: clave-ficticia-aurora

Ambos se consumen igual, y hay dos formas con implicaciones distintas:

      envFrom:                                   # todas las claves como variables
        - configMapRef: { name: aurora-config }
      env:                                       # una clave concreta
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef: { name: aurora-secretos, key: DB_PASSWORD }
      volumeMounts:                              # o como ficheros: el patrón _FILE de 06-01
        - { name: secretos, mountPath: /run/secrets, readOnly: true }

Advertencia importante sobre Secret. Un Secret de Kubernetes está en base64, que no es cifrado: cualquiera con permiso de lectura sobre el objeto ve el valor con un base64 -d. Además, etcd no cifra en reposo salvo que se active explícitamente. Para producción hay que activar el cifrado en reposo, restringir el acceso con RBAC y valorar un gestor externo (Vault, Sealed Secrets, el gestor de secretos del proveedor cloud con el driver CSI). Es una decisión que debes acordar con el responsable de seguridad de tu organización, no un ajuste por defecto aceptable.

Y una diferencia práctica frente a Swarm: montado como volumen, un Secret o ConfigMap actualizado se propaga al fichero del Pod sin reiniciarlo (con retardo de hasta un minuto); inyectado como variable de entorno, no se actualiza nunca hasta que el Pod se recrea.

  1. Namespace

apiVersion: v1
kind: Namespace
metadata:
  name: aurora
  labels: { entorno: produccion }

Un Namespace es una partición lógica del clúster. Da tres cosas: nombres que no colisionan (puede haber un aurora-api en aurora y otro en aurora-staging), un ámbito para RBAC y cuotas, y un límite de red si usas NetworkPolicy. No es un límite de seguridad fuerte por sí mismo: no aísla el kernel, solo la API.

Con kubectl config set-context --current --namespace=aurora te evitas escribir -n aurora en cada comando.

  1. PersistentVolume, PersistentVolumeClaim y StorageClass

Kubernetes separa quién pide almacenamiento de quién lo proporciona, que es justo lo que faltaba en Swarm.

Objeto Quién lo escribe Qué dice
StorageClass La plataforma Cómo se crea el disco (tipo, política de borrado)
PersistentVolumeClaim (PVC) "Quiero 10 GiB con acceso ReadWriteOnce"
PersistentVolume (PV) El aprovisionador El disco real, ya creado
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: aurora-datos }
spec:
  accessModes: [ReadWriteOnce]
  storageClassName: standard
  resources: { requests: { storage: 10Gi } }
Modo de acceso Significado Quién lo soporta
ReadWriteOnce (RWO) Escritura desde un nodo Discos de bloque: EBS, PD, iSCSI
ReadOnlyMany (ROX) Lectura desde varios nodos NFS, almacenamiento de objetos
ReadWriteMany (RWX) Escritura desde varios nodos NFS, CephFS, EFS

Casi todos los discos de bloque de los proveedores cloud son RWO, y de ahí sale un límite muy real: un PVC de ese tipo no puede montarse a la vez en Pods de nodos distintos. Es la razón técnica por la que aurora-db no se escala replicando Pods, y la trataremos en 06-06.

  1. Job, CronJob, StatefulSet y DaemonSet

Objeto Garantía En Aurora Libros
Job Ejecuta hasta completar N veces con éxito Migración del esquema antes de un despliegue
CronJob Crea Jobs según una expresión cron Copia nocturna con pg_dump
StatefulSet Nombre e identidad estables (-0, -1), PVC propio por Pod, orden de arranque y parada aurora-db
DaemonSet Un Pod en cada nodo (y en los que se añadan) Promtail, node-exporter

El DaemonSet es literalmente el mode: global de Swarm. El StatefulSet es lo que Swarm no tenía: cada réplica recibe un nombre estable y su propio volumen, sobrevive al reinicio con la misma identidad y se arranca y se para en orden. Es lo que necesita cualquier cosa con estado, y lo usarás en 06-05.

  1. Anatomía de un manifiesto: etiquetas y selectores

Todos los objetos comparten cuatro campos de primer nivel:

Campo Qué es Ejemplo
apiVersion Grupo y versión de la API apps/v1, v1, networking.k8s.io/v1
kind Tipo de objeto Deployment, Service
metadata Nombre, namespace, etiquetas, anotaciones name: aurora-api
spec El estado deseado replicas: 3
status El estado real (lo escribe el sistema, no tú) readyReplicas: 3

Y ahora lo más importante de toda la lección. En Kubernetes los objetos no se referencian por nombre, sino por etiquetas. Un Service no dice "manda tráfico a los Pods del Deployment aurora-api"; dice "manda tráfico a todo Pod con la etiqueta app: aurora-api".

flowchart LR
    S["Service<br/>selector: app=aurora-api"] -.->|selecciona| P1["Pod app=aurora-api"]
    S -.-> P2["Pod app=aurora-api"]
    S -.-x P3["Pod app=aurora-web"]
    D["Deployment<br/>selector: app=aurora-api"] -->|crea| P1
    D --> P2

Ese acoplamiento débil es potente y frágil a la vez. Potente porque permite cosas como dirigir tráfico a Pods de dos Deployments distintos a la vez, que es la base del blue-green y del canario de 06-07. Frágil porque una errata en una etiqueta no da ningún error: el Service simplemente no encuentra Pods, se queda sin endpoints y el tráfico devuelve 503. Es el fallo silencioso más común, y se diagnostica siempre igual: kubectl get endpoints <servicio>.

Las etiquetas recomendadas por la comunidad, que conviene adoptar desde el principio:

  labels:
    app.kubernetes.io/name: aurora-api
    app.kubernetes.io/component: backend
    app.kubernetes.io/part-of: aurora-libros
    app.kubernetes.io/version: "2.0.0"

  1. kubectl esencial, traducido desde Docker

Tarea Docker / Compose kubectl
Crear o actualizar docker compose up -d kubectl apply -f manifiestos/
Listar docker ps kubectl get pods
Ver todo docker compose ps kubectl get all -n aurora
Detalle y eventos docker inspect kubectl describe pod <p>
Logs docker logs -f kubectl logs -f <p>
Logs del intento anterior kubectl logs <p> --previous
Shell dentro docker exec -it c sh kubectl exec -it <p> -- sh
Acceder a un puerto -p 8080:3000 kubectl port-forward svc/aurora-api 8080:3000
Eliminar docker compose down kubectl delete -f manifiestos/
Escalar docker service scale kubectl scale deploy/aurora-api --replicas=3
Uso de recursos docker stats kubectl top pods
Consultar el esquema docker run --help kubectl explain deployment.spec.strategy
kubectl get pods -o wide                       # añade nodo e IP
kubectl get deploy aurora-api -o yaml          # el objeto entero, con su status
kubectl get pods -o jsonpath='{.items[*].spec.containers[*].image}'
kubectl get pods -w                            # observar cambios en vivo
kubectl describe pod aurora-api-7d9f -n aurora # la sección Events es el 80 % del diagnóstico

Dos comandos merecen destacarse. kubectl explain es la documentación oficial sin salir del terminal y sirve para cualquier campo de cualquier objeto, incluidos los que instales después. Y kubectl describe termina con una lista de eventos que casi siempre contiene la causa exacta del problema: por qué el scheduler no encuentra nodo, por qué falla la descarga de la imagen o por qué una sonda no pasa.

  1. Un clúster local para practicar

Herramienta Cómo funciona Arranque Multinodo Ventaja
kind Nodos como contenedores Docker ~30 s Sí, trivial Ideal para CI; carga imágenes locales
minikube VM o contenedor ~60 s Limitado Addons integrados (Ingress, dashboard)
k3d k3s (Kubernetes ligero) en contenedores ~20 s El más liviano; muy rápido
Docker Desktop Un clúster de un nodo integrado Un clic No Cero instalación adicional
# kind-aurora.yaml — tres nodos y el puerto 80 mapeado para el Ingress
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
    kubeadmConfigPatches:
      - |
        kind: InitConfiguration
        nodeRegistration:
          kubeletExtraArgs: { node-labels: "ingress-ready=true" }
    extraPortMappings:
      - { containerPort: 80, hostPort: 8080, protocol: TCP }
  - role: worker
  - role: worker
kind create cluster --name aurora --config kind-aurora.yaml
kubectl get nodes
# NAME                   STATUS  ROLES          AGE  VERSION
# aurora-control-plane   Ready   control-plane  48s  v1.31.0
# aurora-worker          Ready   <none>         31s  v1.31.0
# aurora-worker2         Ready   <none>         31s  v1.31.0

# Primer contacto, con el servicio más simple de Aurora Libros
kubectl create namespace aurora
kubectl run aurora-cache --image=redis:7-alpine -n aurora --port=6379 --labels=app=aurora-cache
kubectl exec -it aurora-cache -n aurora -- redis-cli ping     # PONG

  1. Glosario Docker → Kubernetes

Lo que ya sabes En Kubernetes Matiz
Contenedor Pod El Pod puede tener varios contenedores
compose.yaml Manifiestos YAML Uno por objeto, o varios separados por ---
docker compose up -d kubectl apply -f . Declarativo en ambos casos
docker compose down kubectl delete -f .
Red de Compose / DNS por nombre Service El DNS lo da el Service, no la red
Puerto publicado Service + Ingress Separación entre exponer y enrutar
Volumen nombrado PVC + StorageClass Se pide, no se crea
deploy.replicas de Swarm spec.replicas del Deployment Idéntico concepto
mode: global DaemonSet Idéntico
Secreto de Swarm Secret Base64, no cifrado por defecto
docker service ps kubectl get pods + describe Los eventos están en describe
healthcheck livenessProbe + readinessProbe + startupProbe Las tres de 06-01
Proyecto de Compose Namespace Espacio de nombres

Errores Comunes y Consejos

  • Crear Pods sueltos. Nadie los recrea si el nodo cae. Salvo para una prueba de treinta segundos, usa siempre un Deployment.
  • Selector y etiquetas que no casan. El error más frecuente y el más silencioso: el Deployment crea Pods que su Service no encuentra. Verifica con kubectl get endpoints.
  • Creer que un Secret está cifrado. Es base64. Actívale el cifrado en reposo y protégelo con RBAC.
  • Esperar que un Ingress funcione sin controlador. El objeto se crea sin quejarse y no enruta nada.
  • Olvidar el namespace. kubectl get pods mira el namespace por defecto y parece que no hay nada. Fija el contexto o usa -n.
  • Definir limits sin requests. Kubernetes copia el límite en la petición y reservas mucho más de lo que necesitas.
  • Editar Pods con kubectl edit. El cambio dura hasta el siguiente despliegue. Modifica el Deployment.
  • Consejo: kubectl describe antes que kubectl logs. Si el Pod no arranca, no hay logs; la causa está en los eventos.
  • Consejo: kubectl apply --dry-run=server -f . valida los manifiestos contra la API real sin crear nada. Úsalo en el pipeline.

Ejercicios

Ejercicio 1. Monta un clúster kind de tres nodos, despliega aurora-cache con un Deployment y su Service ClusterIP, y comprueba desde otro Pod que el nombre aurora-cache resuelve y responde.

Ejercicio 2. Demuestra el bucle de reconciliación y la diferencia entre Pod suelto y Deployment: crea un Pod a mano y otro gestionado por un Deployment, bórralos y observa qué ocurre con cada uno.

Ejercicio 3. Provoca a propósito el fallo silencioso de las etiquetas: cambia el selector de un Service para que no case con ningún Pod y diagnostícalo sin mirar el manifiesto.

Soluciones

Solución 1.

kind create cluster --name aurora --config kind-aurora.yaml
kubectl create namespace aurora
kubectl config set-context --current --namespace=aurora
kubectl apply -f k8s/cache.yaml     # el Deployment y el Service de las secciones 7 y 8
kubectl get deploy,rs,pods,svc
# deployment.apps/aurora-cache             1/1  1  1                   12s
# replicaset.apps/aurora-cache-6b8d4c9f7   1    1  1                   12s
# pod/aurora-cache-6b8d4c9f7-x2klp         1/1  Running  0             12s
# service/aurora-cache  ClusterIP  10.96.184.22  <none>  6379/TCP      12s

En una sola salida está la cadena entera de la sección 7: un Deployment que ha creado un ReplicaSet cuyo nombre lleva el sufijo 6b8d4c9f7 —el hash de la plantilla del Pod—, y ese ReplicaSet ha creado un Pod que hereda el hash y añade un sufijo aleatorio. Ese hash es la pieza clave del despliegue progresivo: cambiar la imagen produce otro hash y, por tanto, un ReplicaSet nuevo.

kubectl run cliente --rm -it --image=redis:7-alpine --restart=Never -- sh -c '
  nslookup aurora-cache | tail -2
  redis-cli -h aurora-cache ping
  redis-cli -h aurora-cache.aurora.svc.cluster.local set libro "Rayuela"'
# Name:    aurora-cache.aurora.svc.cluster.local
# Address: 10.96.184.22
# PONG
# OK

El nombre corto y el completo resuelven a la misma IP, que es la IP virtual del Service, no la del Pod. Esa distinción es la que hace que los Pods puedan morir y renacer con otras direcciones sin que nadie tenga que enterarse: el cliente habla siempre con el Service. Es el mismo servicio que te daba el DNS interno de Compose y Swarm, con una diferencia importante: aquí lo proporciona un objeto explícito que puedes inspeccionar y modificar.

Solución 2.

kubectl run suelto --image=redis:7-alpine --labels=app=prueba
kubectl create deployment gestionado --image=redis:7-alpine --replicas=2
kubectl get pods -o custom-columns=NOMBRE:.metadata.name,DUENO:.metadata.ownerReferences[0].kind
# NOMBRE                       DUENO
# suelto                       <none>
# gestionado-5f7c8d9b4-hq2vn   ReplicaSet
# gestionado-5f7c8d9b4-t8plw   ReplicaSet

kubectl delete pod suelto gestionado-5f7c8d9b4-hq2vn && sleep 5 && kubectl get pods
# NAME                         READY  STATUS   RESTARTS  AGE
# gestionado-5f7c8d9b4-t8plw   1/1    Running  0         2m
# gestionado-5f7c8d9b4-vk9xz   1/1    Running  0         5s

Borraste dos Pods y solo uno volvió, con nombre distinto y cinco segundos de edad. La columna DUENO explica exactamente por qué: el Pod suelto no tiene ownerReferences, así que al borrarlo nadie echó nada en falta; los otros pertenecen a un ReplicaSet cuyo controlador observa la API, vio que tenía un Pod donde debía haber dos y creó uno nuevo.

Esa es la razón práctica de la regla de la sección 6. Un Pod suelto es equivalente a un docker run sin política de reinicio: sobrevive mientras nada lo moleste, y desaparece con su nodo. El detalle del nombre distinto también importa, porque revela que el Pod no se "reinicia": se crea otro, con otra IP, y por eso ninguna configuración puede depender de la identidad de un Pod concreto. Cuando esa identidad sí hace falta —como en aurora-db— existe el StatefulSet.

Solución 3.

kubectl patch svc aurora-cache -p '{"spec":{"selector":{"app":"aurora-cach"}}}'   # errata
kubectl run cliente --rm -it --image=redis:7-alpine --restart=Never -- \
  redis-cli -h aurora-cache -t 3 ping
# Could not connect to Redis at aurora-cache:6379: Connection refused

El diagnóstico, sin abrir ningún fichero:

kubectl get endpoints aurora-cache
kubectl get svc aurora-cache -o jsonpath='{.spec.selector}'; echo
kubectl get pods --show-labels
NAME           ENDPOINTS   AGE
aurora-cache   <none>      14m
{"app":"aurora-cach"}
aurora-cache-6b8d4c9f7-x2klp   1/1  Running  app=aurora-cache,pod-template-hash=6b8d4c9f7

Los ENDPOINTS a <none> son el síntoma decisivo, y la secuencia de tres comandos es la que hay que memorizar: el Service existe, tiene su IP virtual, el DNS resuelve perfectamente... y detrás no hay ningún Pod. Comparando el selector (app=aurora-cach) con las etiquetas reales (app=aurora-cache) aparece la letra que falta.

kubectl patch svc aurora-cache -p '{"spec":{"selector":{"app":"aurora-cache"}}}'
kubectl get endpoints aurora-cache
# aurora-cache   10.244.1.7:6379   15m

Lo pedagógico del ejercicio es lo que no ocurrió: ni el kubectl patch ni el kubectl get svc ni el describe del Deployment dieron el más mínimo aviso. Kubernetes no valida que un selector encuentre algo, porque un Service que aún no tiene Pods es una situación legítima —los tendrá cuando se desplieguen—. El precio de ese acoplamiento débil, que en 06-07 te permitirá conmutar tráfico entre dos versiones cambiando una etiqueta, es que una errata se manifiesta como un 503 en producción y no como un error al aplicar el manifiesto.

De ahí la regla de operación: después de cada kubectl apply que toque un Service, comprueba sus endpoints. Es un comando y evita la clase de incidente más frustrante de esta plataforma.

Conclusión

Ya hablas Kubernetes. Sabes que su corazón es el mismo bucle de reconciliación de Swarm, generalizado a todos los objetos de etcd, y que eso explica que un Pod borrado reaparezca y que un apply repetido no haga nada. Conoces la arquitectura —kube-apiserver como única puerta, etcd como estado, el planificador, los controladores, y en cada nodo el kubelet y el kube-proxy— y la propiedad que la hace robusta: nadie habla con nadie salvo con la API. Y tienes zanjado el malentendido de dockershim: Kubernetes ejecuta containerd por CRI, pero tus imágenes valen igual porque son OCI.

Manejas el catálogo de objetos con su YAML mínimo: el Pod como unidad y por qué no se crea a mano, la cadena Deployment → ReplicaSet → Pods donde el ReplicaSet vacío es tu historial de rollback, el Service con sus cinco variantes y el Ingress que no enruta nada sin un controlador instalado, ConfigMap y Secret —con la advertencia de que base64 no es cifrado—, el Namespace, la tríada PVC/PV/StorageClass con los modos de acceso que explicarán en 06-06 por qué una base de datos no se replica sin más, y los cuatro controladores restantes, entre ellos el DaemonSet que es el mode: global de Swarm y el StatefulSet que Swarm no tenía.

Sobre todo, has interiorizado la pieza central: etiquetas y selectores. Todo se conecta por etiquetas, nunca por nombre, y lo has comprobado rompiendo un selector a propósito para ver el fallo más silencioso de la plataforma —un Service perfectamente sano con ENDPOINTS <none>— y diagnosticándolo con tres comandos. Traduces cada operación de Docker a su kubectl equivalente, tienes un clúster kind de tres nodos con aurora-cache corriendo dentro, y un glosario que convierte lo que ya sabías en lo que acabas de aprender.

En la siguiente lección, Desplegando Contenedores Docker en Kubernetes, llega la aplicación entera. Escribirás los manifiestos reales de los cuatro servicios: el StatefulSet de aurora-db con sus volumeClaimTemplates y su Service headless, el Deployment de aurora-api con las tres sondas de 06-01, su securityContext endurecido y su configuración inyectada, el Ingress de aurora-web, y los organizarás con Kustomize en bases y overlays por entorno, con la tabla de estados de un Pod —Pending, ImagePullBackOff, CrashLoopBackOff, OOMKilled— y su comando de diagnóstico para cuando algo no arranque.

Docker: De Principiante a Avanzado

Módulo 1: Introducción a Docker

Módulo 2: Trabajando con Imágenes Docker

Módulo 3: Contenedores Docker

Módulo 4: Docker Compose

Módulo 5: Conceptos Avanzados de Docker

Módulo 6: Docker en Producción

Módulo 7: Ecosistema y Herramientas de Docker

© Copyright 2026. Todos los derechos reservados