La lliçó anterior va acabar amb un diagnòstic precís: tots els volums que coneixíem —emptyDir, configMap, secret, projectedmoren amb el pod, i hostPath sobreviu però lliga les dades a un node concret i obre un forat de seguretat inacceptable. Perquè postgres-reserves conservi les reserves cal una cosa diferent: un objecte d'emmagatzematge amb vida pròpia al clúster, independent de qualsevol pod. Aquest objecte és el PersistentVolume (PV). En aquesta lliçó veuràs per què Kubernetes el va separar del consum, desmuntaràs la seva especificació camp a camp —inclòs el parany dels modes d'accés, que s'apliquen per node i no per pod—, en recorreràs les fases amb un diagrama d'estats, i crearàs un PersistentVolume real de 10 GiB per a la base de dades de Rutas Norte al teu clúster de pràctiques.

Contingut

  1. El problema real: qui administra i qui consumeix
  2. Què és un PersistentVolume
  3. Anatomia de l'objecte: capacity i volumeMode
  4. Els modes d'accés i el parany del node
  5. La política de reclamació: Retain, Delete i l'obsolet Recycle
  6. storageClassName, mountOptions i nodeAffinity
  7. Els backends possibles i quins modes admet cada família
  8. Cicle de vida: les fases del PV
  9. Què fer amb un PV en Released
  10. Pràctica: un PV de 10 GiB per a postgres-reserves
  11. Per què l'aprovisionament estàtic no escala

  1. El problema real: qui administra i qui consumeix

Abans de mirar un sol camp YAML, cal entendre quin problema organitzatiu resol aquesta abstracció, perquè si no, la parella PV/PVC sembla burocràcia innecessària.

A Rutas Norte hi ha dos papers diferents i no se solapen:

Equip de plataforma Equip d'aplicació
Sap Quines cabines de disc hi ha, quines classes de rendiment, en quines zones, quant costa cada GiB, com es fan els snapshots Que postgres-reserves necessita 10 GiB ràpids i que no els pot perdre
No sap Què necessita cada aplicació concreta Si a sota hi ha un EBS gp3, una LUN d'iSCSI o un directori del node
Escriu PersistentVolume i StorageClass PersistentVolumeClaim
Recurs Sense namespace: és del clúster Amb namespace: viu al costat de l'aplicació

Si el manifest de postgres-reserves hagués d'anomenar directament un disc d'AWS amb el seu identificador, passarien tres coses dolentes: el manifest deixaria de ser portable entre minikube, preproducció i producció; el desenvolupador necessitaria credencials i coneixement de la infraestructura; i qualsevol canvi de proveïdor obligaria a reescriure tots els manifests de la plataforma.

La solució de Kubernetes és un contracte amb dues cares:

flowchart TB
    subgraph PLATAFORMA["Equip de plataforma (recursos de cluster)"]
        SC["StorageClass<br/>rutasnorte-rapida<br/>(05-04)"]
        PV["PersistentVolume<br/>capacity: 10Gi<br/>accessModes: RWO<br/>reclaimPolicy: Retain"]
    end
    subgraph APP["Equip d'aplicacio (namespace rutas-norte-pro)"]
        PVC["PersistentVolumeClaim<br/>'vull 10Gi RWO<br/>de classe rutasnorte-rapida'"]
        POD["Pod postgres-reserves<br/>volumes:<br/>persistentVolumeClaim"]
    end
    CTRL["Controlador PersistentVolume<br/>(kube-controller-manager)"]

    PVC -->|"1. ho sol·licita"| CTRL
    PV -->|"2. candidats disponibles"| CTRL
    CTRL -->|"3. VINCULA (Bound)"| PVC
    SC -.->|"aprovisiona sota demanda"| PV
    POD -->|"4. munta"| PVC

La frase que resumeix la lliçó sencera: el PVC és la demanda, el PV és l'oferta, i el controlador els aparella. Qui desplega l'aplicació escriu únicament la demanda.

  1. Què és un PersistentVolume

Un PersistentVolume és un objecte de l'API que representa una porció concreta d'emmagatzematge ja existent al clúster, aprovisionada per un administrador o creada dinàmicament per una StorageClass.

Tres propietats defineixen la seva naturalesa i convé fixar-les d'entrada:

  1. No té namespace. És un recurs d'àmbit de clúster, com els nodes o les StorageClasses. Ho comproves amb el que vas aprendre a 02-06:
    kubectl api-resources | grep -E "^NAME|persistentvolume"
    
    NAME                    SHORTNAMES   APIVERSION   NAMESPACED   KIND
    persistentvolumeclaims  pvc          v1           true         PersistentVolumeClaim
    persistentvolumes       pv           v1           false        PersistentVolume
    
    El PVC que té namespace: pertany a l'aplicació. El PV no.
  2. El seu cicle de vida és independent del pod. Pots esborrar el Deployment sencer, el pod, fins i tot el PVC, i el PV pot continuar existint amb les dades a dins. Aquesta independència és justament el que faltava a 05-01.
  3. És un objecte declaratiu com qualsevol altre.apiVersion: v1, kind: PersistentVolume, metadata, spec i status, exactament el model de 01-06. Explora'l:
    kubectl explain pv.spec
    kubectl explain pv.spec.accessModes
    

  1. Anatomia de l'objecte: capacity i volumeMode

