La lliçó anterior va acabar amb un problema sense resoldre: api-reserves i botiga-web funcionen a rutas-norte-dev, però com a pods solts. Si el node cau, si el kubelet els desallotja per manca de memòria o si algú els esborra, desapareixen i ningú no els torna a la vida. Necessitem alguna cosa que vigili permanentment i actuï. Aquest alguna cosa és un controlador, i el primer que coneixerem a fons és el ReplicaSet: l'objecte l'única missió del qual és garantir que en tot moment existeixi un nombre concret de pods que encaixin amb un conjunt d'etiquetes. En aquesta lliçó veuràs què és exactament un controlador i com el de ReplicaSet materialitza el bucle de reconciliació que vas estudiar a l'arquitectura, escriuràs el teu primer manifest de ReplicaSet per a botiga-web, comprovaràs l'autoreparació esborrant pods a propòsit, entendràs com l'esborrat en cascada s'aguanta sobre les ownerReferences, descobriràs el mecanisme d'adopció de pods orfes i per què un selector massa ampli és una bomba de rellotgeria, i aprendràs a escalar amb kubectl scale. I acabaràs sabent per què, malgrat tot això, gairebé mai no escriuràs un ReplicaSet a mà.

Contingut

  1. Què és un controlador
  2. El controlador de ReplicaSet i el bucle de reconciliació
  3. Anatomia del manifest: replicas, selector i template
  4. La regla d'or: el template ha d'encaixar amb el selector
  5. Autoreparació en directe amb botiga-web
  6. ownerReferences i esborrat en cascada
  7. Adopció de pods orfes i el perill dels selectors amplis
  8. Escalat manual amb kubectl scale
  9. ReplicaSet enfront de ReplicationController
  10. Per què gairebé mai no es crea un ReplicaSet a mà

  1. Què és un controlador

A Arquitectura de Kubernetes vam definir el bucle de reconciliació com el cor del sistema. Un controlador és simplement un programa que executa aquest bucle per a un tipus d'objecte:

flowchart LR
    A["Observa<br/>l'estat desitjat<br/>(spec)"] --> B["Observa<br/>l'estat real<br/>(món)"]
    B --> C{"Coincideixen?"}
    C -->|Sí| D["No fa res.<br/>Actualitza status"]
    C -->|No| E["Actua per<br/>acostar el real<br/>al desitjat"]
    E --> A
    D --> A

Característiques que comparteixen tots els controladors de Kubernetes i que convé interioritzar:

  • No acaben mai. Són bucles infinits, no scripts d'un sol tret.
  • Són idempotents. Executar el bucle mil vegades amb l'estat ja correcte no canvia res.
  • Només parlen amb el kube-apiserver. Mai no contacten directament amb els nodes ni amb els contenidors. Demanen canvis a l'API i el kubelet els materialitza.
  • Actuen per nivell, no per esdeveniment. No reaccionen a "s'ha esborrat un pod"; comparen constantment quants n'hi ha contra quants n'hi hauria d'haver. Per això són robustos: si el controlador ha estat caigut deu minuts, en tornar corregeix la diferència acumulada sense necessitat de reproduir l'històric.

La majoria viu dins del kube-controller-manager: el de Deployment, el de ReplicaSet, el de Job, el de Namespace, el d'endpoints, el de ServiceAccount... Tots amb la mateixa anatomia i responsabilitats diferents.

  1. El controlador de ReplicaSet i el bucle de reconciliació

Concretem el bucle genèric per al cas que ens ocupa. El controlador de ReplicaSet executa, sense parar, per a cada ReplicaSet del clúster:

  1. Llegeix spec.replicas: quants pods han d'existir (estat desitjat).
  2. Llegeix spec.selector: quines etiquetes identifiquen els seus pods.
  3. Consulta a l'API quants pods d'aquell namespace encaixen amb el selector i no estan acabant (estat real).
  4. Compara:
    • Si falten pods, en crea tants com en falten fent servir spec.template com a motlle.
    • Si en sobren, tria víctimes i les esborra.
    • Si coincideixen, no fa res i actualitza el status.
flowchart TD
    RS["ReplicaSet botiga-web<br/>spec.replicas = 3<br/>spec.selector: app=botiga-web"]
    RS --> Q{"Pods amb app=botiga-web<br/>trobats: 2"}
    Q -->|2 < 3| CREATE["Crear 1 pod<br/>a partir de spec.template"]
    CREATE --> API["kube-apiserver<br/>persisteix el pod nou"]
    API --> SCH["kube-scheduler<br/>li assigna node"]
    SCH --> KUB["kubelet<br/>arrenca els contenidors"]
    KUB --> Q

