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
- De l'aprovisionament estàtic al dinàmic
- Anatomia de la StorageClass
provisioner: qui crea el volumparameters: els detalls del discreclaimPolicyiallowVolumeExpansionvolumeBindingMode: el camp que evita un desastre- La classe per defecte i els seus paranys
storageClassName: ""davant d'ometre el camp- Les classes que porten minikube i els núvols gestionats
- Les classes d'emmagatzematge de Rutas Norte
- Pràctica: un PVC que crea el seu propi volum
- Migrar un PVC d'una classe a una altra
- 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."
- 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:
- És un recurs de clúster (sense namespace), com el PersistentVolume. El gestiona l'equip de plataforma.
- É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.
provisioner: qui crea el volum
provisioner: qui crea el volumEl 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.
parameters: els detalls del disc
parameters: els detalls del discparameters é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: ext4A 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.
reclaimPolicy i allowVolumeExpansion
reclaimPolicy i allowVolumeExpansionreclaimPolicy
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:.volumeBindingModeCom 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 sí 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.
volumeBindingMode: el camp que evita un desastre
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.
- 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 avuiSi 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.
storageClassName: "" davant d'ometre el camp
storageClassName: "" davant d'ometre el campAquesta 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 } }
- 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:
Name: standard IsDefaultClass: Yes
Provisioner: k8s.io/minikube-hostpath
Parameters: <none> AllowVolumeExpansion: <unset>
ReclaimPolicy: Delete VolumeBindingMode: Immediatek8s.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 | Sí | 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 | Sí | RWO | Delete |
| AKS (Azure) | managed-csi, managed-csi-premium |
disk.csi.azure.com |
Managed Disk | Sí | RWO | Delete |
| AKS amb fitxers | azurefile-csi |
file.csi.azure.com |
Azure Files | Sí | 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.
- 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-reservesconté 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 namespaceequivocat (02-06), unkubectl 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. AmbRetain, deixa un PV enReleasedque es rescata en dos minuts amb elkubectl patchde 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 enReleased.
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ó | Sí | Sí |
| Per defecte | No | Sí |
| Xifratge | Sí | Sí |
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ó.
- 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 scNAME 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 12dAra, 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)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-adjuntsFixa't en els quatre detalls que resumeixen la lliçó:
- Ha aparegut un PersistentVolume que tu no vas escriure. L'aprovisionador el va crear en veure el PVC.
- El seu nom és
pvc-<uid-del-pvc>: si un nom de PV comença perpvc-, és dinàmic. CAPACITY: 3Gi, exactament el demanat, iRECLAIM 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.nameNOM CLASSE POLITICA CLAIM
pvc-7f3e1a2b-9c4d-... rutasnorte-estandar Delete informes-adjunts
pvc-2b8c9d0e-1f2a-... rutasnorte-rapida Retain postgres-reserves-dadesDos volums, dues polítiques diferents, cap escrit a mà. Aquest és el catàleg funcionant.
- 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 claimsEl missatge a més t'avança l'única cosa que sí 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:
- 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. - Audita el catàleg el primer dia en qualsevol clúster amb un
kubectl get sc -o custom-columns=...que mostri alhoraprovisioner,reclaimPolicy,allowVolumeExpansion,volumeBindingModei l'anotació de classe per defecte. - Poques classes i amb noms que diguin la intenció.
rutasnorte-rapidairutasnorte-estandars'entenen;sc-gp3-6000iops-encryptedobliga a saber d'AWS i lliga el nom al proveïdor. - 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:
- Crea
rutasnorte-rapidairutasnorte-estandaramb el provisioner local, deixantrutasnorte-estandarcom a classe per defecte i traient la marca astandard. - Verifica amb una sola ordre que només hi ha una classe per defecte.
- Crea un PVC d'1 GiB sense
storageClassNamei comprova en quina classe acaba i amb quina política. - Crea un altre PVC d'1 GiB amb
storageClassName: rutasnorte-rapidai compara la política de reclamació de tots dos PV. - 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: ImmediateL'endemà, el pod de postgres-reserves porta 40 minuts en Pending amb aquest esdeveniment:
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.nameNOM CLASSE POLITICA CLAIM
pvc-a1b2c3d4-... rutasnorte-estandar Delete prova-defecte
pvc-e5f6a7b8-... rutasnorte-rapida Retain prova-rapidaEl 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'incidentvolumeBindingMode: WaitForFirstConsumer, que inverteix l'ordre: primer planifica el pod, després crea el disc a la seva zona. Elimina el conflicte per construcció.reclaimPolicy: Retain, perquè la classe que faltava posavaDeleteper defecte i això és la base de dades de producció.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=1El 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
- Què és Kubernetes?
- Arquitectura de Kubernetes
- Conceptes i Terminologia Clau
- Configuració d'un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objectes, Manifests YAML i el Model Declaratiu
- El Projecte del Curs: la Plataforma Rutas Norte
Mòdul 2: Components Principals de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualitzacions, Rollbacks i Estratègies de Desplegament
- Serveis
- Namespaces
- Etiquetes, Selectors i Anotacions
Mòdul 3: Gestió de Configuració i Secrets
- ConfigMaps
- Secrets
- Variables d'Entorn
- Quotes i Límits de Recursos
- LimitRanges i Classes de Qualitat de Servei (QoS)
- ServiceAccounts i Accés a l'API des dels Pods
Mòdul 4: Xarxes a Kubernetes
- Xarxes de Clúster
- Tipus de Serveis
- DNS Intern i Descobriment de Serveis
- Controladors d'Ingress
- TLS i Gestió de Certificats amb cert-manager
- Polítiques de Xarxa
Mòdul 5: Emmagatzematge a Kubernetes
- Volums
- Volums Persistents
- Reclamacions de Volums Persistents
- Classes d'Emmagatzematge
- Aprovisionament Dinàmic, Expansió i Snapshots
- Còpies de Seguretat i Restauració de Dades
Mòdul 6: Conceptes Avançats de Kubernetes
- StatefulSets
- DaemonSets
- Treballs i CronJobs
- Init Containers, Sidecars i Patrons Multicontenidor
- Planificació: Afinitat, Taints i Toleracions
- Definicions de Recursos Personalitzats (CRDs)
- Operadors i el Patró Controlador
Mòdul 7: Monitoratge i Registre
- Verificacions de Salut i Sondes
- Servidor de Mètriques i kubectl top
- Monitoratge amb Prometheus
- Visualització i Alertes amb Grafana i Alertmanager
- Registre Centralitzat amb Elasticsearch, Fluentd i Kibana (EFK)
- Depuració d'Aplicacions i Esdeveniments del Clúster
Mòdul 8: Seguretat a Kubernetes
- Control d'Accés Basat en Rols (RBAC)
- Contextos de Seguretat i Enduriment del Contenidor
- Polítiques de Seguretat de Pods i Pod Security Standards
- Seguretat de Xarxa
- Seguretat d'Imatges
- Auditoria, Escaneig i Gestió de Vulnerabilitats
Mòdul 9: Escalat i Rendiment
- Autoescalat Horitzontal de Pods
- Autoescalat Vertical de Pods
- Autoescalat de Clúster
- Escalat per Esdeveniments i Mètriques Personalitzades amb KEDA
- Alta Disponibilitat: PodDisruptionBudgets i Topologia
- Ajust de Rendiment
Mòdul 10: Ecosistema i Eines de Kubernetes
- Minikube i Entorns Locals amb kind
- Kubeadm
- Helm
- Kustomize
- GitOps amb Argo CD i Flux
- Kubernetes Gestionat: EKS, AKS i GKE
Mòdul 11: Estudis de Cas i Aplicacions del Món Real
- Desplegament d'una Aplicació Web
- Execució d'Aplicacions amb Estat
- CI/CD amb Kubernetes
- Estratègies de Desplegament: Blue-Green i Canary
- Gestió Multi-Clúster
- Operació en Producció: Incidències, Runbooks i Costos
