Comencemos hablando sobre Jorge Drexler, tal vez es uno de los más grandes cantautores de nuestra época, uno de los más influyentes de la música en español a nivel mundial en la actualidad, y justamente tiene una canción que va muy acorde con el tema de hoy por una frase: 🎶
Ir y venir, seguir y guiar, dar y tener, entrar y salir de fase.
Esa frase es un pequeño esbozo de lo que se viene ahora, ya no controlaremos nuestros pods por sí solos, recurriremos a “algo” que los controlará y los manejará. Ya entenderemos esto más a detalle en este capítulo y en los siguientes.
Hoy hablaremos de los ReplicaSet.
En el post anterior, si recordamos el post anterior terminamos con el problema de los Pods huérfanos. Ahora los pods no seguirán siendo huérfanos, o así queremos 🛟
¿Qué es un ReplicaSet exactamente? 🐑🐑🐑
Es un objeto aparte del Pod, pero un nivel más arriba. ¿Y por qué más arriba, nos preguntaremos? Porque su trabajo es encargarse de que exista siempre la cantidad de réplicas de un Pod que le pedimos. Está pendiente en todo momento si pedimos 3, él se asegura de que haya 3. Ni 2, ni 4. Bastante fácil y sencillo.

Pero ojo, entendamos esta definición con cuidado, porque aquí está el detalle que más confunde al principio. El ReplicaSet no cuida “sus” Pods, cuida cualquier Pod que coincida con un selector -> Label. Ese selector -> label se define con matchLabels, y es literalmente la única forma que tiene de saber qué Pods le tocan.
Entonces, el ReplicaSet replica a nivel de label, no a nivel de nombre ni de imagen ni de nada más. Por eso los labels que vimos en el post anterior eran tan importantes 🏷️
Si hacemos una analogía rápida, lo podemos pensar como un capitán que cuenta tripulantes a bordo. No le importa quién es cada uno, solo mira quién lleva puesto el uniforme (el label), y si falta uno, manda a buscar otro. Y si alguien se sube al barco con el uniforme puesto, lo cuenta como suyo, quiera o no. Esto último lo vamos a ver en un rato, y no sale tan bonito 😬
Nuestro primer ReplicaSet 📜
Todos los archivos de este post los tienes en el repositorio de GitHub del curso, dentro de la carpeta rs. Si prefieres descargarlos en vez de copiar y pegar, ahí están.
Este es firstRs.yaml:
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: frontend
labels:
app: guestbook
tier: frontend
spec:
# modify replicas according to your case
replicas: 3
selector:
matchLabels:
tier: frontend
template:
metadata:
labels:
tier: frontend
spec:
containers:
- name: php-redis
image: us-docker.pkg.dev/google-samples/containers/gke/gb-frontend:v5
El ejemplo sale directo de la documentación oficial de K8s sobre ReplicaSet. Te la recomiendo muchísimo, además de que es muy buena, debería ser siempre nuestro punto de partida para cualquier cosa 📚
Vamos a desglosarlo paso a paso.
apiVersion: apps/v1
Esto es lo primero que nos hace ruido, porque el Pod usaba v1 y aquí aparece apps/v1. ¿Pero por qué?
Porque cada recurso de K8s pertenece a un grupo de API, no, no todos son iguales, cada uno con su propia versión y su rol dentro de k8. Para comprobarlo podemos pedirle al cluster la lista completa:
❯ kubectl api-resources
NAME SHORTNAMES APIVERSION NAMESPACED KIND
.
.
.
replicasets rs apps/v1 true ReplicaSet
.
.
.
Esa es la fila que nos interesa. Vemos dos cosas: el ReplicaSet vive en apps/v1, y además tiene un nombre corto, rs, así que en la terminal podemos escribir rs en vez de replicasets.
kind y metadata
El kind es ReplicaSet, el recurso de k8. Y metadata ya sabemos para qué sirve, nos identifica, nothing new tbh.
spec
Aquí sí hay tres campos nuevos:
- replicas: cuántos Pods queremos corriendo del servicio. Es el número de réplicas que queremos que estén up.
- selector: probablemente lo más importante de todo el archivo. Debajo tiene el
matchLabels, y eso significa que el ReplicaSet va a tomar propiedad (ownership, ya lo vemos en un momento) de todos los Pods que tengan los labels definidos ahí. Guarda esta idea, porque pronto vamos a ver sus implicaciones. - template: lo que va a crear el ReplicaSet cuando le falten Pods. Es, ni más ni menos, la definición de un Pod metida dentro del ReplicaSet. Un Pod sin nombre, porque el nombre se lo pone K8s.
Fijémonos en un detalle que es fácil de pasar por alto, los labels del template tienen que coincidir con el selector. Si no, el ReplicaSet crearía Pods que él mismo no reconocería como suyos, y K8s directamente rechaza ese manifiesto.

