Vam deixar el PersistentVolume pv-postgres-reserves-10gi en fase Available, amb la columna CLAIM buida i 10 GiB esperant algú. Un PV no es munta sol: cal que l'aplicació el reclami, i aquesta reclamació és un objecte amb namespace anomenat PersistentVolumeClaim (PVC). Aquesta és la lliçó en què el deute del mòdul se salda de veritat: escriuràs el PVC, entendràs amb precisió l'algorisme pel qual el controlador aparella demanda i oferta, muntaràs el volum al pod de postgres-reserves i faràs la prova que portem prometent des del mòdul 2 —crear una reserva, esborrar el pod i trobar-la intacta—. Veuràs a més què passa en esborrar un PVC, per què de vegades es queda en Terminating, i per què un Deployment amb un PVC ReadWriteOnce no pot escalar.
Contingut
- Què és exactament un PersistentVolumeClaim
- Anatomia de l'objecte camp a camp
- L'algorisme d'aparellament
- Per què pots rebre més espai del que has demanat
- L'estat
Pendingi el seu diagnòstic - Consumir el PVC des del pod
- El manifest definitiu de
postgres-reserves - La demostració: les dades sobreviuen al pod
- La relació 1:1 i el perill de dos pods sobre un RWO
- Esborrar un PVC: finalitzadors i
Terminating - Per què un Deployment amb PVC no escala
- Què és exactament un PersistentVolumeClaim
Un PersistentVolumeClaim és la petició d'emmagatzematge que fa una aplicació. L'analogia que millor funciona és la dels recursos de còmput que ja coneixes de 03-04:
| Còmput | Emmagatzematge |
|---|---|
El pod demana requests.cpu: 500m |
El PVC demana requests.storage: 10Gi |
| El planificador busca un node amb capacitat | El controlador busca un PV compatible |
Si no hi ha node, el pod queda Pending |
Si no hi ha PV, el PVC queda Pending |
| El pod no sap quina màquina li tocarà | L'app no sap quin disc li tocarà |
Les seves dues propietats definitòries:
- Té namespace. Viu al costat de l'aplicació que el fa servir, es veu afectat per les ResourceQuota del namespace (03-04) i desapareix si s'esborra el namespace. Un pod només pot muntar PVCs del seu propi namespace: no hi ha manera de referenciar un PVC d'un altre.
- És l'única peça d'emmagatzematge que escriu l'equip d'aplicació. En un clúster amb aprovisionament dinàmic ben configurat, un desenvolupador no escriu mai un PersistentVolume; escriu PVCs.
- Anatomia de l'objecte camp a camp
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-reserves-dades
namespace: rutas-norte-dev # SI que te namespace
labels:
app: postgres-reserves
app.kubernetes.io/name: postgres-reserves
app.kubernetes.io/component: base-dades
app.kubernetes.io/part-of: rutas-norte
entorn: dev
spec:
accessModes: ["ReadWriteOnce"] # com el vull muntar
volumeMode: Filesystem # Filesystem (defecte) o Block
resources:
requests:
storage: 10Gi # quant demano, COM A MINIM
storageClassName: rutasnorte-rapida # de quina classe
selector: # (opcional) filtrar PVs per etiquetes
matchLabels: { tipus-disc: ssd }
# volumeName: pv-postgres-reserves-10gi # (opcional) lligar-lo a UN de concretEls camps, amb el seu significat exacte:
| Camp | Què significa | Detall que importa |
|---|---|---|
accessModes |
Modes en què es pretén muntar | Ha de ser un subconjunt dels del PV. S'apliquen per node (05-02) |
resources.requests.storage |
El mínim de capacitat acceptable | Pots rebre'n més. En dinàmic es crea d'aquesta mida exacta |
resources.limits.storage |
Topall superior | Rarament es fa servir; l'apliquen alguns controladors d'admissió |
volumeMode |
Filesystem o Block |
Ha de coincidir amb el del PV, no n'hi ha prou amb ser compatible |
storageClassName |
La classe demanada | "" significa "cap classe"; ometre'l significa "la classe per defecte" |
selector |
Filtre per etiquetes del PV | Només s'aplica a l'aprovisionament estàtic; el dinàmic l'ignora |
volumeName |
Nom d'un PV concret | Vinculació manual. Se salta l'aparellament normal |
Dos camps mereixen un comentari a part.
selector
Funciona igual que els selectors de 02-07, amb matchLabels i matchExpressions, i s'aplica sobre les etiquetes del PV. Serveix per expressar requisits que la capacitat no captura: matchExpressions: [{ key: tipus-disc, operator: In, values: ["ssd", "nvme"] }] significa "vull un disc ràpid, m'és igual quin".
Compte: si el PVC porta selector, l'aprovisionament dinàmic no el pot satisfer (un volum acabat de crear no tindrà aquestes etiquetes), així que el PVC quedarà Pending per sempre en un clúster que només faci servir StorageClasses. És un camp del món estàtic.
volumeName
Lliga el PVC a un PV concret pel seu nom, saltant-se la cerca. És útil en una recuperació —"vull exactament aquell volum, el que té les dades"— i és el complement del claimRef que vas veure a 05-02: si tots dos objectes s'anomenen mútuament, la vinculació és determinista i ningú més no s'hi pot colar.
- L'algorisme d'aparellament
Quan crees un PVC sense volumeName, el controlador de PersistentVolume (part de kube-controller-manager) busca un PV candidat. El procés, en ordre:
flowchart TB
A["Es crea el PVC<br/>status: Pending"] --> B{"Te volumeName?"}
B -->|"Si"| C["Intenta vincular AQUEST PV<br/>si es compatible"]
B -->|"No"| D["Llista els PV en fase Available"]
D --> E{"Mateixa classe? capacity suficient?<br/>accessModes continguts?<br/>volumeMode identic? selector casa?"}
E -->|"No"| X["Descartat"]
E -->|"Si"| J["CANDIDAT"]
J --> K["De tots els candidats,<br/>el de MENOR capacitat"]
K --> L["Bound: s'escriu claimRef al PV<br/>i volumeName al PVC"]
D --> M{"Sense candidats"}
M -->|"Hi ha StorageClass"| N["Aprovisionament dinamic (05-04)"]
M -->|"Sense classe"| O["Segueix Pending"]
Els cinc criteris, sense excepcions:
- La mateixa
storageClassName, comparada com a cadena exacta.""i absent no són el mateix. capacity.storagedel PV ≥requests.storagedel PVC. Major o igual, mai menor.- Els
accessModesdel PVC han d'estar continguts en els del PV. Si el PVC demana["ReadWriteMany"]i el PV ofereix["ReadWriteOnce"], no hi ha tracte. volumeModeidèntic.- El
selectordel PVC, si existeix, ha de casar amb les etiquetes del PV.
Entre tots els candidats, el controlador tria el de menor capacitat, per malgastar el mínim possible. I un cop decidit, la vinculació és bidireccional i exclusiva: s'escriu spec.claimRef al PV i spec.volumeName al PVC. A partir d'aquí tots dos objectes estan casats i cap altre PVC no pot fer servir aquest PV, encara que hi sobri espai.
- Per què pots rebre més espai del que has demanat
De la regla 2 se'n deriva un comportament que sorprèn: requests.storage és un mínim, no una talla exacta. Si demanes 8 GiB i l'únic PV disponible en té 10, t'hi vincules i kubectl get pvc informa de CAPACITY: 10Gi. Conseqüències pràctiques:
- No hi ha devolució. Els 2 GiB de diferència es perden per a la resta del clúster: ningú més no pot fer servir aquest PV.
- La ResourceQuota compta el que s'ha demanat, no el que s'ha obtingut. Si el namespace té
requests.storage: 20Gide quota (03-04), el teu PVC de 8 GiB consumeix 8 GiB de quota encara que en gaudeixis de 10. - En aprovisionament dinàmic això no passa: el volum es crea exactament de la mida demanada (arrodonida al mínim del proveïdor). És una raó més per preferir-lo.
El que mai no passa és el contrari: un PVC no es vincula mai a un PV més petit del que ha demanat.
- L'estat
Pending i el seu diagnòstic
Pending i el seu diagnòsticPending és l'estat que més veuràs i diagnosticaràs. Significa una sola cosa: el PVC no ha trobat volum. Les causes són poques i el mètode per distingir-les és sempre el mateix.
Els esdeveniments del final són la clau:
| Esdeveniment / missatge | Causa | Solució |
|---|---|---|
no persistent volumes available for this claim and no storage class is set |
No hi ha PV compatible i el PVC no té classe que aprovisioni | Crea el PV o assigna una StorageClass |
storageclass.storage.k8s.io "rutasnorte-rapida" not found |
La classe anomenada no existeix | Corregeix el nom o crea la classe (05-04) |
waiting for first consumer to be created before binding |
La classe fa servir volumeBindingMode: WaitForFirstConsumer |
No és un error: crea el pod i es vincularà (05-04) |
failed to provision volume with StorageClass ... |
L'aprovisionador ha fallat (quota del proveïdor, permisos) | Mira els registres del provisionador CSI |
exceeded quota: requests.storage |
La ResourceQuota del namespace ho impedeix | Ajusta la quota o demana menys |
I el procediment de diagnòstic, en ordre, quan l'esdeveniment no n'hi ha prou:
# 1. Hi ha PVs disponibles? De quina classe, mida i modes?
kubectl get pv -o custom-columns=\
NOM:.metadata.name,CAP:.spec.capacity.storage,\
MODES:.spec.accessModes,CLASSE:.spec.storageClassName,FASE:.status.phase
# 2. Existeix la classe que demana el PVC?
kubectl get storageclass
# 3. Els esdeveniments, que gairebe sempre ho diuen tot
kubectl describe pvc postgres-reserves-dades -n rutas-norte-dev | tail -15
# 4. Hi ha quota que ho bloquegi?
kubectl describe resourcequota -n rutas-norte-devUn Pending al PVC arrossega el pod: es queda en Pending també, amb l'esdeveniment del planificador pod has unbound immediate PersistentVolumeClaims. Diagnostica sempre el PVC primer; el pod només és el símptoma.
- Consumir el PVC des del pod
Al pod, el PVC es fa servir a través d'un volum del tipus persistentVolumeClaim. La mecànica volumes/volumeMounts de 05-01 no canvia en absolut:
containers:
- name: postgres
volumeMounts:
- name: dades # (2) on
mountPath: /var/lib/postgresql/data
volumes:
- name: dades # (1) que
persistentVolumeClaim:
claimName: postgres-reserves-dades # el PVC, del MATEIX namespace
readOnly: falseAixò és tot. El pod no esmenta el PersistentVolume enlloc, ni el proveïdor, ni el disc: només el nom del PVC. Aquesta indirecció és la que fa que el mateix manifest funcioni a minikube i en producció.
- El manifest definitiu de
postgres-reserves
postgres-reservesHa arribat el moment d'esborrar el comentari "PROVISIONAL" que arrosseguem des de 02-05. Primer el PVC:
# k8s/base/postgres-reserves-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-reserves-dades
namespace: rutas-norte-dev
labels:
app: postgres-reserves
app.kubernetes.io/name: postgres-reserves
app.kubernetes.io/component: base-dades
app.kubernetes.io/part-of: rutas-norte
entorn: dev
spec:
accessModes: ["ReadWriteOnce"]
volumeMode: Filesystem
resources: { requests: { storage: 10Gi } }
storageClassName: rutasnorte-rapidaNAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
persistentvolumeclaim/postgres-reserves-dades Bound pv-postgres-reserves-10gi 10Gi RWO rutasnorte-rapida 4s
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS AGE
persistentvolume/pv-postgres-reserves-10gi 10Gi RWO Retain Bound rutas-norte-dev/postgres-reserves-dades rutasnorte-rapida 1hBound per tots dos costats. La columna CLAIM del PV, que a la lliçó anterior estava buida, ara anomena el PVC amb el seu namespace. I el Deployment complet:
# k8s/base/postgres-reserves-deployment.yaml (JA NO ES PROVISIONAL)
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres-reserves
namespace: rutas-norte-dev
labels:
app: postgres-reserves
app.kubernetes.io/name: postgres-reserves
app.kubernetes.io/component: base-dades
app.kubernetes.io/part-of: rutas-norte
entorn: dev
spec:
replicas: 1 # UNA. Veure apartat 11
strategy:
type: Recreate # MAI RollingUpdate sobre un RWO
selector:
matchLabels: { app: postgres-reserves, entorn: dev }
template:
metadata:
labels:
app: postgres-reserves
app.kubernetes.io/name: postgres-reserves
app.kubernetes.io/component: base-dades
app.kubernetes.io/part-of: rutas-norte
entorn: dev
spec:
securityContext:
fsGroup: 999 # grup 'postgres' de la imatge oficial:
# dona propietat del volum al proces (08-02)
containers:
- name: postgres
image: postgres:16.4
ports: [{ name: postgres, containerPort: 5432 }]
env:
# username, password i database des del Secret del modul 3
- name: POSTGRES_USER
valueFrom:
secretKeyRef: { name: postgres-reserves-credencials, key: username }
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef: { name: postgres-reserves-credencials, key: password }
- name: POSTGRES_DB
valueFrom:
secretKeyRef: { name: postgres-reserves-credencials, key: database }
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: dades
mountPath: /var/lib/postgresql/data
subPath: pgdata # veure explicacio sobre lost+found
resources: # Guaranteed, decidit a 03-05
requests: { cpu: "1", memory: 2Gi }
limits: { cpu: "1", memory: 2Gi }
volumes:
- name: dades
persistentVolumeClaim:
claimName: postgres-reserves-dadesL'assumpte de lost+found i per què hi ha subPath
Quan un volum de blocs es formata amb ext4, la seva arrel conté un directori lost+found. I l'initdb de PostgreSQL es nega a inicialitzar un directori de dades que no estigui buit:
initdb: error: directory "/var/lib/postgresql/data" exists but is not empty
It contains a lost+found directory, perhaps due to it being a mount point.Hi ha dues maneres de resoldre-ho, i al manifest hi són totes dues, cosa que no és redundància sinó cinturó i tirants:
| Tècnica | Què fa | Ruta efectiva de les dades |
|---|---|---|
subPath: pgdata al volumeMount |
Munta el subdirectori pgdata del volum, no la seva arrel |
<volum>/pgdata muntat a /var/lib/postgresql/data |
PGDATA=/var/lib/postgresql/data/pgdata |
Diu a PostgreSQL que faci servir un subdirectori del punt de muntatge | Un nivell més avall dins del muntatge |
A Rutas Norte adoptem PGDATA com a mecanisme principal (ja venia del mòdul 3) perquè el subPath té un desavantatge seriós: un volum muntat amb subPath no es redimensiona automàticament en alguns controladors, cosa que complica l'expansió de 05-05. Si en tries només un, tria PGDATA per a PostgreSQL i reserva subPath per al cas general d'altres aplicacions.
fsGroup: l'altra fallada clàssica
El procés de PostgreSQL no corre com a root, sinó com l'usuari postgres (UID 999 a la imatge oficial). Un volum acabat de crear sol pertànyer a root:root, així que el procés no hi pot escriure:
initdb: error: could not change permissions of directory
"/var/lib/postgresql/data": Operation not permittedsecurityContext.fsGroup: 999 fa que el kubelet canviï el grup propietari del volum a 999 i hi afegeixi permís d'escriptura en muntar-lo. És la solució estàndard i es tracta en profunditat a 08-02.
- La demostració: les dades sobreviuen al pod
Aquest és el moment del mòdul. Apliquem i repetim exactament l'experiment de 05-01, que llavors va perdre totes les dades.
kubectl apply -f k8s/base/postgres-reserves-pvc.yaml
kubectl apply -f k8s/base/postgres-reserves-deployment.yaml
kubectl rollout status deploy/postgres-reserves -n rutas-norte-devCreem una reserva fictícia:
POD=$(kubectl get pod -n rutas-norte-dev -l app=postgres-reserves \
-o jsonpath='{.items[0].metadata.name}')
kubectl exec -n rutas-norte-dev "$POD" -- psql -U rutasnorte -d reserves -c \
"CREATE TABLE IF NOT EXISTS reserves (
id serial PRIMARY KEY, client text, trajecte text, data date);
INSERT INTO reserves (client, trajecte, data) VALUES
('Marta Iglesias', 'Bilbao-Santander', '2026-08-15'),
('Ignacio Sabater', 'Oviedo-Gijon', '2026-08-16');"I ara la prova. Esborrem el pod, igual que faria un rollout, un desallotjament o la caiguda del node:
kubectl delete pod -n rutas-norte-dev "$POD"
kubectl wait --for=condition=ready pod -n rutas-norte-dev \
-l app=postgres-reserves --timeout=180s
POD=$(kubectl get pod -n rutas-norte-dev -l app=postgres-reserves \
-o jsonpath='{.items[0].metadata.name}')
echo "Pod NOU: $POD"
kubectl exec -n rutas-norte-dev "$POD" -- psql -U rutasnorte -d reserves -c \
"SELECT * FROM reserves;"Pod NOU: postgres-reserves-8f7c4b2d9-q7wnl
id | client | trajecte | data
----+-----------------+-------------------+------------
1 | Marta Iglesias | Bilbao-Santander | 2026-08-15
2 | Ignacio Sabater | Oviedo-Gijon | 2026-08-16
(2 files)Les reserves continuen allà, en un pod diferent, amb un altre nom i una altra IP. El deute del mòdul 2 està saldat. I pots verificar on viuen realment els bytes amb minikube ssh -p rutas-norte -- "sudo ls -l /data/postgres-reserves/pgdata": els fitxers són de l'UID 999 gràcies a fsGroup, i estan al directori del node, fora del contenidor.
Puja l'aposta si vols: esborra el Deployment sencer i torna a aplicar-lo. Les dades continuen, perquè el PVC i el PV no depenen del Deployment.
kubectl delete deploy postgres-reserves -n rutas-norte-dev
kubectl get pvc -n rutas-norte-dev # segueix Bound
kubectl apply -f k8s/base/postgres-reserves-deployment.yaml
kubectl rollout status deploy/postgres-reserves -n rutas-norte-dev
kubectl exec -n rutas-norte-dev deploy/postgres-reserves -- \
psql -U rutasnorte -d reserves -c "SELECT count(*) FROM reserves;" # 2
- La relació 1:1 i el perill de dos pods sobre un RWO
La relació entre PVC i PV és estrictament 1:1. Un PV vinculat no admet un segon PVC ni encara que li sobrin 9 GiB de 10. Comprova-ho:
kubectl create -f - <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: altre-reclamant, namespace: rutas-norte-dev }
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: rutasnorte-rapida
resources: { requests: { storage: 1Gi } }
EOF
kubectl get pvc altre-reclamant -n rutas-norte-devNAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
altre-reclamant Pending rutasnorte-rapida 10sPending: l'únic PV d'aquesta classe està Bound. Esborra'l i continuem.
Ara la part perillosa. Diversos pods sí que poden muntar el mateix PVC, i és aquí on reapareix el parany de 05-02:
| Escenari | Amb ReadWriteOnce |
Amb ReadWriteOncePod |
|---|---|---|
| Dos pods al mateix node | Funciona. Tots dos escriuen alhora → risc de corrupció | El segon queda Pending |
| Dos pods en nodes diferents | El segon no arrenca: Multi-Attach error |
El segon queda Pending |
L'error del segon cas, que veuràs tan bon punt treballis amb un clúster de diversos nodes:
Warning FailedAttachVolume pod/postgres-reserves-8f7c4b2d9-k3mp2
Multi-Attach error for volume "pvc-3a9f..." Volume is already exclusively
attached to one node and can't be attached to anotherAquest missatge és una protecció, no una avaria: el controlador de connexió impedeix que un disc de blocs es connecti a dues màquines. El greu és el primer cas, on ningú no et protegeix. Per això, per a qualsevol càrrega amb estat sobre ReadWriteOnce, la combinació obligatòria a Rutas Norte és:
replicas: 1strategy: Recreate(acaba el pod vell abans de crear el nou)- i, si el driver ho suporta,
ReadWriteOncePoda PV i PVC.
- Esborrar un PVC: finalitzadors i
Terminating
TerminatingEsborrar un PVC en ús és una operació que gairebé mai no surt com el principiant espera, i per bones raons.
L'ordre es queda penjada. I en un altre terminal, l'explicació:
kubectl get pvc postgres-reserves-dades -n rutas-norte-dev
kubectl get pvc postgres-reserves-dades -n rutas-norte-dev \
-o jsonpath='{.metadata.finalizers}'; echoNAME STATUS VOLUME CAPACITY ACCESS MODES AGE
postgres-reserves-dades Terminating pv-postgres-reserves-10gi 10Gi RWO 2h
["kubernetes.io/pvc-protection"]kubernetes.io/pvc-protection és la protecció d'objecte d'emmagatzematge en ús. Mentre un pod actiu estigui fent servir el PVC, el finalitzador no es retira i l'objecte no s'esborra. És el mecanisme de finalitzadors de 01-06 aplicat a evitar que algú faci volar l'emmagatzematge d'una aplicació en marxa.
La solució és eliminar primer el consumidor, mai forçar el finalitzador:
kubectl delete deploy postgres-reserves -n rutas-norte-dev
kubectl get pvc -n rutas-norte-dev # ara si que desapareixMai facis
kubectl patch pvc ... -p '{"metadata":{"finalizers":null}}'. És la recepta que circula per internet per "desencallar" un PVC i el que aconsegueix és deixar el volum real connectat a un node sense cap objecte que el representi: un orfe invisible que s'ha de netejar a mà al proveïdor.
I què li passa al PV
Quan el PVC desapareix, el destí del PV el decideix la seva política de reclamació (05-02):
| Política del PV | En esborrar-se el PVC | Dades? |
|---|---|---|
Retain |
El PV passa a Released, amb el seu claimRef caducat |
Intactes. Rescatable amb kubectl patch |
Delete |
S'esborra el PV i el volum real | Destruïdes |
En el nostre cas el PV queda Released, amb la columna CLAIM mostrant encara rutas-norte-dev/postgres-reserves-dades, i les reserves a dins. Per tornar a la situació operativa, apliques el rescat que ja coneixes i tornes a crear el PVC i el Deployment:
kubectl patch pv pv-postgres-reserves-10gi \
--type=json -p='[{"op": "remove", "path": "/spec/claimRef"}]'
kubectl apply -f k8s/base/postgres-reserves-pvc.yaml
kubectl apply -f k8s/base/postgres-reserves-deployment.yaml
kubectl exec -n rutas-norte-dev deploy/postgres-reserves -- \
psql -U rutasnorte -d reserves -c "SELECT count(*) FROM reserves;" # 2Aquesta és la raó de ser de Retain en una base de dades amb dades personals. Amb Delete, aquest exercici hauria acabat amb la pèrdua de totes les reserves de Rutas Norte.
- Per què un Deployment amb PVC no escala
Última peça, i és una limitació de fons, no un detall. Prova d'escalar:
kubectl scale deploy postgres-reserves -n rutas-norte-dev --replicas=3
kubectl get pods -n rutas-norte-dev -l app=postgres-reservesEn un clúster de diversos nodes veuries dos pods encallats amb Multi-Attach error. A minikube, que té un sol node, veuries una cosa pitjor: els tres pods arrenquen, munten el mateix directori de dades i PostgreSQL comença a queixar-se que ja hi ha una instància fent servir aquest directori (o, amb un altre motor menys protegit, corromp els fitxers en silenci).
La raó és estructural: tots els pods d'un Deployment comparteixen exactament la mateixa plantilla, inclosa la llista de volumes. Si la plantilla diu claimName: postgres-reserves-dades, les tres rèpliques demanen aquest mateix PVC. Un Deployment no sap donar a cada rèplica el seu propi volum.
flowchart TB
subgraph DEP["Deployment: una plantilla per a tots"]
P1["pod-abc"] --> PVC1["PVC postgres-reserves-dades"]
P2["pod-def"] --> PVC1
P3["pod-ghi"] --> PVC1
PVC1 --> PV1["UN sol PV"]
end
subgraph STS["StatefulSet: volumeClaimTemplate (06-01)"]
S0["postgres-reserves-0"] --> C0["PVC dades-...-0"] --> V0["PV propi"]
S1["postgres-reserves-1"] --> C1["PVC dades-...-1"] --> V1["PV propi"]
end
La solució correcta és el StatefulSet, que incorpora un volumeClaimTemplate: crea un PVC per rèplica, amb nom estable i lligat a la identitat del pod, de manera que postgres-reserves-0 sempre retroba el seu volum. És l'objecte que obre el mòdul 6 a 06-01, i ara saps exactament quin problema ve a resoldre.
Mentrestant, la regla operativa per a Rutas Norte és clara i suficient: postgres-reserves és un Deployment d'una sola rèplica amb strategy: Recreate i un PVC ReadWriteOnce. Torna'l al seu lloc:
Errors Comuns i Consells
| Error | Símptoma | Solució |
|---|---|---|
PVC Pending sense mirar els esdeveniments |
Hores perdudes | kubectl describe pvc primer; l'esdeveniment gairebé sempre ho diu |
Confondre storageClassName: "" amb ometre'l |
El PVC no veu el PV estàtic, o fa servir una classe inesperada | "" = sense classe; absent = classe per defecte (05-04) |
Posar selector amb aprovisionament dinàmic |
Pending etern |
El selector només serveix amb PV estàtics |
Muntar el volum a /var/lib/postgresql |
El contenidor no arrenca | És un nivell de més: fes servir /var/lib/postgresql/data |
Oblidar PGDATA o subPath |
directory exists but is not empty ... lost+found |
Fes servir un subdirectori del punt de muntatge |
Oblidar fsGroup |
could not change permissions ... Operation not permitted |
securityContext.fsGroup amb el GID del procés |
RollingUpdate amb un PVC RWO |
Dos pods escrivint, o el nou encallat | strategy: Recreate |
| Escalar un Deployment amb PVC | Multi-Attach error o corrupció silenciosa |
Una rèplica, o StatefulSet (06-01) |
| Forçar l'esborrat traient el finalitzador | Volum orfe connectat a un node | Esborra el pod que el fa servir i deixa que el finalitzador es retiri sol |
| Suposar que esborrar el PVC conserva les dades | Amb Delete, es perden |
Comprova la política del PV abans d'esborrar res |
Consells:
- Ordre de capçalera per veure oferta i demanda alhora:
kubectl get pvc -A -o custom-columns=\ NS:.metadata.namespace,PVC:.metadata.name,ESTAT:.status.phase,\ PV:.spec.volumeName,DEMANAT:.spec.resources.requests.storage,CLASSE:.spec.storageClassName - Esbrina qui fa servir un PVC abans d'esborrar-lo, filtrant
kubectl get pods -o jsonambjqper.spec.volumes[]?.persistentVolumeClaim.claimName. - Anomena els PVC per la seva funció, no per la seva mida:
postgres-reserves-dades, nopvc-10gi. La mida canvia amb l'expansió (05-05); la funció no. - Demana amb folgança però sense exagerar. En estàtic et vincules al PV més petit que et serveixi; en dinàmic crees exactament el que has demanat i el podràs expandir després, però mai reduir-lo.
Exercicis
Exercici 1: la prova de persistència completa
Aplica el PVC i el Deployment definitiu de postgres-reserves de l'apartat 7 a rutas-norte-dev i demostra la persistència en tres nivells creixents:
- Insereix tres reserves fictícies i esborra el pod amb
kubectl delete pod. - Esborra el Deployment sencer i torna a aplicar-lo.
- Esborra el PVC (havent esborrat abans el Deployment) i recupera les dades rescatant el PV.
Documenta a cada pas l'estat del PVC i del PV.
Exercici 2: diagnosticar quatre PVC encallats
Crea aquests quatre PVC a rutas-norte-dev sabent que l'únic PV disponible és pv-postgres-reserves-10gi (10Gi, RWO, classe rutasnorte-rapida, etiqueta tipus-disc: ssd) i que ja està Bound. Per a cadascun, prediu si quedarà Bound o Pending i per què; després comprova-ho.
| PVC | accessModes |
storageClassName |
requests.storage |
|---|---|---|---|
| A | ["ReadWriteOnce"] |
rutasnorte-rapida |
5Gi |
| B | ["ReadWriteMany"] |
rutasnorte-rapida |
5Gi |
| C | ["ReadWriteOnce"] |
"" |
5Gi |
| D | ["ReadWriteOnce"] |
rutasnorte-rapida |
50Gi |
Exercici 3: el PVC de redis-cache
L'equip vol que redis-cache conservi el seu bolcat RDB entre reinicis per no perdre la memòria cau de disponibilitat de places a cada desplegament. Escriu el PVC (redis-cache-dades, 2 GiB) i el fragment del Deployment que el munta a /data, amb les etiquetes de Rutas Norte.
Després respon, raonant: és una bona decisió? Tingues en compte el que es va decidir a 03-05 sobre la naturalesa de redis-cache.
Solucions
Exercici 1
kubectl apply -f k8s/base/postgres-reserves-pvc.yaml
kubectl apply -f k8s/base/postgres-reserves-deployment.yaml
kubectl rollout status deploy/postgres-reserves -n rutas-norte-dev
kubectl exec -n rutas-norte-dev deploy/postgres-reserves -- psql -U rutasnorte -d reserves -c \
"CREATE TABLE IF NOT EXISTS reserves (id serial PRIMARY KEY, client text, trajecte text, data date);
INSERT INTO reserves (client, trajecte, data) VALUES
('Marta Iglesias','Bilbao-Santander','2026-08-15'),
('Ignacio Sabater','Oviedo-Gijon','2026-08-16'),
('Lucia Berenguer','Leon-Ponferrada','2026-08-17');"
# --- Nivell 1: esborrar el pod ---
kubectl delete pod -n rutas-norte-dev -l app=postgres-reserves
kubectl rollout status deploy/postgres-reserves -n rutas-norte-dev
kubectl exec -n rutas-norte-dev deploy/postgres-reserves -- \
psql -U rutasnorte -d reserves -c "SELECT count(*) FROM reserves;" # 3
kubectl get pvc,pv -n rutas-norte-dev # PVC Bound, PV Bound
# --- Nivell 2: esborrar el Deployment ---
kubectl delete deploy postgres-reserves -n rutas-norte-dev
kubectl get pvc -n rutas-norte-dev # Bound: el PVC sobreviu al Deployment
kubectl apply -f k8s/base/postgres-reserves-deployment.yaml
kubectl rollout status deploy/postgres-reserves -n rutas-norte-dev # 3 reserves
# --- Nivell 3: esborrar el PVC ---
kubectl delete deploy postgres-reserves -n rutas-norte-dev
kubectl delete pvc postgres-reserves-dades -n rutas-norte-dev
kubectl get pv # Released (gracies a Retain)
kubectl patch pv pv-postgres-reserves-10gi \
--type=json -p='[{"op":"remove","path":"/spec/claimRef"}]'
kubectl get pv # Available
kubectl apply -f k8s/base/postgres-reserves-pvc.yaml
kubectl apply -f k8s/base/postgres-reserves-deployment.yaml
kubectl rollout status deploy/postgres-reserves -n rutas-norte-dev
kubectl exec -n rutas-norte-dev deploy/postgres-reserves -- \
psql -U rutasnorte -d reserves -c "SELECT * FROM reserves;" # 3 filesResum d'estats:
| Pas | PVC | PV | Dades |
|---|---|---|---|
| Inicial | Bound |
Bound |
3 reserves |
| Després d'esborrar el pod | Bound |
Bound |
3 reserves |
| Després d'esborrar el Deployment | Bound |
Bound |
3 reserves |
| Després d'esborrar el PVC | no existeix | Released |
3 reserves, inaccessibles |
| Després del rescat | Bound |
Bound |
3 reserves |
Si la política hagués estat Delete, el quart pas hauria destruït les dades definitivament.
Exercici 2
| PVC | Predicció | Motiu |
|---|---|---|
| A | Pending |
L'únic PV d'aquesta classe està Bound. La relació és 1:1: els 5 GiB que sobren no són reutilitzables |
| B | Pending |
Encara que el PV estigués lliure, demana ReadWriteMany i el PV només ofereix ReadWriteOnce: els modes del PVC han d'estar continguts en els del PV |
| C | Pending |
storageClassName: "" només casa amb PV sense classe; el nostre té rutasnorte-rapida |
| D | Pending |
Demana 50 GiB i el PV en té 10: la capacitat del PV ha de ser major o igual |
Els quatre queden Pending, cadascun per una raó diferent, i for x in a b c d; do kubectl describe pvc prova-$x -n rutas-norte-dev | tail -3; done ho confirma amb el mateix esdeveniment genèric FailedBinding: no persistent volumes available for this claim and no storage class is set — un recordatori que l'esdeveniment diu que no hi ha candidat, però no per què: això ho has de raonar tu amb els cinc criteris.
Perquè A es vinculés caldria alliberar abans el PV (esborrar el PVC de postgres-reserves i netejar el claimRef), cosa que il·lustra la limitació central de l'aprovisionament estàtic que resol 05-04.
Exercici 3
# k8s/base/redis-cache-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: redis-cache-dades
namespace: rutas-norte-dev
labels:
app: redis-cache
app.kubernetes.io/name: redis-cache
app.kubernetes.io/component: cache
app.kubernetes.io/part-of: rutas-norte
entorn: dev
spec:
accessModes: ["ReadWriteOnce"]
volumeMode: Filesystem
resources: { requests: { storage: 2Gi } }
storageClassName: rutasnorte-estandar
---
# fragment del Deployment de redis-cache
spec:
securityContext:
fsGroup: 999 # usuari redis de la imatge oficial
containers:
- name: redis
image: redis:7.2-alpine
args: ["--save", "60", "1", "--appendonly", "no"]
ports: [{ name: redis, containerPort: 6379 }]
volumeMounts:
- name: dades
mountPath: /data # ruta del RDB a la imatge oficial
volumes:
- name: dades
persistentVolumeClaim: { claimName: redis-cache-dades }És una bona decisió? És acceptable, però no és prioritària i té contrapartides. A 03-05 vam classificar redis-cache com a Burstable precisament perquè és prescindible: el que guarda —la disponibilitat de places en memòria cau— es recalcula consultant postgres-reserves. El que es perd en reiniciar no és informació, és una estona de latència més alta.
A favor: després d'un desplegament, la memòria cau arrenca temperada i postgres-reserves no rep una allau de consultes; en un pont, aquest pic de recàlcul pot ser notable.
En contra, tres coses: el PVC el converteix en una càrrega amb estat, amb replicas: 1 i Recreate obligatoris; una memòria cau persistida pot servir dades caducades si l'RDB és de fa hores i la disponibilitat de places ha canviat, cosa que és pitjor que no tenir memòria cau; i si Redis guarda qualsevol dada personal de clients, aquest volum entra en l'àmbit de protecció de dades i de les còpies de seguretat de 05-06.
Decisió de Rutas Norte: no persistir redis-cache. Si el problema real és el pic de consultes després d'un desplegament, la solució correcta és un preescalfament controlat de la memòria cau en arrencar, no conservar dades de disponibilitat potencialment obsoletes.
Conclusió
El deute que arrossegava Rutas Norte des del mòdul 2 està saldat. Has escrit el PersistentVolumeClaim —l'objecte amb namespace que expressa la demanda de l'aplicació i l'única peça d'emmagatzematge que escriu un equip d'aplicació—, l'has vinculat al PV de la lliçó anterior i has vist la columna CLAIM omplir-se per tots dos costats amb Bound. I sobretot has fet la prova: tres reserves inserides, el pod esborrat, el Deployment esborrat, i les reserves intactes en un pod nou amb un altre nom i una altra IP. Els bytes, verificables al node, viuen fora del contenidor.
Saps com es produeix aquesta vinculació. L'algorisme d'aparellament exigeix cinc condicions —mateixa classe exacta, capacitat suficient, modes d'accés del PVC continguts en els del PV, volumeMode idèntic i selector que casi— i, entre els candidats, tria el menor, d'on surt el comportament que sorprèn: requests.storage és un mínim i pots rebre més espai del que has demanat, sense devolució possible i sense que la quota ho reflecteixi. Quan no hi ha candidat, el PVC queda Pending, i ja no ho diagnostiques a cegues: kubectl describe pvc i els seus esdeveniments distingeixen en segons entre "no hi ha PV compatible", "la classe no existeix", "esperant el primer consumidor" —que no és un error— i "quota excedida".
Domines el manifest definitiu de la base de dades i els tres paranys que l'envolten: el mountPath correcte (/var/lib/postgresql/data, no un nivell més amunt), el subdirectori via PGDATA o subPath que evita la fallada de lost+found, i el fsGroup sense el qual el procés no root no pot escriure al seu propi volum. Coneixes la relació 1:1 entre PVC i PV —un PV vinculat no admet un segon reclamant encara que li sobri espai— i la diferència entre les dues maneres de trencar-se amb ReadWriteOnce: en nodes diferents el Multi-Attach error et protegeix; al mateix node ningú no et protegeix, i per això replicas: 1 amb strategy: Recreate no és una preferència sinó un requisit. I saps esborrar sense destruir: el finalitzador kubernetes.io/pvc-protection deixa el PVC en Terminating mentre un pod el faci servir, es resol eliminant el consumidor i mai forçant el finalitzador; i el que li passa després al PV ho decideix la seva política de reclamació, on Retain t'ha permès recuperar les reserves d'un esborrat que amb Delete hauria estat definitiu.
Queda apuntat el sostre d'aquesta arquitectura: un Deployment té una sola plantilla, així que totes les seves rèpliques demanen el mateix PVC i per tant no escala; la resposta és el volumeClaimTemplate dels StatefulSets (06-01). I queda l'altre sostre, el que obria aquesta lliçó: continuem creant els PersistentVolumes a mà, un per un, amb el malbaratament, la feina manual i els orfes que això implica. La solució és que el clúster creï el volum exacte en el moment en què algú el demana, i l'objecte que ho fa possible és el que portem tres lliçons anomenant a storageClassName sense explicar: Classes d'Emmagatzematge.
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