Un matís decisiu, i que explica tot el que ve després: el ReplicaSet no porta una llista dels pods que ha creat. No té memòria. Cada volta del bucle torna a preguntar "quins pods hi ha amb aquestes etiquetes?". La seva relació amb els pods és purament per etiqueta. Això té dues conseqüències enormes:

  • Si n'esborres un pod seu, a la volta següent veu que en falten i en crea un de nou. Autoreparació.
  • Si apareix per allà un pod amb aquestes mateixes etiquetes creat per un altre, el ReplicaSet el compta com a propi. Adopció. I si amb ell ja en sobren, n'esborrarà algun.

Quan tria qui esborrar en cas que en sobrin: el controlador prioritza eliminar pods Pending sobre Running, els que porten menys temps llestos, els que tenen més reinicis i els que són en nodes amb més rèpliques. És a dir, sacrifica sempre el menys valuós.

  1. Anatomia del manifest: replicas, selector i template

Convertirem botiga-web en una cosa que es cuidi sola. Crea el fitxer al repositori del projecte:

# k8s/base/botiga-web-replicaset.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: botiga-web
  namespace: rutas-norte-dev
  labels:
    app: botiga-web
    app.kubernetes.io/part-of: rutas-norte
    entorn: dev
spec:
  replicas: 3
  selector:
    matchLabels:
      app: botiga-web
      entorn: dev
  template:
    metadata:
      labels:
        app: botiga-web
        app.kubernetes.io/part-of: rutas-norte
        entorn: 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 camp per camp, amb atenció al que canvia respecte del pod solt:

  • apiVersion: apps/v1. Aquí sí que hi ha grup. El Pod és al grup core (v1), però ReplicaSet, Deployment, StatefulSet i DaemonSet viuen al grup apps. Si escrius v1 a seques, l'API rebutjarà el manifest amb no matches for kind "ReplicaSet" in version "v1".
  • metadata (el de dalt): identifica el ReplicaSet, no els pods. Les seves etiquetes serveixen per trobar el mateix ReplicaSet amb kubectl get rs -l ....
  • spec.replicas: 3: l'estat desitjat. És un número, no un rang. L'escalat automàtic per càrrega arriba al mòdul 9.
  • spec.selector.matchLabels: quins pods són meus. Aquí exigim dues etiquetes: un pod ha de tenir app=botiga-web i entorn=dev per comptar. El selector admet també expressions més riques (matchExpressions), que veurem a Etiquetes, Selectors i Anotacions. És un camp obligatori i immutable.
  • spec.template: la plantilla del pod. És exactament un manifest de pod sense apiVersion ni kind, amb el seu propi metadata (etiquetes dels pods fills) i el seu spec (contenidors). És el motlle que el controlador fa servir per fabricar rèpliques.

Fixa't en l'estructura de nina russa, que és el punt on més s'encalla tothom al principi:

ReplicaSet
├── metadata        <- identifica el ReplicaSet
└── spec
    ├── replicas    <- quants pods
    ├── selector    <- quins pods són meus
    └── template
        ├── metadata  <- etiquetes de CADA POD creat
        └── spec      <- contenidors de CADA POD creat

