Portem tres lliçons escrivint storageClassName: rutasnorte-rapida sense haver creat mai aquesta classe, i funcionava perquè l'aparellament estàtic només comparava cadenes de text. S'ha acabat el truc. La StorageClass és l'objecte que converteix aquesta cadena en una cosa amb conseqüències reals: el clúster crea el volum exacte en el moment en què algú el demana, amb el tipus de disc, la política de reclamació i les capacitats que la classe defineix. És el salt de l'aprovisionament estàtic al dinàmic, i canvia completament l'operació diària: ningú no torna a crear un PersistentVolume a mà. En aquesta lliçó desmuntaràs la StorageClass camp a camp —amb especial atenció a volumeBindingMode, responsable d'una de les fallades més cares i difícils de diagnosticar al núvol—, entendràs la classe per defecte i la diferència entre storageClassName: "" i ometre el camp, dissenyaràs les dues classes de Rutas Norte i veuràs aparèixer un PersistentVolume tot sol, sense escriure'l.

Contingut

  1. De l'aprovisionament estàtic al dinàmic
  2. Anatomia de la StorageClass
  3. provisioner: qui crea el volum
  4. parameters: els detalls del disc
  5. reclaimPolicy i allowVolumeExpansion
  6. volumeBindingMode: el camp que evita un desastre
  7. La classe per defecte i els seus paranys
  8. storageClassName: "" davant d'ometre el camp
  9. Les classes que porten minikube i els núvols gestionats
  10. Les classes d'emmagatzematge de Rutas Norte
  11. Pràctica: un PVC que crea el seu propi volum
  12. Migrar un PVC d'una classe a una altra

  1. De l'aprovisionament estàtic al dinàmic

Repassem el flux que has practicat fins ara i comparem-lo amb el que construiràs:

flowchart LR
    subgraph EST["ESTATIC (05-02, 05-03)"]
        direction TB
        A1["Un huma crea el PV<br/>per endavant"] --> A4{"Hi ha PV compatible<br/>quan arriba el PVC?"}
        A4 -->|"Si"| A5["Bound"]
        A4 -->|"No"| A6["Pending<br/>fins que algu actui"]
    end
    subgraph DIN["DINAMIC (aquesta llico)"]
        direction TB
        B1["L'equip crea el PVC<br/>amb storageClassName"] --> B2["L'aprovisionador<br/>crea el volum REAL"]
        B2 --> B3["Es crea el PV<br/>automaticament"] --> B4["Bound, en segons"]
    end

Els quatre problemes de l'estàtic que vam enumerar a 05-02 desapareixen de cop:

Problema de l'estàtic Com el resol el dinàmic
Feina manual al camí crític L'equip d'aplicació s'autoserveix: crea el PVC i el volum apareix
Malbaratament per ajust de mides El volum es crea de la mida exacta demanada
Volums orfes Amb reclaimPolicy: Delete, el volum es destrueix en esborrar el PVC
Topologia a cegues volumeBindingMode: WaitForFirstConsumer crea el volum on el pod hi cap

El canvi de mentalitat és aquest: la plataforma deixa d'oferir volums concrets i passa a oferir catàleg de serveis. "Tenim disc ràpid amb retenció i disc estàndard barat; demana el que necessitis i el clúster te'l crea."

  1. Anatomia de la StorageClass

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: rutasnorte-rapida            # el nom es el que s'escriu al PVC
  labels: { app.kubernetes.io/part-of: rutas-norte }
  annotations: { storageclass.kubernetes.io/is-default-class: "false" }
provisioner: ebs.csi.aws.com         # QUI crea el volum
parameters:                          # COM el crea (especific del provisioner)
  type: gp3
  iops: "6000"
  throughput: "250"
  encrypted: "true"
  fsType: ext4
reclaimPolicy: Retain                # que hereten els PV creats
allowVolumeExpansion: true           # es pot fer gran despres?
volumeBindingMode: WaitForFirstConsumer
mountOptions: ["noatime"]
allowedTopologies:                   # (opcional) restringir zones
  - matchLabelExpressions:
      - { key: topology.kubernetes.io/zone, values: ["eu-west-1a", "eu-west-1b"] }

Dues característiques estructurals abans d'entrar en els camps:

  1. És un recurs de clúster (sense namespace), com el PersistentVolume. El gestiona l'equip de plataforma.
  2. És pràcticament immutable. Llevat d'un grapat de camps (allowVolumeExpansion, les anotacions), una StorageClass no es pot modificar un cop creada: s'ha d'esborrar i tornar a crear. I compte, esborrar una StorageClass no afecta els PV ja creats, que continuen funcionant amb la configuració que tenien.

  1. provisioner: qui crea el volum

El provisioner identifica el component que atendrà les peticions d'aquesta classe. És l'única part veritablement obligatòria, juntament amb el nom.

Provisioner Qui l'implementa On es fa servir
k8s.io/minikube-hostpath minikube El teu clúster de pràctiques
rancher.io/local-path local-path-provisioner kind, k3s, clústers de desenvolupament
ebs.csi.aws.com Driver CSI d'AWS EBS EKS
disk.csi.azure.com Driver CSI d'Azure Disk AKS
pd.csi.storage.gke.io Driver CSI de Google PD GKE
efs.csi.aws.com Driver CSI d'AWS EFS EKS, quan cal ReadWriteMany
kubernetes.io/no-provisioner Ningú Classes per a volums local, que es creen a mà

