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

  1. Què és exactament un PersistentVolumeClaim
  2. Anatomia de l'objecte camp a camp
  3. L'algorisme d'aparellament
  4. Per què pots rebre més espai del que has demanat
  5. L'estat Pending i el seu diagnòstic
  6. Consumir el PVC des del pod
  7. El manifest definitiu de postgres-reserves
  8. La demostració: les dades sobreviuen al pod
  9. La relació 1:1 i el perill de dos pods sobre un RWO
  10. Esborrar un PVC: finalitzadors i Terminating
  11. Per què un Deployment amb PVC no escala

  1. 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:

  1. 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.
  2. É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.

  1. 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 concret

Els 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.

  1. 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:

  1. La mateixa storageClassName, comparada com a cadena exacta. "" i absent no són el mateix.
  2. capacity.storage del PV ≥ requests.storage del PVC. Major o igual, mai menor.
  3. Els accessModes del PVC han d'estar continguts en els del PV. Si el PVC demana ["ReadWriteMany"] i el PV ofereix ["ReadWriteOnce"], no hi ha tracte.
  4. volumeMode idèntic.
  5. El selector del 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.

  1. 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: 20Gi de 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.

  1. L'estat Pending i el seu diagnòstic

Pending é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.

kubectl describe pvc postgres-reserves-dades -n rutas-norte-dev

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-dev

Un 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.

  1. 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: false

Això é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ó.

  1. El manifest definitiu de postgres-reserves

Ha 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-rapida
kubectl apply -f k8s/base/postgres-reserves-pvc.yaml
kubectl get pvc,pv -n rutas-norte-dev
NAME                                            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   1h

Bound 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-dades

L'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 permitted

securityContext.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.

  1. 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-dev

Creem 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

  1. 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-dev
NAME              STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS        AGE
altre-reclamant   Pending                                      rutasnorte-rapida   10s

Pending: 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 another

Aquest 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: 1
  • strategy: Recreate (acaba el pod vell abans de crear el nou)
  • i, si el driver ho suporta, ReadWriteOncePod a PV i PVC.

  1. Esborrar un PVC: finalitzadors i Terminating

Esborrar un PVC en ús és una operació que gairebé mai no surt com el principiant espera, i per bones raons.

kubectl delete pvc postgres-reserves-dades -n rutas-norte-dev
persistentvolumeclaim "postgres-reserves-dades" deleted

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}'; echo
NAME                      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 desapareix

Mai 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;"   # 2

Aquesta é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.

  1. 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-reserves

En 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:

kubectl scale deploy postgres-reserves -n rutas-norte-dev --replicas=1

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:

  1. 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
    
  2. Esbrina qui fa servir un PVC abans d'esborrar-lo, filtrant kubectl get pods -o json amb jq per .spec.volumes[]?.persistentVolumeClaim.claimName.
  3. Anomena els PVC per la seva funció, no per la seva mida: postgres-reserves-dades, no pvc-10gi. La mida canvia amb l'expansió (05-05); la funció no.
  4. 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:

  1. Insereix tres reserves fictícies i esborra el pod amb kubectl delete pod.
  2. Esborra el Deployment sencer i torna a aplicar-lo.
  3. 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 files

Resum 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

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