En tancar la lliçó anterior va quedar apuntada una anomalia. Al mòdul 4 vam instal·lar cert-manager i vam començar a escriure manifests amb kind: Certificate i kind: ClusterIssuer. Al mòdul 5 vam crear objectes kind: VolumeSnapshot. Cap d'aquests tipus no existeix a Kubernetes: no vénen al binari de l'apiserver, no apareixen a l'especificació original de l'API. I tanmateix el clúster els tracta exactament igual que un Deployment: kubectl get, kubectl describe, kubectl explain, validació de camps, control d'accés RBAC, emmagatzematge a etcd, kubectl edit.
Això no és màgia. És la característica més important de Kubernetes des del punt de vista de l'ecosistema: l'API es pot estendre. Qualsevol pot afegir tipus d'objecte nous i, a partir d'aquell moment, el clúster els gestiona com si sempre hi haguessin estat.
En aquesta lliçó aprendrem a fer-ho. Definirem el tipus RutaProgramada per a Rutas Norte —origen, destí, horaris, places, vehicle— amb validació real, i el farem servir amb les mateixes eines de sempre. I acabarem amb l'advertiment més important del tema: un CRD sense un controlador al darrere és només una base de dades amb formulari. Qui hi posa el programari al darrere és l'operador, i aquest és el tema de la lliçó següent.
Contingut
- Què significa estendre l'API de Kubernetes
- Les tres maneres d'estendre Kubernetes
- Anatomia de l'objecte
CustomResourceDefinition - L'esquema OpenAPI v3: validació declarativa
- Subrecursos:
statusiscale additionalPrinterColumns:kubectl getútil- Exemple complet: el CRD
RutaProgramada - Fer servir el recurs com qualsevol objecte natiu
- Versionatge i conversió
- Quan NO crear un CRD
- Què significa estendre l'API de Kubernetes
Convé tenir clar què és realment l'apiserver. No és "el cervell de Kubernetes": és un servidor REST amb emmagatzematge a etcd, autenticació, autorització, validació i notificació de canvis. Els objectes que serveix —Pods, Services, Deployments— són dades amb esquema. Tota la intel·ligència és als controladors que observen aquestes dades i actuen.
Estendre l'API significa ensenyar-li un tipus d'objecte nou. A partir d'aquell moment, aquell tipus obté gratis tota la infraestructura de l'apiserver:
| El que obtens gratis | Què significa |
|---|---|
| Endpoints REST | /apis/<grup>/<versió>/namespaces/<ns>/<plural> amb GET, POST, PUT, PATCH, DELETE, WATCH |
| Persistència a etcd | Alta disponibilitat, transaccions, historial de revisions |
| Validació | Rebuig de manifests mal formats abans de desar-los |
| RBAC | Rols i permisos sobre el teu tipus, com sobre qualsevol altre (08-01) |
kubectl complet |
get, describe, edit, apply, delete, explain, label, patch |
| Auditoria | Cada canvi queda registrat al log d'auditoria |
| Watch | Notificació en temps real de canvis: la base dels controladors |
metadata estàndard |
labels, annotations, ownerReferences, finalizers, resourceVersion |
Aquesta llista és la raó per la qual tot l'ecosistema de Kubernetes es construeix així. Quan cert-manager va voler modelar "un certificat que ha d'existir i renovar-se", no va inventar un format de configuració propi ni un servidor a part: va definir un tipus Certificate i va deixar que Kubernetes fes la resta.
Recursos personalitzats que ja has fet servir en aquest curs, probablement sense adonar-te'n:
| Recurs | Grup d'API | Qui l'aporta | On va aparèixer |
|---|---|---|---|
Certificate, Issuer, ClusterIssuer |
cert-manager.io |
cert-manager | 04-05 |
VolumeSnapshot, VolumeSnapshotClass |
snapshot.storage.k8s.io |
snapshot-controller | 05-05 |
Backup, Restore, Schedule |
velero.io |
Velero | 05-06 |
ServiceMonitor, PrometheusRule |
monitoring.coreos.com |
Prometheus Operator | 07-03 |
IPPool, NetworkSet |
crd.projectcalico.org |
Calico | 04-01 |
Els pots veure al teu propi clúster:
NAME CREATED AT
certificates.cert-manager.io 2026-07-12T09:14:22Z
challenges.acme.cert-manager.io 2026-07-12T09:14:22Z
clusterissuers.cert-manager.io 2026-07-12T09:14:22Z
issuers.cert-manager.io 2026-07-12T09:14:23Z
volumesnapshotclasses.snapshot.storage.k8s.io 2026-07-19T11:02:41Z
volumesnapshotcontents.snapshot.storage.k8s.io 2026-07-19T11:02:41Z
volumesnapshots.snapshot.storage.k8s.io 2026-07-19T11:02:41ZI comprovar quins dels tipus disponibles són natius i quins afegits:
NAME SHORTNAMES APIVERSION NAMESPACED KIND
certificaterequests cr,crs cert-manager.io/v1 true CertificateRequest
certificates cert,certs cert-manager.io/v1 true Certificate
clusterissuers cert-manager.io/v1 false ClusterIssuer
issuers cert-manager.io/v1 true Issuer
- Les tres maneres d'estendre Kubernetes
Hi ha tres mecanismes, amb propòsits diferents. Convé distingir-los perquè sovint es confonen.
| Mecanisme | Què afegeix | Complexitat | Quan fer-lo servir |
|---|---|---|---|
| CustomResourceDefinition (CRD) | Tipus d'objecte nous, servits pel mateix apiserver | Baixa: un YAML | El 95 % dels casos |
| Capa d'agregació de l'API | Un servidor d'API propi en què l'apiserver delega | Alta: cal escriure i operar un servidor | Dades que no van a etcd o lògica de lectura especial |
| Webhooks d'admissió | Interceptar i modificar o rebutjar objectes ja existents | Mitjana: un servidor HTTPS | Validar o injectar sobre tipus natius o propis |
CRD
Declares l'esquema en un YAML i l'apiserver comença a servir el tipus. Els objectes es desen a etcd com qualsevol altre. És el que fan servir cert-manager, Velero, Prometheus Operator i pràcticament tot l'ecosistema.
Limitacions: les dades viuen a etcd (no és lloc per a volums de dades grans ni per a escriptures molt freqüents) i no pots personalitzar com es llegeixen o s'emmagatzemen.
Capa d'agregació
Registres un objecte APIService que diu a l'apiserver "per al grup metrics.k8s.io, delega en aquest Service". L'apiserver actua de proxy cap al teu servidor.
L'exemple canònic és el metrics-server que vam activar com a addon a 01-04: les seves mètriques són dades volàtils d'alta freqüència que no han d'anar a etcd, així que se serveixen des de memòria mitjançant agregació. El veurem a 07-02.
Requereix escriure un servidor d'API complet (autenticació, autorització, versionatge) i operar-lo amb alta disponibilitat. La regla és senzilla: si dubtes, fes servir un CRD.
Webhooks d'admissió
No afegeixen tipus: intercepten peticions a tipus que ja existeixen, en el camí entre la validació de l'apiserver i l'emmagatzematge.
- Mutating webhook: modifica l'objecte. És el que fa una malla de serveis en injectar un sidecar a cada pod.
- Validating webhook: accepta o rebutja. És el que aplica polítiques de seguretat complexes.
Un CRD pot tenir un webhook de validació associat per a regles que l'esquema OpenAPI no pot expressar, com ara "l'hora d'arribada ha de ser posterior a la de sortida" o "no hi pot haver dues rutes amb el mateix codi".
- Anatomia de l'objecte
CustomResourceDefinition
CustomResourceDefinitionUn CRD és, ell mateix, un objecte de Kubernetes del grup apiextensions.k8s.io/v1. Aquesta és la seva estructura, amb l'exemple de RutaProgramada que desenvoluparem:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
# OBLIGATORI: el nom ha de ser exactament <plural>.<group>
name: rutesprogramades.rutasnorte.example
spec:
group: rutasnorte.example
scope: Namespaced
names:
kind: RutaProgramada
listKind: RutaProgramadaList
plural: rutesprogramades
singular: rutaprogramada
shortNames: ["ruta", "rutes"]
categories: ["rutasnorte", "all"]
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
# ... l'esquema, apartat 4 ...group
El grup d'API sota el qual viu el tipus. Ha de ser un nom de domini que controlis o que sigui clarament teu, per evitar col·lisions. Convenció habitual: <producte>.<el-teu-domini>.
El grup es combina amb la versió per formar l'apiVersion dels teus objectes: rutasnorte.example/v1.
names
| Camp | Què és | Exemple |
|---|---|---|
kind |
El kind: del manifest, en CamelCase singular |
RutaProgramada |
listKind |
El kind de la llista; per convenció <kind>List |
RutaProgramadaList |
plural |
Nom a la URL de l'API i a kubectl, en minúscules | rutesprogramades |
singular |
Àlies singular per a kubectl | rutaprogramada |
shortNames |
Abreviatures: kubectl get rutes |
["ruta", "rutes"] |
categories |
Grups per a kubectl get <categoria> |
["rutasnorte", "all"] |
Les categories són més útils del que sembla. Amb categories: ["rutasnorte"], una sola ordre llista tots els recursos personalitzats de la teva plataforma:
Compte amb incloure-hi all: fa que els teus objectes apareguin a kubectl get all, que ja és una ordre sorollosa. Fes-ho servir només si el teu recurs és realment de primer nivell per als usuaris del clúster.
scope
| Valor | Significat | Exemples reals |
|---|---|---|
Namespaced |
L'objecte viu en un namespace | Certificate, VolumeSnapshot, RutaProgramada |
Cluster |
És global al clúster, sense namespace | ClusterIssuer, VolumeSnapshotClass, StorageClass |
La decisió és important i no es pot canviar després sense esborrar el CRD (i amb ell tots els seus objectes). Criteri: si equips o entorns diferents han de tenir versions separades i aïllades del recurs, és Namespaced. Si representa configuració global d'infraestructura, és Cluster.
Per a RutaProgramada triem Namespaced: les rutes de rutas-norte-dev són dades de prova i no s'han de barrejar amb les de rutas-norte-pro.
versions
Una llista, perquè un CRD pot servir diverses versions alhora:
versions:
- name: v1alpha1
served: false # ja no se serveix: els clients antics reben error
storage: false
schema: {...}
- name: v1beta1
served: true # se serveix, per a clients que encara la fan servir
storage: false
schema: {...}
- name: v1
served: true
storage: true # EXACTAMENT UNA versió pot tenir storage: true
schema: {...}| Camp | Significat |
|---|---|
served |
Si l'apiserver accepta peticions en aquella versió |
storage |
Si els objectes es desen a etcd amb aquell esquema. Només una versió el pot tenir |
La distinció és subtil però crucial. Un objecte creat com a v1beta1 es converteix a la versió d'emmagatzematge abans de desar-se, i es converteix de tornada en llegir-lo en v1beta1. Les dades a etcd tenen una sola forma; les versions són vistes sobre elles. Hi tornarem a l'apartat 9.
- L'esquema OpenAPI v3: validació declarativa
Sense esquema, un recurs personalitzat acceptaria qualsevol YAML. Amb esquema, l'apiserver valida abans de desar i rebutja el que no encaixi, amb un missatge concret. És el que fa que un CRD se senti com un tipus natiu.
L'esquema va a versions[].schema.openAPIV3Schema i és un subconjunt d'OpenAPI v3.
Tipus i estructura bàsica
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required: ["origen", "desti", "horaris", "places"]
properties:
origen:
type: string
minLength: 2
maxLength: 60
places:
type: integer
minimum: 1
maximum: 90
activa:
type: boolean
default: true
horaris:
type: array
minItems: 1
maxItems: 24
items:
type: stringRestriccions disponibles per tipus:
| Tipus | Restriccions | Ús típic |
|---|---|---|
string |
minLength, maxLength, pattern, enum, format |
Codis, noms, matrícules |
integer |
minimum, maximum, exclusiveMinimum, multipleOf |
Places, preus en cèntims |
number |
Igual que integer, amb decimals | Coordenades |
boolean |
— | Interruptors |
array |
minItems, maxItems, uniqueItems, items |
Horaris, parades |
object |
properties, required, additionalProperties |
Estructures imbricades |
required i default
origen:
type: string
# sense default: si és a required, és obligatori
activa:
type: boolean
default: true # si no s'indica, l'apiserver l'hi posa
classe:
type: string
enum: ["estandar", "supra", "nocturn"]
default: "estandar"Els valors per defecte els aplica l'apiserver en desar, no kubectl. Conseqüència pràctica: si llegeixes l'objecte després de crear-lo, veuràs els defaults ja escrits. Això els fa fiables per a qualsevol consumidor.
Validació per patró i per enumeració
codi:
type: string
# Format de Rutas Norte: dues lletres, guionet, tres dígits (RN-041)
pattern: '^[A-Z]{2}-[0-9]{3}$'
matriculaVehicle:
type: string
pattern: '^[0-9]{4}[A-Z]{3}$'
horaSortida:
type: string
pattern: '^([01][0-9]|2[0-3]):[0-5][0-9]$'
diesOperacio:
type: array
minItems: 1
maxItems: 7
uniqueItems: true
items:
type: string
enum: ["Dl", "Dt", "Dc", "Dj", "Dv", "Ds", "Dg"]Els pattern fan servir sintaxi RE2 (la de Go), no PCRE. No admeten lookahead ni lookbehind ni referències cap enrere. Per a regles més complexes hi ha dues opcions: regles de validació CEL (x-kubernetes-validations) o un webhook de validació.
x-kubernetes-preserve-unknown-fields
Per defecte, l'apiserver elimina silenciosament qualsevol camp que no sigui a l'esquema. Això s'anomena poda (pruning) i és una de les causes més desconcertants de "el meu camp va desaparèixer": apliques un manifest amb un camp mal escrit, kubectl no protesta, i en llegir l'objecte aquell camp no hi és.
Per permetre camps arbitraris en una part concreta:
metadadesExternes:
type: object
x-kubernetes-preserve-unknown-fields: true
description: "Dades lliures del sistema de venda heretat"Fes-ho servir amb moderació: cada punt on ho poses és un punt on perds validació i on un error tipogràfic passa desapercebut.
Descripcions
places:
type: integer
minimum: 1
maximum: 90
description: "Places totals del vehicle assignat a aquesta ruta"Les description no són decoratives: són el que retorna kubectl explain. Escriure-les converteix el teu CRD en un tipus autodocumentat, i és la diferència entre un recurs que la gent sap fer servir i un que requereix llegir el codi font.
- Subrecursos:
status i scale
status i scale - name: v1
served: true
storage: true
subresources:
status: {}
scale:
specReplicasPath: .spec.vehiclesAssignats
statusReplicasPath: .status.vehiclesActius
labelSelectorPath: .status.selector
schema:
openAPIV3Schema: {...}El subrecurs status
Activar status: {} té tres efectes concrets:
- S'habilita l'endpoint
/status, que s'actualitza de manera independent de l'objecte principal. - Les escriptures sobre l'objecte principal ignoren canvis a
.status. - Les escriptures sobre
/statusignoren canvis a.spec.
Aquesta separació no és burocràcia: reflecteix la divisió fonamental del model declaratiu que vam estudiar a 01-06.
spec |
status |
|
|---|---|---|
| Qui escriu | L'usuari o el sistema de desplegament | El controlador |
| Què expressa | L'estat desitjat | L'estat observat |
| Es versiona a Git | Sí | No |
| Es pot reconstruir | No: és la intenció | Sí: és una observació |
Sense aquest subrecurs, un controlador que volgués actualitzar l'estat hauria de fer un update de l'objecte complet, i correria el risc de sobreescriure un canvi de spec que l'usuari acabés de fer. I a la inversa: un kubectl apply de l'usuari esborraria l'estat que el controlador acabava d'escriure.
Activa'l sempre en qualsevol CRD que hagi de tenir un controlador.
Un patró universal a Kubernetes per al status són les condicions:
status:
type: object
properties:
fase:
type: string
enum: ["Pendent", "Programada", "Activa", "Cancelada"]
placesVenudes:
type: integer
condicions:
type: array
items:
type: object
required: ["type", "status"]
properties:
type:
type: string
status:
type: string
enum: ["True", "False", "Unknown"]
lastTransitionTime:
type: string
format: date-time
reason:
type: string
message:
type: stringÉs la mateixa estructura que has vist a kubectl describe pod (Ready, PodScheduled, Initialized) i a kubectl describe node. Seguir la convenció fa que els teus objectes es llegeixin com els natius i que eines genèriques com kubectl wait --for=condition=... funcionin sobre ells.
El subrecurs scale
Habilita kubectl scale sobre el teu propi recurs, i amb ell la possibilitat que un HorizontalPodAutoscaler (09-01) hi actuï.
scale:
specReplicasPath: .spec.vehiclesAssignats # on és el nombre desitjat
statusReplicasPath: .status.vehiclesActius # on és l'observat
labelSelectorPath: .status.selector # selector dels objectes gestionatsEl que fa l'ordre és escriure un 3 a .spec.vehiclesAssignats. Que això es tradueixi en tres autobusos és feina del controlador; el subrecurs només estandarditza la interfície.
additionalPrinterColumns: kubectl get útil
additionalPrinterColumns: kubectl get útilSense configurar res, kubectl get d'un recurs personalitzat mostra dues columnes inútils:
Amb additionalPrinterColumns es declara què mostrar, mitjançant rutes JSONPath sobre l'objecte:
additionalPrinterColumns:
- name: Codi
type: string
jsonPath: .spec.codi
- name: Origen
type: string
jsonPath: .spec.origen
- name: Destí
type: string
jsonPath: .spec.desti
- name: Places
type: integer
jsonPath: .spec.places
- name: Fase
type: string
jsonPath: .status.fase
- name: Venudes
type: integer
jsonPath: .status.placesVenudes
priority: 1 # només amb -o wide
- name: Antiguitat
type: date
jsonPath: .metadata.creationTimestampResultat:
NAME CODI ORIGEN DESTÍ PLACES FASE ANTIGUITAT
rn-041-bilbao-santander RN-041 Bilbao Santander 55 Activa 3m
rn-088-oviedo-leon RN-088 Oviedo Lleó 38 Activa 3mDetalls:
typeadmetstring,integer,number,booleanidate. Ambdate, kubectl formata com a antiguitat relativa ("3m", "2d").priority: 0(per defecte) mostra la columna sempre;priority: 1només amb-o wide.- La columna
NAMEhi és sempre i no es configura.
És un detall petit amb un impacte enorme en la usabilitat. Compara mentalment kubectl get pods —que mostra READY, STATUS, RESTARTS i AGE— amb el que seria si només mostrés nom i edat.
- Exemple complet: el CRD
RutaProgramada
RutaProgramadaHo ajuntem tot. Rutas Norte vol modelar les seves rutes com a objectes de Kubernetes, de manera que l'equip d'operacions les gestioni amb les mateixes eines i el mateix flux de GitOps que la resta de la plataforma.
# k8s/base/crd-rutaprogramada.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: rutesprogramades.rutasnorte.example
labels:
app.kubernetes.io/part-of: rutas-norte
spec:
group: rutasnorte.example
scope: Namespaced
names:
kind: RutaProgramada
listKind: RutaProgramadaList
plural: rutesprogramades
singular: rutaprogramada
shortNames: ["ruta", "rutes"]
categories: ["rutasnorte"]
versions:
- name: v1
served: true
storage: true
subresources:
status: {}
additionalPrinterColumns:
- name: Codi
type: string
jsonPath: .spec.codi
- name: Origen
type: string
jsonPath: .spec.origen
- name: Destí
type: string
jsonPath: .spec.desti
- name: Places
type: integer
jsonPath: .spec.places
- name: Fase
type: string
jsonPath: .status.fase
- name: Venudes
type: integer
jsonPath: .status.placesVenudes
priority: 1
- name: Antiguitat
type: date
jsonPath: .metadata.creationTimestamp
schema:
openAPIV3Schema:
type: object
description: "Una ruta d'autobús programada de Rutas Norte S.L."
required: ["spec"]
properties:
spec:
type: object
description: "Estat desitjat de la ruta"
required: ["codi", "origen", "desti", "horaris", "places"]
properties:
codi:
type: string
description: "Codi comercial de la ruta, format XX-999 (ex. RN-041)"
pattern: '^[A-Z]{2}-[0-9]{3}$'
origen:
type: string
description: "Ciutat d'origen del trajecte"
minLength: 2
maxLength: 60
desti:
type: string
description: "Ciutat de destí del trajecte"
minLength: 2
maxLength: 60
duracioMinuts:
type: integer
description: "Durada estimada del trajecte en minuts"
minimum: 10
maximum: 1440
default: 120
places:
type: integer
description: "Places totals ofertes a cada sortida"
minimum: 1
maximum: 90
classe:
type: string
description: "Categoria comercial del servei"
enum: ["estandar", "supra", "nocturn"]
default: "estandar"
activa:
type: boolean
description: "Si la ruta admet vendes actualment"
default: true
horaris:
type: array
description: "Hores de sortida diàries en format HH:MM"
minItems: 1
maxItems: 24
uniqueItems: true
items:
type: string
pattern: '^([01][0-9]|2[0-3]):[0-5][0-9]$'
diesOperacio:
type: array
description: "Dies de la setmana en què opera la ruta"
minItems: 1
maxItems: 7
uniqueItems: true
default: ["Dl", "Dt", "Dc", "Dj", "Dv", "Ds", "Dg"]
items:
type: string
enum: ["Dl", "Dt", "Dc", "Dj", "Dv", "Ds", "Dg"]
paradesIntermedies:
type: array
description: "Parades entre origen i destí, en ordre"
maxItems: 20
items:
type: object
required: ["localitat", "minutDesDeSortida"]
properties:
localitat:
type: string
minLength: 2
maxLength: 60
minutDesDeSortida:
type: integer
minimum: 1
maximum: 1439
vehicle:
type: object
description: "Vehicle assignat a la ruta"
required: ["matricula"]
properties:
matricula:
type: string
description: "Matrícula espanyola sense separadors (ex. 4471BCD)"
pattern: '^[0-9]{4}[A-Z]{3}$'
model:
type: string
maxLength: 80
accessible:
type: boolean
default: true
preuBaseCentims:
type: integer
description: "Preu base del bitllet en cèntims d'euro"
minimum: 0
maximum: 100000
metadadesVendaHeretada:
type: object
description: "Camps lliures del sistema de venda heretat"
x-kubernetes-preserve-unknown-fields: true
status:
type: object
description: "Estat observat, escrit pel controlador"
properties:
fase:
type: string
enum: ["Pendent", "Programada", "Activa", "Cancelada"]
placesVenudes:
type: integer
minimum: 0
ultimaSortidaProgramada:
type: string
format: date-time
observedGeneration:
type: integer
description: "metadata.generation que el controlador va processar per últim cop"
condicions:
type: array
items:
type: object
required: ["type", "status"]
properties:
type:
type: string
status:
type: string
enum: ["True", "False", "Unknown"]
lastTransitionTime:
type: string
format: date-time
reason:
type: string
maxLength: 128
message:
type: string
maxLength: 512A partir d'aquest instant, RutaProgramada és un tipus de primera classe al clúster. Ningú no ha reiniciat res, i tots els kubectl del món que apuntin a aquest clúster ja el coneixen.
Instàncies
# k8s/entorns/pro/rutes-programades.yaml
apiVersion: rutasnorte.example/v1
kind: RutaProgramada
metadata:
name: rn-041-bilbao-santander
namespace: rutas-norte-pro
labels:
app.kubernetes.io/part-of: rutas-norte
entorn: pro
corredor: cantabric
spec:
codi: "RN-041"
origen: "Bilbao"
desti: "Santander"
duracioMinuts: 95
places: 55
classe: "estandar"
horaris: ["07:00", "09:30", "12:00", "15:30", "18:00", "20:30"]
diesOperacio: ["Dl", "Dt", "Dc", "Dj", "Dv", "Ds", "Dg"]
paradesIntermedies:
- localitat: "Castro Urdiales"
minutDesDeSortida: 40
- localitat: "Laredo"
minutDesDeSortida: 58
vehicle:
matricula: "4471BCD"
model: "Setra S 415 (fictici)"
accessible: true
preuBaseCentims: 1150
---
apiVersion: rutasnorte.example/v1
kind: RutaProgramada
metadata:
name: rn-088-oviedo-leon
namespace: rutas-norte-pro
labels:
app.kubernetes.io/part-of: rutas-norte
entorn: pro
corredor: nord-oest
spec:
codi: "RN-088"
origen: "Oviedo"
desti: "Lleó"
duracioMinuts: 130
places: 38
classe: "supra"
horaris: ["06:45", "14:15", "19:45"]
diesOperacio: ["Dl", "Dt", "Dc", "Dj", "Dv"]
vehicle:
matricula: "9902XKL"
accessible: true
preuBaseCentims: 1490rutaprogramada.rutasnorte.example/rn-041-bilbao-santander created
rutaprogramada.rutasnorte.example/rn-088-oviedo-leon createdLa validació en acció
Aquesta és la part que demostra que l'esquema no és decoratiu. Un manifest amb diversos errors:
apiVersion: rutasnorte.example/v1
kind: RutaProgramada
metadata:
name: ruta-invalida
namespace: rutas-norte-dev
spec:
codi: "rn41" # no compleix ^[A-Z]{2}-[0-9]{3}$
origen: "A" # minLength és 2
desti: "Gijón"
places: 250 # maximum és 90
classe: "premium" # no és a l'enum
horaris: ["25:00"] # hora invàlida
vehicle:
matricula: "4471-BCD" # el guionet no està permèsThe RutaProgramada "ruta-invalida" is invalid:
* spec.codi: Invalid value: "rn41": spec.codi in body should match '^[A-Z]{2}-[0-9]{3}$'
* spec.origen: Invalid value: "A": spec.origen in body should be at least 2 chars long
* spec.places: Invalid value: 250: spec.places in body should be less than or equal to 90
* spec.classe: Unsupported value: "premium": supported values: "estandar", "supra", "nocturn"
* spec.horaris[0]: Invalid value: "25:00": spec.horaris[0] in body should match '^([01][0-9]|2[0-3]):[0-5][0-9]$'
* spec.vehicle.matricula: Invalid value: "4471-BCD": spec.vehicle.matricula in body should match '^[0-9]{4}[A-Z]{3}$'Sis errors, tots detectats abans de desar res, amb la ruta exacta del camp i la regla violada. I també els omesos:
kubectl apply -f - <<'EOF'
apiVersion: rutasnorte.example/v1
kind: RutaProgramada
metadata:
name: ruta-incompleta
namespace: rutas-norte-dev
spec:
codi: "RN-999"
origen: "Burgos"
EOFThe RutaProgramada "ruta-incompleta" is invalid:
* spec.desti: Required value
* spec.horaris: Required value
* spec.places: Required valueAquesta validació gratuïta, sense escriure una línia de codi, és la raó per la qual val la pena invertir temps en un bon esquema.
- Fer servir el recurs com qualsevol objecte natiu
Tot el que saps de kubectl ja funciona sobre RutaProgramada.
NAME CODI ORIGEN DESTÍ PLACES FASE ANTIGUITAT
rn-041-bilbao-santander RN-041 Bilbao Santander 55 2m
rn-088-oviedo-leon RN-088 Oviedo Lleó 38 2mLa columna FASE és buida perquè .status no l'escriu ningú: no hi ha controlador. Aquest buit és la lliçó de l'apartat 10.
kubectl get rutes -n rutas-norte-pro -l corredor=cantabric
kubectl get rutaprogramada rn-041-bilbao-santander -n rutas-norte-pro -o yaml | head -30apiVersion: rutasnorte.example/v1
kind: RutaProgramada
metadata:
creationTimestamp: "2026-08-05T20:14:02Z"
generation: 1
labels:
app.kubernetes.io/part-of: rutas-norte
corredor: cantabric
entorn: pro
name: rn-041-bilbao-santander
namespace: rutas-norte-pro
resourceVersion: "184722"
uid: 3f1a9c04-8e2b-4c71-9a55-71b0e2d8c4f3
spec:
activa: true # <- default aplicat per l'apiserver
classe: estandar
codi: RN-041
desti: Santander
diesOperacio: [Dl, Dt, Dc, Dj, Dv, Ds, Dg]
duracioMinuts: 95Observa activa: true: no era al manifest i l'apiserver el va escriure a partir del default de l'esquema.
kubectl explain
GROUP: rutasnorte.example
KIND: RutaProgramada
VERSION: v1
FIELD: vehicle <Object>
DESCRIPTION:
Vehicle assignat a la ruta
FIELDS:
accessible <boolean>
matricula <string> -required-
Matrícula espanyola sense separadors (ex. 4471BCD)
model <string>La documentació surt directament de l'esquema. Un company que no hagi vist mai aquest CRD el pot descobrir tot sol, sense llegir codi ni buscar un wiki.
describe, edit, patch, label
Name: rn-088-oviedo-leon
Namespace: rutas-norte-pro
Labels: app.kubernetes.io/part-of=rutas-norte
corredor=nord-oest
entorn=pro
API Version: rutasnorte.example/v1
Kind: RutaProgramada
Spec:
Activa: true
Classe: supra
Codi: RN-088
Desti: Lleó
Dies Operacio: Dl, Dt, Dc, Dj, Dv
Duracio Minuts: 130
Horaris: 06:45, 14:15, 19:45
Origen: Oviedo
Places: 38
Preu Base Centims: 1490
Vehicle:
Accessible: true
Matricula: 9902XKL
Events: <none># Editar interactivament, amb validació en desar
kubectl edit rutaprogramada rn-041-bilbao-santander -n rutas-norte-pro
# Apedaçar un camp
kubectl patch rutaprogramada rn-041-bilbao-santander -n rutas-norte-pro \
--type=merge -p '{"spec":{"preuBaseCentims":1250}}'
# Etiquetar
kubectl label rutaprogramada rn-041-bilbao-santander -n rutas-norte-pro temporada=estiu
# Observar canvis en temps real: la base de qualsevol controlador
kubectl get rutes -n rutas-norte-pro --watchEscriure el status
Com que vam habilitar el subrecurs, l'estat s'escriu pel seu propi endpoint. Un controlador ho faria mitjançant l'API; a mà, amb kubectl:
kubectl patch rutaprogramada rn-041-bilbao-santander -n rutas-norte-pro \
--subresource=status --type=merge -p '{
"status": {
"fase": "Activa",
"placesVenudes": 37,
"observedGeneration": 1,
"condicions": [{
"type": "VehicleAssignat",
"status": "True",
"reason": "MatriculaValida",
"message": "Vehicle 4471BCD assignat i disponible",
"lastTransitionTime": "2026-08-05T20:20:00Z"
}]
}
}'
kubectl get rutes -n rutas-norte-pro -o wideNAME CODI ORIGEN DESTÍ PLACES FASE VENUDES ANTIGUITAT
rn-041-bilbao-santander RN-041 Bilbao Santander 55 Activa 37 8m
rn-088-oviedo-leon RN-088 Oviedo Lleó 38 8mI ara funciona fins i tot kubectl wait sobre una condició pròpia:
kubectl wait --for=condition=VehicleAssignat \
rutaprogramada/rn-041-bilbao-santander -n rutas-norte-pro --timeout=30sAixò és el que es guanya seguint les convencions: eines genèriques que mai no van saber de rutes d'autobús funcionen sobre el teu tipus.
RBAC sobre el recurs propi
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: gestor-rutes
namespace: rutas-norte-pro
rules:
- apiGroups: ["rutasnorte.example"]
resources: ["rutesprogramades"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: ["rutasnorte.example"]
resources: ["rutesprogramades/status"]
verbs: ["get", "update", "patch"]Fixa't que rutesprogramades/status és un recurs separat a efectes de permisos: es pot donar accés d'escriptura al spec sense permetre falsejar el status. Aquesta granularitat l'habilita el subrecurs, i és una altra raó per activar-lo. RBAC en profunditat és la lliçó 08-01.
- Versionatge i conversió
El versionatge d'un CRD és la part més difícil, i convé saber-ho abans de publicar la primera versió.
La progressió habitual
| Versió | Estabilitat | Compromisos |
|---|---|---|
v1alpha1 |
Experimental | Pot canviar o desaparèixer sense avís; desactivada per defecte en molts projectes |
v1beta1 |
En proves | Canvis incompatibles possibles però anunciats; sol haver-hi migració |
v1 |
Estable | Compatibilitat cap enrere garantida; els camps existents no canvien de significat |
Un cop a v1, no pots esborrar un camp ni canviar-ne la semàntica. Pots afegir camps opcionals amb valor per defecte, i poca cosa més. Per això convé començar en v1alpha1 i no promoure a v1 fins que el model estigui assentat.
El problema que fa difícil la migració
Recordem el mecanisme: només una versió té storage: true. Tots els objectes es desen amb l'esquema d'aquella versió, i les altres versions són vistes que es converteixen al vol.
Suposem que v1alpha1 tenia un sol camp horari i v1 el substitueix per una llista horaris. Com es converteix un objecte desat amb l'esquema antic?
conversion: None (per defecte)
No es converteix res: l'objecte es retorna tal qual, canviant només l'apiVersion. Funciona únicament si els esquemes són compatibles camp a camp, és a dir, si entre versions només has afegit camps opcionals. Per a qualsevol canvi estructural és insuficient.
conversion: Webhook
spec:
conversion:
strategy: Webhook
webhook:
conversionReviewVersions: ["v1"]
clientConfig:
service:
namespace: rutas-norte-sistema
name: rutes-webhook-conversio
path: /convertir
port: 443
caBundle: <certificat en base64>L'apiserver crida el teu servidor HTTPS cada vegada que algú llegeix o escriu un objecte en una versió diferent de la d'emmagatzematge. El teu servidor rep l'objecte en una versió i el retorna en una altra.
El que això implica a la pràctica:
- Cal escriure, desplegar i operar un servidor HTTPS amb certificat vàlid (aquí cert-manager de 04-05 és l'aliat natural).
- Ha de ser ràpid i altament disponible: si el webhook no respon, ningú no pot llegir ni escriure aquests objectes. Un webhook de conversió caigut bloqueja el recurs sencer.
- La conversió ha de ser bidireccional i sense pèrdua. Si
v1alpha1.horari(string) es converteix av1.horaris(llista d'un) i de tornada, cal decidir què passa quan la llista té tres elements. La convenció és desar el que no hi cap en una anotació.
La migració completa
# 1. Publicar la versió nova servint-la, sense canviar l'emmagatzematge
# versions: v1alpha1 (served, storage) + v1 (served)
# 2. Canviar l'emmagatzematge a v1
# versions: v1alpha1 (served) + v1 (served, storage)
# 3. Reescriure tots els objectes existents perquè es desin amb l'esquema nou
kubectl get rutesprogramades -A -o json | kubectl replace -f -
# 4. Comprovar quines versions queden registrades com a emmagatzemades
kubectl get crd rutesprogramades.rutasnorte.example \
-o jsonpath='{.status.storedVersions}{"\n"}'# 5. Un cop tots els objectes són a v1, treure v1alpha1 de storedVersions
# editant .status.storedVersions, i només llavors deixar de servir-la.El pas 3 és el que s'oblida. Mentre storedVersions contingui la versió antiga, no la pots eliminar del CRD: l'apiserver s'hi nega, perquè hi hauria objectes a etcd que ja no sabria interpretar.
Conclusió pràctica: dissenya l'esquema amb cura des del principi. És molt més barat pensar dos dies el model de dades que operar un webhook de conversió durant anys.
- Quan NO crear un CRD
Els CRD són fàcils de crear i per això se'n creen de més. Abans d'escriure'n un, passa aquest filtre.
No el creïs si un ConfigMap n'hi ha prou
Si l'únic que necessites és desar configuració que algú llegeix, un ConfigMap (03-01) fa la feina amb zero infraestructura.
| Indici | Eina |
|---|---|
| Dades de configuració que una aplicació llegeix en arrencar | ConfigMap |
| Dades que un controlador ha de reconciliar contínuament | CRD |
| Estructura senzilla, un consumidor, sense validació | ConfigMap |
| Estructura complexa que diversos equips escriuen i que s'ha de validar | CRD |
| Necessites RBAC granular per tipus de dada | CRD |
Necessites watch, status i condicions |
CRD |
No el creïs si ningú no reconciliarà res
Aquest és l'advertiment central de la lliçó, i mereix enunciar-se sense embuts:
Un CRD sense controlador és una base de dades amb formulari de validació.
La nostra RutaProgramada està creada. Les dues rutes existeixen a etcd. Es validen, s'etiqueten, es versionen a Git, surten a kubectl get. I no passa absolutament res. No arrenca cap autobús. No es crea cap Deployment. La columna FASE continua buida llevat de quan la vam omplir a mà.
El valor d'un recurs personalitzat no és al recurs: és al bucle de reconciliació que l'observa i actua. Sense ell tens un YAML validat, i això ho aconsegueixes més barat amb un esquema JSON al teu repositori i una comprovació al pipeline de CI.
Preguntes de control:
- Qui observarà aquest recurs i què farà? Si no hi ha resposta concreta, no creïs el CRD.
- Què escriurà a
.status? Si res, probablement volies un ConfigMap. - Què passa si algú l'esborra? Si la resposta és "res", no és un recurs de Kubernetes: és un document.
Altres senyals d'alarma
- Dades d'alta freqüència. etcd no és una base de dades de sèries temporals. Un CRD que s'actualitza cada segon la degradarà. Per a això hi ha la capa d'agregació (metrics-server) o un sistema extern (Prometheus, 07-03).
- Molts objectes. Milers d'instàncies d'un CRD ocupen etcd i alenteixen els
list. Si n'esperes desenes de milers, replanteja't el model. - Objectes grans. El límit pràctic d'un objecte a etcd és aproximadament 1 MiB. Un CRD no és lloc per adjuntar fitxers.
- Dades que canvien a mà constantment. Si el recurs l'editarà una persona deu vegades al dia, potser el que necessites és una aplicació amb interfície, no un CRD.
- Modelar domini de negoci pur. Aquí convé ser honest amb el nostre propi exemple: modelar rutes d'autobús com a objectes de Kubernetes té sentit si les rutes es tradueixen en recursos del clúster (un Deployment de venda per ruta, un CronJob d'informes per corredor). Si són només files d'una taula, el seu lloc és
postgres-reserves, no etcd.
Alternatives abans de decidir-te
| Necessitat | Alternativa al CRD |
|---|---|
| Configuració per entorn | ConfigMap + Kustomize (10-04) |
| Plantilles de manifests | Helm (10-03) |
| Validació de manifests al pipeline | Esquemes JSON + kubeconform a CI |
| Polítiques sobre objectes existents | Webhook d'admissió o Kyverno |
| Dades de negoci | Una base de dades |
Errors Comuns i Consells
Anomenar malament el CRD. metadata.name ha de ser exactament <plural>.<group>. Si no, l'apiserver rebutja l'objecte amb un missatge que despista força.
Camps que desapareixen sense avís. L'apiserver poda tot el que no sigui a l'esquema, en silenci. Si un camp "no es desa", gairebé sempre està mal escrit o falta a l'esquema. Compara amb kubectl get ... -o yaml.
Oblidar el subrecurs status. Sense ell, el controlador i l'usuari es trepitgen les escriptures. Activa'l des del primer dia: afegir-lo després obliga a revisar tot el codi del controlador.
Començar directament en v1. T'obliga a compatibilitat per sempre amb un model que encara no has provat. Comença en v1alpha1 mentre el disseny s'assenta.
x-kubernetes-preserve-unknown-fields: true a l'arrel. Desactiva la validació de tot l'objecte. Si el necessites, acota'l al subarbre concret.
Regex amb sintaxi PCRE. Els pattern fan servir RE2. Un (?=...) provoca un error en crear el CRD, no en fer-lo servir.
Esborrar un CRD sense pensar-hi. kubectl delete crd <nom> esborra totes les instàncies d'aquell tipus a tots els namespaces, sense confirmació. És irreversible llevat de còpia de seguretat.
Objectes orfes després de desinstal·lar un operador. Si esborres l'operador però no els seus CRD, queden tipus registrats sense ningú que els atengui. I si esborres els CRD abans que el controlador retiri els seus finalitzadors, els objectes es queden penjats en Terminating per sempre.
Consell: escriu les description de tots els camps. Alimenten kubectl explain i converteixen el CRD en autodocumentat. És el millor retorn per esforç de tota la lliçó.
Consell: posa additionalPrinterColumns des del principi. Un kubectl get que només mostra nom i edat fa inutilitzable un recurs que per la resta està ben dissenyat.
Consell: prova la validació amb manifests deliberadament trencats. És l'única manera de comprovar que l'esquema fa el que creus. Desa aquests manifests com a proves de regressió del CRD.
Consell: fes servir kubectl explain --recursive per veure l'arbre complet de l'esquema d'un cop d'ull, molt útil en revisar un CRD aliè.
Exercicis
Exercici 1: crear un CRD amb validació
Crea un CRD ParadaAutobus al grup rutasnorte.example, abast Namespaced, versió v1, amb nom curt parada i categoria rutasnorte. El seu spec ha de tenir:
codi(string, obligatori, patró^P-[0-9]{4}$)localitat(string, obligatori, entre 2 i 60 caràcters)andanes(enter, entre 1 i 20, per defecte 1)accessible(booleà, per defectetrue)serveis(array de strings de l'enumtaquilla,cafeteria,consigna,wc, sense repeticions)
Afegeix-hi columnes per a codi, localitat, andanes i antiguitat. Crea una parada vàlida i una altra d'invàlida i comprova els missatges d'error.
Exercici 2: subrecurs status i condicions
Amplia el CRD anterior amb el subrecurs status, amb els camps operativa (booleà), viatgersDia (enter) i condicions (array amb l'estructura estàndard). Afegeix-hi una columna Operativa. Escriu l'estat mitjançant --subresource=status i comprova que un kubectl apply posterior del spec no l'esborra.
Exercici 3: decidir entre CRD i ConfigMap
Per a cadascun d'aquests quatre casos de Rutas Norte, decideix si correspon un CRD o un ConfigMap i justifica la resposta en una o dues frases:
- Els missatges de la interfície de
botiga-weben castellà, català i anglès. - Una definició de "corredor comercial" que, en crear-se, ha de provocar automàticament el desplegament d'un CronJob d'informes i una NetworkPolicy propis.
- La llista de dominis permesos per a CORS a
api-reserves. - Un "entorn de proves efímer" que un desenvolupador sol·licita i que ha de crear un namespace, una còpia de
postgres-reservesdes d'un snapshot i esborrar-se sol als 7 dies.
Solucions
Solució 1
# /tmp/crd-paradaautobus.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: paradesautobus.rutasnorte.example
labels:
app.kubernetes.io/part-of: rutas-norte
spec:
group: rutasnorte.example
scope: Namespaced
names:
kind: ParadaAutobus
listKind: ParadaAutobusList
plural: paradesautobus
singular: paradaautobus
shortNames: ["parada", "parades"]
categories: ["rutasnorte"]
versions:
- name: v1
served: true
storage: true
additionalPrinterColumns:
- name: Codi
type: string
jsonPath: .spec.codi
- name: Localitat
type: string
jsonPath: .spec.localitat
- name: Andanes
type: integer
jsonPath: .spec.andanes
- name: Antiguitat
type: date
jsonPath: .metadata.creationTimestamp
schema:
openAPIV3Schema:
type: object
description: "Una parada física de la xarxa de Rutas Norte"
required: ["spec"]
properties:
spec:
type: object
required: ["codi", "localitat"]
properties:
codi:
type: string
description: "Codi de parada, format P-9999"
pattern: '^P-[0-9]{4}$'
localitat:
type: string
description: "Localitat on es troba la parada"
minLength: 2
maxLength: 60
andanes:
type: integer
description: "Nombre d'andanes disponibles"
minimum: 1
maximum: 20
default: 1
accessible:
type: boolean
description: "Si la parada té accessibilitat completa"
default: true
serveis:
type: array
description: "Serveis disponibles a la parada"
uniqueItems: true
maxItems: 4
items:
type: string
enum: ["taquilla", "cafeteria", "consigna", "wc"]NAME SHORTNAMES APIVERSION NAMESPACED KIND
paradesautobus parada,parades rutasnorte.example/v1 true ParadaAutobusParada vàlida:
kubectl apply -f - <<'EOF'
apiVersion: rutasnorte.example/v1
kind: ParadaAutobus
metadata:
name: p-0041-bilbao-termibus
namespace: rutas-norte-dev
labels:
app.kubernetes.io/part-of: rutas-norte
entorn: dev
spec:
codi: "P-0041"
localitat: "Bilbao"
andanes: 12
serveis: ["taquilla", "cafeteria", "wc"]
EOF
kubectl get parades -n rutas-norte-devparadaautobus.rutasnorte.example/p-0041-bilbao-termibus created
NAME CODI LOCALITAT ANDANES ANTIGUITAT
p-0041-bilbao-termibus P-0041 Bilbao 12 9sParada invàlida:
kubectl apply -f - <<'EOF'
apiVersion: rutasnorte.example/v1
kind: ParadaAutobus
metadata:
name: parada-dolenta
namespace: rutas-norte-dev
spec:
codi: "PARADA41"
localitat: "X"
andanes: 50
serveis: ["taquilla", "taquilla", "parking"]
EOFThe ParadaAutobus "parada-dolenta" is invalid:
* spec.codi: Invalid value: "PARADA41": spec.codi in body should match '^P-[0-9]{4}$'
* spec.localitat: Invalid value: "X": spec.localitat in body should be at least 2 chars long
* spec.andanes: Invalid value: 50: spec.andanes in body should be less than or equal to 20
* spec.serveis: Invalid value: ["taquilla","taquilla","parking"]: spec.serveis in body should have unique items
* spec.serveis[2]: Unsupported value: "parking": supported values: "taquilla", "cafeteria", "consigna", "wc"Cinc regles diferents comprovades —patró, longitud, rang, unicitat i enumeració— sense una línia de codi.
# El default també es va aplicar
kubectl get parada p-0041-bilbao-termibus -n rutas-norte-dev \
-o jsonpath='{.spec.accessible}{"\n"}'Solució 2
S'afegeix al CRD el bloc subresources, l'esquema de status i la columna:
- name: v1
served: true
storage: true
subresources:
status: {}
additionalPrinterColumns:
- name: Codi
type: string
jsonPath: .spec.codi
- name: Localitat
type: string
jsonPath: .spec.localitat
- name: Andanes
type: integer
jsonPath: .spec.andanes
- name: Operativa
type: boolean
jsonPath: .status.operativa
- name: Antiguitat
type: date
jsonPath: .metadata.creationTimestamp
schema:
openAPIV3Schema:
type: object
required: ["spec"]
properties:
spec:
# ... igual que a la solució 1 ...
status:
type: object
description: "Estat observat de la parada"
properties:
operativa:
type: boolean
viatgersDia:
type: integer
minimum: 0
condicions:
type: array
items:
type: object
required: ["type", "status"]
properties:
type:
type: string
status:
type: string
enum: ["True", "False", "Unknown"]
lastTransitionTime:
type: string
format: date-time
reason:
type: string
message:
type: stringkubectl apply -f /tmp/crd-paradaautobus.yaml
kubectl patch parada p-0041-bilbao-termibus -n rutas-norte-dev \
--subresource=status --type=merge -p '{
"status": {
"operativa": true,
"viatgersDia": 3184,
"condicions": [{
"type": "AndanesDisponibles",
"status": "True",
"reason": "TotesLliures",
"message": "12 de 12 andanes operatives",
"lastTransitionTime": "2026-08-05T21:02:00Z"
}]
}
}'
kubectl get parades -n rutas-norte-devComprovació de la independència entre spec i status:
# Reaplicar el spec original, que NO conté status
kubectl apply -f - <<'EOF'
apiVersion: rutasnorte.example/v1
kind: ParadaAutobus
metadata:
name: p-0041-bilbao-termibus
namespace: rutas-norte-dev
labels:
app.kubernetes.io/part-of: rutas-norte
entorn: dev
spec:
codi: "P-0041"
localitat: "Bilbao"
andanes: 14
serveis: ["taquilla", "cafeteria", "wc", "consigna"]
EOF
kubectl get parades -n rutas-norte-devLes andanes es van actualitzar a 14 i OPERATIVA continua en true: l'apply no va tocar el status, perquè el subrecurs l'aïlla. Sense ell, aquell apply hauria esborrat tot l'estat que el controlador acabava de calcular. Aquest és exactament el motiu pel qual cal activar-lo.
kubectl wait --for=condition=AndanesDisponibles \
parada/p-0041-bilbao-termibus -n rutas-norte-dev --timeout=10s# Neteja (esborra el CRD i totes les seves instàncies)
kubectl delete crd paradesautobus.rutasnorte.exampleSolució 3
1. Missatges de la interfície en tres idiomes → ConfigMap.
Són dades de configuració que botiga-web llegeix en arrencar. Ningú no ha de reconciliar res: no hi ha estat desitjat a perseguir, només text per muntar com a volum o injectar com a variables. Un ConfigMap per idioma, versionat a Git, resol el cas sense infraestructura addicional.
2. Corredor comercial que desplega un CronJob i una NetworkPolicy → CRD. Aquest és el cas canònic. Hi ha un estat desitjat ("existeix el corredor Cantàbric") que s'ha de traduir en recursos reals del clúster, i alguna cosa els ha de crear, mantenir si algú els esborra a mà i netejar quan el corredor desaparegui. Això és un bucle de reconciliació, és a dir, un CRD més un controlador. Sense el controlador el CRD no valdria res.
3. Dominis permesos per a CORS → ConfigMap.
És una llista de cadenes que l'aplicació llegeix. Si a més la vols validar, un esquema JSON al pipeline de CI és més barat que un CRD. La prova que no necessita CRD: ningú no escriuria res al seu .status.
4. Entorn de proves efímer amb caducitat → CRD.
Requereix reconciliació activa i contínua: crear un namespace, restaurar postgres-reserves des d'un snapshot (05-05), vigilar el pas del temps i esborrar-ho tot als 7 dies. El status tindria camps amb sentit (fase, dataCaducitat, namespaceCreat) i l'esborrat en cascada es resoldria amb ownerReferences. És un operador de manual, i la lliçó següent explica com s'escriu.
Conclusió
Estendre l'API de Kubernetes significa ensenyar-li un tipus d'objecte nou, i a canvi aquell tipus hereta gratis els endpoints REST, la persistència a etcd, la validació, l'RBAC, l'auditoria, el watch i tot kubectl. És la característica que explica l'ecosistema sencer: els Certificate de cert-manager, els VolumeSnapshot del mòdul 5 i els Backup de Velero que ja has fet servir són recursos personalitzats.
De les tres maneres d'estendre —CRD, capa d'agregació i webhooks d'admissió—, el CRD cobreix la immensa majoria dels casos i només requereix un YAML. N'hem recorregut l'anatomia: group, names amb els seus plurals i dreceres, scope, i versions amb la distinció crucial entre served (es pot demanar) i storage (es desa així, i només una versió el pot tenir).
L'esquema OpenAPI v3 és el que converteix un CRD en un tipus de veritat: tipus, required, default, enum, pattern, rangs i x-kubernetes-preserve-unknown-fields quan cal permetre camps lliures. Els subrecursos status —que separa el desitjat de l'observat i evita que l'usuari i el controlador es trepitgin— i scale —que habilita kubectl scale— i les additionalPrinterColumns completen l'experiència.
Hem construït el CRD RutaProgramada amb validació real, hem comprovat que rebutja sis errors diferents amb missatges precisos, i l'hem manejat amb get, describe, explain, edit, patch, label, wait i RBAC exactament igual que un Deployment. També hem vist per què el versionatge és la part difícil, i per què un webhook de conversió caigut bloqueja el recurs sencer.
I hem acabat al lloc on calia acabar: les nostres dues rutes existeixen, es validen i surten a kubectl get, però no passa res. La columna FASE és buida perquè ningú no l'escriu. Un CRD sense controlador és una base de dades amb formulari.
El que falta és el programari que observi aquests objectes, compari el desitjat amb l'observat i actuï: el bucle de reconciliació que coneixem des de 01-02. Un recurs personalitzat més un controlador que el reconcilia és, exactament, un operador. És el que fa cert-manager amb els Certificate, el que faria un controlador de RutaProgramada, i el que resoldrà per fi la mancança que vam deixar oberta a 06-01: que postgres-reserves continua sent un StatefulSet artesanal que no sap replicar-se ni commutar per error. Aquest és el tema de la lliçó següent, l'última del mòdul: Operadors i el Patró Controlador.
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