Abans d'aplicar-lo, retira el pod solt de botiga-web per no interferir en l'experiment (veuràs a l'apartat 7 exactament què passaria si no ho féssim):

kubectl delete pod botiga-web --ignore-not-found
kubectl apply -f k8s/base/botiga-web-replicaset.yaml
kubectl get rs,pods
pod "botiga-web" deleted
replicaset.apps/botiga-web created

NAME                         DESIRED   CURRENT   READY   AGE
replicaset.apps/botiga-web   3         3         3       8s

NAME                       READY   STATUS    RESTARTS   AGE
pod/api-reserves           1/1     Running   0          51m
pod/botiga-web-4kx7d       1/1     Running   0          8s
pod/botiga-web-9wq2m       1/1     Running   0          8s
pod/botiga-web-pv6cl       1/1     Running   0          8s

Dues observacions sobre aquella sortida:

  • Els pods s'anomenen botiga-web-<sufix aleatori>. El controlador genera el nom a partir del nom del ReplicaSet més cinc caràcters aleatoris, perquè no hi pot haver tres objectes amb el mateix nom en un namespace.
  • Les columnes del ReplicaSet: DESIRED és spec.replicas; CURRENT, quants pods existeixen; READY, quants estan llestos per rebre trànsit. Quan les tres coincideixen, el bucle és en equilibri.

  1. La regla d'or: el template ha d'encaixar amb el selector

Aquesta és la restricció que més manifests rebutjats provoca:

Les etiquetes de spec.template.metadata.labels han de satisfer spec.selector. Si no, l'API rebutja l'objecte.

La raó és de pura lògica: si el ReplicaSet fabriqués pods que el seu propi selector no reconeix, comptaria zero pods propis, en crearia tres més, tampoc no els reconeixeria, en crearia tres més… un bucle infinit que ompliria el clúster. Kubernetes ho impedeix d'arrel validant el manifest.

Comprova-ho provocant l'error a propòsit:

sed 's/app: botiga-web$/app: botiga-web-mal/' k8s/base/botiga-web-replicaset.yaml \
  | kubectl apply --dry-run=server -f -
The ReplicaSet "botiga-web" is invalid: spec.template.metadata.labels:
  Invalid value: map[string]string{"app":"botiga-web-mal", ...}:
  `selector` does not match template `labels`

Fixa't en l'asimetria, que sí que és vàlida i molt útil: el template pot tenir més etiquetes que el selector. Al nostre manifest el selector demana app i entorn, però els pods porten a més app.kubernetes.io/part-of. Això està permès; el que és prohibit és que en falti alguna de les que el selector exigeix.

Com a norma de projecte: mantén el selector tan petit i estable com sigui possible (una o dues etiquetes que identifiquin inequívocament el component i l'entorn) i afegeix la resta de metadades només al template.

  1. Autoreparació en directe amb botiga-web

Ha arribat el moment de comprovar el que el mòdul 1 ens va deixar pendent. Obre dues terminals.

A la primera, posa't a observar:

kubectl get pods -l app=botiga-web -w

A la segona, mata un pod a traïció (fes servir un dels noms reals del teu clúster):

kubectl delete pod botiga-web-9wq2m

A la primera terminal veuràs una cosa així:

NAME               READY   STATUS        RESTARTS   AGE
botiga-web-4kx7d   1/1     Running       0          4m
botiga-web-9wq2m   1/1     Running       0          4m
botiga-web-pv6cl   1/1     Running       0          4m
botiga-web-9wq2m   1/1     Terminating   0          4m
botiga-web-t8m4r   0/1     Pending       0          0s
botiga-web-t8m4r   0/1     ContainerCreating 0      0s
botiga-web-9wq2m   0/1     Terminating   0          4m
botiga-web-t8m4r   1/1     Running       0          2s

Menys de dos segons. I una cosa molt reveladora: el pod substitut (botiga-web-t8m4r) apareix en Pending abans que l'esborrat de l'anterior hagi acabat. El controlador no espera que el vell desaparegui del tot; tan bon punt l'API marca el pod com a acabant, deixa de comptar-lo i actua.

Els esdeveniments del ReplicaSet ho expliquen per escrit:

kubectl describe rs botiga-web | tail -6
Events:
  Type    Reason            Age   From                   Message
  ----    ------            ----  ----                   -------
  Normal  SuccessfulCreate  4m    replicaset-controller  Created pod: botiga-web-4kx7d
  Normal  SuccessfulCreate  4m    replicaset-controller  Created pod: botiga-web-9wq2m
  Normal  SuccessfulCreate  4m    replicaset-controller  Created pod: botiga-web-pv6cl
  Normal  SuccessfulCreate  9s    replicaset-controller  Created pod: botiga-web-t8m4r

Compara-ho amb el que va passar al mòdul 1 en esborrar el pod solt: res. Silenci. Aquí, en canvi, hi ha un replicaset-controller signant cada creació. Aquesta és, en una línia, la diferència entre un contenidor i una plataforma. Per a Rutas Norte significa que la caiguda nocturna de quatre hores i mitja del març s'hauria resolt sola en dos segons, de matinada, sense que ningú se n'assabentés.

Un experiment encara més contundent: esborra'ls tots alhora.

kubectl delete pods -l app=botiga-web
kubectl get pods -l app=botiga-web
pod "botiga-web-4kx7d" deleted
pod "botiga-web-pv6cl" deleted
pod "botiga-web-t8m4r" deleted

NAME               READY   STATUS    RESTARTS   AGE
botiga-web-2jf9x   1/1     Running   0          3s
botiga-web-hs4bd   1/1     Running   0          3s
botiga-web-zq7nm   1/1     Running   0          3s

Tres pods nous, amb noms nous i IPs noves. I aquí treu el cap el problema següent del curs: si les IPs canvien cada vegada, a quina adreça es connecta botiga-web per parlar amb api-reserves? Aquesta és la feina dels Serveis.

  1. ownerReferences i esborrat en cascada

Com sap Kubernetes que aquests pods "pertanyen" al ReplicaSet, si la relació és per etiquetes? Perquè, en crear-los, el controlador estampa a cada pod una referència al seu propietari:

kubectl get pod botiga-web-2jf9x -o jsonpath='{.metadata.ownerReferences}' | python3 -m json.tool
[
    {
        "apiVersion": "apps/v1",
        "kind": "ReplicaSet",
        "name": "botiga-web",
        "uid": "6c1f8f0e-2b7a-4a91-9a3d-1d8f2c5e77b1",
        "controller": true,
        "blockOwnerDeletion": true
    }
]

Camp per camp:

Camp Significat
kind / name / uid Qui és el propietari. L'uid importa: si esborres el ReplicaSet i en crees un altre amb el mateix nom, és un objecte diferent
controller: true Aquest propietari és el controlador del pod. Només n'hi pot haver un
blockOwnerDeletion: true El propietari no es considera esborrat fins que aquest fill desaparegui

Sobre aquestes referències treballa el garbage collector, un altre controlador del kube-controller-manager: recorre els objectes, i quan en troba un el propietari del qual ja no existeix, l'esborra. Això és l'esborrat en cascada, i és la raó que kubectl delete rs botiga-web s'endugui també els tres pods.

Kubernetes ofereix tres polítiques de propagació:

Política Comportament Com es demana
Background (per defecte) Esborra el propietari immediatament; el recol·lector esborra els fills després kubectl delete rs botiga-web
Foreground Marca el propietari com a "en esborrat", esborra primer els fills i al final el propietari --cascade=foreground
Orphan Esborra només el propietari i deixa vius els fills, eliminant-los les ownerReferences --cascade=orphan

La tercera és un recurs d'emergència molt útil, i de passada ens prepara el terreny per a l'apartat següent:

kubectl delete rs botiga-web --cascade=orphan
kubectl get rs,pods -l app=botiga-web
replicaset.apps "botiga-web" deleted

NAME                   READY   STATUS    RESTARTS   AGE
pod/botiga-web-2jf9x   1/1     Running   0          6m
pod/botiga-web-hs4bd   1/1     Running   0          6m
pod/botiga-web-zq7nm   1/1     Running   0          6m

El ReplicaSet ja no existeix, però els tres pods continuen servint trànsit. Ara són orfes: ningú no els vigila. Si n'esborres un, no torna.

  1. Adopció de pods orfes i el perill dels selectors amplis

Tenim tres pods orfes amb les etiquetes app=botiga-web, entorn=dev. Torna a crear el ReplicaSet:

kubectl apply -f k8s/base/botiga-web-replicaset.yaml
kubectl get rs,pods -l app=botiga-web
replicaset.apps/botiga-web created

NAME                         DESIRED   CURRENT   READY   AGE
replicaset.apps/botiga-web   3         3         3       4s

NAME                   READY   STATUS    RESTARTS   AGE
pod/botiga-web-2jf9x   1/1     Running   0          8m
pod/botiga-web-hs4bd   1/1     Running   0          8m
pod/botiga-web-zq7nm   1/1     Running   0          8m

Mira bé les edats: 8 minuts. El ReplicaSet acaba de néixer i no ha creat ni un sol pod. Ha adoptat els tres orfes perquè encaixaven amb el seu selector i no tenien propietari. En adoptar-los, els ha escrit les seves ownerReferences:

kubectl get pod botiga-web-2jf9x -o jsonpath='{.metadata.ownerReferences[0].name}{"\n"}'
botiga-web

Les regles exactes de l'adopció:

  1. El pod ha de ser al mateix namespace.
  2. Les seves etiquetes han d'encaixar amb el selector.
  3. No ha de tenir ja unes ownerReferences amb controller: true. Un pod amb propietari no es roba: el ReplicaSet l'ignora.

El perill: selectors massa amplis

Aquí hi ha el parany. Suposem que algú, amb pressa, defineix el ReplicaSet de botiga-web amb un selector mandrós:

# MALAMENT: selector massa ampli
spec:
  replicas: 3
  selector:
    matchLabels:
      app.kubernetes.io/part-of: rutas-norte   # tots els components porten aquesta etiqueta!

Aquest selector encaixa amb tots els pods de la plataforma: botiga-web, api-reserves, redis-cache, el worker... Conseqüències, en cadena:

  1. El ReplicaSet compta tots els pods de Rutas Norte que hi ha al namespace. Suposem-ne 5.
  2. El seu estat desitjat són 3. En sobren 2.
  3. Tria dues víctimes i les esborra. Pot perfectament esborrar api-reserves i el worker.
  4. Els controladors d'aquests components els recreen, el ReplicaSet torna a veure que en sobren i els esborra un altre cop. Guerra de controladors.

És un incident real i força habitual en clústers joves, i el símptoma —pods que s'esborren sols sense explicació— és desconcertant fins que s'entén el mecanisme. Simulem-lo amb seguretat per veure-ho amb els nostres propis ulls:

# Un pod solt que porta per casualitat les etiquetes del selector
kubectl run intrus --image=nginx:1.27-alpine \
  --labels="app=botiga-web,entorn=dev,app.kubernetes.io/part-of=rutas-norte"
sleep 5
kubectl get pods -l app=botiga-web
pod/intrus created

NAME               READY   STATUS        RESTARTS   AGE
intrus             1/1     Terminating   0          5s
botiga-web-2jf9x   1/1     Running       0          12m
botiga-web-hs4bd   1/1     Running       0          12m
botiga-web-zq7nm   1/1     Running       0          12m

El ReplicaSet ha adoptat intrus, n'ha comptat 4 on n'hi havia d'haver 3 i l'ha executat a l'acte (era el més jove, la víctima preferent). Si en lloc d'un nginx de prova hagués estat un pod important, hauria caigut igual.

Les regles del projecte que se'n deriven:

  • El selector ha d'identificar el component de manera inequívoca: app: <component> més entorn: <entorn>, mai només part-of.
  • Cap component no comparteix selector amb un altre.
  • No creïs mai pods solts amb les etiquetes d'un component governat. Per depurar, fes servir etiquetes diferents o pods efímers amb --rm.
kubectl delete pod intrus --ignore-not-found

  1. Escalat manual amb kubectl scale

Canviar el nombre de rèpliques és canviar spec.replicas. Hi ha tres formes.

Imperativa, ràpida (per a incidències):

kubectl scale replicaset botiga-web --replicas=5
kubectl get rs botiga-web
replicaset.apps/botiga-web scaled

NAME         DESIRED   CURRENT   READY   AGE
botiga-web   5         5         5       15m

Condicional, molt útil en scripts per evitar trepitjar canvis d'altri:

kubectl scale replicaset botiga-web --current-replicas=5 --replicas=8

Si en aquell moment no hi ha exactament 5 rèpliques, l'ordre falla en lloc d'aplicar el canvi.

Declarativa, la correcta segons les convencions del projecte: edita replicas: 5 al fitxer, fes commit i aplica.

kubectl apply -f k8s/base/botiga-web-replicaset.yaml
Forma Avantatge Problema
kubectl scale Instantani, ideal en un incident El clúster deixa de coincidir amb Git; el pròxim apply reverteix el canvi
Editar el manifest Traçable, revisable, reproduïble Més lent

La regla que ja coneixes del mòdul 1 continua vigent: si un canvi no és a Git, no existeix. Escala amb kubectl scale per apagar un foc, però porta el canvi al fitxer immediatament després.

Torna a 3 abans de continuar:

kubectl scale replicaset botiga-web --replicas=3

I observa què passa en reduir:

kubectl get pods -l app=botiga-web
NAME               READY   STATUS        RESTARTS   AGE
botiga-web-2jf9x   1/1     Running       0          16m
botiga-web-hs4bd   1/1     Running       0          16m
botiga-web-k9dpq   1/1     Terminating   0          40s
botiga-web-w2sxv   1/1     Terminating   0          40s
botiga-web-zq7nm   1/1     Running       0          16m

Els que se'n van són els més joves, exactament com havíem anticipat: el controlador sacrifica el menys consolidat.

  1. ReplicaSet enfront de ReplicationController

Veuràs documentació antiga i respostes de fòrums parlant de ReplicationController. És l'antecessor del ReplicaSet, de l'època de Kubernetes 1.0, i continua existint per compatibilitat, però no l'has de fer servir.

Aspecte ReplicationController ReplicaSet
Grup d'API v1 (core) apps/v1
Selector Només igualtat simple (app: botiga-web) Igualtat i conjunts (matchExpressions: in, notin, exists)
Camp del selector spec.selector com a mapa pla spec.selector.matchLabels / matchExpressions
El fa servir el Deployment No Sí
Estat Obsolet de fet Vigent

La diferència funcional rellevant és el selector de conjunts. Gràcies a matchExpressions, un Deployment pot llançar consultes com "pods d'api-reserves el pod-template-hash dels quals sigui un d'aquests dos", que és just el que necessita per gestionar dues versions simultànies durant una actualització progressiva. Amb el selector pla del ReplicationController allò no era expressable, i per això els Deployments es van construir sobre ReplicaSets.

Resum operatiu: si veus kind: ReplicationController, és codi heretat. Migra a Deployment.

  1. Per què gairebé mai no es crea un ReplicaSet a mà

I aquí arriba la sorpresa de la lliçó: després de tot això, a la teva vida professional escriuràs molt pocs ReplicaSets. El que escriuràs són Deployments.

La raó és que al ReplicaSet li falta l'essencial per operar un servei: no sap canviar de versió. És magnífic mantenint N còpies d'una plantilla, però si modifiques la imatge del seu template:

kubectl set image rs/botiga-web nginx=nginx:1.27.1-alpine
kubectl get pods -l app=botiga-web -o custom-columns=NOM:.metadata.name,IMATGE:.spec.containers[0].image
replicaset.apps/botiga-web image updated

NOM                IMATGE
botiga-web-2jf9x   nginx:1.27-alpine
botiga-web-hs4bd   nginx:1.27-alpine
botiga-web-zq7nm   nginx:1.27-alpine

Els pods continuen amb la imatge vella. El ReplicaSet només fa servir el template quan crea un pod; els existents no es toquen perquè l'estat desitjat ("tres pods amb aquestes etiquetes") continua satisfet. Per desplegar la versió nova hauries d'esborrar-los a mà, un a un, esperant entremig. Sense control de ritme, sense verificació, sense marxa enrere.

Això és precisament el que aporta el Deployment: gestiona ReplicaSets, no pods. Quan canvies la imatge, crea un ReplicaSet nou amb la versió nova i va traspassant rèpliques d'un a l'altre de forma controlada.

Necessitat ReplicaSet Deployment
Mantenir N rèpliques vives Sí Sí (a través d'un ReplicaSet)
Escalar Sí Sí
Actualitzar la imatge sense tall No Sí
Tornar a la versió anterior No Sí (rollout undo)
Historial de revisions No Sí
Pausar un desplegament a mitges No Sí

Per això la regla del projecte és:

A Rutas Norte no es creen ReplicaSets a mà. Es creen Deployments, i ells gestionen els seus ReplicaSets.

Aleshores, per a què serveix aquesta lliçó? Per entendre el nivell intermedi. Quan a la lliçó següent vegis dos ReplicaSets del mateix Deployment convivint durant una actualització, o quan en producció hagis d'esbrinar per què hi ha pods de dues versions alhora, l'explicació serà en el que acabes d'aprendre. Els ReplicaSets no desapareixen: es tornen invisibles perquè els gestiona un altre.

Deixa el clúster llest per a la lliçó següent:

kubectl delete rs botiga-web
kubectl delete pod api-reserves --ignore-not-found
kubectl get all
replicaset.apps "botiga-web" deleted
pod "api-reserves" deleted

No resources found in rutas-norte-dev namespace.

Errors Comuns i Consells

  • Fer servir apiVersion: v1 per a un ReplicaSet. És a apps/v1. L'error és no matches for kind "ReplicaSet" in version "v1".
  • Etiquetes del template que no encaixen amb el selector. L'API rebutja l'objecte amb selector does not match template labels. Revisa que estàs mirant spec.template.metadata.labels i no el metadata.labels de dalt: són llocs diferents.
  • Selectors massa amplis. És l'error més destructiu d'aquesta lliçó: un ReplicaSet pot adoptar i esborrar pods d'un altre component. Selector = component + entorn, sempre.
  • Crear pods solts amb les etiquetes d'un component governat. Seran adoptats i, molt probablement, executats en segons.
  • Esperar que canviar el template actualitzi els pods existents. No ho fa. Només afecta els pods que es creïn a partir d'aquell moment. Aquesta és la mancança que resol el Deployment.
  • Intentar canviar el selector d'un ReplicaSet existent. És immutable. Cal esborrar i recrear (o, a la pràctica, deixar que el Deployment gestioni el canvi creant un ReplicaSet nou).
  • Confondre CURRENT amb READY. CURRENT compta pods que existeixen; READY, pods que poden atendre trànsit. Un 3/3 a CURRENT amb 0 a READY és un desplegament trencat.
  • Consell: kubectl get rs -o wide afegeix les columnes CONTAINERS, IMAGES i SELECTOR. És la manera més ràpida de veure d'un cop d'ull quina versió serveix cada ReplicaSet.
  • Consell: quan alguna cosa es comporti de forma inexplicable, mira sempre kubectl get pod <nom> -o jsonpath='{.metadata.ownerReferences}'. Saber qui és el propietari d'un pod resol la meitat dels misteris.

Exercicis

Exercici 1: ReplicaSet d'api-reserves i autoreparació

Escriu k8s/base/api-reserves-replicaset.yaml amb un ReplicaSet de 2 rèpliques d'api-reserves a rutas-norte-dev, reaprofitant el contenidor de la lliçó Pods (imatge node:20-alpine amb el servidor mínim) i respectant les tres etiquetes del projecte. El selector ha de fer servir app i entorn.

  1. Aplica'l i comprova que hi ha 2 pods.
  2. Esborra'n un i mesura quant triga a aparèixer el substitut.
  3. Esbrina, amb una sola ordre, qui és el propietari del pod nou.
  4. Mostra els esdeveniments del ReplicaSet que documenten la creació.

Exercici 2: Orfes i adopció

Partint del ReplicaSet de l'exercici anterior:

  1. Esborra'l amb la política que deixa vius els pods i comprova que hi continuen sent.
  2. Verifica que els pods ja no tenen propietari.
  3. Torna a aplicar el mateix manifest i demostra, mirant l'edat dels pods, que no s'han creat pods nous.
  4. Explica en tres línies per què això és un avantatge operatiu real i què hauria passat si el ReplicaSet nou hagués demanat 1 rèplica en lloc de 2.

Exercici 3: Investigar un selector perillós

Un company ha desplegat a rutas-norte-dev aquest manifest i des d'aleshores "els pods s'esborren sols":

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"]
  1. Explica exactament què està passant i per què.
  2. Prediu què passaria si estiguessin funcionant els 2 pods d'api-reserves de l'exercici 1 quan s'aplica.
  3. Proposa el manifest corregit.
  4. Indica quina ordre faries servir, en un clúster real, per identificar el culpable d'un esborrat inesperat.

Solucions

Solució 1

# k8s/base/api-reserves-replicaset.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: api-reserves
  namespace: rutas-norte-dev
  labels:
    app: api-reserves
    app.kubernetes.io/part-of: rutas-norte
    entorn: dev
spec:
  replicas: 2
  selector:
    matchLabels:
      app: api-reserves
      entorn: dev
  template:
    metadata:
      labels:
        app: api-reserves
        app.kubernetes.io/part-of: rutas-norte
        entorn: 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({servei: 'api-reserves', pod: process.env.HOSTNAME}));
              }).listen(3000, () => console.log('api-reserves escoltant al 3000'));
          ports:
            - name: http
              containerPort: 3000
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "500m"
              memory: "256Mi"