Probemos el ReplicaSet 🚀
Apliquemos el archivo:
❯ kubectl apply -f firstRs.yaml
replicaset.apps/frontend created
Y revisamos que esté corriendo:
❯ kubectl get rs
NAME DESIRED CURRENT READY AGE
frontend 3 3 3 8m4s
Tres deseados, tres actuales, tres listos. ¡Todo perfecto! 🎉
Veamos los Pods. Al principio:
❯ kubectl get pods
NAME READY STATUS RESTARTS AGE
frontend-8v9ws 0/1 ContainerCreating 0 7s
frontend-bjd7q 0/1 ContainerCreating 0 7s
frontend-p96mr 0/1 ContainerCreating 0 7s
Y pasado un rato:
❯ kubectl get pods
NAME READY STATUS RESTARTS AGE
frontend-8v9ws 1/1 Running 0 59s
frontend-bjd7q 1/1 Running 0 59s
frontend-p96mr 1/1 Running 0 59s
Fíjate en los nombres. Los Pods creados por un ReplicaSet llevan el nombre del ReplicaSet seguido de un sufijo aleatorio que les asigna K8s. Ya no somos nosotros quienes los nombramos 👶
Ownership: quién es dueño de quién 🔗
Entremos a uno de esos Pods a ver qué tiene por dentro:
❯ kubectl get pod frontend-8v9ws -o yaml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: "2026-10-08T22:05:39Z"
generateName: frontend-
generation: 1
labels:
tier: frontend
name: frontend-8v9ws
namespace: default
ownerReferences:
- apiVersion: apps/v1
blockOwnerDeletion: true
controller: true
kind: ReplicaSet
name: frontend
uid: be9c45b2-110c-477d-ad08-4e372b028d5a
resourceVersion: "225730"
uid: d25ffbc2-a174-4b65-a0f4-9dcd734af5b6
spec:
containers:
- image: us-docker.pkg.dev/google-samples/containers/gke/gb-frontend:v5
imagePullPolicy: IfNotPresent
name: php-redis
...
La salida es larga, pero lo que nos importa es el campo ownerReferences. Es un atributo que el ReplicaSet le añade al Pod al crearlo, y básicamente es una descripción de quién es su dueño. Dentro hay un uid, el identificador único del ReplicaSet al que pertenece el Pod.
¿Cómo comprobamos que es el mismo? Pidiéndole el YAML al ReplicaSet:
❯ kubectl get rs -o yaml
apiVersion: v1
items:
- apiVersion: apps/v1
kind: ReplicaSet
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"apps/v1","kind":"ReplicaSet", ... }
creationTimestamp: "2026-10-08T22:05:39Z"
generation: 1
labels:
app: guestbook
tier: frontend
name: frontend
namespace: default
resourceVersion: "225745"
uid: be9c45b2-110c-477d-ad08-4e372b028d5a
Mismo uid (be9c45b2...). A eso se le llama ownership, y es la forma en la que un ReplicaSet reclama a un Pod como suyo 🏴☠️, sí, así de simple.

¿De verdad funciona? Matemos un Pod 🔫
Hasta ahora solo hemos visto que se crean. Probemos lo importante, que se mantengan. Borramos uno:
❯ kubectl get pods
NAME READY STATUS RESTARTS AGE
frontend-8v9ws 1/1 Running 0 14m
frontend-bjd7q 1/1 Running 0 14m
frontend-p96mr 1/1 Running 0 14m
❯ kubectl delete pods frontend-8v9ws
pod "frontend-8v9ws" deleted from default namespace
❯ kubectl get pods
NAME READY STATUS RESTARTS AGE
frontend-6srrv 1/1 Running 0 8s
frontend-bjd7q 1/1 Running 0 15m
frontend-p96mr 1/1 Running 0 15m
¿Viste? Borramos frontend-8v9ws y a los segundos ya hay un frontend-6srrv de 8 segundos de edad ocupando su lugar. Ese es el ir y venir de la canción: los Pods van y vienen, pero el número se queda donde lo pedimos 🔁 siempre, de es