Dos casos especials que convé reconèixer. kubernetes.io/no-provisioner es fa servir per a classes que no aprovisionen res, cosa que sona absurda fins que veus per a què serveix: agrupar volums local creats a mà i aprofitar el volumeBindingMode: WaitForFirstConsumer perquè la planificació respecti la ubicació del disc. I els provisioners amb prefix kubernetes.io/ (com kubernetes.io/aws-ebs) són els antics controladors in-tree, eliminats del codi de Kubernetes: en un clúster 1.30+ no funcionen, i si heretes manifests amb aquests noms cal migrar-los al driver CSI equivalent.

Per saber quins drivers CSI hi ha realment instal·lats al teu clúster: kubectl get csidrivers.

  1. parameters: els detalls del disc

parameters és un mapa opac per a Kubernetes: es passa tal qual al provisioner, que és qui l'interpreta. Per això les claus vàlides depenen completament del driver, i una clau mal escrita no dona error en crear la classe, sinó en crear el primer PVC.

Exemples reals per proveïdor:

# AWS EBS: SSD de proposit general amb IOPS i rendiment a mida
parameters:
  type: gp3               # gp2, gp3, io1, io2, st1, sc1
  iops: "6000"
  throughput: "250"       # MiB/s
  encrypted: "true"
  kmsKeyId: "arn:aws:kms:eu-west-1:111122223333:key/abcd-1234"
  fsType: ext4

A Google Cloud les claus equivalents són type (de pd-standard a pd-extreme) i replication-type: regional-pd, que replica el disc entre dues zones; a Azure són skuName (Standard_LRS, StandardSSD_LRS, Premium_LRS, UltraSSD_LRS) i cachingmode.

Un paràmetre que no ha de faltar en cap classe de Rutas Norte és el de xifratge en repòs. postgres-reserves guarda nom, DNI, telèfon i correu dels clients; aquest disc ha d'estar xifrat, igual que ja ho estan els Secrets a etcd des de 03-02. A AWS és encrypted: "true"; a Azure i Google el xifratge en repòs ve activat per defecte i el que es configura és la clau.

  1. reclaimPolicy i allowVolumeExpansion

reclaimPolicy

Els PV creats per aquesta classe hereten aquesta política. Els valors són els de 05-02: Retain o Delete. I el valor per defecte, si no el poses, és Delete.

Això mereix un avís en majúscules: les classes per defecte de tots els núvols gestionats fan servir Delete. És a dir, en un clúster acabat de crear, si el teu equip esborra el PVC d'una base de dades —o el namespace que el conté—, el disc es destrueix. Comprova-ho sempre primer de tot en arribar a un clúster nou:

kubectl get sc -o custom-columns=\
NOM:.metadata.name,POLITICA:.reclaimPolicy,EXPANSIO:.allowVolumeExpansion,\
MODE:.volumeBindingMode
NOM        POLITICA   EXPANSIO    MODE
standard   Delete     true        Immediate
gp2        Delete     true        WaitForFirstConsumer

Com que la classe és gairebé immutable, no li pots canviar la política; el que es fa és crear una classe pròpia amb Retain per a les dades que importen. Això és exactament el que farem amb rutasnorte-rapida.

allowVolumeExpansion

Declara si els PVC d'aquesta classe es poden fer més grans després de creats. És dels pocs camps que que es poden modificar en calent, amb kubectl patch sc rutasnorte-rapida -p '{"allowVolumeExpansion": true}'.

Posa'l a true en tota classe destinada a dades que creixen. El cost és zero i l'alternativa —migrar les dades a un volum més gran amb la base de dades parada— és una operació de matinada. La mecànica de l'expansió es detalla a 05-05, inclòs el fet que no es pot reduir.

  1. volumeBindingMode: el camp que evita un desastre

És el camp menys comprès de la StorageClass i el que més incidents causa al núvol. Té dos valors:

Mode Quan es crea i vincula el volum Conseqüència
Immediate Així que es crea el PVC, sense saber on anirà el pod El volum pot néixer on el pod no hi cap
WaitForFirstConsumer Quan apareix el primer pod que fa servir el PVC El volum neix on el planificador ha decidit posar el pod

El cas concret que ho explica

Rutas Norte té el seu clúster de producció repartit en tres zones de disponibilitat: eu-west-1a, eu-west-1b i eu-west-1c. Amb volumeBindingMode: Immediate:

sequenceDiagram
    participant Dev as Equip
    participant API as apiserver
    participant Prov as Aprovisionador CSI
    participant Sched as Planificador
    Dev->>API: crea PVC de 200Gi
    API->>Prov: hi ha un PVC pendent
    Prov->>Prov: crea el disc a eu-west-1c<br/>(zona triada A CEGUES)
    Prov->>API: PV amb nodeAffinity zone=eu-west-1c
    Note over API: PVC Bound. Tot sembla correcte.
    Dev->>API: crea el pod de postgres-reserves
    API->>Sched: planifica aquest pod
    Sched-->>API: Pod Pending: el volum exigeix eu-west-1c<br/>i alli no hi ha CPU/memoria lliure

El pod es queda Pending indefinidament amb un esdeveniment del tipus:

0/6 nodes are available: 4 node(s) had volume node affinity conflict,
2 Insufficient cpu. preemption: 0/6 nodes are available.

I no hi ha arranjament còmode: el disc ja existeix a la zona equivocada i no es mou. Cal esborrar el PVC, esborrar el disc i tornar a començar, confiant en la sort.