kubectl apply -f k8s/base/api-reserves-replicaset.yaml
kubectl get rs api-reserves
kubectl get pods -l app=api-reserves
replicaset.apps/api-reserves created

NAME           DESIRED   CURRENT   READY   AGE
api-reserves   2         2         2       12s

NAME                 READY   STATUS    RESTARTS   AGE
api-reserves-c8n4t   1/1     Running   0          12s
api-reserves-xq7dz   1/1     Running   0          12s
kubectl delete pod api-reserves-c8n4t && kubectl get pods -l app=api-reserves
pod "api-reserves-c8n4t" deleted

NAME                 READY   STATUS    RESTARTS   AGE
api-reserves-m3kp9   1/1     Running   0          2s
api-reserves-xq7dz   1/1     Running   0          1m

El substitut apareix en 1-2 segons.

kubectl get pod api-reserves-m3kp9 \
  -o jsonpath='{.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}{"\n"}'
kubectl describe rs api-reserves | tail -5
ReplicaSet/api-reserves

Events:
  Type    Reason            Age   From                   Message
  Normal  SuccessfulCreate  1m    replicaset-controller  Created pod: api-reserves-c8n4t
  Normal  SuccessfulCreate  1m    replicaset-controller  Created pod: api-reserves-xq7dz
  Normal  SuccessfulCreate  8s    replicaset-controller  Created pod: api-reserves-m3kp9