Un PV complet, amb tots els camps que importen, comentat:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-postgres-reserves-10gi       # sense namespace: es del cluster
  labels:
    app: postgres-reserves
    app.kubernetes.io/part-of: rutas-norte
    entorn: dev
    tipus-disc: ssd                    # util per als selectors del PVC (05-03)
spec:
  capacity:
    storage: 10Gi                      # mida declarada del volum
  volumeMode: Filesystem               # Filesystem (per defecte) o Block
  accessModes:
    - ReadWriteOnce                    # un sol NODE en lectura-escriptura
  persistentVolumeReclaimPolicy: Retain  # que fer quan s'alliberi
  storageClassName: rutasnorte-rapida  # la classe a la qual pertany
  mountOptions:                        # opcions passades al muntatge
    - noatime
  hostPath:                            # el backend: NOMES per a proves
    path: /data/postgres-reserves
    type: DirectoryOrCreate

capacity.storage

La mida del volum, en unitats de Kubernetes (Gi = 2^30 bytes, G = 10^9 bytes; fes servir sempre Gi). És una dada declarada, no mesurada: Kubernetes no comprova que el disc tingui realment aquesta mida. Amb un backend hostPath de proves pots escriure capacity: 10Gi sobre un directori d'un disc de 4 GiB i ningú no protestarà fins que s'ompli. La seva funció real és servir de criteri en l'aparellament amb el PVC.

Avui storage és l'únic recurs admès a capacity; atributs com les IOPS s'expressen mitjançant els parameters de la StorageClass (05-04).

volumeMode: Filesystem davant de Block

Filesystem (per defecte) Block
Què rep el pod Un directori muntat a mountPath Un dispositiu de blocs cru a devicePath
Formatatge El fa Kubernetes/CSI (ext4, xfs) No hi ha sistema de fitxers
Camp al contenidor volumeMounts volumeDevices
Ús típic El 99 % dels casos, inclòs PostgreSQL Bases de dades que gestionen la seva pròpia E/S, sistemes d'emmagatzematge distribuït

En mode bloc el contenidor no fa servir volumeMounts sinó volumeDevices, amb devicePath: /dev/xvda en lloc de mountPath: el dispositiu apareix cru, sense sistema de fitxers.

A Rutas Norte fem servir Filesystem a tot arreu. Block només té sentit si l'aplicació sap escriure directament sobre el dispositiu, i PostgreSQL no ho fa.

  1. Els modes d'accés i el parany del node

Els accessModes declaren de quines maneres es pot muntar el volum. Són quatre:

Mode Abreviatura Significat exacte
ReadWriteOnce RWO Lectura i escriptura des d'un sol node
ReadOnlyMany ROX Només lectura des de molts nodes
ReadWriteMany RWX Lectura i escriptura des de molts nodes
ReadWriteOncePod RWOP Lectura i escriptura des d'un sol pod a tot el clúster

El parany és a ReadWriteOnce, i gairebé tothom hi cau almenys una vegada. El "Once" es refereix a un node, no a un pod. Això vol dir que dos, tres o vint pods poden muntar simultàniament el mateix volum RWO en lectura i escriptura si el planificador els col·loca al mateix node. Cap capa de Kubernetes no ho impedirà.

La conseqüència per a una base de dades és directa i greu: si un rollout amb estratègia RollingUpdate aixeca un pod nou de postgres-reserves al mateix node abans d'acabar el vell, tindràs dos processos de PostgreSQL sobre el mateix directori de dades. Per això postgres-reserves porta strategy: Recreate des del mòdul 2, i per això existeix accessModes: ["ReadWriteOncePod"].

ReadWriteOncePod és l'única garantia que un sol pod pot fer servir el volum; el segon es queda en Pending amb un esdeveniment explícit. És un mode estable des de Kubernetes 1.29 i requereix que el driver CSI el suporti. Quan el driver ho permet, és l'elecció correcta per a una base de dades amb una única rèplica.

Dos advertiments més sobre els modes:

  • La llista és una declaració de capacitats, no una restricció activa. Un PV pot declarar ["ReadWriteOnce", "ReadOnlyMany"] per dir que admet tots dos usos; el PVC en triarà un.
  • El que un backend admet de veritat no depèn del que escriguis. Pots posar ReadWriteMany en un PV suportat per un disc de blocs d'AWS, i Kubernetes ho acceptarà sense piular; la fallada apareixerà en intentar muntar-lo des del segon node. La taula de l'apartat 7 recull què admet realment cada família.

  1. La política de reclamació: Retain, Delete i l'obsolet Recycle