Amb WaitForFirstConsumer l'ordre s'inverteix: el planificador decideix primer, considerant CPU, memòria, afinitats, taints i toleracions (06-05), i després l'aprovisionador crea el disc a la zona d'aquell node. El conflicte és impossible per construcció.

L'efecte secundari que cal reconèixer per no espantar-se: amb WaitForFirstConsumer, un PVC acabat de crear es queda en Pending a propòsit fins que existeixi un pod que el faci servir, i kubectl describe pvc ho diu amb l'esdeveniment WaitForFirstConsumer: waiting for first consumer to be created before binding.

Això no és un error. És el mode funcionant. Tan bon punt creïs el Deployment, el PVC passa a Bound en segons.

Regla de Rutas Norte: WaitForFirstConsumer a totes les classes, sense excepció. L'únic escenari on Immediate és preferible és un clúster d'una sola zona amb emmagatzematge de xarxa uniforme, i ni tan sols allà aporta res.

  1. La classe per defecte i els seus paranys

Un clúster pot tenir una StorageClass marcada com a per defecte. Quan un PVC omet el camp storageClassName, un controlador d'admissió li injecta el nom d'aquesta classe.

Es marca amb l'anotació storageclass.kubernetes.io/is-default-class: "true" al metadata de la classe, i a la sortida de kubectl get sc apareix assenyalada com standard (default). Canviar-la són un parell de kubectl patch:

# treure la marca a l'actual
kubectl patch sc standard -p \
  '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'

# posar-la a la nova
kubectl patch sc rutasnorte-estandar -p \
  '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

Si hi ha dues classes per defecte

Res no impedeix marcar-ne dues, i no és un error de configuració detectable a simple vista. El comportament és el següent: el controlador d'admissió tria la creada més recentment i ignora les altres. En versions anteriors arribava a rebutjar el PVC. En qualsevol cas és una configuració incorrecta, i el seu símptoma és dels pitjors: els PVC funcionen, però uns acaben en discos ràpids i cars i d'altres en discos lents, sense patró aparent.

kubectl get sc -o json | jq -r '.items[] |
  select(.metadata.annotations["storageclass.kubernetes.io/is-default-class"]=="true") |
  .metadata.name'          # si retorna mes duna linia, arregla-ho avui

Si no hi ha cap classe per defecte

Aleshores un PVC que ometi storageClassName no rep cap classe i es queda Pending per sempre, buscant un PV estàtic que probablement no existeix, amb l'esdeveniment ProvisioningFailed: no persistent volumes available for this claim and no storage class is set.

  1. storageClassName: "" davant d'ometre el camp

Aquesta distinció val una secció pròpia perquè és subtil, s'oblida constantment i produeix fallades desconcertants.

Al PVC Què fa el controlador d'admissió Amb quin PV pot casar
Camp omès Li injecta la classe per defecte Amb PV d'aquesta classe, o se n'aprovisiona un
storageClassName: "" No toca res: el PVC es queda sense classe Només amb PV sense classe (estàtics)
storageClassName: la-meva-classe No toca res Amb PV de la-meva-classe, o se n'aprovisiona un

La cadena buida és la forma explícita de dir: "desactiva l'aprovisionament dinàmic per a aquest PVC; vull un PV estàtic concret". És el que cal escriure quan restaures un volum preexistent i no vols que el clúster te'n creï un de nou i buit al costat. L'error clàssic i el seu símptoma: en un clúster amb classe per defecte, algú crea un PV estàtic sense classe per restaurar unes dades, escriu un PVC ometent storageClassName, i en lloc de vincular-se al PV amb les dades, el clúster aprovisiona un volum nou i buit. El PVC queda Bound, l'aplicació arrenca sense errors i la base de dades està buida. És una fallada silenciosa, i la protecció és doble: storageClassName: "" i volumeName apuntant al PV concret.

# Restauracio: vull AQUEST volum, no un de nou
spec:
  storageClassName: ""                       # sense classe: res de dinamic
  volumeName: pv-postgres-reserves-10gi      # i a mes, exactament aquest
  accessModes: ["ReadWriteOnce"]
  resources: { requests: { storage: 10Gi } }

  1. Les classes que porten minikube i els núvols gestionats

El teu clúster de pràctiques ja té una classe, proveïda per l'addon storage-provisioner que vas activar a 01-04:

kubectl describe sc standard
Name:                  standard          IsDefaultClass:  Yes
Provisioner:           k8s.io/minikube-hostpath
Parameters:            <none>            AllowVolumeExpansion: <unset>
ReclaimPolicy:         Delete            VolumeBindingMode:    Immediate

k8s.io/minikube-hostpath crea directoris sota /tmp/hostpath-provisioner/<namespace>/<nom-pvc> dins de la VM. Les seves limitacions són les esperables d'un clúster d'un node: no admet expansió ni snapshots, i la seva política és Delete. Serveix perfectament per aprendre el flux, però no per practicar el de 05-05; allà instal·larem el driver CSI de hostpath, que sí que les suporta.

Taula comparativa orientativa de les classes típiques que trobaràs:

Entorn Classe habitual Provisioner Suport Expansió Modes Política
minikube standard k8s.io/minikube-hostpath Directori de la VM No Tots (un node) Delete
kind / k3s local-path rancher.io/local-path Directori del node No RWO Delete
EKS (AWS) gp2 / gp3 ebs.csi.aws.com EBS RWO Delete
EKS amb fitxers efs-sc efs.csi.aws.com EFS N/A RWX Delete
GKE (Google) standard-rwo, premium-rwo pd.csi.storage.gke.io Persistent Disk RWO Delete
AKS (Azure) managed-csi, managed-csi-premium disk.csi.azure.com Managed Disk RWO Delete
AKS amb fitxers azurefile-csi file.csi.azure.com Azure Files RWX Delete

Els preus i rendiments varien i no té sentit memoritzar-los; el que sí que convé retenir és el patró: cada núvol ofereix com a mínim un disc de blocs estàndard, un de premium i un sistema de fitxers compartit més car per a ReadWriteMany, i les classes preinstal·lades vénen amb Delete.

  1. Les classes d'emmagatzematge de Rutas Norte

Amb tot l'anterior, el disseny del catàleg de la plataforma: dues classes, cadascuna amb una intenció clara.

rutasnorte-rapida: per a les dades que no es poden perdre

# k8s/base/storageclass-rapida.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: rutasnorte-rapida
  labels: { app.kubernetes.io/part-of: rutas-norte }
  annotations:
    storageclass.kubernetes.io/is-default-class: "false"
    rutasnorte.example/descripcio: "SSD xifrat per a bases de dades. Reté el volum en esborrar el PVC."
provisioner: ebs.csi.aws.com          # a pre i pro; a minikube veure mes avall
parameters:
  type: gp3
  iops: "6000"
  throughput: "250"
  encrypted: "true"                   # dades personals de clients: obligatori
  fsType: ext4
reclaimPolicy: Retain                 # <-- la decisio central
allowVolumeExpansion: true            # la BD creixera cada temporada alta
volumeBindingMode: WaitForFirstConsumer
mountOptions: ["noatime"]

Per què Retain a la classe de la base de dades. És la decisió més important del catàleg i convé poder defensar-la:

  • El PVC de postgres-reserves conté nom, DNI, telèfon i correu de tots els clients i totes les reserves venudes. La seva pèrdua no és una incidència tècnica: és una aturada de negoci i un incident de dades personals.
  • Els mecanismes que poden esborrar un PVC sense que ningú ho pretengui són molts i quotidians: un kubectl delete namespace equivocat (02-06), un kubectl delete -f k8s/ sobre el directori sencer, una eina de GitOps que sincronitza i "poda" recursos que ja no són a Git (10-05), un script de neteja d'entorns.
  • Amb Delete, qualsevol d'aquests accidents destrueix el disc de manera irreversible. Amb Retain, deixa un PV en Released que es rescata en dos minuts amb el kubectl patch de 05-02.
  • El cost de Retain és real però petit: volums orfes que s'han de netejar a mà i que es continuen facturant. Es compensa amb una revisió mensual de PV en Released.

La regla general que se'n deriva: Retain per a dades de negoci, Delete per a dades reconstruïbles.

rutasnorte-estandar: per a tota la resta

# k8s/base/storageclass-estandar.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: rutasnorte-estandar
  labels: { app.kubernetes.io/part-of: rutas-norte }
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
    rutasnorte.example/descripcio: "Disc estandard per a dades reconstruibles. S'esborra amb el PVC."
provisioner: ebs.csi.aws.com
parameters: { type: gp3, encrypted: "true", fsType: ext4 }
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer

És la classe per defecte, i aquesta elecció també és deliberada: si algú crea un PVC sense pensar, que caigui a la classe barata i esborrable, no a la cara i retinguda. La classe de la base de dades s'ha de demanar explícitament, cosa que obliga a una decisió conscient.

rutasnorte-rapida rutasnorte-estandar
Ús previst postgres-reserves, volum de còpies (05-06) Espais de treball, adjunts, informes
Política de reclamació Retain Delete
Rendiment SSD amb IOPS aprovisionades SSD estàndard
Expansió
Per defecte No
Xifratge

Al clúster de pràctiques

A minikube no existeix ebs.csi.aws.com. Per practicar, es creen les mateixes classes amb el provisioner local. Fixa't que el nom no canvia: és l'únic que veuen els manifests de l'aplicació, i per això el PVC de postgres-reserves és idèntic a minikube i en producció.

# k8s/entorns/dev/storageclasses-minikube.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: rutasnorte-rapida
  labels: { app.kubernetes.io/part-of: rutas-norte }
provisioner: k8s.io/minikube-hostpath
reclaimPolicy: Retain                # el contracte: es conserva
allowVolumeExpansion: false          # el provisioner de minikube no ho suporta
volumeBindingMode: Immediate         # minikube te un sol node
# --- i una segona classe rutasnorte-estandar identica llevat de:
#     reclaimPolicy: Delete i l'anotacio is-default-class: "true"

Que la configuració de dev difereixi de la de producció en parameters i provisioner és normal i correcte: el que ha de romandre idèntic és el contracte, és a dir, el nom de la classe i la seva política de reclamació.

  1. Pràctica: un PVC que crea el seu propi volum

Apliquem les classes i comprovem l'efecte. Primer, la classe per defecte al seu lloc:

kubectl apply -f k8s/entorns/dev/storageclasses-minikube.yaml
kubectl patch sc standard -p \
  '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'
kubectl get sc
NAME                            PROVISIONER                RECLAIMPOLICY   VOLUMEBINDINGMODE   AGE
rutasnorte-estandar (default)   k8s.io/minikube-hostpath   Delete          Immediate           5s
rutasnorte-rapida               k8s.io/minikube-hostpath   Retain          Immediate           5s
standard                        k8s.io/minikube-hostpath   Delete          Immediate           12d