Solució 2

# 1. Esborrat deixant orfes
kubectl delete rs api-reserves --cascade=orphan
kubectl get rs,pods -l app=api-reserves
replicaset.apps "api-reserves" deleted

NAME                     READY   STATUS    RESTARTS   AGE
pod/api-reserves-m3kp9   1/1     Running   0          4m
pod/api-reserves-xq7dz   1/1     Running   0          5m
# 2. Sense propietari
kubectl get pod api-reserves-m3kp9 -o jsonpath='{.metadata.ownerReferences}{"\n"}'

La sortida buida confirma que ja no tenen ownerReferences.

# 3. Readopcio
kubectl apply -f k8s/base/api-reserves-replicaset.yaml
kubectl get rs,pods -l app=api-reserves
replicaset.apps/api-reserves created

NAME                           DESIRED   CURRENT   READY   AGE
replicaset.apps/api-reserves   2         2         2       3s

NAME                     READY   STATUS    RESTARTS   AGE
pod/api-reserves-m3kp9   1/1     Running   0          5m
pod/api-reserves-xq7dz   1/1     Running   0          6m

Els pods tenen 5 i 6 minuts, mentre que el ReplicaSet en té 3 segons: no s'ha creat res de nou, s'han adoptat els existents.

  1. L'avantatge operatiu és que permet substituir el controlador sense tocar el servei: si necessites recrear el ReplicaSet (per exemple, perquè cal canviar-ne el selector, que és immutable), l'esborres amb --cascade=orphan, apliques el nou i els pods continuen atenent peticions sense un sol segon de tall. Si el ReplicaSet nou hagués demanat 1 rèplica, hauria adoptat els dos orfes, n'hauria comptat 2 on n'hi havia d'haver 1 i n'hauria esborrat un immediatament, triant el més jove.

