La lección anterior terminó con un problema sin resolver: api-reservas y tienda-web corren en rutas-norte-dev, pero como pods sueltos. Si el nodo cae, si el kubelet los desaloja por falta de memoria o si alguien los borra, desaparecen y nadie los devuelve a la vida. Necesitamos algo que vigile permanentemente y actúe. Ese algo es un controlador, y el primero que vamos a conocer a fondo es el ReplicaSet: el objeto cuya única misión es garantizar que en todo momento exista un número concreto de pods que casen con un conjunto de etiquetas. En esta lección verás qué es exactamente un controlador y cómo el de ReplicaSet materializa el bucle de reconciliación que estudiaste en la arquitectura, escribirás tu primer manifiesto de ReplicaSet para tienda-web, comprobarás la autorreparación borrando pods a propósito, entenderás cómo el borrado en cascada se apoya en ownerReferences, descubrirás el mecanismo de adopción de pods huérfanos y por qué un selector demasiado amplio es una bomba de relojería, y aprenderás a escalar con kubectl scale. Y terminarás sabiendo por qué, pese a todo esto, casi nunca escribirás un ReplicaSet a mano.
Contenido
- Qué es un controlador
- El controlador de ReplicaSet y el bucle de reconciliación
- Anatomía del manifiesto:
replicas,selectorytemplate - La regla de oro: el
templatedebe casar con elselector - Autorreparación en directo con
tienda-web ownerReferencesy borrado en cascada- Adopción de pods huérfanos y el peligro de los selectores amplios
- Escalado manual con
kubectl scale - ReplicaSet frente a ReplicationController
- Por qué casi nunca se crea un ReplicaSet a mano
- Qué es un controlador
En Arquitectura de Kubernetes definimos el bucle de reconciliación como el corazón del sistema. Un controlador es simplemente un programa que ejecuta ese bucle para un tipo de objeto:
flowchart LR
A["Observa<br/>el estado deseado<br/>(spec)"] --> B["Observa<br/>el estado real<br/>(mundo)"]
B --> C{"¿Coinciden?"}
C -->|Sí| D["No hace nada.<br/>Actualiza status"]
C -->|No| E["Actúa para<br/>acercar el real<br/>al deseado"]
E --> A
D --> A
Características que comparten todos los controladores de Kubernetes y que conviene interiorizar:
- No terminan nunca. Son bucles infinitos, no scripts de un solo disparo.
- Son idempotentes. Ejecutar el bucle mil veces con el estado ya correcto no cambia nada.
- Solo hablan con el kube-apiserver. Nunca contactan directamente con los nodos ni con los contenedores. Piden cambios a la API y el kubelet los materializa.
- Actúan por nivel, no por evento. No reaccionan a "se ha borrado un pod"; comparan constantemente cuántos hay contra cuántos debería haber. Por eso son robustos: si el controlador estuvo caído diez minutos, al volver corrige la diferencia acumulada sin necesidad de reproducir el histórico.
La mayoría vive dentro del kube-controller-manager: el de Deployment, el de ReplicaSet, el de Job, el de Namespace, el de endpoints, el de ServiceAccount... Todos con la misma anatomía y responsabilidades distintas.
- El controlador de ReplicaSet y el bucle de reconciliación
Concretemos el bucle genérico para el caso que nos ocupa. El controlador de ReplicaSet ejecuta, sin parar, para cada ReplicaSet del clúster:
- Lee
spec.replicas: cuántos pods deben existir (estado deseado). - Lee
spec.selector: qué etiquetas identifican a sus pods. - Consulta a la API cuántos pods de ese namespace casan con el selector y no están terminando (estado real).
- Compara:
- Si faltan pods, crea tantos como falten usando
spec.templatecomo molde. - Si sobran, elige víctimas y las borra.
- Si coinciden, no hace nada y actualiza el
status.
- Si faltan pods, crea tantos como falten usando
flowchart TD
RS["ReplicaSet tienda-web<br/>spec.replicas = 3<br/>spec.selector: app=tienda-web"]
RS --> Q{"Pods con app=tienda-web<br/>encontrados: 2"}
Q -->|2 < 3| CREATE["Crear 1 pod<br/>a partir de spec.template"]
CREATE --> API["kube-apiserver<br/>persiste el nuevo pod"]
API --> SCH["kube-scheduler<br/>le asigna nodo"]
SCH --> KUB["kubelet<br/>arranca los contenedores"]
KUB --> Q
Un matiz decisivo, y que explica todo lo que viene después: el ReplicaSet no lleva una lista de los pods que ha creado. No tiene memoria. Cada vuelta del bucle vuelve a preguntar "¿qué pods hay con estas etiquetas?". Su relación con los pods es puramente por etiqueta. Esto tiene dos consecuencias enormes:
- Si borras un pod suyo, en la siguiente vuelta ve que faltan y crea uno nuevo. Autorreparación.
- Si aparece por ahí un pod con esas mismas etiquetas creado por otro, el ReplicaSet lo cuenta como propio. Adopción. Y si con él ya sobran, borrará a alguien.
Cuándo elige a quién borrar cuando sobran: el controlador prioriza eliminar pods Pending sobre Running, los que llevan menos tiempo listos, los que tienen más reinicios y los que están en nodos con más réplicas. Es decir, sacrifica siempre lo menos valioso.
- Anatomía del manifiesto:
replicas, selector y template
replicas, selector y templateVamos a convertir tienda-web en algo que se cuide solo. Crea el fichero en el repositorio del proyecto:
# k8s/base/tienda-web-replicaset.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: tienda-web
namespace: rutas-norte-dev
labels:
app: tienda-web
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
replicas: 3
selector:
matchLabels:
app: tienda-web
entorno: dev
template:
metadata:
labels:
app: tienda-web
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- name: http
containerPort: 80
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "128Mi"Lectura campo a campo, con atención a lo que cambia respecto al pod suelto:
apiVersion: apps/v1. Aquí sí hay grupo. El Pod está en el grupo core (v1), pero ReplicaSet, Deployment, StatefulSet y DaemonSet viven en el grupoapps. Si escribesv1a secas, la API rechazará el manifiesto conno matches for kind "ReplicaSet" in version "v1".metadata(el de arriba): identifica al ReplicaSet, no a los pods. Sus etiquetas sirven para encontrar el propio ReplicaSet conkubectl get rs -l ....spec.replicas: 3: el estado deseado. Es un número, no un rango. El escalado automático por carga llega en el módulo 9.spec.selector.matchLabels: qué pods son míos. Aquí exigimos dos etiquetas: un pod debe tenerapp=tienda-webyentorno=devpara contar. El selector admite también expresiones más ricas (matchExpressions), que veremos en Etiquetas, Selectores y Anotaciones. Es un campo obligatorio e inmutable.spec.template: la plantilla del pod. Es exactamente un manifiesto de pod sinapiVersionnikind, con su propiometadata(etiquetas de los pods hijos) y suspec(contenedores). Es el molde que el controlador usa para fabricar réplicas.
Fíjate en la estructura de muñeca rusa, que es el punto donde más se traba todo el mundo al principio:
ReplicaSet
├── metadata <- identifica al ReplicaSet
└── spec
├── replicas <- cuántos pods
├── selector <- qué pods son míos
└── template
├── metadata <- etiquetas de CADA POD creado
└── spec <- contenedores de CADA POD creadoAntes de aplicarlo, retira el pod suelto de tienda-web para no interferir en el experimento (verás en el apartado 7 exactamente qué pasaría si no lo hiciéramos):
kubectl delete pod tienda-web --ignore-not-found
kubectl apply -f k8s/base/tienda-web-replicaset.yaml
kubectl get rs,podspod "tienda-web" deleted
replicaset.apps/tienda-web created
NAME DESIRED CURRENT READY AGE
replicaset.apps/tienda-web 3 3 3 8s
NAME READY STATUS RESTARTS AGE
pod/api-reservas 1/1 Running 0 51m
pod/tienda-web-4kx7d 1/1 Running 0 8s
pod/tienda-web-9wq2m 1/1 Running 0 8s
pod/tienda-web-pv6cl 1/1 Running 0 8sDos observaciones sobre esa salida:
- Los pods se llaman
tienda-web-<sufijo aleatorio>. El controlador genera el nombre a partir del nombre del ReplicaSet más cinco caracteres aleatorios, porque no puede haber tres objetos con el mismo nombre en un namespace. - Las columnas del ReplicaSet:
DESIREDesspec.replicas;CURRENT, cuántos pods existen;READY, cuántos están listos para recibir tráfico. Cuando las tres coinciden, el bucle está en equilibrio.
- La regla de oro: el
template debe casar con el selector
template debe casar con el selectorEsta es la restricción que más manifiestos rechazados provoca:
Las etiquetas de
spec.template.metadata.labelsdeben satisfacerspec.selector. Si no, la API rechaza el objeto.
La razón es de pura lógica: si el ReplicaSet fabricara pods que su propio selector no reconoce, contaría cero pods propios, crearía tres más, tampoco los reconocería, crearía otros tres… un bucle infinito que llenaría el clúster. Kubernetes lo impide de raíz validando el manifiesto.
Compruébalo provocando el error a propósito:
sed 's/app: tienda-web$/app: tienda-web-mal/' k8s/base/tienda-web-replicaset.yaml \
| kubectl apply --dry-run=server -f -The ReplicaSet "tienda-web" is invalid: spec.template.metadata.labels:
Invalid value: map[string]string{"app":"tienda-web-mal", ...}:
`selector` does not match template `labels`Nota la asimetría, que sí es válida y muy útil: el template puede tener más etiquetas que el selector. En nuestro manifiesto el selector pide app y entorno, pero los pods llevan además app.kubernetes.io/part-of. Eso está permitido; lo prohibido es que falte alguna de las que el selector exige.
Como norma de proyecto: mantén el selector lo más pequeño y estable posible (una o dos etiquetas que identifiquen inequívocamente al componente y el entorno) y añade el resto de metadatos solo en el template.
- Autorreparación en directo con
tienda-web
tienda-webHa llegado el momento de comprobar lo que el módulo 1 nos dejó pendiente. Abre dos terminales.
En la primera, ponte a observar:
En la segunda, mata un pod a traición (usa uno de los nombres reales de tu clúster):
En la primera terminal verás algo así:
NAME READY STATUS RESTARTS AGE
tienda-web-4kx7d 1/1 Running 0 4m
tienda-web-9wq2m 1/1 Running 0 4m
tienda-web-pv6cl 1/1 Running 0 4m
tienda-web-9wq2m 1/1 Terminating 0 4m
tienda-web-t8m4r 0/1 Pending 0 0s
tienda-web-t8m4r 0/1 ContainerCreating 0 0s
tienda-web-9wq2m 0/1 Terminating 0 4m
tienda-web-t8m4r 1/1 Running 0 2sMenos de dos segundos. Y algo muy revelador: el pod sustituto (tienda-web-t8m4r) aparece en Pending antes de que el borrado del anterior haya terminado. El controlador no espera a que el viejo desaparezca del todo; en cuanto la API marca el pod como terminando, deja de contarlo y actúa.
Los eventos del ReplicaSet lo cuentan por escrito:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SuccessfulCreate 4m replicaset-controller Created pod: tienda-web-4kx7d
Normal SuccessfulCreate 4m replicaset-controller Created pod: tienda-web-9wq2m
Normal SuccessfulCreate 4m replicaset-controller Created pod: tienda-web-pv6cl
Normal SuccessfulCreate 9s replicaset-controller Created pod: tienda-web-t8m4rCompara con lo que ocurrió en el módulo 1 al borrar el pod suelto: nada. Silencio. Aquí, en cambio, hay un replicaset-controller firmando cada creación. Esta es, en una línea, la diferencia entre un contenedor y una plataforma. Para Rutas Norte significa que la caída nocturna de cuatro horas y media de marzo se habría resuelto sola en dos segundos, de madrugada, sin que nadie se enterara.
Un experimento aún más contundente: bórralos todos a la vez.
pod "tienda-web-4kx7d" deleted
pod "tienda-web-pv6cl" deleted
pod "tienda-web-t8m4r" deleted
NAME READY STATUS RESTARTS AGE
tienda-web-2jf9x 1/1 Running 0 3s
tienda-web-hs4bd 1/1 Running 0 3s
tienda-web-zq7nm 1/1 Running 0 3sTres pods nuevos, con nombres nuevos e IPs nuevas. Y ahí asoma el siguiente problema del curso: si las IPs cambian cada vez, ¿a qué dirección se conecta tienda-web para hablar con api-reservas? Ese es el trabajo de los Servicios.
ownerReferences y borrado en cascada
ownerReferences y borrado en cascada¿Cómo sabe Kubernetes que esos pods "pertenecen" al ReplicaSet, si la relación es por etiquetas? Porque, al crearlos, el controlador estampa en cada pod una referencia a su dueño:
[
{
"apiVersion": "apps/v1",
"kind": "ReplicaSet",
"name": "tienda-web",
"uid": "6c1f8f0e-2b7a-4a91-9a3d-1d8f2c5e77b1",
"controller": true,
"blockOwnerDeletion": true
}
]Campo a campo:
| Campo | Significado |
|---|---|
kind / name / uid |
Quién es el dueño. El uid importa: si borras el ReplicaSet y creas otro con el mismo nombre, es un objeto distinto |
controller: true |
Este dueño es el controlador del pod. Solo puede haber uno |
blockOwnerDeletion: true |
El dueño no se considera borrado hasta que este hijo desaparezca |
Sobre esas referencias trabaja el garbage collector, otro controlador del kube-controller-manager: recorre los objetos, y cuando encuentra uno cuyo dueño ya no existe, lo borra. Esto es el borrado en cascada, y es la razón de que kubectl delete rs tienda-web se lleve también los tres pods.
Kubernetes ofrece tres políticas de propagación:
| Política | Comportamiento | Cómo se pide |
|---|---|---|
Background (por defecto) |
Borra el dueño de inmediato; el recolector borra los hijos después | kubectl delete rs tienda-web |
Foreground |
Marca el dueño como "en borrado", borra primero los hijos y al final el dueño | --cascade=foreground |
Orphan |
Borra solo el dueño y deja vivos a los hijos, eliminándoles el ownerReferences |
--cascade=orphan |
La tercera es un recurso de emergencia muy útil, y de paso nos prepara el terreno para el apartado siguiente:
replicaset.apps "tienda-web" deleted
NAME READY STATUS RESTARTS AGE
pod/tienda-web-2jf9x 1/1 Running 0 6m
pod/tienda-web-hs4bd 1/1 Running 0 6m
pod/tienda-web-zq7nm 1/1 Running 0 6mEl ReplicaSet ya no existe, pero los tres pods siguen sirviendo tráfico. Ahora son huérfanos: nadie los vigila. Si borras uno, no vuelve.
- Adopción de pods huérfanos y el peligro de los selectores amplios
Tenemos tres pods huérfanos con las etiquetas app=tienda-web, entorno=dev. Vuelve a crear el ReplicaSet:
replicaset.apps/tienda-web created
NAME DESIRED CURRENT READY AGE
replicaset.apps/tienda-web 3 3 3 4s
NAME READY STATUS RESTARTS AGE
pod/tienda-web-2jf9x 1/1 Running 0 8m
pod/tienda-web-hs4bd 1/1 Running 0 8m
pod/tienda-web-zq7nm 1/1 Running 0 8mMira bien las edades: 8 minutos. El ReplicaSet acaba de nacer y no ha creado ni un solo pod. Ha adoptado los tres huérfanos porque casaban con su selector y no tenían dueño. Al adoptarlos, les ha escrito su ownerReferences:
Las reglas exactas de la adopción:
- El pod debe estar en el mismo namespace.
- Sus etiquetas deben casar con el selector.
- No debe tener ya un
ownerReferencesconcontroller: true. Un pod con dueño no se roba: el ReplicaSet lo ignora.
El peligro: selectores demasiado amplios
Aquí está la trampa. Supongamos que alguien, con prisa, define el ReplicaSet de tienda-web con un selector perezoso:
# MAL: selector demasiado amplio
spec:
replicas: 3
selector:
matchLabels:
app.kubernetes.io/part-of: rutas-norte # ¡todos los componentes llevan esta etiqueta!Ese selector casa con todos los pods de la plataforma: tienda-web, api-reservas, redis-cache, el worker... Consecuencias, en cadena:
- El ReplicaSet cuenta todos los pods de Rutas Norte que hay en el namespace. Supongamos 5.
- Su estado deseado son 3. Sobran 2.
- Elige dos víctimas y las borra. Puede perfectamente borrar
api-reservasy el worker. - Los controladores de esos componentes los recrean, el ReplicaSet vuelve a ver que sobran y los borra otra vez. Guerra de controladores.
Es un incidente real y bastante habitual en clústeres jóvenes, y el síntoma —pods que se borran solos sin explicación— es desconcertante hasta que se entiende el mecanismo. Simulémoslo con seguridad para verlo con nuestros propios ojos:
# Un pod suelto que lleva por casualidad las etiquetas del selector
kubectl run intruso --image=nginx:1.27-alpine \
--labels="app=tienda-web,entorno=dev,app.kubernetes.io/part-of=rutas-norte"
sleep 5
kubectl get pods -l app=tienda-webpod/intruso created
NAME READY STATUS RESTARTS AGE
intruso 1/1 Terminating 0 5s
tienda-web-2jf9x 1/1 Running 0 12m
tienda-web-hs4bd 1/1 Running 0 12m
tienda-web-zq7nm 1/1 Running 0 12mEl ReplicaSet ha adoptado a intruso, ha contado 4 donde debía haber 3 y lo ha ejecutado en el acto (era el más joven, la víctima preferente). Si en vez de un nginx de prueba hubiera sido un pod importante, se habría ido igual.
Las reglas del proyecto que se derivan de esto:
- El selector debe identificar al componente de forma inequívoca:
app: <componente>másentorno: <entorno>, nunca solopart-of. - Ningún componente comparte selector con otro.
- Nunca crees pods sueltos con las etiquetas de un componente gobernado. Para depurar, usa etiquetas distintas o pods efímeros con
--rm.
- Escalado manual con
kubectl scale
kubectl scaleCambiar el número de réplicas es cambiar spec.replicas. Hay tres formas.
Imperativa, rápida (para incidencias):
Condicional, muy útil en scripts para evitar pisar cambios de otros:
Si en ese momento no hay exactamente 5 réplicas, el comando falla en lugar de aplicar el cambio.
Declarativa, la correcta según las convenciones del proyecto: edita replicas: 5 en el fichero, haz commit y aplica.
| Forma | Ventaja | Problema |
|---|---|---|
kubectl scale |
Instantáneo, ideal en un incidente | El clúster deja de coincidir con Git; el próximo apply revierte el cambio |
| Editar el manifiesto | Trazable, revisable, reproducible | Más lento |
La regla que ya conoces del módulo 1 sigue vigente: si un cambio no está en Git, no existe. Escala con kubectl scale para apagar un fuego, pero lleva el cambio al fichero inmediatamente después.
Vuelve a 3 antes de seguir:
Y observa qué pasa al reducir:
NAME READY STATUS RESTARTS AGE
tienda-web-2jf9x 1/1 Running 0 16m
tienda-web-hs4bd 1/1 Running 0 16m
tienda-web-k9dpq 1/1 Terminating 0 40s
tienda-web-w2sxv 1/1 Terminating 0 40s
tienda-web-zq7nm 1/1 Running 0 16mLos que se van son los más jóvenes, exactamente como anticipamos: el controlador sacrifica lo menos consolidado.
- ReplicaSet frente a ReplicationController
Verás documentación antigua y respuestas de foros hablando de ReplicationController. Es el antecesor del ReplicaSet, de la época de Kubernetes 1.0, y sigue existiendo por compatibilidad, pero no debes usarlo.
| Aspecto | ReplicationController | ReplicaSet |
|---|---|---|
| Grupo de API | v1 (core) |
apps/v1 |
| Selector | Solo igualdad simple (app: tienda-web) |
Igualdad y conjuntos (matchExpressions: in, notin, exists) |
| Campo del selector | spec.selector como mapa plano |
spec.selector.matchLabels / matchExpressions |
| Lo usa el Deployment | No | Sí |
| Estado | Obsoleto de hecho | Vigente |
La diferencia funcional relevante es el selector de conjuntos. Gracias a matchExpressions, un Deployment puede lanzar consultas como "pods de api-reservas cuyo pod-template-hash sea uno de estos dos", que es justo lo que necesita para gestionar dos versiones simultáneas durante una actualización progresiva. Con el selector plano del ReplicationController eso no era expresable, y por eso los Deployments se construyeron sobre ReplicaSets.
Resumen operativo: si ves kind: ReplicationController, es código heredado. Migra a Deployment.
- Por qué casi nunca se crea un ReplicaSet a mano
Y aquí llega la sorpresa de la lección: después de todo esto, en tu vida profesional escribirás muy pocos ReplicaSets. Lo que escribirás son Deployments.
La razón es que al ReplicaSet le falta lo esencial para operar un servicio: no sabe cambiar de versión. Es magnífico manteniendo N copias de una plantilla, pero si modificas la imagen de su template:
kubectl set image rs/tienda-web nginx=nginx:1.27.1-alpine
kubectl get pods -l app=tienda-web -o custom-columns=NOMBRE:.metadata.name,IMAGEN:.spec.containers[0].imagereplicaset.apps/tienda-web image updated
NOMBRE IMAGEN
tienda-web-2jf9x nginx:1.27-alpine
tienda-web-hs4bd nginx:1.27-alpine
tienda-web-zq7nm nginx:1.27-alpineLos pods siguen con la imagen vieja. El ReplicaSet solo usa el template cuando crea un pod; los existentes no se tocan porque el estado deseado ("tres pods con estas etiquetas") sigue satisfecho. Para desplegar la nueva versión tendrías que borrarlos a mano, uno a uno, esperando entre medias. Sin control de ritmo, sin verificación, sin marcha atrás.
Eso es precisamente lo que aporta el Deployment: gestiona ReplicaSets, no pods. Cuando cambias la imagen, crea un ReplicaSet nuevo con la versión nueva y va trasvasando réplicas de uno a otro de forma controlada.
| Necesidad | ReplicaSet | Deployment |
|---|---|---|
| Mantener N réplicas vivas | Sí | Sí (a través de un ReplicaSet) |
| Escalar | Sí | Sí |
| Actualizar la imagen sin corte | No | Sí |
| Volver a la versión anterior | No | Sí (rollout undo) |
| Historial de revisiones | No | Sí |
| Pausar un despliegue a medias | No | Sí |
Por eso la regla del proyecto es:
En Rutas Norte no se crean ReplicaSets a mano. Se crean Deployments, y ellos gestionan sus ReplicaSets.
Entonces, ¿para qué sirve esta lección? Para entender el nivel intermedio. Cuando en la lección siguiente veas dos ReplicaSets del mismo Deployment conviviendo durante una actualización, o cuando en producción tengas que averiguar por qué hay pods de dos versiones a la vez, la explicación estará en lo que acabas de aprender. Los ReplicaSets no desaparecen: se vuelven invisibles porque los gestiona otro.
Deja el clúster listo para la lección siguiente:
replicaset.apps "tienda-web" deleted
pod "api-reservas" deleted
No resources found in rutas-norte-dev namespace.Errores Comunes y Consejos
- Usar
apiVersion: v1para un ReplicaSet. Está enapps/v1. El error esno matches for kind "ReplicaSet" in version "v1". - Etiquetas del
templateque no casan con elselector. La API rechaza el objeto conselector does not match template labels. Revisa que estás mirandospec.template.metadata.labelsy no elmetadata.labelsde arriba: son sitios distintos. - Selectores demasiado amplios. Es el error más destructivo de esta lección: un ReplicaSet puede adoptar y borrar pods de otro componente. Selector = componente + entorno, siempre.
- Crear pods sueltos con las etiquetas de un componente gobernado. Serán adoptados y, muy probablemente, ejecutados en segundos.
- Esperar que cambiar el
templateactualice los pods existentes. No lo hace. Solo afecta a los pods que se creen a partir de ese momento. Esa es la carencia que resuelve el Deployment. - Intentar cambiar el
selectorde un ReplicaSet existente. Es inmutable. Hay que borrar y recrear (o, en la práctica, dejar que el Deployment gestione el cambio creando un ReplicaSet nuevo). - Confundir
CURRENTconREADY.CURRENTcuenta pods que existen;READY, pods que pueden atender tráfico. Un3/3enCURRENTcon0enREADYes un despliegue roto. - Consejo:
kubectl get rs -o wideañade las columnasCONTAINERS,IMAGESySELECTOR. Es la forma más rápida de ver de un vistazo qué versión sirve cada ReplicaSet. - Consejo: cuando algo se comporte de forma inexplicable, mira siempre
kubectl get pod <nombre> -o jsonpath='{.metadata.ownerReferences}'. Saber quién es el dueño de un pod resuelve la mitad de los misterios.
Ejercicios
Ejercicio 1: ReplicaSet de api-reservas y autorreparación
Escribe k8s/base/api-reservas-replicaset.yaml con un ReplicaSet de 2 réplicas de api-reservas en rutas-norte-dev, reutilizando el contenedor de la lección Pods (imagen node:20-alpine con el servidor mínimo) y respetando las tres etiquetas del proyecto. El selector debe usar app y entorno.
- Aplícalo y comprueba que hay 2 pods.
- Borra uno y mide cuánto tarda en aparecer el sustituto.
- Averigua, con un solo comando, quién es el dueño del pod nuevo.
- Muestra los eventos del ReplicaSet que documentan la creación.
Ejercicio 2: Huérfanos y adopción
Partiendo del ReplicaSet del ejercicio anterior:
- Bórralo con la política que deja vivos a los pods y comprueba que siguen ahí.
- Verifica que los pods ya no tienen dueño.
- Vuelve a aplicar el mismo manifiesto y demuestra, mirando la edad de los pods, que no se han creado pods nuevos.
- Explica en tres líneas por qué esto es una ventaja operativa real y qué habría pasado si el ReplicaSet nuevo hubiera pedido 1 réplica en lugar de 2.
Ejercicio 3: Investigar un selector peligroso
Un compañero ha desplegado en rutas-norte-dev este manifiesto y desde entonces "los pods se borran solos":
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: monitor-plataforma
namespace: rutas-norte-dev
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/part-of: rutas-norte
template:
metadata:
labels:
app.kubernetes.io/part-of: rutas-norte
spec:
containers:
- name: monitor
image: busybox:1.36
command: ["sh", "-c", "while true; do sleep 30; done"]- Explica exactamente qué está ocurriendo y por qué.
- Predice qué pasaría si estuvieran corriendo los 2 pods de
api-reservasdel ejercicio 1 cuando se aplica. - Propón el manifiesto corregido.
- Indica qué comando usarías, en un clúster real, para identificar al culpable de un borrado inesperado.
Soluciones
Solución 1
# k8s/base/api-reservas-replicaset.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: api-reservas
namespace: rutas-norte-dev
labels:
app: api-reservas
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
replicas: 2
selector:
matchLabels:
app: api-reservas
entorno: dev
template:
metadata:
labels:
app: api-reservas
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
containers:
- name: api
image: node:20-alpine
command: ["node", "-e"]
args:
- |
const http = require('http');
http.createServer((req, res) => {
res.writeHead(200, {'Content-Type': 'application/json'});
res.end(JSON.stringify({servicio: 'api-reservas', pod: process.env.HOSTNAME}));
}).listen(3000, () => console.log('api-reservas escuchando en 3000'));
ports:
- name: http
containerPort: 3000
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"kubectl apply -f k8s/base/api-reservas-replicaset.yaml
kubectl get rs api-reservas
kubectl get pods -l app=api-reservasreplicaset.apps/api-reservas created
NAME DESIRED CURRENT READY AGE
api-reservas 2 2 2 12s
NAME READY STATUS RESTARTS AGE
api-reservas-c8n4t 1/1 Running 0 12s
api-reservas-xq7dz 1/1 Running 0 12spod "api-reservas-c8n4t" deleted
NAME READY STATUS RESTARTS AGE
api-reservas-m3kp9 1/1 Running 0 2s
api-reservas-xq7dz 1/1 Running 0 1mEl sustituto aparece en 1-2 segundos.
kubectl get pod api-reservas-m3kp9 \
-o jsonpath='{.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}{"\n"}'
kubectl describe rs api-reservas | tail -5ReplicaSet/api-reservas
Events:
Type Reason Age From Message
Normal SuccessfulCreate 1m replicaset-controller Created pod: api-reservas-c8n4t
Normal SuccessfulCreate 1m replicaset-controller Created pod: api-reservas-xq7dz
Normal SuccessfulCreate 8s replicaset-controller Created pod: api-reservas-m3kp9Solución 2
# 1. Borrado dejando huerfanos
kubectl delete rs api-reservas --cascade=orphan
kubectl get rs,pods -l app=api-reservasreplicaset.apps "api-reservas" deleted
NAME READY STATUS RESTARTS AGE
pod/api-reservas-m3kp9 1/1 Running 0 4m
pod/api-reservas-xq7dz 1/1 Running 0 5mLa salida vacía confirma que ya no tienen ownerReferences.
# 3. Readopcion
kubectl apply -f k8s/base/api-reservas-replicaset.yaml
kubectl get rs,pods -l app=api-reservasreplicaset.apps/api-reservas created
NAME DESIRED CURRENT READY AGE
replicaset.apps/api-reservas 2 2 2 3s
NAME READY STATUS RESTARTS AGE
pod/api-reservas-m3kp9 1/1 Running 0 5m
pod/api-reservas-xq7dz 1/1 Running 0 6mLos pods tienen 5 y 6 minutos, mientras que el ReplicaSet tiene 3 segundos: no se ha creado nada nuevo, se han adoptado los existentes.
- La ventaja operativa es que permite sustituir el controlador sin tocar el servicio: si necesitas recrear el ReplicaSet (por ejemplo, porque hay que cambiar su selector, que es inmutable), lo borras con
--cascade=orphan, aplicas el nuevo y los pods siguen atendiendo peticiones sin un solo segundo de corte. Si el ReplicaSet nuevo hubiera pedido 1 réplica, habría adoptado los dos huérfanos, habría contado 2 donde debía haber 1 y habría borrado uno de inmediato, eligiendo al más joven.
Solución 3
-
El selector
app.kubernetes.io/part-of: rutas-nortecasa con todos los pods de la plataforma, porque esa etiqueta es común a los seis componentes por convención del proyecto. El ReplicaSetmonitor-plataformaadopta cuanto encuentra en el namespace, compara con sureplicas: 1y borra todo lo que sobre. Como los otros controladores recrean sus pods, se entra en un ciclo de creación y borrado continuo: la "guerra de controladores". -
Con los 2 pods de
api-reservascorriendo, al aplicar el manifiesto el monitor los adoptaría junto a su propio pod (3 en total), vería que sobran 2 y borraría dos de ellos, muy probablemente los deapi-reservassi son los más jóvenes. El ReplicaSet deapi-reservaslos recrearía, el monitor los volvería a borrar, y la API se llenaría de eventosSuccessfulDeleteySuccessfulCreate. Con toda probabilidad,api-reservasdejaría de dar servicio de forma intermitente. -
Manifiesto corregido: selector propio y exclusivo del componente.
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: monitor-plataforma
namespace: rutas-norte-dev
labels:
app: monitor-plataforma
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
replicas: 1
selector:
matchLabels:
app: monitor-plataforma # identifica SOLO a este componente
entorno: dev
template:
metadata:
labels:
app: monitor-plataforma
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
containers:
- name: monitor
image: busybox:1.36
command: ["sh", "-c", "while true; do sleep 30; done"]- Para identificar al culpable de un borrado inesperado:
# Que controladores han borrado pods recientemente en el namespace
kubectl get events -n rutas-norte-dev --sort-by=.lastTimestamp \
--field-selector reason=SuccessfulDelete
# Quien es el dueno actual de cada pod: revela adopciones indebidas
kubectl get pods -n rutas-norte-dev \
-o custom-columns=POD:.metadata.name,DUENO:.metadata.ownerReferences[0].name
# Que selectores hay declarados y si se solapan
kubectl get rs -n rutas-norte-dev -o wideConclusión
Ya tienes el primer controlador del curso completamente desmontado. Sabes que un controlador es un bucle infinito, idempotente y por nivel que solo habla con el kube-apiserver, y has visto cómo el controlador de ReplicaSet lo concreta: lee spec.replicas, cuenta los pods que casan con spec.selector y crea o borra hasta cuadrar, usando spec.template como molde. Conoces la estructura de muñeca rusa del manifiesto y la regla innegociable de que las etiquetas del template deben satisfacer el selector.
Has comprobado la autorreparación en primera persona: borraste un pod de tienda-web y volvió en dos segundos; los borraste todos y volvieron los tres. Ese es, literalmente, el problema que le costó a Rutas Norte cuatro horas y media de ventas una noche de marzo, resuelto. Entiendes que la relación dueño-hijo se materializa en ownerReferences, que el garbage collector la usa para el borrado en cascada, y que las tres políticas de propagación —Background, Foreground y Orphan— te dan control fino, incluida la maniobra de sustituir un controlador sin cortar el servicio. Has visto la cara amable de la adopción de huérfanos y su cara peligrosa: un selector demasiado amplio convierte a un ReplicaSet en un destructor de pods ajenos. Y sabes escalar con kubectl scale, sin olvidar que el cambio debe acabar siempre en Git.
Pero también has descubierto su límite: cambiar la imagen del template no actualiza los pods existentes. Un ReplicaSet mantiene, no despliega. No sabe cambiar de versión, ni volver atrás, ni llevar un historial. Todo eso lo aporta el nivel superior, que es exactamente la lección siguiente: Deployments. Allí convertiremos por fin el pod suelto de tienda-web en un Deployment de 3 réplicas, crearemos el de api-reservas, y veremos aparecer bajo el capó, gestionado automáticamente, el mismo ReplicaSet que acabamos de aprender a leer.
Curso de Kubernetes
Módulo 1: Introducción a Kubernetes
- ¿Qué es Kubernetes?
- Arquitectura de Kubernetes
- Conceptos y Terminología Clave
- Configuración de un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objetos, Manifiestos YAML y el Modelo Declarativo
- El Proyecto del Curso: la Plataforma Rutas Norte
Módulo 2: Componentes Principales de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualizaciones, Rollbacks y Estrategias de Despliegue
- Servicios
- Namespaces
- Etiquetas, Selectores y Anotaciones
Módulo 3: Gestión de Configuración y Secretos
- ConfigMaps
- Secrets
- Variables de Entorno
- Cuotas y Límites de Recursos
- LimitRanges y Clases de Calidad de Servicio (QoS)
- ServiceAccounts y Acceso a la API desde los Pods
Módulo 4: Redes en Kubernetes
- Redes de Clúster
- Tipos de Servicios
- DNS Interno y Descubrimiento de Servicios
- Controladores de Ingress
- TLS y Gestión de Certificados con cert-manager
- Políticas de Red
Módulo 5: Almacenamiento en Kubernetes
- Volúmenes
- Volúmenes Persistentes
- Reclamaciones de Volúmenes Persistentes
- Clases de Almacenamiento
- Aprovisionamiento Dinámico, Expansión y Snapshots
- Copias de Seguridad y Restauración de Datos
Módulo 6: Conceptos Avanzados de Kubernetes
- StatefulSets
- DaemonSets
- Trabajos y CronJobs
- Init Containers, Sidecars y Patrones Multi-Contenedor
- Planificación: Afinidad, Taints y Tolerations
- Definiciones de Recursos Personalizados (CRDs)
- Operadores y el Patrón Controlador
Módulo 7: Monitoreo y Registro
- Verificaciones de Salud y Sondas
- Servidor de Métricas y kubectl top
- Monitoreo con Prometheus
- Visualización y Alertas con Grafana y Alertmanager
- Registro Centralizado con Elasticsearch, Fluentd y Kibana (EFK)
- Depuración de Aplicaciones y Eventos del Clúster
Módulo 8: Seguridad en Kubernetes
- Control de Acceso Basado en Roles (RBAC)
- Contextos de Seguridad y Endurecimiento del Contenedor
- Políticas de Seguridad de Pods y Pod Security Standards
- Seguridad de Red
- Seguridad de Imágenes
- Auditoría, Escaneo y Gestión de Vulnerabilidades
Módulo 9: Escalado y Rendimiento
- Autoescalado Horizontal de Pods
- Autoescalado Vertical de Pods
- Autoescalado de Clúster
- Escalado por Eventos y Métricas Personalizadas con KEDA
- Alta Disponibilidad: PodDisruptionBudgets y Topología
- Ajuste de Rendimiento
Módulo 10: Ecosistema y Herramientas de Kubernetes
- Minikube y Entornos Locales con kind
- Kubeadm
- Helm
- Kustomize
- GitOps con Argo CD y Flux
- Kubernetes Gestionado: EKS, AKS y GKE
Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real
- Despliegue de una Aplicación Web
- Ejecución de Aplicaciones con Estado
- CI/CD con Kubernetes
- Estrategias de Despliegue: Blue-Green y Canary
- Gestión Multi-Clúster
- Operación en Producción: Incidencias, Runbooks y Costes