persistentVolumeReclaimPolicy decideix què passa amb el volum i amb les dades quan el PVC que l'utilitzava s'esborra. És el camp amb més conseqüències de l'objecte.

Política En esborrar el PVC Estat del PV Es perden les dades?
Retain No es fa res amb el volum Passa a Released No. Queden intactes, esperant intervenció manual
Delete S'esborra el PV i el volum real subjacent Desapareix l'objecte PV Sí, de manera irreversible
Recycle Un rm -rf /volum/* i torna a Available Available

Recycle és obsolet i no s'ha de fer servir: només funcionava amb hostPath i NFS, esborrava amb un pod auxiliar sense cap garantia, i no encaixa amb el model CSI. El seu substitut és l'aprovisionament dinàmic (05-04): en lloc de reciclar un volum usat, se'n crea un de nou i net.

La decisió pràctica a Rutas Norte:

  • postgres-reservesRetain. L'escenari que cal evitar és que algú esborri per error el namespace rutas-norte-pro —recorda de 02-06 que kubectl delete ns s'emporta els PVC— i que això destrueixi les dades de tots els clients. Amb Retain, el PVC desapareix però el volum continua allà i es pot recuperar.
  • Volums reconstruïbles (memòria cau, espais de treball) → Delete. Recrear és més barat que netejar a mà, i Delete evita facturar discos orfes.

Un detall important que confon molt: en l'aprovisionament dinàmic el PV hereta la política de la StorageClass, i la majoria de les classes per defecte dels núvols fan servir Delete. És a dir, si no fas res, esborrar un PVC de producció esborra el disc. Es comprova amb kubectl get storageclass -o custom-columns=NOM:.metadata.name,POLITICA:.reclaimPolicy.

I en un PV ja existent es pot canviar en calent, que és la maniobra defensiva estàndard abans de tocar res:

kubectl patch pv pv-postgres-reserves-10gi \
  -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'

  1. storageClassName, mountOptions i nodeAffinity

storageClassName

És una etiqueta d'aparellament: un PVC només es vincula a un PV la classe del qual coincideixi exactament. Tres situacions que cal distingir amb cura (i que tornarem a veure a 05-04):

Al PV Al PVC Resultat
storageClassName: rapida storageClassName: rapida Es poden aparellar
Camp absent o "" storageClassName: "" Es poden aparellar (PV estàtic "sense classe")
Camp absent Camp absent El PVC farà servir la classe per defecte i no mirarà aquest PV

Un PV estàtic pot portar un storageClassName que no correspon a cap StorageClass existent (per exemple manual). És una pràctica habitual i perfectament vàlida: la cadena només es fa servir per casar oferta i demanda.

mountOptions

Opcions que es passen tal qual al muntatge del sistema de fitxers —noatime i nodiratime per reduir escriptures, o hard i nfsvers=4.1 en NFS—. Kubernetes no les valida: si el backend no les admet, el muntatge falla i el pod es queda en ContainerCreating amb un esdeveniment de FailedMount.

nodeAffinity

Restringeix des de quins nodes es pot muntar el volum. És obligatori en els volums de tipus local, perquè el disc està físicament en una màquina i el planificador ho ha de saber per no col·locar el pod on el volum no existeix.

spec:
  local:
    path: /mnt/discs/ssd1
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values: ["rutas-norte"]      # el node de minikube

Aquest és un cas d'emmagatzematge condicionant la planificació, un tema que s'aprofundeix a 06-05. Al núvol el mecanisme és idèntic però la clau sol ser topology.kubernetes.io/zone: un disc creat a eu-west-1a només es pot muntar des de nodes d'aquesta zona, i d'aquí neix el problema del volumeBindingMode que resoldrà 05-04.

  1. Els backends possibles i quins modes admet cada família

L'spec del PV conté exactament una clau de backend, que defineix d'on surt l'emmagatzematge de veritat.

hostPath i local: per a proves i per a discos locals

hostPath com a backend d'un PV té els mateixos perills descrits a 05-01 i només s'ha de fer servir en clústers d'un sol node com el teu minikube. local és el seu cosí seriós: també fa servir un disc de la màquina, però és un tipus pensat per a producció, respecta la planificació gràcies a la nodeAffinity obligatòria i no permet rutes arbitràries del sistema. Tot i així, un volum local no sobreviu a la pèrdua del node: la redundància és responsabilitat de l'aplicació (replicació de PostgreSQL, per exemple).

NFS: el clàssic compartit

spec:
  capacity: { storage: 100Gi }
  accessModes: ["ReadWriteMany"]     # diversos nodes alhora: aquesta es la seva gracia
  nfs:
    server: nas.rutasnorte.example
    path: /exports/adjunts
  mountOptions: ["hard", "nfsvers=4.1"]

NFS és la via més senzilla per aconseguir ReadWriteMany, útil per exemple perquè les tres rèpliques de botiga-web comparteixin un directori d'imatges de trajectes. No és adequat per al fitxer de dades de PostgreSQL: el bloqueig de fitxers sobre NFS és una font clàssica de corrupció.

Controladors CSI: el que es fa servir en producció

En un clúster gestionat no escriuràs gairebé mai un PV a mà; els crearà el driver CSI del proveïdor. Quan els inspeccionis, veuràs una clau csi amb driver: ebs.csi.aws.com, el volumeHandle (l'identificador del disc real al proveïdor), el fsType i uns volumeAttributes.

L'arquitectura de CSI s'estudia a fons a 05-05.

Quins modes admet realment cada família

Taula orientativa; la veritat última la marca el driver i la seva versió.

Família RWO ROX RWX RWOP Nota
hostPath (un node) Tot "funciona" perquè només hi ha un node. No és producció
local No Lligat al node per nodeAffinity
NFS L'opció clàssica per compartir
Discos de blocs al núvol (EBS, PD, Azure Disk) No No Un disc només es connecta a una màquina
Fitxers compartits al núvol (EFS, Filestore, Azure Files) Més car i amb més latència
Ceph RBD No Blocs
CephFS Sistema de fitxers distribuït

La regla que se'n deriva: si necessites ReadWriteMany, necessites un sistema de fitxers compartit, i això és una decisió d'arquitectura i de cost, no un camp YAML. Per a postgres-reserves no ens cal: una base de dades amb una rèplica vol ReadWriteOnce (o millor ReadWriteOncePod) sobre un disc de blocs ràpid.

  1. Cicle de vida: les fases del PV

El PV té un camp status.phase amb quatre valors possibles:

stateDiagram-v2
    [*] --> Available: es crea el PV (estatic)<br/>o l'aprovisiona la StorageClass
    Available --> Bound: el controlador l'aparella<br/>amb un PVC compatible
    Bound --> Released: s'esborra el PVC<br/>(les dades SEGUEIXEN alli)
    Released --> [*]: reclaimPolicy Delete<br/>-> s'esborra PV i volum
    Released --> Available: intervencio manual<br/>(reclaimPolicy Retain)
    Available --> Failed: fallada en l'aprovisionament<br/>o en la recuperacio
    Bound --> Failed: fallada del backend
    Failed --> [*]: correccio manual
Fase Significat Què fer
Available Lliure i disponible per ser vinculat Res; espera un PVC
Bound Vinculat a un PVC concret Estat normal d'operació
Released El PVC s'ha esborrat; el PV conserva les dades però no es pot reutilitzar tal qual Recuperar les dades o alliberar-lo a mà (apartat 9)
Failed La recuperació automàtica ha fallat Investigar amb kubectl describe pv

Comprovació al clúster:

kubectl get pv
NAME                          CAPACITY  ACCESS MODES  RECLAIM POLICY  STATUS      CLAIM                                     STORAGECLASS        AGE
pv-postgres-reserves-10gi     10Gi      RWO           Retain          Bound       rutas-norte-dev/postgres-reserves-dades   rutasnorte-rapida   4m
pv-adjunts-50gi               50Gi      RWX           Delete          Available                                             rutasnorte-estandar 4m

La columna CLAIM et diu, per a cada PV, quin PVC de quin namespace el té agafat. És la vista d'un cop d'ull que més es fa servir en el dia a dia.

  1. Què fer amb un PV en Released

Aquesta és una situació operativa que apareix sempre i desconcerta la primera vegada. Algú esborra el PVC de postgres-reserves; com que la política és Retain, el PV passa a Released. Crees de nou el mateix PVC, amb el mateix nom i la mateixa petició… i es queda en Pending per sempre, tot i que el PV que vol és allà amb 10Gi lliures.

La causa és que el PV conserva a spec.claimRef la referència al PVC anterior, inclòs el seu uid:

kubectl get pv pv-postgres-reserves-10gi -o jsonpath='{.spec.claimRef}'; echo
{"kind":"PersistentVolumeClaim","namespace":"rutas-norte-dev",
 "name":"postgres-reserves-dades","uid":"1c5b7f2a-3d9e-4c11-9f88-0a2b6d4e7c31"}

Encara que el PVC nou es digui igual, el seu uid és diferent, així que no casa. I aquest bloqueig és deliberat: és la protecció que impedeix que les dades d'un inquilí acabin muntades per error al pod d'un altre.

El procediment correcte té tres camins, en ordre de preferència:

a) Reutilitzar el volum per al mateix propòsit (el cas d'una recuperació després d'un esborrat accidental). S'elimina la referència caducada i el PV torna a Available:

kubectl patch pv pv-postgres-reserves-10gi \
  --type=json -p='[{"op": "remove", "path": "/spec/claimRef"}]'

kubectl get pv pv-postgres-reserves-10gi
NAME                        CAPACITY  ACCESS MODES  RECLAIM POLICY  STATUS      CLAIM   STORAGECLASS        AGE
pv-postgres-reserves-10gi   10Gi      RWO           Retain          Available           rutasnorte-rapida   1h

Ara qualsevol PVC compatible el pot agafar. Les dades continuen a dins.

b) Preassignar-lo a un PVC concret, que és més segur que deixar-lo solt. S'edita el claimRef deixant namespace i name però sense uid: el PV només acceptarà aquest PVC exacte.

spec:
  claimRef:
    apiVersion: v1
    kind: PersistentVolumeClaim
    namespace: rutas-norte-dev
    name: postgres-reserves-dades
    # sense uid: s'emplenara en vincular-se

c) Descartar-lo. Si les dades ja no valen, s'esborra el PV i es neteja el volum real a mà (amb Retain, esborrar l'objecte PV no esborra les dades del backend: al núvol el disc es continua facturant).

Consell operatiu: abans de reutilitzar un PV Released que contingui dades, fes-ne una còpia. L'apartat 10 crea el volum; la còpia de seguretat s'estudia a 05-06, però la regla s'aplica des d'avui.

  1. Pràctica: un PV de 10 GiB per a postgres-reserves

Anem a crear el volum que salda el deute del mòdul. A minikube el suport serà un directori del node, i aquí hostPath és acceptable per ser un clúster d'un únic node i de proves; el manifest porta un comentari que ho deixa explícit perquè ningú no el copiï a producció.

Primer preparem el directori al node:

minikube ssh -p rutas-norte -- "sudo mkdir -p /data/postgres-reserves && \
  sudo chmod 777 /data/postgres-reserves && ls -ld /data/postgres-reserves"

A minikube, /data és un directori que persisteix als reinicis de la màquina virtual, a diferència de la major part del sistema de fitxers. Per això és el lloc correcte per a aquesta pràctica.

Ara el PersistentVolume:

# k8s/base/pv-postgres-reserves.yaml
#
# AVIS: el backend hostPath es valid UNICAMENT al minikube de practiques,
# que es un cluster d'un sol node. En pre i pro aquest PV el crea el driver CSI
# de forma dinamica a partir de la StorageClass rutasnorte-rapida (veure 05-04).
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-postgres-reserves-10gi
  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
    tipus-disc: ssd
spec:
  capacity:
    storage: 10Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  # Retain: si algu esborra el PVC (o el namespace sencer), les dades
  # de reserves i clients NO es destrueixen. Decisio deliberada.
  persistentVolumeReclaimPolicy: Retain
  storageClassName: rutasnorte-rapida
  mountOptions:
    - noatime
  hostPath:
    path: /data/postgres-reserves
    type: DirectoryOrCreate
kubectl apply -f k8s/base/pv-postgres-reserves.yaml
kubectl get pv pv-postgres-reserves-10gi
persistentvolume/pv-postgres-reserves-10gi created

NAME                        CAPACITY  ACCESS MODES  RECLAIM POLICY  STATUS      CLAIM  STORAGECLASS        AGE
pv-postgres-reserves-10gi   10Gi      RWO           Retain          Available          rutasnorte-rapida   3s

Available: existeix, està lliure i espera un PVC. Fixa't que no hi ha cap namespace a la sortida: és un recurs del clúster.

Vista detallada:

kubectl describe pv pv-postgres-reserves-10gi
Name:              pv-postgres-reserves-10gi
Finalizers:        [kubernetes.io/pv-protection]
StorageClass:      rutasnorte-rapida
Status:            Available
Claim:
Reclaim Policy:    Retain
Access Modes:      RWO
VolumeMode:        Filesystem
Capacity:          10Gi
Node Affinity:     <none>
Source:
    Type:          HostPath (bare host directory volume)
    Path:          /data/postgres-reserves
    HostPathType:  DirectoryOrCreate
Events:            <none>

Dos detalls que val la pena llegir:

  • Finalizers: [kubernetes.io/pv-protection]. És el mecanisme de 01-06: impedeix que el PV s'esborri mentre estigui vinculat a un PVC. Si esborres un PV en ús, l'objecte queda en Terminating fins que deixa d'estar-ho.
  • Claim: buit. Ningú no l'ha reclamat encara. Aquest buit l'omplirà el PVC de la lliçó següent.

Ara comprova el que encara no passa: el pod de postgres-reserves continua sense fer-lo servir. Un PV no es munta sol; cal un PVC que el reclami. Això és exactament el contingut de 05-03.

  1. Per què l'aprovisionament estàtic no escala

El que acabes de fer s'anomena aprovisionament estàtic: un humà crea el PV a mà, per endavant, i espera que algú el reclami. Funciona, i per a un directori a minikube o per a una cabina d'emmagatzematge fixa en un centre de dades propi és perfectament raonable. Però com a model general falla per quatre bandes:

Problema Per què fa mal
Feina manual al camí crític Cada equip que desplega alguna cosa amb estat ha d'esperar que un administrador li creï el PV. La plataforma es converteix en un coll d'ampolla
Ajust de mides impossible Si prepares PV de 10, 20 i 100 GiB, un PVC de 12 GiB es vincularà al de 20 i malgastaràs 8 GiB; i si en demana 150 no hi haurà candidat encara que hi hagi terabytes lliures
Volums orfes Els PV Released amb Retain s'acumulen; al núvol continuen facturant encara que ningú no els faci servir
Topologia a cegues L'administrador crea el PV en una zona sense saber on acabarà el pod; l'aparellament pot deixar el pod impossible de planificar

La solució és invertir l'ordre: en lloc de crear volums per si de cas, crear el volum exacte en el moment en què algú el demana. Això és l'aprovisionament dinàmic, i l'objecte que el fa possible és la StorageClass, que ja hem anomenat a storageClassName sense explicar-la. La veuràs a 05-04 i la maquinària que hi ha al darrere, CSI, a 05-05.

La bona notícia és que el que estàs aprenent no canvia: en aprovisionament dinàmic continuen existint PV amb exactament aquests camps —capacitat, modes d'accés, política de reclamació, afinitat de node—; l'única diferència és qui els escriu.

Errors Comuns i Consells

Error Símptoma Solució
Creure que ReadWriteOnce significa "un pod" Dos pods escriuen alhora i corrompen les dades Significa un node. Fes servir ReadWriteOncePod i strategy: Recreate
Declarar ReadWriteMany sobre un disc de blocs El pod del segon node no arrenca: Multi-Attach error Kubernetes no valida els modes; consulta la taula del backend
Deixar la política Delete en un volum de dades S'esborra un PVC i desapareixen les dades Retain en tot el que guardi dades de negoci
Intentar reutilitzar un PV Released El PVC nou es queda en Pending sense explicació clara Elimina spec.claimRef amb kubectl patch --type=json
Crear un PV local sense nodeAffinity El pod es planifica en un node sense el disc: FailedMount La nodeAffinity és obligatòria a local
Suposar que capacity mesura alguna cosa El volum s'omple molt abans del que s'ha declarat capacity és una dada declarada, no mesurada
hostPath en un clúster de diversos nodes Les dades "canvien" segons on caigui el pod Només en clústers d'un node; en producció, CSI
Posar mountOptions que el backend no admet Pod encallat en ContainerCreating, esdeveniment FailedMount Comprova abans les opcions del driver
Buscar el PV amb -n <namespace> "No resources found" tot i que existeix El PV no té namespace

Consells operatius:

  1. Etiqueta els PV. Sense etiquetes no els pots fer servir amb selector des d'un PVC ni filtrar-los en una auditoria. Fes servir les mateixes convencions que la resta de Rutas Norte, inclosa app.kubernetes.io/part-of.
  2. Auditoria ràpida de risc — quins volums es destruirien en esborrar el seu PVC:
    kubectl get pv -o custom-columns=\
    NOM:.metadata.name,POLITICA:.spec.persistentVolumeReclaimPolicy,\
    ESTAT:.status.phase,CLAIM:.spec.claimRef.name | grep Delete
    
  3. Abans de qualsevol operació delicada, canvia la política a Retain amb kubectl patch. És reversible i ha salvat moltes bases de dades.
  4. kubectl get pv,pvc -A en una sola ordre et dona la foto completa d'oferta i demanda del clúster.

Exercicis

Exercici 1: el PV de la base de dades i les seves fases

Crea el PersistentVolume pv-postgres-reserves-10gi de l'apartat 10 al teu minikube. Després:

  1. Comprova que és un recurs sense namespace i que la seva fase és Available.
  2. Escriu un fitxer dins del directori del node i verifica que hi és.
  3. Canvia la seva política de reclamació a Delete amb kubectl patch i torna a deixar-la en Retain, comprovant el canvi a cada pas.
  4. Intenta esborrar el PV i explica quin finalitzador apareix a kubectl describe.

Exercici 2: dissenyar tres PV per a Rutas Norte

L'equip de plataforma ha de preparar l'emmagatzematge de tres necessitats. Per a cadascuna, decideix capacity, accessModes, persistentVolumeReclaimPolicy, volumeMode i el tipus de backend, i justifica cada elecció:

  • A. Les dades de postgres-reserves en producció: 200 GiB, una sola instància, contenen dades personals de clients, rendiment crític.
  • B. Un directori d'imatges de trajectes que les tres rèpliques de botiga-web han de llegir simultàniament, i que un procés d'administració actualitza un cop al dia: 20 GiB.
  • C. Un espai de treball de 500 GiB perquè informes-ocupacio generi l'informe nocturn i el borri en acabar.

Escriu el manifest del cas B.

Exercici 3: rescatar un PV bloquejat

Reprodueix i resol l'incident clàssic:

  1. Crea un PV pv-practica-rescat d'1 GiB amb Retain, classe manual i suport hostPath a /data/rescat.
  2. Crea un PVC anomenat dades-practica a rutas-norte-dev que el vinculi i escriu un fitxer testimoni des d'un pod.
  3. Esborra el PVC i observa la fase del PV.
  4. Torna a crear el mateix PVC. Què passa i per què?
  5. Arregla-ho sense perdre el fitxer testimoni.

Solucions

Exercici 1

# 1
kubectl apply -f k8s/base/pv-postgres-reserves.yaml
kubectl get pv -n rutas-norte-dev pv-postgres-reserves-10gi   # el -n s'IGNORA
kubectl api-resources --namespaced=false | grep persistentvolumes
kubectl get pv pv-postgres-reserves-10gi -o jsonpath='{.status.phase}'; echo
persistentvolumes    v1    false    PersistentVolume
Available
# 2
minikube ssh -p rutas-norte -- "echo 'testimoni del PV' | sudo tee /data/postgres-reserves/PROVA.txt"

# 3
for P in Delete Retain; do
  kubectl patch pv pv-postgres-reserves-10gi \
    -p "{\"spec\":{\"persistentVolumeReclaimPolicy\":\"$P\"}}"
  kubectl get pv pv-postgres-reserves-10gi \
    -o jsonpath='{.spec.persistentVolumeReclaimPolicy}'; echo
done

# 4
kubectl describe pv pv-postgres-reserves-10gi | grep -i finalizers
Delete
Retain
Finalizers:  [kubernetes.io/pv-protection]

Mentre el PV estigui Available l'esborrat és immediat: el finalitzador es retira sol. Si estigués Bound a un PVC, l'objecte quedaria en Terminating fins que el PVC desaparegués. És la protecció de volum en ús, germana de la pvc-protection que veuràs a 05-03.

Exercici 2

Cas capacity accessModes reclaimPolicy volumeMode Backend
A. postgres-reserves pro 200Gi ReadWriteOncePod Retain Filesystem Disc de blocs SSD per CSI
B. Imatges de trajectes 20Gi ReadWriteMany Retain Filesystem NFS o fitxer compartit al núvol
C. Espai d'informes-ocupacio 500Gi ReadWriteOnce Delete Filesystem Disc de blocs estàndard

Justificacions:

  • A. Una sola instància de PostgreSQL: ReadWriteOncePod garanteix que cap segon pod no el pugui muntar, ni tan sols al mateix node, cosa que elimina el risc de corrupció per dos processos. Retain perquè conté dades personals de clients i un esborrat accidental del PVC no les pot destruir. Disc de blocs per rendiment: els fitxers compartits afegeixen una latència inacceptable per a una base de dades.
  • B. Diversos lectors en nodes diferents exigeixen ReadWriteMany, i això obliga a un sistema de fitxers compartit: un disc de blocs no serveix aquí per molt que s'escrigui RWX al YAML. Retain perquè les imatges són contingut de negoci recuperable però costós de regenerar.
  • C. Dades completament reconstruïbles: Delete evita pagar 500 GiB orfes cada nit. En realitat, el cas C és el candidat perfecte per a un volum efímer genèric (05-01), que ni tan sols requereix crear el PV.
# k8s/base/pv-imatges-trajectes.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-imatges-trajectes-20gi
  labels:
    app: botiga-web
    app.kubernetes.io/name: imatges-trajectes
    app.kubernetes.io/part-of: rutas-norte
    entorn: pro
spec:
  capacity:
    storage: 20Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteMany          # les 3 repliques de botiga-web llegeixen alhora
  persistentVolumeReclaimPolicy: Retain
  storageClassName: rutasnorte-compartida
  mountOptions:
    - hard
    - nfsvers=4.1
  nfs:
    server: nas.rutasnorte.example
    path: /exports/imatges-trajectes

Exercici 3

# 1
minikube ssh -p rutas-norte -- "sudo mkdir -p /data/rescat && sudo chmod 777 /data/rescat"

kubectl apply -f - <<'EOF'
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-practica-rescat
  labels: { app: practica, entorn: dev }
spec:
  capacity: { storage: 1Gi }
  accessModes: ["ReadWriteOnce"]
  persistentVolumeReclaimPolicy: Retain
  storageClassName: manual
  hostPath: { path: /data/rescat, type: DirectoryOrCreate }
EOF

# 2
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: dades-practica
  namespace: rutas-norte-dev
  labels: { app: practica, entorn: dev }
spec:
  accessModes: ["ReadWriteOnce"]
  storageClassName: manual
  resources: { requests: { storage: 1Gi } }
EOF

kubectl get pv pv-practica-rescat
minikube ssh -p rutas-norte -- "echo 'RESERVA-2026-0815-MARTA' | sudo tee /data/rescat/testimoni.txt"

# 3
kubectl delete pvc dades-practica -n rutas-norte-dev
kubectl get pv pv-practica-rescat
NAME                  CAPACITY  ACCESS MODES  RECLAIM POLICY  STATUS     CLAIM                            STORAGECLASS  AGE
pv-practica-rescat    1Gi       RWO           Retain          Released   rutas-norte-dev/dades-practica   manual        2m
# 4
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: dades-practica
  namespace: rutas-norte-dev
spec:
  accessModes: ["ReadWriteOnce"]
  storageClassName: manual
  resources: { requests: { storage: 1Gi } }
EOF

kubectl get pvc dades-practica -n rutas-norte-dev
kubectl describe pvc dades-practica -n rutas-norte-dev | tail -5
NAME             STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
dades-practica   Pending                                      manual         20s

Es queda en Pending. Per què: el PV està en Released, no en Available, perquè conserva un spec.claimRef que apunta al PVC anterior amb el seu uid. El PVC nou es diu igual però té un altre uid, així que el controlador no el considera candidat. A més, un PV en fase Released mai no s'ofereix per vincular.

# 5
kubectl get pv pv-practica-rescat -o jsonpath='{.spec.claimRef.uid}'; echo
kubectl patch pv pv-practica-rescat \
  --type=json -p='[{"op": "remove", "path": "/spec/claimRef"}]'
kubectl get pvc dades-practica -n rutas-norte-dev
minikube ssh -p rutas-norte -- "cat /data/rescat/testimoni.txt"
NAME             STATUS   VOLUME               CAPACITY   ACCESS MODES   STORAGECLASS   AGE
dades-practica   Bound    pv-practica-rescat   1Gi        RWO            manual         2m

RESERVA-2026-0815-MARTA

El fitxer testimoni continua intacte: Retain va complir la seva funció. Per netejar, esborra el PVC, repeteix el patch del claimRef i esborra el PV.

Conclusió

Has incorporat l'abstracció que sosté tot l'emmagatzematge de Kubernetes, i el primer que has entès és per què existeix: separa qui administra l'emmagatzematge —l'equip de plataforma, que escriu PersistentVolumes i StorageClasses, recursos del clúster— de qui el consumeix —l'equip d'aplicació, que escriu un PersistentVolumeClaim al seu namespace sense saber què hi ha a sota—. Gràcies a aquesta separació, el manifest de postgres-reserves és idèntic a minikube i en producció encara que a sota hi hagi un directori del node o un SSD d'un núvol.

Coneixes l'especificació completa del PV. La capacity, que és declarada i no mesurada. El volumeMode, amb Filesystem per al 99 % dels casos i Block per a qui gestioni la seva pròpia E/S. Els quatre modes d'accés, i sobretot el parany que més car es paga: ReadWriteOnce significa un node, no un pod, de manera que dos pods a la mateixa màquina poden escriure alhora sobre els fitxers de PostgreSQL; la garantia real és ReadWriteOncePod, estable des de 1.29. La política de reclamació, amb Retain per a tot el que guardi dades de negoci, Delete per al que és reconstruïble —i l'avís que les classes per defecte dels núvols vénen en Delete, així que esborrar un PVC esborra el disc— i Recycle obsolet i substituït per l'aprovisionament dinàmic. I storageClassName com a etiqueta d'aparellament, mountOptions sense validació prèvia i nodeAffinity obligatòria als volums local, primer exemple d'emmagatzematge que condiciona la planificació.

Saps quins backends existeixen i què admet cada família de veritat —hostPath i local per a proves i discos locals, NFS com a via senzilla a ReadWriteMany, i els controladors CSI en producció— i tens clar que escriure ReadWriteMany sobre un disc de blocs no el converteix en compartit. Domines les fases (Available, Bound, Released, Failed) i, molt en particular, l'operació que apareix sempre en un incident real: un PV atrapat en Released pel seu claimRef caducat, que es rescata eliminant aquest camp amb kubectl patch --type=json sense perdre ni un sol byte.

I has creat el volum: pv-postgres-reserves-10gi, Retain, ReadWriteOnce, classe rutasnorte-rapida, esperant en fase Available amb la columna CLAIM buida. Aquest buit és l'argument de la lliçó següent. Un PersistentVolume no es munta sol: algú l'ha de reclamar des del namespace de l'aplicació, i aquest algú és l'objecte que converteix per fi en persistent la base de dades de Rutas Norte. El construïm a Reclamacions de Volums Persistents, on a més farem la prova que portem tot el mòdul prometent: crear una reserva, esborrar el pod i trobar-la intacta.

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