Esto es lo que hace un ReplicaSet, velar porque siempre exista el número de réplicas deseado. El Pod que murió no volvió, murió de verdad, lo que apareció es uno nuevo con otro nombre y otra identidad. Quédate con esto, que vuelve a aparecer más abajo.
El peligro de los bare Pods 😈
Si recordamos, al inicio dijimos que el ReplicaSet solo se encarga de que exista una cantidad exacta de Pods que coincidan con los labels del selector. Veamos qué implicaciones tiene eso con un experimento.
Primero borramos el ReplicaSet anterior:
❯ kubectl delete -f firstRs.yaml
Ahora creamos un Pod por nuestra cuenta, a mano, con una imagen de nginx, y le añadimos un label on the fly en la terminal:
❯ kubectl run nginx-pod --image=nginx:1.14.2
pod/nginx-pod created
❯ kubectl label pod nginx-pod app=frontend
pod/nginx-pod labeled
❯ kubectl get pod nginx-pod -o yaml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: "2026-10-08T22:32:32Z"
generation: 1
labels:
app: frontend
run: nginx-pod
El Pod ya tiene el label app: frontend. A un Pod creado así, sin que nadie lo gestione, K8s lo llama bare Pod (Pod desnudo, sin controlador encima 🫣).
Ahora creamos este ReplicaSet, secondRs.yaml:
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: frontend-python-rs
labels:
app: frontend
spec:
replicas: 5 # Number of pods to be created
selector: # Match the labels of the pods to be managed by this ReplicaSet
matchLabels:
app: frontend
template: # The pod template used by this ReplicaSet if it needs to create new pods
metadata:
labels:
app: frontend
spec:
containers:
- name: pythonserverone
image: python:3.12-alpine3.24
command: ['sh', '-c', 'echo cont1 > index.html && python -m http.server 8081']
- name: pythonservertwo
image: python:3.12-alpine3.24
command: ['sh', '-c', 'echo cont2 > index.html && python -m http.server 8082']
Pedimos 5 réplicas, y en las spec definimos un template de 2 contenedores cada uno corriendo su imagen de python (el ejemplo del post pasado). Pensaríamos que el ReplicaSet crearía 5 versiones de este pod, ¿cierto? Veamos qué pasa por nosotros mismos:
❯ kubectl apply -f secondRs.yaml
replicaset.apps/frontend-python-rs created
❯ kubectl get rs
NAME DESIRED CURRENT READY AGE
frontend-python-rs 5 5 5 2m7s
❯ kubectl get pods
NAME READY STATUS RESTARTS AGE
frontend-python-rs-b7bk5 2/2 Running 0 2m12s
frontend-python-rs-cmst6 2/2 Running 0 2m12s
frontend-python-rs-khfmk 2/2 Running 0 2m12s
frontend-python-rs-wx7wk 2/2 Running 0 2m12s
nginx-pod 1/1 Running 0 16m
Interesante, ¿no? Pedimos 5 y el ReplicaSet dice que tiene 5 (CURRENT 5), pero solo creó 4 Pods con nuestro template. Fíjate en el quinto, nginx-pod, que ni siquiera es de Python, es el nginx que creamos a mano.
¿Qué pasó? Que al nacer, el ReplicaSet salió a buscar Pods que ya existieran con el label app: frontend, encontró a nginx-pod, y lo adoptó. Como ya tenía 1 de los 5, solo le faltaba crear 4.
Vamos a comprobarlo con el uid:
❯ kubectl get rs frontend-python-rs -o yaml | grep uid
uid: 6de80a2b-5495-4532-bea3-92fc6fd8bb1a
❯ kubectl get pod nginx-pod -o yaml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: "2026-10-08T22:32:32Z"
generation: 1
labels:
app: frontend
run: nginx-pod
name: nginx-pod
namespace: default
ownerReferences:
- apiVersion: apps/v1
blockOwnerDeletion: true
controller: true
kind: ReplicaSet
name: frontend-python-rs
uid: 6de80a2b-5495-4532-bea3-92fc6fd8bb1a
resourceVersion: "227752"
Mismo uid, el nginx-pod ahora tiene dueño y es el ReplicaSet de Python. Un Pod que nunca fue parte del plan terminó contando como réplica, y ahora tenemos un ReplicaSet de Python con un nginx infiltrado 🕵️