Solució 3

  1. El selector app.kubernetes.io/part-of: rutas-norte encaixa amb tots els pods de la plataforma, perquè aquesta etiqueta és comuna als sis components per convenció del projecte. El ReplicaSet monitor-plataforma adopta tot el que troba al namespace, ho compara amb el seu replicas: 1 i esborra tot el que sobra. Com que els altres controladors recreen els seus pods, s'entra en un cicle de creació i esborrat continu: la "guerra de controladors".

  2. Amb els 2 pods d'api-reserves funcionant, en aplicar el manifest el monitor els adoptaria juntament amb el seu propi pod (3 en total), veuria que en sobren 2 i n'esborraria dos, molt probablement els d'api-reserves si són els més joves. El ReplicaSet d'api-reserves els recrearia, el monitor els tornaria a esborrar, i l'API s'ompliria d'esdeveniments SuccessfulDelete i SuccessfulCreate. Amb tota probabilitat, api-reserves deixaria de donar servei de forma intermitent.

  3. Manifest corregit: selector propi i exclusiu del component.

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: monitor-plataforma
  namespace: rutas-norte-dev
  labels:
    app: monitor-plataforma
    app.kubernetes.io/part-of: rutas-norte
    entorn: dev
spec:
  replicas: 1
  selector:
    matchLabels:
      app: monitor-plataforma      # identifica NOMES aquest component
      entorn: dev
  template:
    metadata:
      labels:
        app: monitor-plataforma
        app.kubernetes.io/part-of: rutas-norte
        entorn: dev
    spec:
      containers:
        - name: monitor
          image: busybox:1.36
          command: ["sh", "-c", "while true; do sleep 30; done"]
  1. Per identificar el culpable d'un esborrat inesperat:
# Quins controladors han esborrat pods recentment al namespace
kubectl get events -n rutas-norte-dev --sort-by=.lastTimestamp \
  --field-selector reason=SuccessfulDelete

# Qui es el propietari actual de cada pod: revela adopcions indegudes
kubectl get pods -n rutas-norte-dev \
  -o custom-columns=POD:.metadata.name,PROPIETARI:.metadata.ownerReferences[0].name

# Quins selectors hi ha declarats i si se solapen
kubectl get rs -n rutas-norte-dev -o wide

Conclusió

Ja tens el primer controlador del curs completament desmuntat. Saps que un controlador és un bucle infinit, idempotent i per nivell que només parla amb el kube-apiserver, i has vist com el controlador de ReplicaSet el concreta: llegeix spec.replicas, compta els pods que encaixen amb spec.selector i crea o esborra fins a quadrar, fent servir spec.template com a motlle. Coneixes l'estructura de nina russa del manifest i la regla innegociable que les etiquetes del template han de satisfer el selector.

Has comprovat l'autoreparació en primera persona: vas esborrar un pod de botiga-web i va tornar en dos segons; els vas esborrar tots i van tornar els tres. Aquest és, literalment, el problema que va costar a Rutas Norte quatre hores i mitja de vendes una nit de març, resolt. Entens que la relació propietari-fill es materialitza en les ownerReferences, que el garbage collector la fa servir per a l'esborrat en cascada, i que les tres polítiques de propagació —Background, Foreground i Orphan— et donen control fi, inclosa la maniobra de substituir un controlador sense tallar el servei. Has vist la cara amable de l'adopció d'orfes i la seva cara perillosa: un selector massa ampli converteix un ReplicaSet en un destructor de pods aliens. I saps escalar amb kubectl scale, sense oblidar que el canvi ha d'acabar sempre a Git.

