Tanquem el mòdul d'emmagatzematge amb una promesa pendent. La plataforma Rutas Norte ja té còpies de seguretat verificades i les reserves sobreviuen a l'esborrat d'un pod, però postgres-reserves continua sent un Deployment d'una sola rèplica amb strategy: Recreate. Aquesta configuració no és un descuit: és l'única que funciona, perquè un Deployment aplica la mateixa plantilla de pod —i per tant el mateix PersistentVolumeClaim— a totes les seves rèpliques. Si pugem a dues, totes dues intenten muntar el mateix volum ReadWriteOnce i la segona es queda esperant eternament.
El problema de fons és que un Deployment tracta els seus pods com a bestiar intercanviable: qualsevol serveix, tant se val el nom, tant se val l'ordre, tant se val quin disc toqui. Per a una base de dades això és exactament el contrari del que necessitem. Una rèplica de PostgreSQL necessita saber qui és, necessita el seu propi disc i necessita arrencar en un ordre concret respecte de les altres.
El StatefulSet és el controlador que aporta aquestes tres garanties. En aquesta lliçó l'estudiarem a fons, migrarem postgres-reserves de Deployment a StatefulSet sense perdre el volum existent, i acabarem amb un advertiment honest que marcarà la resta del mòdul: un StatefulSet dona identitat i disc, però no replica dades per tu.
Contingut
- Què garanteix un StatefulSet que un Deployment no pot donar
- Identitat estable: noms ordinals i DNS per pod
serviceNamei el Service headlessvolumeClaimTemplates: un disc per rèplica- Ordre d'arrencada i aturada:
podManagementPolicy - Estratègies d'actualització:
RollingUpdate,partitioniOnDelete - Migració pràctica de
postgres-reservesa StatefulSet - Escalat i què passa amb els PVC
- El límit honest: identitat no és replicació
- Segon exemple:
redis-cache
- Què garanteix un StatefulSet que un Deployment no pot donar
Un StatefulSet és, com el Deployment, un controlador que manté un nombre desitjat de pods a partir d'una plantilla. La diferència és què promet sobre aquests pods.
| Aspecte | Deployment | StatefulSet |
|---|---|---|
| Nom dels pods | Aleatori: api-reserves-7d9f8-x2k4p |
Ordinal i fix: postgres-reserves-0 |
| Identitat després d'un reinici | Es perd (nom nou) | Es conserva (mateix nom i mateix disc) |
| Emmagatzematge | Un de compartit per tota la plantilla | Un de propi per rèplica, creat automàticament |
| Nom DNS individual | No | Sí, amb Service headless |
| Ordre de creació | Tots alhora | -0, després -1, després -2 (per defecte) |
| Ordre d'esborrat | Arbitrari | Ordinal descendent: -2, -1, -0 |
| Escalat a zero i tornada | Pods completament nous | Mateixos noms, mateixos discos |
| Cas d'ús típic | Processos sense estat | Bases de dades, cues, sistemes de consens |
Les tres garanties, dites amb precisió:
- Identitat de xarxa estable. Cada pod rep un nom ordinal derivat del nom del StatefulSet i un hostname que hi coincideix. Aquest nom no canvia encara que el pod es reiniciï, es recreï o acabi en un altre node.
- Emmagatzematge estable per rèplica. Cada pod obté el seu propi PVC generat a partir d'una plantilla. El pod
postgres-reserves-0sempre munta el volum depostgres-reserves-0, mai el de-1. - Desplegament i escalat ordenats. Els pods es creen i s'eliminen en seqüència, esperant que cadascun estigui llest abans de continuar (si així es configura).
Un advertiment previ que estalvia disgustos: fer servir un StatefulSet no fa que una aplicació sigui distribuïda. Si la teva aplicació no sap què fer amb tres còpies d'ella mateixa, tres rèpliques en un StatefulSet només et donen tres instàncies aïllades que es trepitgen. Hi tornarem a l'apartat 9.
- Identitat estable: noms ordinals i DNS per pod
Un StatefulSet anomenat postgres-reserves amb replicas: 3 produeix exactament aquests pods:
NAME READY STATUS RESTARTS AGE
postgres-reserves-0 1/1 Running 0 8m
postgres-reserves-1 1/1 Running 0 7m
postgres-reserves-2 1/1 Running 0 6mEl sufix és un ordinal que comença a zero i és contigu. No hi ha salts: si existeix -2, existeixen -0 i -1. Si esborres a mà postgres-reserves-1, el controlador el recrea amb exactament el mateix nom i li torna a muntar exactament el mateix PVC. Des del punt de vista de l'aplicació, el pod "s'ha reiniciat", no "ha estat substituït per un altre".
Aquest ordinal es reflecteix en tres llocs que l'aplicació pot consultar:
- El nom del pod (
metadata.name), accessible per Downward API tal com vam veure al mòdul 3. - El hostname dins del contenidor, que coincideix amb el nom del pod.
- El subdomini que forma el registre DNS, si hi ha Service headless.
# Comprovar el hostname des de dins del pod
kubectl exec -n rutas-norte-dev postgres-reserves-1 -- hostnameAixò és el que permet que un script d'arrencada decideixi el seu propi paper:
# Fragment típic d'un entrypoint de base de dades replicada
ORDINAL="${HOSTNAME##*-}" # de "postgres-reserves-1" extreu "1"
if [ "$ORDINAL" = "0" ]; then
echo "Arrenco com a primari"
else
echo "Arrenco com a rèplica de postgres-reserves-0"
fiAquest patró —deduir el rol de l'ordinal— és la base de moltes imatges de bases de dades preparades per a Kubernetes. També és, com veurem, fràgil: si el pod -0 mor i un altre ha de prendre el relleu, l'ordinal ja no diu la veritat.
serviceName i el Service headless
serviceName i el Service headlessEl camp serviceName és obligatori en un StatefulSet i apunta al nom d'un Service headless, aquell que a la lliçó 04-02 vam definir amb clusterIP: None i del qual vam dir que el seu ús real arribaria aquí.
Recordem per què. Un Service normal té una IP virtual i reparteix el trànsit entre tots els pods que casen amb el seu selector. Això és justament el que no volem per a una base de dades replicada: si escric a "la IP del servei", no sé si estic escrivint al primari o a una rèplica de només lectura.
Un Service headless no crea IP virtual ni regles de balanceig. En el seu lloc, CoreDNS publica un registre per a cada pod:
# k8s/base/postgres-reserves-headless-service.yaml
apiVersion: v1
kind: Service
metadata:
name: postgres-reserves-nodes
namespace: rutas-norte-dev
labels:
app: postgres-reserves
app.kubernetes.io/part-of: rutas-norte
entorn: dev
spec:
clusterIP: None # <- això el converteix en headless
selector:
app: postgres-reserves # només app i entorn entren als selectors
entorn: dev
ports:
- name: postgresql
port: 5432
targetPort: 5432Amb aquest Service, cada pod del StatefulSet obté un FQDN propi amb la forma:
És a dir:
postgres-reserves-0.postgres-reserves-nodes.rutas-norte-dev.svc.cluster.local
postgres-reserves-1.postgres-reserves-nodes.rutas-norte-dev.svc.cluster.local
postgres-reserves-2.postgres-reserves-nodes.rutas-norte-dev.svc.cluster.localCadascun resol a la IP concreta d'aquest pod, no a una IP virtual. Ho podem comprovar:
kubectl run -n rutas-norte-dev depurador --rm -it --restart=Never \
--image=busybox:1.36 -- \
nslookup postgres-reserves-1.postgres-reserves-nodes.rutas-norte-dev.svc.cluster.localServer: 10.96.0.10
Address: 10.96.0.10:53
Name: postgres-reserves-1.postgres-reserves-nodes.rutas-norte-dev.svc.cluster.local
Address: 10.244.1.37A més, consultar el nom del Service headless retorna totes les IP dels pods llestos, cosa que serveix per descobrir la llista de membres:
kubectl run -n rutas-norte-dev depurador --rm -it --restart=Never \
--image=busybox:1.36 -- \
nslookup postgres-reserves-nodes.rutas-norte-dev.svc.cluster.localName: postgres-reserves-nodes.rutas-norte-dev.svc.cluster.local
Address: 10.244.1.37
Address: 10.244.2.19
Address: 10.244.0.28A la pràctica es fan servir dos Services sobre el mateix StatefulSet:
| Service | Tipus | Per a què |
|---|---|---|
postgres-reserves-nodes |
Headless (clusterIP: None) |
Identitat DNS per pod; obligatori per al StatefulSet |
postgres-reserves |
ClusterIP normal | Punt d'entrada estable per a api-reserves i worker-notificacions |
El ClusterIP normal és el que ja coneixen els altres components des del mòdul 4; no cal tocar la configuració d'api-reserves.
graph LR A[api-reserves] -->|postgres-reserves:5432| S[Service ClusterIP] S --> P0[postgres-reserves-0] H[Service headless<br/>postgres-reserves-nodes] -.DNS per pod.-> P0 H -.DNS per pod.-> P1[postgres-reserves-1] H -.DNS per pod.-> P2[postgres-reserves-2] P0 --- V0[(PVC dades-...-0)] P1 --- V1[(PVC dades-...-1)] P2 --- V2[(PVC dades-...-2)]
volumeClaimTemplates: un disc per rèplica
volumeClaimTemplates: un disc per rèplicaAquí hi ha la peça que trenca el sostre que vam detectar a 05-03. En lloc de referenciar un PVC ja existent, el StatefulSet declara una plantilla de PVC, i el controlador en crea un per a cada rèplica.
volumeClaimTemplates:
- metadata:
name: dades
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: rutasnorte-rapida
resources:
requests:
storage: 20GiEl nom del PVC resultant segueix una regla fixa:
Amb la plantilla anomenada dades i el StatefulSet postgres-reserves:
NAME STATUS VOLUME CAPACITY STORAGECLASS AGE
dades-postgres-reserves-0 Bound pvc-8a1c... 20Gi rutasnorte-rapida 9m
dades-postgres-reserves-1 Bound pvc-3f77... 20Gi rutasnorte-rapida 8m
dades-postgres-reserves-2 Bound pvc-b204... 20Gi rutasnorte-rapida 7mDins de la plantilla de pod, el volum es munta referint-se al nom de la plantilla, no al del PVC concret:
volumeMounts:
- name: dades # coincideix amb volumeClaimTemplates[].metadata.name
mountPath: /var/lib/postgresql/data
subPath: pgdataI ara el fet crucial, el que més disgustos evita i el que més disgustos causa segons com es miri:
Els PVC creats per
volumeClaimTemplatesNO s'esborren automàticament. Ni en reduir rèpliques, ni en esborrar el StatefulSet sencer.
És una decisió de disseny deliberada: les dades són el més valuós i Kubernetes no les llença per tu. Si esborres el StatefulSet postgres-reserves i el tornes a crear amb el mateix nom i la mateixa plantilla, els pods es reconnecten als volums que ja existien. Això converteix l'esborrat i la recreació del StatefulSet en una operació segura, cosa que aprofitarem a la migració.
Des de Kubernetes 1.27 hi ha un camp estable per modular aquest comportament:
spec:
persistentVolumeClaimRetentionPolicy:
whenDeleted: Retain # Retain (per defecte) | Delete
whenScaled: Retain # Retain (per defecte) | Delete| Camp | Valor | Efecte |
|---|---|---|
whenDeleted |
Retain |
En esborrar el StatefulSet, els PVC sobreviuen (per defecte) |
whenDeleted |
Delete |
En esborrar el StatefulSet, s'esborren tots els seus PVC |
whenScaled |
Retain |
En reduir rèpliques, el PVC del pod eliminat sobreviu (per defecte) |
whenScaled |
Delete |
En reduir rèpliques, s'esborra el PVC del pod eliminat |
Per a postgres-reserves deixarem tots dos en Retain, cosa que a més és coherent amb la classe rutasnorte-rapida, la política de reclamació de la qual és Retain. Per a una memòria cau com redis-cache, Delete és defensable.
- Ordre d'arrencada i aturada:
podManagementPolicy
podManagementPolicyPer defecte (podManagementPolicy: OrderedReady), el StatefulSet crea el pod -0, espera que estigui Running i Ready, i només llavors crea el -1. En esborrar o reduir, va a l'inrevés: primer l'ordinal més alt.
Això és exactament el que necessita una base de dades on les rèpliques es connecten al primari: no té sentit arrencar la rèplica abans que el node del qual ha de copiar.
Però té un cost. Si el pod -0 no arriba mai a Ready —imatge mal escrita, sonda mal configurada, disc no disponible—, els altres no es creen mai. Un StatefulSet bloquejat a l'ordinal 0 és una de les incidències més habituals.
| Política | Creació | Esborrat | Quan fer-la servir |
|---|---|---|---|
OrderedReady (per defecte) |
Seqüencial, esperant Ready |
Seqüencial descendent | Bases de dades amb primari/rèplica, sistemes de consens amb arrencada ordenada |
Parallel |
Tots alhora | Tots alhora | Membres simètrics que no depenen els uns dels altres i que només necessiten identitat i disc |
Nota important: podManagementPolicy és immutable. No es pot canviar en un StatefulSet existent; cal esborrar-lo (amb --cascade=orphan si no vols tocar els pods) i recrear-lo.
- Estratègies d'actualització:
RollingUpdate, partition i OnDelete
RollingUpdate, partition i OnDeleteQuan canvies la plantilla de pod —per exemple, puges de postgres:16.4 a postgres:16.6—, el StatefulSet aplica la seva updateStrategy.
RollingUpdate (per defecte)
Actualitza els pods en ordre ordinal invers: primer -2, després -1, per últim -0. Espera que cadascun estigui Ready abans de passar al següent.
L'ordre invers és intencionat: si -0 és el primari, s'actualitza l'últim, minimitzant el temps en què el clúster està sense primari. I com que cada pod conserva el seu disc, el pod actualitzat torna amb les seves dades intactes.
El camp partition
Dins de RollingUpdate hi ha un camp que converteix l'actualització en un desplegament per fases:
La regla és simple: només s'actualitzen els pods l'ordinal dels quals és més gran o igual que partition. Amb partition: 2 i tres rèpliques, només s'actualitza postgres-reserves-2. Els pods -0 i -1 es queden amb la versió antiga encara que hagis canviat la plantilla.
Això permet un alliberament canari manual i controlat:
# 1. Preparar: encara no s'actualitza ningú
kubectl patch statefulset postgres-reserves -n rutas-norte-pre \
-p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":3}}}}'
# 2. Canviar la imatge (encara no passa res, partition == replicas)
kubectl set image statefulset/postgres-reserves -n rutas-norte-pre \
postgres=postgres:16.6
# 3. Alliberar només l'ordinal 2 i observar-lo una estona
kubectl patch statefulset postgres-reserves -n rutas-norte-pre \
-p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":2}}}}'
# 4. Si tot va bé, baixar la partició fins a zero
kubectl patch statefulset postgres-reserves -n rutas-norte-pre \
-p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":0}}}}'Si al pas 3 el pod -2 falla, n'hi ha prou amb tornar a pujar partition i revertir la imatge: els altres dos no s'han tocat mai.
OnDelete
El controlador no actualitza res automàticament. Els pods es recreen amb la plantilla nova només quan tu els esborres a mà. És el màxim control possible i es fa servir quan l'actualització requereix passos externs (migrar l'esquema, promoure manualment un primari, verificar la integritat de les dades entre pod i pod).
| Estratègia | Qui decideix quan | Ordre | Risc |
|---|---|---|---|
RollingUpdate (partition: 0) |
Kubernetes | Ordinal descendent | Toca tots els pods sense supervisió |
RollingUpdate amb partition: N |
Tu, baixant N | Només ordinals ≥ N | Baix; permet canari i marxa enrere |
OnDelete |
Tu, esborrant pods | El que triïs | Oblidar pods sense actualitzar durant mesos |
- Migració pràctica de
postgres-reserves a StatefulSet
postgres-reserves a StatefulSetAquest és l'exercici central de la lliçó: passar d'un Deployment d'una rèplica amb un PVC anomenat postgres-reserves-dades a un StatefulSet, sense perdre les dades.
L'obstacle és de nomenclatura. El StatefulSet exigirà un PVC anomenat dades-postgres-reserves-0, mentre que el que existeix es diu postgres-reserves-dades. Els PVC no es poden reanomenar. Però sí que podem reutilitzar el PersistentVolume subjacent, que és on hi ha les dades de veritat.
Pas 0: còpia de seguretat i finestra d'aturada
Abans de res, i amb el que hem après a 05-06:
kubectl create job -n rutas-norte-pre --from=cronjob/copia-reserves copia-premigracio
kubectl wait --for=condition=complete job/copia-premigracio -n rutas-norte-pre --timeout=600sLa migració implica aturada de servei. postgres-reserves és d'un sol node: no hi ha manera de fer-la en calent.
Pas 1: identificar el PV i protegir-lo
# Assegurar que el PV no s'esborra en desaparèixer el PVC
kubectl patch pv pvc-4c81ae30-9b7f-4a02-8d55-1f2a6c0e7b91 \
-p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'Amb la classe rutasnorte-rapida ja és Retain, però comprovar-ho és gratis i equivocar-se és irreversible.
Pas 2: aturar el Deployment
kubectl scale deployment postgres-reserves -n rutas-norte-pre --replicas=0
kubectl wait --for=delete pod -l app=postgres-reserves -n rutas-norte-pre --timeout=300sPas 3: alliberar el PV i esborrar el PVC antic
kubectl delete pvc postgres-reserves-dades -n rutas-norte-pre
kubectl get pv pvc-4c81ae30-9b7f-4a02-8d55-1f2a6c0e7b91NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM
pvc-4c81... 20Gi RWO Retain Released rutas-norte-pre/postgres-reserves-dadesEl PV queda en Released: conserva les dades però no admet un PVC nou fins que es netegi la referència a l'anterior.
Ara el PV torna a Available.
Pas 4: crear el PVC amb el nom que el StatefulSet espera
# k8s/entorns/pre/postgres-reserves-pvc-migrat.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: dades-postgres-reserves-0 # nom exacte que generarà el StatefulSet
namespace: rutas-norte-pre
labels:
app: postgres-reserves
app.kubernetes.io/part-of: rutas-norte
entorn: pre
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: rutasnorte-rapida
volumeName: pvc-4c81ae30-9b7f-4a02-8d55-1f2a6c0e7b91 # enllaça amb el PV existent
resources:
requests:
storage: 20GivolumeName força l'aparellament amb aquell PV concret, en lloc d'aprovisionar-ne un de nou i buit.
Pas 5: el StatefulSet
# k8s/base/postgres-reserves-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres-reserves
namespace: rutas-norte-pre
labels:
app: postgres-reserves
app.kubernetes.io/part-of: rutas-norte
entorn: pre
spec:
serviceName: postgres-reserves-nodes # obligatori: el Service headless
replicas: 1
podManagementPolicy: OrderedReady
updateStrategy:
type: RollingUpdate
rollingUpdate:
partition: 0
persistentVolumeClaimRetentionPolicy:
whenDeleted: Retain
whenScaled: Retain
selector:
matchLabels:
app: postgres-reserves
entorn: pre
template:
metadata:
labels:
app: postgres-reserves
app.kubernetes.io/part-of: rutas-norte
entorn: pre
spec:
serviceAccountName: postgres-reserves
automountServiceAccountToken: false
terminationGracePeriodSeconds: 60
securityContext:
fsGroup: 999
containers:
- name: postgres
image: postgres:16.4
ports:
- name: postgresql
containerPort: 5432
env:
- name: POSTGRES_DB
value: reserves
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-reserves-credencials
key: usuari
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-reserves-credencials
key: password
resources:
requests:
cpu: "1"
memory: 2Gi
limits:
cpu: "1"
memory: 2Gi
volumeMounts:
- name: dades
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: dades
labels:
app: postgres-reserves
app.kubernetes.io/part-of: rutas-norte
entorn: pre
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: rutasnorte-rapida
resources:
requests:
storage: 20GiPunts que val la pena assenyalar d'aquest manifest:
serviceNameapunta al Service headless, no al ClusterIP normal. És un error freqüent confondre'ls.- Els recursos mantenen la classe
Guaranteedque vam fixar a 03-05:requestsiguals alimits, 1 CPU i 2 GiB. - Es conserven la ServiceAccount dedicada i
automountServiceAccountToken: falsede 03-06. PGDATAapunta a un subdirectori del punt de muntatge. PostgreSQL es nega a inicialitzar sobre un directori que continguilost+found, cosa habitual en volums de blocs formatats.- El selector només fa servir
appientorn, segons la convenció del projecte. I recorda:spec.selectorés immutable també aquí.
Pas 6: aplicar i verificar
kubectl apply -f k8s/entorns/pre/postgres-reserves-pvc-migrat.yaml
kubectl apply -f k8s/base/postgres-reserves-headless-service.yaml
kubectl apply -f k8s/base/postgres-reserves-statefulset.yaml
kubectl rollout status statefulset/postgres-reserves -n rutas-norte-prekubectl exec -n rutas-norte-pre postgres-reserves-0 -- \
psql -U rutasnorte -d reserves -c 'SELECT count(*) FROM reserves;'Les dades continuen allà. Ara ja es pot esborrar el Deployment antic:
- Escalat i què passa amb els PVC
Escalar un StatefulSet és idèntic en sintaxi a escalar un Deployment:
En pujar, el controlador crea -1, espera que estigui Ready, crea -2. Cadascun provoca la creació d'un PVC nou a partir de la plantilla, i amb rutasnorte-rapida i volumeBindingMode: WaitForFirstConsumer el volum s'aprovisiona a la zona on el scheduler hagi col·locat el pod.
En baixar a una rèplica:
kubectl scale statefulset postgres-reserves -n rutas-norte-pre --replicas=1
kubectl get pvc -n rutas-norte-pre -l app=postgres-reservesNAME STATUS VOLUME CAPACITY STORAGECLASS
dades-postgres-reserves-0 Bound pvc-4c81... 20Gi rutasnorte-rapida
dades-postgres-reserves-1 Bound pvc-9d02... 20Gi rutasnorte-rapida
dades-postgres-reserves-2 Bound pvc-71bb... 20Gi rutasnorte-rapidaEls pods -1 i -2 han desaparegut, però els seus PVC continuen allà (amb whenScaled: Retain). Conseqüències pràctiques:
- Avantatge: si tornes a pujar a 3, els pods recuperen els seus discos amb les seves dades. L'escalat és reversible.
- Cost: aquests volums continuen facturant. En un proveïdor cloud, tres discos SSD de 20 GiB costen el mateix estiguin muntats o no.
La neteja, quan n'estiguis segur, és manual i explícita:
Amb la classe rutasnorte-rapida en Retain, el PV passarà a Released i continuarà conservant les dades fins que un administrador l'elimini. Aquesta fricció és deliberada.
- El límit honest: identitat no és replicació
Arribem al punt més important de la lliçó, i el que més es malinterpreta.
Hem escalat postgres-reserves a tres rèpliques. Això no és un clúster de PostgreSQL. Són tres processos de PostgreSQL, cadascun amb el seu disc buit i independent, sense cap relació entre ells. Si api-reserves escrivís contra el Service ClusterIP, les reserves caurien aleatòriament en una de les tres bases de dades i les dades quedarien escampades i incoherents.
El que un StatefulSet aporta i el que no:
| El StatefulSet et dona | El StatefulSet NO et dona |
|---|---|
| Nom estable per rèplica | Elecció de qui és el primari |
| Disc propi per rèplica | Còpia del contingut d'un disc a un altre |
| DNS individual per pod | Commutació per error quan el primari cau |
| Ordre d'arrencada i aturada | Redirecció d'escriptures al primari nou |
| Actualització ordenada i inversa | Coherència de dades entre rèpliques |
| Reconnexió al mateix disc després d'un reinici | Còpies de seguretat ni recuperació a un instant |
La replicació l'ha d'implementar l'aplicació. En PostgreSQL això significa configurar la replicació per streaming, crear l'usuari de replicació, executar pg_basebackup a les rèpliques, mantenir un primary_conninfo correcte i, sobretot, tenir alguna cosa que decideixi i executi la promoció quan el primari mor.
Aquest "alguna cosa" es pot escriure a mà: initContainers que detecten l'ordinal, sondes que comproven el rol, un script que reescriu els Services. És exactament la feina que fan els operadors, i per això la lliçó 06-07 tancarà el mòdul substituint aquest StatefulSet artesanal per un operador de PostgreSQL que sí que sap fer tot això.
Regla pràctica per al teu dia a dia:
Si l'aplicació ja sap funcionar en clúster (Kafka, Cassandra, etcd, Elasticsearch, ZooKeeper), un StatefulSet ben configurat és suficient. Si no ho sap (PostgreSQL, MySQL, MongoDB en mode simple), un StatefulSet et dona la infraestructura però necessites un operador o un servei gestionat que aporti la lògica.
Mentrestant, postgres-reserves en producció continua amb una sola rèplica. Això és honest: una sola rèplica ben protegida i restaurable és millor que tres rèpliques que fingeixen ser un clúster.
- Segon exemple:
redis-cache
redis-cacheredis-cache és un cas diferent i il·lustratiu. Guarda la disponibilitat de places, és una dada reconstruïble des de postgres-reserves, i perdre-la només provoca un pic temporal de consultes.
Mereix un StatefulSet? Depèn de què vulguis:
- Redis com a memòria cau pura, sense persistència: un Deployment és perfectament vàlid. No hi ha estat a preservar.
- Redis amb persistència AOF/RDB per no arrencar en fred després d'un reinici: StatefulSet, perquè volem que el pod retrobi el seu fitxer.
Rutas Norte tria la segona opció per a producció, perquè després d'un reinici en un pont festiu una memòria cau buida dispara la càrrega sobre la base de dades justament en el pitjor moment.
# k8s/base/redis-cache-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-cache
namespace: rutas-norte-pro
labels:
app: redis-cache
app.kubernetes.io/part-of: rutas-norte
entorn: pro
spec:
serviceName: redis-cache-nodes
replicas: 3
podManagementPolicy: Parallel # els tres són simètrics, no hi ha ordre a respectar
persistentVolumeClaimRetentionPolicy:
whenDeleted: Delete # és memòria cau: no val la pena conservar discos orfes
whenScaled: Delete
selector:
matchLabels:
app: redis-cache
entorn: pro
template:
metadata:
labels:
app: redis-cache
app.kubernetes.io/part-of: rutas-norte
entorn: pro
spec:
automountServiceAccountToken: false
terminationGracePeriodSeconds: 30
containers:
- name: redis
image: redis:7.4.1-alpine
args:
- "--appendonly"
- "yes"
- "--maxmemory"
- "700mb"
- "--maxmemory-policy"
- "allkeys-lru"
ports:
- name: redis
containerPort: 6379
resources:
requests:
cpu: 100m
memory: 512Mi
limits:
cpu: 500m
memory: 1Gi
volumeMounts:
- name: dades
mountPath: /data
volumeClaimTemplates:
- metadata:
name: dades
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: rutasnorte-estandar
resources:
requests:
storage: 2GiDiferències deliberades respecte de postgres-reserves, i el raonament que hi ha darrere de cadascuna:
| Decisió | postgres-reserves |
redis-cache |
Motiu |
|---|---|---|---|
podManagementPolicy |
OrderedReady |
Parallel |
Les instàncies de memòria cau no depenen les unes de les altres; arrencar-les totes tres alhora és més ràpid |
whenScaled / whenDeleted |
Retain |
Delete |
Les dades de memòria cau són reconstruïbles; les de reserves no |
| StorageClass | rutasnorte-rapida |
rutasnorte-estandar |
La memòria cau viu en memòria; el disc només serveix per a l'arrencada en calent |
| QoS | Guaranteed |
Burstable |
La memòria cau tolera contenció de CPU; la base de dades no |
Aquí sí que tenim tres rèpliques de veritat útils, perquè cada instància atén la seva porció de trànsit de manera independent i api-reserves pot repartir consultes entre elles mitjançant el ClusterIP. No hi ha coherència a garantir: si una instància no té la clau, es consulta la base de dades i s'omple.
Errors Comuns i Consells
Confondre serviceName amb el Service normal. serviceName ha d'apuntar al Service headless. Si apunta a un ClusterIP normal, el StatefulSet arrenca sense queixar-se però els noms DNS per pod no funcionen, i les rèpliques que intentin localitzar-se entre si fallaran de manera desconcertant.
Oblidar de crear el Service headless. Kubernetes no valida que el Service de serviceName existeixi. Els pods arrenquen i tot sembla bé fins que alguna cosa intenta resoldre postgres-reserves-1.postgres-reserves-nodes... i obté NXDOMAIN.
Esperar que esborrar el StatefulSet esborri els discos. Amb la política per defecte (Retain) no ho fa. Després de diverses iteracions de proves acabes amb desenes de PVC orfes consumint quota. Revisa-ho periòdicament: kubectl get pvc -A | grep -v Bound no n'hi ha prou, perquè continuen Bound. Busca per etiqueta i compara amb els StatefulSets existents.
Escalar una base de dades i creure que ja està replicada. L'error conceptual més car d'aquesta lliçó. Tres pods de PostgreSQL amb tres discos buits són tres bases de dades diferents. Rellegeix l'apartat 9.
StatefulSet encallat a l'ordinal 0. Amb OrderedReady, si -0 no arriba a Ready, res no avança. Diagnòstic: kubectl describe pod postgres-reserves-0 i kubectl get events -n <ns> --sort-by=.lastTimestamp. Causes típiques: PVC en Pending perquè no hi ha node amb la classe d'emmagatzematge requerida, o sonda de disponibilitat mal configurada.
Canviar volumeClaimTemplates en un StatefulSet existent. És un camp pràcticament immutable: només es permet modificar resources.requests.storage (i des de 1.31 amb la porta de característiques corresponent). Qualsevol altre canvi provoca un error de validació. Si necessites canviar la StorageClass, cal migrar com a l'apartat 7.
Redimensionar el disc. No editis el volumeClaimTemplates esperant que es propagui als PVC ja existents. Cal ampliar cada PVC individualment (kubectl patch pvc dades-postgres-reserves-0 ...) tal com vam veure a 05-05, i actualitzar la plantilla perquè les rèpliques futures neixin amb la mida nova.
Consell: fes servir sempre subPath amb PostgreSQL. Els volums de blocs formatats en ext4 porten un directori lost+found a l'arrel, i initdb es nega a treballar sobre un directori no buit. Muntar a /var/lib/postgresql/data amb PGDATA=/var/lib/postgresql/data/pgdata resol el problema netament.
Consell: kubectl delete statefulset X --cascade=orphan. Esborra el StatefulSet deixant els pods vius. És la maniobra per canviar camps immutables (selector, podManagementPolicy, serviceName) sense interrompre el servei: esborres l'objecte, apliques el manifest corregit i el controlador adopta els pods existents, igual que vam veure amb els ReplicaSets a 02-02.
Exercicis
Exercici 1: identitat i persistència de l'ordinal
Al namespace rutas-norte-dev del teu minikube (perfil rutas-norte), desplega un StatefulSet anomenat notes-demo amb 3 rèpliques de busybox:1.36, un Service headless anomenat notes-demo-nodes i un volumeClaimTemplates de 100 MiB amb la classe rutasnorte-estandar, muntat a /dades. Cada pod ha d'escriure el seu nom a /dades/qui-soc.txt en arrencar i després dormir.
Després:
- Comprova els noms dels pods i dels PVC generats.
- Escriu un contingut addicional al pod
notes-demo-1. - Esborra aquest pod i verifica que el pod nou recupera el fitxer.
Exercici 2: desplegament per fases amb partition
Sobre el StatefulSet de l'exercici 1, canvia la imatge a busybox:1.37 fent servir partition de manera que només s'actualitzi el pod d'ordinal 2. Verifica que notes-demo-0 i notes-demo-1 continuen amb la imatge antiga. Després completa l'actualització.
Exercici 3: escalat i destí dels PVC
Amb el StatefulSet de l'exercici 1 en 3 rèpliques:
- Redueix-lo a 1 rèplica i comprova quants PVC queden.
- Torna a pujar a 3 i verifica que
notes-demo-2recupera el seu fitxer, no un de nou. - Configura
persistentVolumeClaimRetentionPolicy.whenScaled: Delete, redueix un altre cop a 1 i comprova la diferència. - Neteja-ho tot.
Solucions
Solució 1
# /tmp/notes-demo.yaml
apiVersion: v1
kind: Service
metadata:
name: notes-demo-nodes
namespace: rutas-norte-dev
labels:
app: notes-demo
entorn: dev
spec:
clusterIP: None
selector:
app: notes-demo
entorn: dev
ports:
- name: fictici
port: 9999
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: notes-demo
namespace: rutas-norte-dev
labels:
app: notes-demo
app.kubernetes.io/part-of: rutas-norte
entorn: dev
spec:
serviceName: notes-demo-nodes
replicas: 3
selector:
matchLabels:
app: notes-demo
entorn: dev
template:
metadata:
labels:
app: notes-demo
app.kubernetes.io/part-of: rutas-norte
entorn: dev
spec:
automountServiceAccountToken: false
containers:
- name: escriptor
image: busybox:1.36
command:
- sh
- -c
- 'echo "soc $(hostname)" >> /dades/qui-soc.txt; sleep 86400'
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 50m
memory: 32Mi
volumeMounts:
- name: dades
mountPath: /dades
volumeClaimTemplates:
- metadata:
name: dades
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: rutasnorte-estandar
resources:
requests:
storage: 100Mikubectl apply -f /tmp/notes-demo.yaml
kubectl rollout status statefulset/notes-demo -n rutas-norte-dev
kubectl get pods -n rutas-norte-dev -l app=notes-demo
kubectl get pvc -n rutas-norte-dev -l app=notes-demoNAME READY STATUS RESTARTS AGE
notes-demo-0 1/1 Running 0 40s
notes-demo-1 1/1 Running 0 32s
notes-demo-2 1/1 Running 0 24s
NAME STATUS VOLUME CAPACITY STORAGECLASS
dades-notes-demo-0 Bound pvc-a1b2... 100Mi rutasnorte-estandar
dades-notes-demo-1 Bound pvc-c3d4... 100Mi rutasnorte-estandar
dades-notes-demo-2 Bound pvc-e5f6... 100Mi rutasnorte-estandarEls PVC segueixen el patró <plantilla>-<statefulset>-<ordinal>, tal com havíem anticipat.
# 2. Escriure contingut addicional
kubectl exec -n rutas-norte-dev notes-demo-1 -- \
sh -c 'echo "reserva fictícia 4471" >> /dades/qui-soc.txt'
# 3. Esborrar el pod i esperar el substitut
kubectl delete pod notes-demo-1 -n rutas-norte-dev
kubectl wait --for=condition=ready pod/notes-demo-1 -n rutas-norte-dev --timeout=120s
kubectl exec -n rutas-norte-dev notes-demo-1 -- cat /dades/qui-soc.txtEl fitxer conserva les dues línies anteriors i n'afegeix una tercera de la nova arrencada. El pod es diu igual i ha retrobat el seu disc: identitat estable i emmagatzematge estable, exactament el que un Deployment no pot prometre.
Solució 2
# Fixar la partició per sobre de l'ordinal màxim abans de tocar res
kubectl patch statefulset notes-demo -n rutas-norte-dev \
-p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":3}}}}'
kubectl set image statefulset/notes-demo -n rutas-norte-dev escriptor=busybox:1.37
# Res no s'ha mogut: partition (3) > ordinal màxim (2)
kubectl get pods -n rutas-norte-dev -l app=notes-demo \
-o custom-columns=POD:.metadata.name,IMATGE:.spec.containers[0].image# Alliberar només l'ordinal 2
kubectl patch statefulset notes-demo -n rutas-norte-dev \
-p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":2}}}}'
kubectl rollout status statefulset/notes-demo -n rutas-norte-dev
kubectl get pods -n rutas-norte-dev -l app=notes-demo \
-o custom-columns=POD:.metadata.name,IMATGE:.spec.containers[0].imageNomés l'ordinal més alt s'ha actualitzat. Si això fos una base de dades amb el primari a -0, hauríem provat la versió nova a la rèplica menys crítica.
# Completar l'actualització
kubectl patch statefulset notes-demo -n rutas-norte-dev \
-p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":0}}}}'
kubectl rollout status statefulset/notes-demo -n rutas-norte-devL'ordre observat als esdeveniments serà -1 primer i -0 després: sempre descendent.
Solució 3
# 1. Reduir a 1
kubectl scale statefulset notes-demo -n rutas-norte-dev --replicas=1
kubectl get pods,pvc -n rutas-norte-dev -l app=notes-demoNAME READY STATUS RESTARTS AGE
pod/notes-demo-0 1/1 Running 0 14m
NAME STATUS VOLUME CAPACITY
persistentvolumeclaim/dades-notes-demo-0 Bound pvc-a1b2... 100Mi
persistentvolumeclaim/dades-notes-demo-1 Bound pvc-c3d4... 100Mi
persistentvolumeclaim/dades-notes-demo-2 Bound pvc-e5f6... 100MiUn pod, tres PVC. És el comportament per defecte (whenScaled: Retain) i la causa habitual de factures d'emmagatzematge inexplicables.
# 2. Tornar a pujar i comprovar que -2 recupera el seu fitxer
kubectl scale statefulset notes-demo -n rutas-norte-dev --replicas=3
kubectl wait --for=condition=ready pod/notes-demo-2 -n rutas-norte-dev --timeout=120s
kubectl exec -n rutas-norte-dev notes-demo-2 -- cat /dades/qui-soc.txtDues línies: la de l'arrencada original i la de la nova. El disc és el mateix d'abans.
# 3. Canviar la política i reduir de nou
kubectl patch statefulset notes-demo -n rutas-norte-dev \
-p '{"spec":{"persistentVolumeClaimRetentionPolicy":{"whenScaled":"Delete","whenDeleted":"Retain"}}}'
kubectl scale statefulset notes-demo -n rutas-norte-dev --replicas=1
kubectl get pvc -n rutas-norte-dev -l app=notes-demoNAME STATUS VOLUME CAPACITY STORAGECLASS
dades-notes-demo-0 Bound pvc-a1b2... 100Mi rutasnorte-estandarAra els PVC de -1 i -2 s'han esborrat en desaparèixer els seus pods. Per a una memòria cau és el que es desitja; per a postgres-reserves seria una manera ràpida de perdre les reserves d'un any.
# 4. Neteja
kubectl delete -f /tmp/notes-demo.yaml
kubectl delete pvc -n rutas-norte-dev -l app=notes-demoConclusió
El StatefulSet resol el sostre que arrossegàvem des de la lliçó 05-03. Amb ell, cada rèplica té nom ordinal estable, el seu propi PVC generat des de volumeClaimTemplates i un nom DNS individual gràcies al Service headless que serviceName exigeix. L'arrencada i l'aturada són ordenades (OrderedReady) o simultànies (Parallel) segons convingui, i les actualitzacions baixen en ordre invers amb la possibilitat de frenar-les per fases mitjançant partition.
Hem migrat postgres-reserves de Deployment a StatefulSet reutilitzant el PersistentVolume existent —el truc és alliberar el claimRef i crear un PVC amb el nom exacte que el controlador espera— i hem convertit redis-cache en un StatefulSet de tres rèpliques amb decisions oposades i ben raonades a cada camp.
I hem estat honestos amb el límit: el StatefulSet aporta infraestructura, no lògica. PostgreSQL no es replica pel fet d'estar dins d'un. Aquesta mancança és la que motivarà els operadors de la lliçó 06-07, que tancaran aquest mòdul substituint el nostre StatefulSet artesanal per programari que sap promoure un primari, crear rèpliques de lectura i recuperar a un instant concret.
Abans d'això hi ha altres maneres d'executar càrregues de treball que encara no coneixem. Fins ara totes les nostres càrregues es distribueixen on el scheduler vulgui i en el nombre de còpies que demanem. Però hi ha una categoria de procés —els agents d'infraestructura: recol·lectors de registres, exportadors de mètriques, connectors de xarxa— per als quals "quantes rèpliques" és la pregunta equivocada: la resposta correcta sempre és una per node, ni una més ni una menys, i automàticament als nodes que s'afegeixin demà. Aquest és l'objecte de la lliçó següent: els DaemonSets.
Curs de Kubernetes
Mòdul 1: Introducció a Kubernetes
- Què és Kubernetes?
- Arquitectura de Kubernetes
- Conceptes i Terminologia Clau
- Configuració d'un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objectes, Manifests YAML i el Model Declaratiu
- El Projecte del Curs: la Plataforma Rutas Norte
Mòdul 2: Components Principals de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualitzacions, Rollbacks i Estratègies de Desplegament
- Serveis
- Namespaces
- Etiquetes, Selectors i Anotacions
Mòdul 3: Gestió de Configuració i Secrets
- ConfigMaps
- Secrets
- Variables d'Entorn
- Quotes i Límits de Recursos
- LimitRanges i Classes de Qualitat de Servei (QoS)
- ServiceAccounts i Accés a l'API des dels Pods
Mòdul 4: Xarxes a Kubernetes
- Xarxes de Clúster
- Tipus de Serveis
- DNS Intern i Descobriment de Serveis
- Controladors d'Ingress
- TLS i Gestió de Certificats amb cert-manager
- Polítiques de Xarxa
Mòdul 5: Emmagatzematge a Kubernetes
- Volums
- Volums Persistents
- Reclamacions de Volums Persistents
- Classes d'Emmagatzematge
- Aprovisionament Dinàmic, Expansió i Snapshots
- Còpies de Seguretat i Restauració de Dades
Mòdul 6: Conceptes Avançats de Kubernetes
- StatefulSets
- DaemonSets
- Treballs i CronJobs
- Init Containers, Sidecars i Patrons Multicontenidor
- Planificació: Afinitat, Taints i Toleracions
- Definicions de Recursos Personalitzats (CRDs)
- Operadors i el Patró Controlador
Mòdul 7: Monitoratge i Registre
- Verificacions de Salut i Sondes
- Servidor de Mètriques i kubectl top
- Monitoratge amb Prometheus
- Visualització i Alertes amb Grafana i Alertmanager
- Registre Centralitzat amb Elasticsearch, Fluentd i Kibana (EFK)
- Depuració d'Aplicacions i Esdeveniments del Clúster
Mòdul 8: Seguretat a Kubernetes
- Control d'Accés Basat en Rols (RBAC)
- Contextos de Seguretat i Enduriment del Contenidor
- Polítiques de Seguretat de Pods i Pod Security Standards
- Seguretat de Xarxa
- Seguretat d'Imatges
- Auditoria, Escaneig i Gestió de Vulnerabilitats
Mòdul 9: Escalat i Rendiment
- Autoescalat Horitzontal de Pods
- Autoescalat Vertical de Pods
- Autoescalat de Clúster
- Escalat per Esdeveniments i Mètriques Personalitzades amb KEDA
- Alta Disponibilitat: PodDisruptionBudgets i Topologia
- Ajust de Rendiment
Mòdul 10: Ecosistema i Eines de Kubernetes
- Minikube i Entorns Locals amb kind
- Kubeadm
- Helm
- Kustomize
- GitOps amb Argo CD i Flux
- Kubernetes Gestionat: EKS, AKS i GKE
Mòdul 11: Estudis de Cas i Aplicacions del Món Real
- Desplegament d'una Aplicació Web
- Execució d'Aplicacions amb Estat
- CI/CD amb Kubernetes
- Estratègies de Desplegament: Blue-Green i Canary
- Gestió Multi-Clúster
- Operació en Producció: Incidències, Runbooks i Costos