Ara, i això és l'important: esborrem el PersistentVolume estàtic que vam crear a mà a 05-02, comprovem amb kubectl get pv que no en queda cap (No resources found) i creem un PVC nou sense cap PV esperant.

# k8s/base/informes-adjunts-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: informes-adjunts
  namespace: rutas-norte-dev
  labels: { app: informes-ocupacio, app.kubernetes.io/part-of: rutas-norte, entorn: dev }
spec:
  accessModes: ["ReadWriteOnce"]
  resources: { requests: { storage: 3Gi } }
  # sense storageClassName: fara servir la classe per defecte (rutasnorte-estandar)
kubectl apply -f k8s/base/informes-adjunts-pvc.yaml
kubectl get pvc,pv -n rutas-norte-dev
NAME                                     STATUS  VOLUME                 CAPACITY  STORAGECLASS
persistentvolumeclaim/informes-adjunts   Bound   pvc-7f3e1a2b-9c4d-...  3Gi       rutasnorte-estandar
NAME                                     RECLAIM POLICY  STATUS  CLAIM
persistentvolume/pvc-7f3e1a2b-9c4d-...   Delete          Bound   rutas-norte-dev/informes-adjunts

Fixa't en els quatre detalls que resumeixen la lliçó:

  1. Ha aparegut un PersistentVolume que tu no vas escriure. L'aprovisionador el va crear en veure el PVC.
  2. El seu nom és pvc-<uid-del-pvc>: si un nom de PV comença per pvc-, és dinàmic.
  3. CAPACITY: 3Gi, exactament el demanat, i RECLAIM POLICY: Delete, heretada de la classe per defecte.

I la comprovació que el PVC de la base de dades, en demanar la classe rutasnorte-rapida, obté un volum amb Retain:

kubectl apply -f k8s/base/postgres-reserves-pvc.yaml
kubectl apply -f k8s/base/postgres-reserves-deployment.yaml
kubectl get pv -o custom-columns=\
NOM:.metadata.name,CLASSE:.spec.storageClassName,\
POLITICA:.spec.persistentVolumeReclaimPolicy,CLAIM:.spec.claimRef.name
NOM                   CLASSE                POLITICA   CLAIM
pvc-7f3e1a2b-9c4d-... rutasnorte-estandar   Delete     informes-adjunts
pvc-2b8c9d0e-1f2a-... rutasnorte-rapida     Retain     postgres-reserves-dades

Dos volums, dues polítiques diferents, cap escrit a mà. Aquest és el catàleg funcionant.

  1. Migrar un PVC d'una classe a una altra

Última peça operativa, i la resposta curta és contundent: no es pot canviar la classe d'un PVC existent. El camp storageClassName és immutable un cop vinculat, i amb raó: la classe determina el disc físic, i un disc no canvia de tipus per editar un YAML. Un kubectl patch pvc ... -p '{"spec":{"storageClassName":"rutasnorte-rapida"}}' s'estavella contra:

The PersistentVolumeClaim "postgres-reserves-dades" is invalid:
spec: Forbidden: spec is immutable after creation except resources.requests
and volumeAttributesClassName for bound claims

El missatge a més t'avança l'única cosa que que es pot canviar: resources.requests, és a dir, l'expansió de 05-05. El procediment real de migració implica copiar les dades, i hi ha tres variants segons el que et puguis permetre:

a) Amb aturada (l'estàndard per a una base de dades)

# 1. Copia de seguretat ABANS de res (05-06)
kubectl exec -n rutas-norte-dev deploy/postgres-reserves -- \
  pg_dump -U rutasnorte -d reserves -Fc -f /tmp/previ-migracio.dump
# 2. Aturar l'escriptura: sense pods, no hi ha canvis al volum
kubectl scale deploy postgres-reserves -n rutas-norte-dev --replicas=0
# 3. Crear el PVC desti postgres-reserves-dades-v2 amb la classe nova
kubectl apply -f k8s/base/postgres-reserves-pvc-v2.yaml
# 4. Un Job que munta TOTS DOS volums i copia
apiVersion: batch/v1
kind: Job
metadata:
  name: migrar-volum-postgres
  namespace: rutas-norte-dev
  labels: { app: postgres-reserves, app.kubernetes.io/part-of: rutas-norte, entorn: dev }
spec:
  backoffLimit: 2
  template:
    metadata:
      labels: { app: postgres-reserves, entorn: dev }
    spec:
      restartPolicy: Never
      containers:
        - name: copiar
          image: busybox:1.36
          # -a preserva permisos, propietaris i marques de temps: imprescindible
          command: ["sh", "-c", "cp -a /origen/. /desti/ && ls -la /desti"]
          volumeMounts:
            - { name: origen, mountPath: /origen, readOnly: true }
            - { name: desti,  mountPath: /desti }
          resources:
            requests: { cpu: 200m, memory: 256Mi }
            limits:   { cpu: "1",  memory: 512Mi }
      volumes:                                       # els DOS PVC alhora
        - name: origen
          persistentVolumeClaim: { claimName: postgres-reserves-dades }
        - name: desti
          persistentVolumeClaim: { claimName: postgres-reserves-dades-v2 }
kubectl apply -f migrar-volum-postgres.yaml
kubectl wait --for=condition=complete job/migrar-volum-postgres -n rutas-norte-dev --timeout=600s
# 5. Apuntar el Deployment al PVC nou i arrencar
kubectl patch deploy postgres-reserves -n rutas-norte-dev --type=json -p='[
  {"op":"replace",
   "path":"/spec/template/spec/volumes/0/persistentVolumeClaim/claimName",
   "value":"postgres-reserves-dades-v2"}]'