Però també n'has descobert el límit: canviar la imatge del template no actualitza els pods existents. Un ReplicaSet manté, no desplega. No sap canviar de versió, ni tornar enrere, ni portar un historial. Tot això ho aporta el nivell superior, que és exactament la lliçó següent: Deployments. Allà convertirem per fi el pod solt de botiga-web en un Deployment de 3 rèpliques, crearem el d'api-reserves, i veurem aparèixer sota el capó, gestionat automàticament, el mateix ReplicaSet que acabem d'aprendre a llegir.

Curs de Kubernetes

Mòdul 1: Introducció a Kubernetes

Mòdul 2: Components Principals de Kubernetes

Mòdul 3: Gestió de Configuració i Secrets

Mòdul 4: Xarxes a Kubernetes

Mòdul 5: Emmagatzematge a Kubernetes

Mòdul 6: Conceptes Avançats de Kubernetes

Mòdul 7: Monitoratge i Registre

Mòdul 8: Seguretat a Kubernetes

Mòdul 9: Escalat i Rendiment

Mòdul 10: Ecosistema i Eines de Kubernetes

Mòdul 11: Estudis de Cas i Aplicacions del Món Real

Mòdul 12: Preparació per a la Certificació de Kubernetes

© Copyright 2026. Tots els drets reservats