Ves lo peligroso de un bare pod, si ese nginx-pod se muere, el ReplicaSet lo va a reemplazar con un Pod de Python, no con un nginx. Y si borras el ReplicaSet, el nginx-pod se va con él.
Por eso no deberíamos crear Pods por nuestra cuenta cuando hay controladores en juego, o por norma general. Esto es dar y tener de la canción, el ReplicaSet da Pods, pero también se queda con todos los que pueda tener si el label coincide. La lección es que los labels hay que elegirlos con cuidado, y que los Pods los debe gestionar un objeto de nivel superior, no nosotros a mano.
Idempotencia ♻️
Antes de cerrar quiero mostrar dos cosas. La primera es la idempotencia. Si volvemos a aplicar el mismo archivo:
❯ kubectl apply -f secondRs.yaml
replicaset.apps/frontend-python-rs unchanged
unchanged. K8s compara el estado deseado con el actual, ve que son iguales y no hace nada. Eso es ser idempotente: aplicar lo mismo una y otra vez da siempre el mismo resultado. Es una de las razones por las que los manifiestos son tan cómodos, puedes correr apply sin miedo 😌
Cambiar el template no cambia los Pods 🪤
Ahora otro punto muy importante que se debe entender de las limitantes y las verdaderas responsabilidades de un ReplicaSet, y esta se entiende mejor con otro ejemplo.
Este es thirdRs.yaml:
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: third-rs
labels:
type: balancer
spec:
replicas: 3 # Number of pods to be created
selector: # Match the labels of the pods to be managed by this ReplicaSet
matchLabels:
type: balancer
template: # The pod template used by this ReplicaSet if it needs to create new pods
metadata:
labels:
type: balancer
spec:
containers:
- name: nginx-balancer
image: nginx:1.14.2
Lo creamos y verificamos la imagen de uno de sus Pods:
❯ kubectl apply -f thirdRs.yaml
replicaset.apps/third-rs created
❯ kubectl get pods
NAME READY STATUS RESTARTS AGE
third-rs-49qdl 1/1 Running 0 8s
third-rs-ms82p 1/1 Running 0 8s
third-rs-rvw2v 1/1 Running 0 8s
❯ kubectl get pod third-rs-49qdl -o yaml | grep image
- image: nginx:1.14.2
imagePullPolicy: IfNotPresent
image: nginx:1.14.2
imageID: docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d
Ahora imagina que por la razón que sea queremos hacer un downgrade a nginx:1.14.1. Cambiamos la imagen en el YAML (image: nginx:1.14.1, todo lo demás igual) y aplicamos:
❯ kubectl apply -f thirdRs.yaml
replicaset.apps/third-rs configured
configured, el cambio se aplicó. ¿O no? Veamos los Pods:
❯ kubectl get pods
NAME READY STATUS RESTARTS AGE
third-rs-49qdl 1/1 Running 0 3m21s
third-rs-ms82p 1/1 Running 0 3m21s
third-rs-rvw2v 1/1 Running 0 3m21s
❯ kubectl get pod third-rs-49qdl -o yaml | grep image
- image: nginx:1.14.2
imagePullPolicy: IfNotPresent
image: nginx:1.14.2
imageID: docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d
Los mismos Pods, de hace 3 minutos, con la imagen vieja. El ReplicaSet guardó el cambio en su template, pero a los Pods que ya existen no les hace nada, porque lo único que le importa es la cantidad. El template solo se usa cuando tiene que crear un Pod nuevo.
Eso lo comprobamos borrando uno:
❯ kubectl delete pod third-rs-49qdl
pod "third-rs-49qdl" deleted from default namespace
❯ kubectl get pods
NAME READY STATUS RESTARTS AGE
third-rs-694l7 1/1 Running 0 11s
third-rs-ms82p 1/1 Running 0 4m34s
third-rs-rvw2v 1/1 Running 0 4m34s
❯ kubectl get pod third-rs-694l7 -o yaml | grep image
- image: nginx:1.14.1
imagePullPolicy: IfNotPresent
image: nginx:1.14.1
imageID: docker-pullable://nginx@sha256:32fdf92b4e986e109e4db0865758020cb0c3b70d6ba80d02fe87bad5cc3dc228
El Pod nuevo (third-rs-694l7) sí nació con 1.14.1, y sus dos hermanos siguen con 1.14.2. Para que todos tengan la versión nueva tendríamos que ir borrándolos uno por uno (o todos de golpe) y dejar que el ReplicaSet los reponga. Funciona, pero hacerlo a mano en producción es un dolor de cabeza, y tener Pods de dos versiones conviviendo no es precisamente lo que queremos 😵

Por eso es tan importante entender qué hace un ReplicaSet y, sobre todo, qué no hace. Mantiene el número de Pods, y ya. No actualiza, no hace rollouts, no revierte.
Lo que viene 👀
Para eso K8s tiene otro recurso, un nivel todavía más arriba que el ReplicaSet, que se encarga de gestionar los ReplicaSet por nosotros y hacer las actualizaciones de forma ordenada. Se llama Deployment, y es el tema del próximo post.
Esa es la parte de seguir y guiar de la canción: el ReplicaSet sigue a sus Pods, y el Deployment guía al ReplicaSet 🧭
La canción del post
Nsqk saca disco en siete días a partir de esta publicación. Es uno de mis artistas favoritos de México por sus sonidos y por la experimentación con distintos géneros que ha hecho. Su álbum ATP me lo sé de memoria, está muy bueno.
Pero esta vez quiero recomendar un cover, y es de las pocas veces que digo que el cover supera al original. Es su versión de Nunca estoy, del álbum El Madrileño de C. Tangana, que tocó en 2023 en su concierto de Roy en Monterrey. ES UNO DE LOS MEJORES COVERS QUE HE ESCUCHADO EN MI VIDA 🔥
No me has llamado.
Comentarios