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
- Por qué existe Kubernetes y qué añade
- El modelo declarativo y los controladores
- Arquitectura del clúster
- El runtime tras dockershim: CRI y OCI
- Los objetos fundamentales de un vistazo
Pod: la unidad de despliegueReplicaSetyDeploymentService, sus tipos eIngressConfigMapySecretNamespacePersistentVolume,PersistentVolumeClaimyStorageClassJob,CronJob,StatefulSetyDaemonSet- Anatomía de un manifiesto: etiquetas y selectores
kubectlesencial, traducido desde Docker- Un clúster local para practicar
- Glosario Docker → Kubernetes
- 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.
- 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.
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.
- 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.
- 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.
- 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 | Sí |
Service |
IP y nombre DNS estables ante un conjunto de Pods | Sí |
Ingress |
Enrutado HTTP/HTTPS de entrada por host y ruta | Sí |
ConfigMap |
Configuración no sensible | Sí |
Secret |
Configuración sensible (codificada en base64) | Sí |
Namespace |
Espacio de nombres lógico dentro del clúster | Sí |
PersistentVolumeClaim |
Petición de almacenamiento persistente | Sí |
StorageClass |
Cómo se aprovisiona ese almacenamiento | La da el clúster |
Job / CronJob |
Tarea que termina / tarea periódica | Sí |
StatefulSet |
Pods con identidad y disco estables | Sí |
DaemonSet |
Un Pod por nodo | Sí |
Pod: la unidad de despliegue
Pod: la unidad de despliegueUn 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.
ReplicaSet y Deployment
ReplicaSet y DeploymentapiVersion: 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:
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.
Service, sus tipos e Ingress
Service, sus tipos e IngressLos 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 contenedorLos 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.
ConfigMap y Secret
ConfigMap y SecretapiVersion: 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-auroraAmbos 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 unbase64 -d. Además,etcdno 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.
Namespace
NamespaceUn 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.
PersistentVolume, PersistentVolumeClaim y StorageClass
PersistentVolume, PersistentVolumeClaim y StorageClassKubernetes 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) |
Tú | "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.
Job, CronJob, StatefulSet y DaemonSet
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.
- 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"
kubectl esencial, traducido desde Docker
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ósticoDos 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.
- 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 | 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: workerkind 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
- 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
Secretestá cifrado. Es base64. Actívale el cifrado en reposo y protégelo con RBAC. - Esperar que un
Ingressfuncione sin controlador. El objeto se crea sin quejarse y no enruta nada. - Olvidar el namespace.
kubectl get podsmira el namespace por defecto y parece que no hay nada. Fija el contexto o usa-n. - Definir
limitssinrequests. 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 describeantes quekubectl 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 12sEn 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
# OKEl 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 5sBorraste 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 refusedEl 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-labelsNAME ENDPOINTS AGE
aurora-cache <none> 14m
{"app":"aurora-cach"}
aurora-cache-6b8d4c9f7-x2klp 1/1 Running app=aurora-cache,pod-template-hash=6b8d4c9f7Los 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 15mLo 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
- ¿Qué es Docker?
- Instalando Docker
- Arquitectura de Docker
- Comandos Básicos de Docker
- Entendiendo las Imágenes de Docker
- Creando tu Primer Contenedor Docker
- El Proyecto del Curso: la Plataforma Aurora Libros
Módulo 2: Trabajando con Imágenes Docker
- Docker Hub y Repositorios
- Construyendo Imágenes Docker
- Conceptos Básicos de Dockerfile
- Instrucciones Avanzadas del Dockerfile
- Gestionando Imágenes Docker
- Etiquetado y Publicación de Imágenes
Módulo 3: Contenedores Docker
- Ejecutando Contenedores
- Ciclo de Vida del Contenedor
- Gestionando Contenedores
- Inspección y Depuración de Contenedores
- Redes en Docker
- Persistencia de Datos con Volúmenes
- Límites de Recursos y Políticas de Reinicio
Módulo 4: Docker Compose
- Introducción a Docker Compose
- Definiendo Servicios en Docker Compose
- Comandos de Docker Compose
- Aplicaciones Multi-Contenedor
- Variables de Entorno en Docker Compose
- Perfiles, Overrides y Múltiples Entornos
- Desarrollo Local con Docker Compose
Módulo 5: Conceptos Avanzados de Docker
- Profundización en Redes Docker
- Opciones de Almacenamiento Docker
- Mejores Prácticas de Seguridad en Docker
- Optimizando Imágenes Docker
- Builds Avanzadas con BuildKit y Buildx
- Registro y Monitoreo en Docker
- El Runtime por Dentro: Namespaces, Cgroups y Capas
Módulo 6: Docker en Producción
- Preparar una Imagen para Producción
- CI/CD con Docker
- Orquestando Contenedores con Docker Swarm
- Introducción a Kubernetes
- Desplegando Contenedores Docker en Kubernetes
- Escalado y Balanceo de Carga
- Estrategias de Despliegue y Rollback