kubectl scale deploy postgres-reserves -n rutas-norte-dev --replicas=1
# 6. VERIFICAR abans d'esborrar res
kubectl exec -n rutas-norte-dev deploy/postgres-reserves -- \
  psql -U rutasnorte -d reserves -c "SELECT count(*) FROM reserves;"

El pas 6 no és opcional. No esborris el PVC d'origen fins haver verificat les dades al destí, i encara així deixa'l uns dies. Recorda que si la seva classe és Retain, esborrar-lo deixa el PV en Released recuperable.

b) Amb la còpia lògica com a intermediari

Per a una base de dades sol ser més net: pg_dump de l'origen, crear el PVC nou buit, arrencar PostgreSQL sobre ell i restaurar amb pg_restore. És més lent però valida les dades pel camí (una còpia binària arrossegaria una corrupció; un bolcat lògic no es restaura si està corrupte). Es detalla a 05-06.

c) Sense aturada

Requereix replicació a nivell d'aplicació: aixecar una rèplica de PostgreSQL sobre el volum nou, sincronitzar-la i promoure-la. És un procediment de base de dades, no de Kubernetes, i es delega en un operador (06-07).

Errors Comuns i Consells

Error Símptoma Solució
Deixar la classe per defecte del núvol en un volum de dades S'esborra el PVC i desapareix el disc Classe pròpia amb reclaimPolicy: Retain
Fer servir volumeBindingMode: Immediate en diverses zones volume node affinity conflict, pod Pending per sempre WaitForFirstConsumer sempre
Espantar-se del WaitForFirstConsumer Es creu que el PVC està trencat És l'esperat: es vincula en crear el pod
Dues classes marcades per defecte Volums en classes impredictibles Deixa'n una de sola; comprova-ho amb jq
Cap classe per defecte PVC sense classe Pending per sempre Marca'n una, o anomena la classe a cada PVC
Confondre "" amb ometre el camp S'aprovisiona un volum buit en lloc de fer servir el PV amb les dades "" i volumeName en tota restauració
Intentar canviar la classe d'un PVC spec is immutable after creation Copiar les dades a un PVC nou
Oblidar allowVolumeExpansion No es pot fer gran el disc de la BD sense aturada Posa'l a true; és modificable en calent
Fer servir provisioners kubernetes.io/* El PVC no s'aprovisiona mai Els in-tree estan eliminats; fes servir CSI
No xifrar el disc de la base de dades Dades personals en clar a l'emmagatzematge encrypted: "true" o el seu equivalent

Consells:

  1. Documenta cada classe amb una anotació (rutasnorte.example/descripcio). Qui tria la classe sol ser un desenvolupador que no sap què hi ha a sota, i aquesta línia li estalvia preguntar.
  2. Audita el catàleg el primer dia en qualsevol clúster amb un kubectl get sc -o custom-columns=... que mostri alhora provisioner, reclaimPolicy, allowVolumeExpansion, volumeBindingMode i l'anotació de classe per defecte.
  3. Poques classes i amb noms que diguin la intenció. rutasnorte-rapida i rutasnorte-estandar s'entenen; sc-gp3-6000iops-encrypted obliga a saber d'AWS i lliga el nom al proveïdor.
  4. Els noms de classe són el contracte entre entorns. Mantén els mateixos noms a dev, pre i pro encara que a sota hi hagi provisioners diferents: així els manifests de l'aplicació són idèntics.

Exercicis

Exercici 1: muntar el catàleg de Rutas Norte

Al teu minikube:

  1. Crea rutasnorte-rapida i rutasnorte-estandar amb el provisioner local, deixant rutasnorte-estandar com a classe per defecte i traient la marca a standard.
  2. Verifica amb una sola ordre que només hi ha una classe per defecte.
  3. Crea un PVC d'1 GiB sense storageClassName i comprova en quina classe acaba i amb quina política.
  4. Crea un altre PVC d'1 GiB amb storageClassName: rutasnorte-rapida i compara la política de reclamació de tots dos PV.
  5. Esborra els dos PVC i explica per què un dels PV desapareix i l'altre no.

Exercici 2: l'incident de la zona

Un company de l'equip de plataforma ha creat aquesta classe per al clúster de producció, que té nodes a eu-west-1a, eu-west-1b i eu-west-1c:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata: { name: rutasnorte-rapida }
provisioner: ebs.csi.aws.com
parameters: { type: gp3 }
volumeBindingMode: Immediate

L'endemà, el pod de postgres-reserves porta 40 minuts en Pending amb aquest esdeveniment:

0/9 nodes are available: 6 node(s) had volume node affinity conflict,
3 Insufficient memory.

Explica què ha passat exactament, per què el PVC sí que està Bound mentre el pod no arrenca, com es resol l'incident avui i quins tres canvis cal fer a la classe perquè no torni a passar.

Exercici 3: el PVC que es va vincular al volum equivocat

L'equip ha de restaurar les dades de postgres-reserves de preproducció. Un administrador ha creat a mà un PV anomenat pv-restauracio-pre (20 GiB, Retain, sense storageClassName) que apunta al volum amb les dades recuperades. Un desenvolupador aplica aquest PVC:

apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: postgres-reserves-dades, namespace: rutas-norte-pre }
spec:
  accessModes: ["ReadWriteOnce"]
  resources: { requests: { storage: 20Gi } }

El PVC queda Bound en segons, PostgreSQL arrenca sense errors… i la base de dades està buida. Explica què ha passat i escriu el PVC correcte.

Solucions

Exercici 1

# 1
kubectl apply -f k8s/entorns/dev/storageclasses-minikube.yaml
kubectl patch sc standard -p \
  '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'

# 2
kubectl get sc -o json | jq -r \
  '.items[] | select(.metadata.annotations["storageclass.kubernetes.io/is-default-class"]=="true") | .metadata.name'
# -> rutasnorte-estandar (una sola linia: correcte)

# 3 i 4: dos PVC d'1Gi, un SENSE storageClassName (prova-defecte)
#        i un altre amb storageClassName: rutasnorte-rapida (prova-rapida)
kubectl apply -f pvc-proves-classes.yaml

kubectl get pv -o custom-columns=\
NOM:.metadata.name,CLASSE:.spec.storageClassName,\
POLITICA:.spec.persistentVolumeReclaimPolicy,CLAIM:.spec.claimRef.name
NOM                CLASSE                POLITICA   CLAIM
pvc-a1b2c3d4-...   rutasnorte-estandar   Delete     prova-defecte
pvc-e5f6a7b8-...   rutasnorte-rapida     Retain     prova-rapida

El PVC sense classe va rebre rutasnorte-estandar per injecció del controlador d'admissió (comprova-ho amb kubectl get pvc prova-defecte -o yaml: el camp apareix escrit a l'objecte, encara que tu no l'hi posessis). I en esborrar tots dos PVC (pas 5), kubectl get pv només retorna pvc-e5f6a7b8-... 1Gi Retain Released rutas-norte-dev/prova-rapida.

Només en queda un. El de rutasnorte-estandar tenia Delete i es va destruir amb el seu PVC; el de rutasnorte-rapida va heretar Retain de la seva classe i va quedar en Released amb les dades a dins. És exactament la protecció que buscàvem per a la base de dades. Neteja: kubectl delete pv pvc-e5f6a7b8-....

Exercici 2

Què ha passat. La classe fa servir volumeBindingMode: Immediate (i a més és el valor per defecte, així que n'hi ha prou d'ometre'l per caure al parany). En crear-se el PVC, l'aprovisionador d'EBS va crear el disc immediatament, triant una zona sense cap informació sobre on acabaria el pod —diguem eu-west-1c—. El PV resultant porta una nodeAffinity que exigeix topology.kubernetes.io/zone=eu-west-1c.

Per què el PVC està Bound i el pod no arrenca. Són dues decisions independents: la vinculació PVC-PV la fa el controlador de PersistentVolume i només mira capacitat, modes i classe; la col·locació del pod la fa el planificador, que a més de l'afinitat del volum avalua CPU, memòria i taints. El PVC està perfectament vinculat; el que no existeix és un node que satisfaci alhora la zona del disc i els recursos del pod. Els 6 node(s) had volume node affinity conflict són els nodes de les altres dues zones, i els 3 Insufficient memory són els d'eu-west-1c, que sí que valen per zona però estan plens —postgres-reserves demana 2 GiB amb QoS Guaranteed (03-05)—.

Com es resol avui. Cal decidir entre dos camins. L'opció A és fer lloc a eu-west-1c —localitza els nodes amb kubectl get nodes -L topology.kubernetes.io/zone, mira els seus Allocated resources amb kubectl describe node, i allibera càrrega o afegeix-hi un node—. L'opció B és refer el volum: esborrar el Deployment i el PVC de rutas-norte-pro, corregir la classe i tornar a aplicar.

L'opció B només és acceptable perquè el volum encara no té dades. Si en tingués, l'única sortida seria una restauració des de còpia de seguretat (05-06), perquè un disc EBS no canvia de zona.

Els tres canvis a la classe:

provisioner: ebs.csi.aws.com
parameters: { type: gp3, encrypted: "true" }   # 4t: dades personals (recomanable)
reclaimPolicy: Retain                  # 1: no destruir les dades en esborrar el PVC
allowVolumeExpansion: true             # 2: poder creixer sense aturada
volumeBindingMode: WaitForFirstConsumer  # 3: L'arranjament de l'incident
  1. volumeBindingMode: WaitForFirstConsumer, que inverteix l'ordre: primer planifica el pod, després crea el disc a la seva zona. Elimina el conflicte per construcció.
  2. reclaimPolicy: Retain, perquè la classe que faltava posava Delete per defecte i això és la base de dades de producció.
  3. allowVolumeExpansion: true, per no haver de repetir aquesta maniobra el dia que els 200 GiB es quedin curts.

Recorda que la StorageClass no es pot editar: s'ha d'esborrar i recrear. Els PV ja existents conserven la configuració amb què van néixer.

Exercici 3

Què ha passat. El PVC omet storageClassName. El controlador d'admissió li va injectar aleshores la classe per defecte del clúster (rutasnorte-estandar). A partir d'aquí, el PV pv-restauracio-pre va quedar descartat com a candidat —està sense classe, i la classe ha de coincidir exactament— i al seu lloc l'aprovisionador va crear un volum nou, buit i de 20 GiB. El PVC es va vincular a aquell volum nou en segons, PostgreSQL va trobar un directori buit, va executar initdb i va arrencar tan content: una base de dades nova i buida, sense ni un sol error als registres.

És la fallada més perillosa de la lliçó precisament perquè no falla res. I hi ha un dany col·lateral: el PV amb les dades continua Available, però si ningú no se n'adona i l'aplicació comença a operar sobre la base buida, es perd la finestra de restauració.

El PVC correcte porta doble protecció:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-reserves-dades
  namespace: rutas-norte-pre
  labels: { app: postgres-reserves, app.kubernetes.io/part-of: rutas-norte, entorn: pre }
spec:
  accessModes: ["ReadWriteOnce"]
  volumeMode: Filesystem
  # 1: cadena BUIDA -> sense classe -> res d'aprovisionament dinamic
  storageClassName: ""
  # 2: a mes, exactament AQUEST volum i cap altre
  volumeName: pv-restauracio-pre
  resources: { requests: { storage: 20Gi } }

I el procediment segur de restauració, per no repetir l'incident:

# 1. Preassignar el PV al PVC (claimRef sense uid): ningu mes el pot agafar
kubectl patch pv pv-restauracio-pre -p '{"spec":{"claimRef":{
  "apiVersion":"v1","kind":"PersistentVolumeClaim",
  "namespace":"rutas-norte-pre","name":"postgres-reserves-dades"}}}'
# 2. Crear el PVC i VERIFICAR a quin volum s'ha vinculat
kubectl apply -f pvc-restauracio.yaml
kubectl get pvc postgres-reserves-dades -n rutas-norte-pre \
  -o jsonpath='{.spec.volumeName}'; echo      # ha de dir pv-restauracio-pre
# 3. Nomes llavors, arrencar l'aplicacio i comptar les reserves
kubectl scale deploy postgres-reserves -n rutas-norte-pre --replicas=1

El pas 2 —comprovar volumeName abans d'arrencar l'aplicació— és la verificació que hauria evitat l'incident.

Conclusió

Has fet el salt que canvia l'operació diària d'un clúster: de l'aprovisionament estàtic, on un humà crea cada PersistentVolume per endavant amb el seu malbaratament, les seves esperes i els seus orfes, al dinàmic, on l'equip d'aplicació crea un PVC i el volum exacte apareix en segons. Has vist néixer un PV que no vas escriure, amb el nom pvc-<uid> que delata el seu origen, de la mida justa demanada i amb la política heretada de la seva classe.

Coneixes la StorageClass camp a camp. El provisioner, que identifica el driver —CSI en producció, k8s.io/minikube-hostpath en pràctiques, i mai els kubernetes.io/* in-tree, ja eliminats—. Els parameters, opacs per a Kubernetes i específics de cada driver, on va el tipus de disc, les IOPS i, obligatòriament a Rutas Norte, el xifratge en repòs. La reclaimPolicy que hereten els PV, amb l'avís que cal comprovar el primer dia en qualsevol clúster nou: les classes per defecte dels núvols vénen en Delete, així que esborrar un PVC esborra el disc. L'allowVolumeExpansion, que no costa res i evita una matinada de migració. I, sobretot, el volumeBindingMode: Immediate crea el disc a cegues i el pot deixar en una zona on el pod no hi cap, produint un volume node affinity conflict irreparable sense restaurar de còpia; WaitForFirstConsumer inverteix l'ordre —primer planifica el pod, després crea el disc on aquell pod és— i elimina el problema per construcció, al preu d'un Pending deliberat que ja no t'espanta.

Domines la classe per defecte: la seva anotació storageclass.kubernetes.io/is-default-class, el desordre que produeix tenir-ne dues i el Pending etern de no tenir-ne cap. I distingeixes amb precisió els dos silencis: ometre storageClassName deixa que t'injectin la classe per defecte, mentre que storageClassName: "" significa "sense classe, res de dinàmic, vull un PV estàtic". Aquesta diferència de dues cometes és el que separa una restauració correcta d'una base de dades buida que arrenca sense ni un sol error, com vas veure al tercer exercici.

I has dissenyat el catàleg de la plataforma: rutasnorte-rapida amb Retain per a postgres-reserves, perquè el disc guarda dades personals de clients i hi ha massa maneres quotidianes d'esborrar un PVC sense voler; i rutasnorte-estandar amb Delete com a classe per defecte, perquè un descuit caigui sempre en el que és barat i esborrable i la classe protegida s'hagi de demanar a consciència. Els noms es mantenen idèntics a dev, pre i pro encara que a sota canviïn el provisioner i els paràmetres: el nom de la classe és el contracte entre entorns. I saps que la classe d'un PVC és immutable, de manera que migrar significa copiar les dades, amb aturada i amb verificació abans d'esborrar res.

Queda per obrir la caixa negra. Hem dit "l'aprovisionador crea el volum" sense explicar qui és, com s'assabenta que hi ha un PVC pendent, com connecta el disc al node i com el munta al pod. I hi ha dues capacitats que la StorageClass anuncia però que encara no hem fet servir: l'expansió que habilita allowVolumeExpansion i els snapshots, imprescindibles abans de tocar l'esquema d'una base de dades en producció. Tot això és la maquinària de la Container Storage Interface, i és la lliçó següent: Aprovisionament Dinàmic, Expansió i Snapshots.

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