La lliçó anterior va acabar amb una llista honesta dels defectes de Helm: les plantilles deixen de ser YAML vàlid, nindent i els espais en blanc són una font constant d'errors, i cal aprendre el motor de plantilles de Go abans de poder tocar res. Kustomize neix precisament d'aquest descontentament i parteix d'una premissa oposada: no fer servir plantilles en absolut.
A Kustomize, els teus manifestos continuen sent YAML de Kubernetes perfectament vàlid. Pots obrir-los a l'editor i que et validi l'esquema, aplicar-los amb kubectl apply -f, i llegir-los sense desxifrar res. El que canvia entre entorns no s'expressa amb variables, sinó amb superposicions: petits fitxers que declaren les diferències respecte a una base comuna.
En aquesta lliçó migrarem el k8s/ de Rutas Norte de 120 fitxers duplicats a una base més tres superposicions, resoldrem de forma neta el problema del reinici davant de canvis de configuració que a 03-03 arreglàvem amb una anotació a mà, i acabarem comparant Helm i Kustomize sense favoritismes.
Contingut
- La filosofia sense plantilles
- Kustomize dins de kubectl i com a binari propi
- El model base + superposicions
kustomization.yamlcamp a camp- Pedaços: fusió estratègica i JSON Patch
- Generadors de ConfigMaps i Secrets
- Components: trossos opcionals reutilitzables
- Flux de treball: kustomize, diff i apply
- Migració completa de Rutas Norte
- Helm davant de Kustomize
- Combinar Helm i Kustomize
- Errors comuns i consells
- Exercicis
- Conclusió
- La filosofia sense plantilles
Compara els dos enfocaments per al mateix objectiu: que api-reserves tingui 1 rèplica en desenvolupament i 4 en producció.
Amb Helm, el manifest deixa de ser YAML de Kubernetes: replicas: {{ .Values.apiReserves.replicaCount }}, image: {{ .Values.imageRegistry }}/api-reserves:{{ .Values.apiReserves.image.tag }}. Amb Kustomize, la base és YAML normal i corrent (replicas: 1, image: registry.rutasnorte.example/api-reserves:2.4.0), aplicable amb kubectl apply -f, i la superposició declara les diferències:
# k8s/entorns/pro/kustomization.yaml
resources:
- ../../base
replicas:
- { name: api-reserves, count: 4 }
images:
- name: registry.rutasnorte.example/api-reserves
newTag: "2.4.0"
digest: sha256:9c1e4a7b3d2f8e6a...| Helm | Kustomize | |
|---|---|---|
| Què és un manifest | Una plantilla que produeix YAML | YAML vàlid des del principi |
| Com es personalitza | Substituint variables abans de renderitzar | Transformant el YAML ja renderitzat |
| Què cal aprendre | Go templates + Sprig + estructura de charts | Un fitxer: kustomization.yaml |
| Es pot aplicar sense l'eina | No | Sí, la base sí |
| L'editor valida l'esquema | No | Sí |
Kustomize funciona com una canalització de transformacions: llegeix els manifestos base, aplica una sèrie d'operacions declarades (canviar el namespace, afegir etiquetes, substituir la imatge, aplicar pedaços) i emet el YAML resultant.
flowchart LR
B["k8s/base/<br/>YAML vàlid"] --> T1[namespace] --> T2[labels] --> T3[images]
T3 --> T4[replicas] --> T5[patches] --> T6[generators] --> O["YAML final<br/>per al clúster"]
OV["overlay pro/<br/>kustomization.yaml"] -.declara.-> T1 & T3 & T5 & T6
style B fill:#e8f4ff
style O fill:#e8ffe8
Aquesta naturalesa de "transformar YAML existent" té una conseqüència elegant: Kustomize entén els tipus de Kubernetes. Quan fusiona dues llistes de contenidors sap que la clau de correlació és name; quan canvia una imatge sap on són els camps image en un Deployment, un StatefulSet, un CronJob o un DaemonSet. Helm, que només concatena text, no en sap res.
- Kustomize dins de kubectl i com a binari propi
kubectl kustomize k8s/entorns/pro # veure el resultat sense aplicar
kubectl apply -k k8s/entorns/pro # aplicar
kubectl diff -k k8s/entorns/pro # comparar amb el clúster
kubectl delete -k k8s/entorns/proAvantatge enorme: no hi ha res a instal·lar. Qualsevol amb kubectl pot desplegar. Desavantatge: la versió integrada va endarrerida respecte de la independent i no es pot actualitzar sense actualitzar kubectl.
curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
kustomize build k8s/entorns/pro | kubectl apply -f -| Aspecte | kubectl -k |
kustomize |
|---|---|---|
| Instal·lació | Ja el tens | Descàrrega a part |
| Versió i funcions noves | Amb retard de mesos | Immediates |
Generadors externs i helmCharts |
Limitat | Complet |
Recomanació per a Rutas Norte: kubectl -k per a l'ús interactiu diari; el binari amb versió fixada a la canalització ci-rutasnorte, perquè tothom generi exactament el mateix YAML.
- El model base + superposicions
k8s/
├── base/
│ ├── kustomization.yaml
│ ├── botiga-web/ (kustomization + deployment + service + hpa + ingress)
│ ├── api-reserves/ (+ servicemonitor + config/)
│ ├── postgres-reserves/ (recurs de l'operador, 06-07)
│ ├── redis-cache/
│ ├── worker-notificacions/ (+ scaledobject de KEDA, 09-04)
│ └── informes-ocupacio/ (cronjob)
├── components/
│ ├── politiques-xarxa/ (NetworkPolicies, 04-06)
│ ├── alta-disponibilitat/ (PDB + topologia, 09-05)
│ └── observabilitat/ (ServiceMonitors + regles, 07-03)
└── entorns/
├── dev/ (kustomization.yaml + config.env + recursos-patch.yaml)
├── pre/ (ídem)
└── pro/ (ídem + ingress-patch.yaml)Tres conceptes:
- Base: els manifestos comuns. Ha de ser desplegable per si sola i conté els valors més conservadors.
- Superposició (overlay): un directori que referencia una base i declara les diferències. Una per entorn.
- Component: un tros reutilitzable que les superposicions activen opcionalment.
# k8s/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources: # Kustomize és recursiu: cadascun té el seu
- botiga-web
- api-reserves
- postgres-reserves
- redis-cache
- worker-notificacions
- informes-ocupacio
labels:
- includeSelectors: false # veure l'apartat 4 sobre per què
pairs:
app.kubernetes.io/part-of: rutas-norte
app.kubernetes.io/managed-by: kustomize# k8s/base/api-reserves/deployment.yaml
# YAML de Kubernetes pur. Valors conservadors, propis de desenvolupament.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-reserves
labels: { app: api-reserves }
spec:
replicas: 1
selector:
matchLabels: { app: api-reserves }
strategy:
rollingUpdate: { maxSurge: 1, maxUnavailable: 0 }
template:
metadata:
labels: { app: api-reserves }
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
spec:
serviceAccountName: api-reserves
securityContext:
runAsNonRoot: true
runAsUser: 10001
seccompProfile: { type: RuntimeDefault }
containers:
- name: api
image: registry.rutasnorte.example/api-reserves:2.4.0
ports:
- { name: http, containerPort: 8080 }
envFrom:
- configMapRef: { name: api-reserves-config }
- secretRef: { name: api-reserves-credencials }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
livenessProbe:
httpGet: { path: /salut/viu, port: http }
initialDelaySeconds: 15
readinessProbe:
httpGet: { path: /salut/preparat, port: http }
initialDelaySeconds: 5
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { memory: 256Mi }
volumeMounts: [{ name: tmp, mountPath: /tmp }]
volumes: [{ name: tmp, emptyDir: {} }]Fixa't que aquest fitxer és directament aplicable amb kubectl apply -f. Això no ho pots fer amb una plantilla de Helm: és l'avantatge principal de l'enfocament.
I la superposició de desenvolupament són onze línies:
# k8s/entorns/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: rutas-norte-dev
resources: [../../base]
labels:
- includeSelectors: false
pairs: { entorn: dev }
images:
- { name: registry.rutasnorte.example/api-reserves, newTag: dev-abc123f }
- { name: registry.rutasnorte.example/botiga-web, newTag: dev-abc123f }
configMapGenerator:
- name: api-reserves-config
behavior: merge
envs: [config.env]
patches:
- path: recursos-patch.yaml
kustomization.yaml camp a camp
kustomization.yaml camp a campresources
Fitxers, directoris amb el seu propi kustomization.yaml, o URL remotes:
resources:
- deployment.yaml
- ../../base
- github.com/kubernetes-sigs/kustomize/examples/multibases?ref=v5.4.3Sobre els recursos remots: fixa sempre ?ref= a una etiqueta o commit. Sense això apuntes a la branca principal i el teu desplegament canvia quan algú aliè fa un commit. És el mateix principi que --version a Helm i les etiquetes immutables d'imatge (08-05).
namespace, namePrefix i nameSuffix
namespace: rutas-norte-pro estableix metadata.namespace a tots els objectes generats i actualitza les referències creuades (el namespace d'un subjects de RoleBinding, per exemple). Un sol camp elimina la principal duplicació entre entorns. Els objectes d'àmbit de clúster (ClusterRole, StorageClass, CRD) no en resulten afectats: Kustomize ho sap.
namePrefix: rn- i nameSuffix: -pro reanomenen els objectes, i —molt important— Kustomize actualitza les referències: el scaleTargetRef de l'HPA, el serviceName del StatefulSet, el name del ConfigMap a envFrom, el backend.service.name de l'Ingress. Rutas Norte no els fa servir, perquè ja separa entorns per namespace i afegir -pro al nom de tot complica les ordres de diagnòstic; són útils quan desplegues dues instàncies al mateix namespace.
labels i per què commonLabels està desaconsellat
Aquest apartat mereix atenció especial perquè és un parany real que trenca desplegaments.
# FORMA MODERNA I CORRECTA
labels:
- includeSelectors: false # <-- LA CLAU
pairs:
entorn: pro
app.kubernetes.io/part-of: rutas-nortecommonLabels (la forma antiga) afegeix les etiquetes a metadata.labels i també a spec.selector.matchLabels del Deployment, a spec.template.metadata.labels del pod i a spec.selector del Service. I aquí hi ha el problema: spec.selector d'un Deployment és immutable (02-03, 02-07). Si la base ja està desplegada i afegeixes una etiqueta, el següent apply falla:
The Deployment "api-reserves" is invalid: spec.selector: Invalid value:
v1.LabelSelector{...}: field is immutableL'única sortida és esborrar el Deployment i recrear-lo, amb tall de servei. A rutas-norte-pro, a les onze del matí.
Hi ha un segon problema, més subtil: si afegeix entorn: pro al selector del Service, aquest Service deixarà de trobar els pods que ja existien sense aquesta etiqueta. Trànsit a enlloc, sense cap error visible.
| Camp | Toca els selectors | Quan fer-lo servir |
|---|---|---|
labels amb includeSelectors: false |
No | Per defecte, sempre |
labels amb includeSelectors: true |
Sí | Només en un desplegament nou des de zero |
commonLabels |
Sí (equival a true) |
Desaconsellat; existeix per compatibilitat |
commonAnnotations |
N/A (no són selectors) | Sense risc |
Regla de Rutas Norte: les etiquetes de selector es defineixen una vegada a la base (app: api-reserves) i no es toquen mai. Tota la resta s'afegeix amb includeSelectors: false.
images
images:
- { name: registry.rutasnorte.example/api-reserves, newTag: "2.4.0" }
# Amb digest present, és el digest el que mana (08-05)
- name: registry.rutasnorte.example/botiga-web
newTag: "3.1.2"
digest: sha256:9c1e4a7b3d2f8e6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a
# Canviar el registre sencer (rèplica en una altra regió)
- name: registry.rutasnorte.example/worker-notificacions
newName: registry-eu.rutasnorte.example/worker-notificacions
newTag: "1.8.4"Kustomize busca el camp image a qualsevol tipus que el tingui, inclosos els initContainers. No cal dir-li on ha de mirar. Això és el que la canalització ci-rutasnorte modifica a cada desplegament:
cd k8s/entorns/pre
kustomize edit set image registry.rutasnorte.example/api-reserves=registry.rutasnorte.example/api-reserves:rc-${GIT_SHA}kustomize edit modifica el kustomization.yaml al disc. Combinat amb un commit automàtic, és la peça que connecta la construcció de la imatge amb GitOps (10-05).
replicas
Estalvia escriure un pedaç per a una cosa tan comuna. Però atenció, reprenent 09-01: si api-reserves té un HPA, declarar replicas aquí és contraproduent. Cada kubectl apply -k tornarà el Deployment a 4 rèpliques encara que l'HPA el tingui a 15 per la càrrega del pont de maig, amb una davallada de capacitat fins que l'HPA reaccioni.
Per a components amb HPA, la solució correcta és no declarar replicas enlloc (ni a la base ni a la superposició) i deixar que minReplicas de l'HPA governi. Kubernetes no exigeix el camp: per defecte val 1 i l'HPA el puja immediatament. La peça que falta —que Argo CD no marqui deriva quan l'HPA canviï aquest camp— la resoldrem a 10-05 amb ignoreDifferences.
- Pedaços: fusió estratègica i JSON Patch
Els camps anteriors cobreixen l'habitual. Per a tota la resta, hi ha pedaços.
Fusió estratègica (strategic merge)
Escrius un YAML parcial amb la mateixa estructura de l'objecte. Kustomize el fusiona entenent la semàntica dels tipus de Kubernetes.
# k8s/entorns/pro/recursos-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-reserves # nom i tipus identifiquen quin objecte cal pedaçar
spec:
template:
spec:
containers:
# 'name' és la clau de correlació de la llista de contenidors:
# Kustomize fusiona amb el contenidor 'api', NO substitueix la llista.
- name: api
resources:
requests: { cpu: 250m, memory: 512Mi }
limits: { memory: 1Gi }
env:
- { name: LOG_LEVEL, value: warn }El que fa especial la fusió estratègica és que Kustomize coneix l'esquema: sap que containers es correlaciona per name, ports per containerPort, volumeMounts per mountPath. Un pedaç que només menciona el contenidor api deixa intactes els sidecars.
Es pot escriure en línia amb patch: |- dins del kustomization.yaml, útil per a canvis petits. I hi ha dues directives especials: $patch: replace substitueix una llista sencera en comptes de fusionar-la, i $patch: delete elimina l'element que coincideixi.
JSON Patch (RFC 6902)
Quan la fusió estratègica no arriba —perquè cal operar sobre índexs de llista, o sobre un CRD que Kustomize no coneix— es fa servir JSON Patch: una llista d'operacions explícites.
patches:
- target: { kind: Deployment, name: api-reserves }
patch: |-
# '-' significa "afegir al final de la llista"
- op: add
path: /spec/template/spec/containers/0/env/-
value: { name: REGIO, value: eu-oest }
- op: replace
path: /spec/template/spec/containers/0/resources/limits/memory
value: 2Gi
- op: remove
path: /spec/template/spec/containers/0/livenessProbe/initialDelaySeconds
# '/' i '~' en una clau s'escapen com a ~1 i ~0
- op: add
path: /spec/template/metadata/annotations/rutasnorte.example~1revisio
value: "7"Detalls que cal conèixer: els camins comencen a l'arrel de l'objecte i els índexs de llista són numèrics (/spec/template/spec/containers/0/); replace falla si el camí no existeix, mentre que add el crea o el substitueix, així que davant del dubte fes servir add.
El camp target: a qui s'aplica el pedaç
Aquí hi ha la potència real del sistema. Un pedaç es pot dirigir a molts objectes alhora.
patches:
- target: { kind: Deployment, name: api-reserves }
path: recursos-patch.yaml
# A TOTS els Deployments (el nom admet expressió regular)
- target: { kind: Deployment, name: ".*" }
patch: |-
- op: add
path: /spec/template/metadata/annotations/rutasnorte.example~1revisat
value: "2026-08-05"
# Per etiqueta
- target: { labelSelector: "app.kubernetes.io/part-of=rutas-norte", kind: Deployment }
patch: |-
- op: add
path: /spec/template/spec/priorityClassName
value: rutas-norte-alta
# Per grup i versió d'API: necessari amb CRDs
- target: { group: keda.sh, version: v1alpha1, kind: ScaledObject, name: worker-notificacions }
patch: |-
- op: replace
path: /spec/maxReplicaCount
value: 30Els selectors disponibles a target són kind, name (exacte o regex), namespace, group, version, labelSelector i annotationSelector.
patchesStrategicMerge i patchesJson6902 són obsolets
Veuràs molt codi antic amb aquests dos camps. El camp unificat patches els reemplaça tots dos: detecta automàticament si el contingut és una fusió estratègica o un JSON Patch, i admet target amb selectors en els dos casos, cosa que els antics no permetien.
| Camp obsolet | Substitut | Avantatge del nou |
|---|---|---|
patchesStrategicMerge |
patches amb path |
Admet target amb selectors |
patchesJson6902 |
patches amb target |
Sintaxi unificada, pedaç en línia |
commonLabels |
labels amb includeSelectors |
Control sobre els selectors immutables |
bases |
resources |
Un sol concepte |
vars |
replacements |
Més potent i predictible |
kustomize edit fix migra els camps obsolets automàticament.
- Generadors de ConfigMaps i Secrets
Aquesta és la funcionalitat més elegant de Kustomize, i resol netament un problema que a 03-03 solucionàvem a mà.
configMapGenerator:
# A partir de literals
- name: api-reserves-config
literals: [LOG_LEVEL=info, RESERVA_TTL_MINUTS=15, CACHE_HOST=redis-cache]
# A partir de fitxers: cada fitxer és una clau amb el seu contingut
- name: api-reserves-plantilles
files:
- config/app.propietats
- plantilla-correu=config/correu-reserva.html # reanomenar la clau
# A partir d'un fitxer de variables: cada línia, una clau
- name: api-reserves-entorn
envs: [config/produccio.env]El sufix de hash: la joia de la corona
Kustomize afegeix un hash del contingut al nom. I —això és l'important— actualitza automàticament totes les referències, així que el Deployment queda amb configMapRef: { name: api-reserves-config-9t2hmf6b4d }.
Pensa en el que implica. Quan canvies LOG_LEVEL d'info a warn: canvia el contingut → canvia el hash → canvia la referència al Deployment → canvia la plantilla del pod → el Deployment fa un rollout automàticament.
flowchart LR
A["Canvies<br/>config.env"] --> B["Hash nou:<br/>...-c7d4k9m2t8"] --> C["Canvia la referència<br/>al Deployment"]
C --> D["Canvia la plantilla<br/>del pod"] --> E["Rollout automàtic<br/>amb la config nova"]
style E fill:#e8ffe8
Això resol, de forma nativa i sense trucs, el problema de 03-03. Allà vam explicar que un ConfigMap actualitzat no reinicia els pods i que l'apedaçament era afegir a mà una anotació amb el hash. Amb Kustomize no hi ha apedaçament: és el comportament per defecte. I comparat amb Helm (10-03), on calia escriure checksum/config: {{ include ... | sha256sum }} a cada plantilla, aquí no escrius res.
Avantatge addicional: el rollback funciona de debò. Com que cada versió de la configuració és un objecte diferent amb nom diferent, un kubectl rollout undo torna el pod a la referència anterior, i el ConfigMap vell continua existint mentre algun ReplicaSet el referenciï. Amb un ConfigMap de nom fix, el rollback tornaria el codi vell però amb la configuració nova: el pitjor de tots dos mons.
Desactivar el hash
També per generador, amb options: { disableNameSuffixHash: true } dins d'una entrada concreta.
Quan cal desactivar-lo? Només quan alguna cosa externa referencia el ConfigMap per un nom fix que Kustomize no pot actualitzar: un CRD d'un operador que Kustomize no sap interpretar, un pod que rellegeix el ConfigMap en calent a propòsit, o un de compartit entre aplicacions gestionades per sistemes diferents. Consell ferm: no el desactivis sense una raó concreta. El hash és la millor característica de Kustomize.
behavior: estendre un generador de la base
Si la base defineix api-reserves-config amb LOG_LEVEL=info, RESERVA_TTL_MINUTS=15 i CACHE_HOST=redis-cache, i la superposició de producció declara:
configMapGenerator:
- name: api-reserves-config
behavior: merge # <-- fusiona amb el de la base
envs: [config.env] # LOG_LEVEL=warn, PASSARELLA_URL=...El resultat combina les tres claus heretades amb LOG_LEVEL sobreescrit i PASSARELLA_URL afegida.
behavior |
Efecte |
|---|---|
| (sense especificar) | En crea un de nou; falla si ja existeix amb aquest nom |
merge |
Fusiona amb el de la base, sobreescrivint les claus coincidents |
replace |
Substitueix completament el de la base |
secretGenerator
La mateixa mecànica, amb codificació base64 automàtica i suport de type: kubernetes.io/tls per a certificats des de fitxers.
Advertiment crític:
secretGeneratorno xifra res. Base64 no és xifratge (03-02). Si poses la contrasenya depostgres-reservesen unliteralso en un fitxer del repositori, aquesta contrasenya està en clar a Git per sempre, fins i tot si després l'esborres.
La forma correcta a Rutas Norte, reprenent el que vam anunciar a 03-02, és referenciar un Secret xifrat amb SOPS o Sealed Secrets, o millor encara un recurs de l'External Secrets Operator que va a buscar el valor a Vault:
# k8s/entorns/pro/external-secret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata: { name: api-reserves-credencials }
spec:
refreshInterval: 1h
secretStoreRef: { name: vault-rutasnorte, kind: ClusterSecretStore }
target: { name: api-reserves-credencials, creationPolicy: Owner }
data:
- secretKey: BD_PASSWORD
remoteRef: { key: rutas-norte/pro/postgres, property: password }
- secretKey: PASSARELLA_TOKEN
remoteRef: { key: rutas-norte/pro/pagaments, property: token }Aquest fitxer és innocu: només diu on és el secret, no quin és. Hi tornarem a 10-05.
- Components: trossos opcionals reutilitzables
Un component és com una superposició, però pensat per ser inclòs per diverses. Resol el cas "això ho vull a pre i a pro, però no a dev". Rutas Norte té tres candidats clars: les NetworkPolicies (04-06), l'alta disponibilitat (09-05) i l'observabilitat (07-03). En desenvolupament fan nosa; als altres dos entorns són obligatoris.
# k8s/components/politiques-xarxa/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component # <-- Component, NO Kustomization
resources: [netpol.yaml]# k8s/components/politiques-xarxa/netpol.yaml (fragment)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: postgres-nomes-api }
spec:
podSelector:
matchLabels: { app: postgres-reserves }
policyTypes: [Ingress]
ingress:
- from:
- podSelector:
matchLabels: { app: api-reserves }
ports: [{ protocol: TCP, port: 5432 }]Un component també pot tenir pedaços, no només recursos:
# k8s/components/alta-disponibilitat/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component
resources: [pdb.yaml]
patches:
- target: { kind: Deployment, labelSelector: "app.kubernetes.io/part-of=rutas-norte" }
path: topologia-patch.yaml# topologia-patch.yaml — el 'target' mana, així que el nom s'ignora
apiVersion: apps/v1
kind: Deployment
metadata: { name: NO-IMPORTA }
spec:
template:
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels: { app.kubernetes.io/part-of: rutas-norte }I les superposicions l'activen: dev no en porta cap, pre porta politiques-xarxa i observabilitat, i pro hi afegeix a més alta-disponibilitat.
| Base | Superposició | Component | |
|---|---|---|---|
kind / apiVersion |
Kustomization / v1beta1 |
Kustomization / v1beta1 |
Component / v1alpha1 |
| Es fa servir des de | resources |
S'aplica directament | components |
| Quantes vegades | Una per superposició | Una | Diverses superposicions |
| Propòsit | El comú | Un entorn concret | Una capacitat opcional |
Els components s'apliquen en l'ordre en què apareixen, després dels resources. Si dos pedacen el mateix camp, guanya l'últim.
- Flux de treball: kustomize, diff i apply
# 1. VEURE el resultat abans de res. Sempre el primer pas.
kubectl kustomize k8s/entorns/pro > /tmp/pro-generat.yaml
# 2. Validar contra el servidor (esquema real + webhooks d'admissió)
kubectl kustomize k8s/entorns/pro | kubectl apply --dry-run=server -f -
# 3. COMPARAR amb el que hi ha al clúster. El pas decisiu.
kubectl diff -k k8s/entorns/pro--- LIVE
+++ MERGED
envFrom:
- configMapRef:
- name: api-reserves-config-9t2hmf6b4d
+ name: api-reserves-config-c7d4k9m2t8
resources:
limits:
- memory: 1Gi
+ memory: 2GiAquesta sortida explica exactament el que passarà: canvia el hash de la configuració (hi haurà rollout) i puja el límit de memòria. És l'equivalent a helm diff de la lliçó anterior, però integrat a kubectl, sense plugins.
# 4. Aplicar i verificar
kubectl apply -k k8s/entorns/pro
kubectl rollout status deploy/api-reserves -n rutas-norte-pro --timeout=300sPoda i validació a la canalització
Un problema real: si esborres un manifest del repositori, kubectl apply -k no esborra l'objecte del clúster. Es queda orfe. Existeix --prune amb --applyset i --prune-allowlist, però és empipador i cal enumerar els tipus. És un altre dels problemes que GitOps (10-05) resol de forma nativa: Argo CD i Flux saben quins objectes els pertanyen i els poden ells sols.
#!/usr/bin/env bash
# ci/validar-manifestos.sh — gratis i sense clúster, ideal per a cada PR
set -euo pipefail
for entorn in dev pre pro; do
kustomize build "k8s/entorns/${entorn}" > "/tmp/${entorn}.yaml" # YAML vàlid?
kubeconform -strict -summary "/tmp/${entorn}.yaml" # esquema?
kyverno apply politiques/ --resource "/tmp/${entorn}.yaml" # polítiques (08-03)?
done
- Migració completa de Rutas Norte
L'abans i el després
ABANS DESPRÉS
k8s/ k8s/
├── dev/ 38 fitxers ├── base/ 26 fitxers
├── pre/ 38 fitxers ├── components/ 7 fitxers
└── pro/ 38 fitxers └── entorns/ 10 fitxers
= 114 fitxers = 43 fitxersI l'important no és el nombre de fitxers, sinó que cada línia de configuració existeix una sola vegada.
El procés, pas a pas
Pas 1: triar la base. L'entorn més simple, normalment dev. Copiar els seus manifestos a k8s/base/, traient-los el namespace (el posarà la superposició) amb yq -i 'del(.metadata.namespace)' k8s/base/**/*.yaml.
Pas 2: escriure els kustomization.yaml de la base (apartat 3).
Pas 3: calcular el diferencial real de cada entorn.
< namespace: rutas-norte-dev > namespace: rutas-norte-pro
< replicas: 1 > replicas: 4
< image: ...api-reserves:dev-abc > image: ...api-reserves:2.4.0
< requests: {cpu: 50m, mem: 64Mi} > requests: {cpu: 250m, mem: 512Mi}Quatre diferències. Tres es resolen amb camps de kustomization.yaml (namespace, replicas, images) i només una necessita pedaç (resources).
Pas 4: escriure les superposicions.
# k8s/entorns/pro/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: rutas-norte-pro
resources:
- ../../base
- external-secret.yaml
components:
- ../../components/politiques-xarxa
- ../../components/observabilitat
- ../../components/alta-disponibilitat
labels:
- includeSelectors: false
pairs: { entorn: pro }
commonAnnotations:
rutasnorte.example/equip: plataforma
rutasnorte.example/criticitat: alta
images:
- name: registry.rutasnorte.example/api-reserves
newTag: "2.4.0"
digest: sha256:9c1e4a7b3d2f8e6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a
- { name: registry.rutasnorte.example/botiga-web, newTag: "3.1.2" }
# botiga-web i api-reserves NO porten 'replicas': els governa l'HPA (09-01)
replicas:
- { name: informes-ocupacio-llancador, count: 1 }
configMapGenerator:
- name: api-reserves-config
behavior: merge
envs: [config.env]
patches:
- path: recursos-patch.yaml
- path: ingress-patch.yaml # certificat real de Let's Encrypt (04-05)
- target: { kind: HorizontalPodAutoscaler, name: api-reserves }
patch: |-
- { op: replace, path: /spec/minReplicas, value: 4 }
- { op: replace, path: /spec/maxReplicas, value: 20 }Pas 5: verificar que la migració no canvia res. Aquest és el pas que dona confiança.
Un diff buit significa que pots fer kubectl apply -k i no passarà absolutament res. La migració és una operació sense risc.
Pas 6: esborrar els directoris vells amb git rm -r k8s/dev k8s/pre k8s/pro i actualitzar la canalització.
El resultat en xifres
| Mètrica | Abans | Després |
|---|---|---|
| Fitxers YAML / línies totals | 114 / ~4 800 | 43 / ~1 700 |
Llocs on canviar el límit de memòria d'api-reserves |
3 | 1 |
| Risc que un entorn quedi desincronitzat | Alt | Nul per construcció |
| Reinici en canviar la configuració | Manual (anotació) | Automàtic (hash) |
| Poder respondre "què hi ha a pro?" | No | kubectl kustomize k8s/entorns/pro |
- Helm davant de Kustomize
| Criteri | Helm | Kustomize |
|---|---|---|
| Corba d'aprenentatge | Alta: Go templates, Sprig, nindent |
Baixa: un fitxer declaratiu |
| Llegibilitat dels fonts | Baixa: {{- if }}, {{ toYaml | nindent }} |
Alta: els manifestos són YAML vàlid |
| Llegibilitat de la personalització | Alta: values.yaml és pla i explícit |
Mitjana: cal seguir la cadena de pedaços |
| Instal·lació | Binari a part | Integrat a kubectl |
| Distribució a tercers | Excel·lent: .tgz versionat, OCI |
Pobra: es comparteix un repositori Git |
| Ecosistema | Enorme | Escàs: gairebé no hi ha bases publicades |
| Estat al clúster | Sí: Secrets de release | No: només genera YAML |
| Rollback i neteja integrats | Sí: rollback, uninstall |
No: poda manual i empipadora |
| Hooks i ordenació | Sí | No (ho cobreixen les onades d'Argo CD, 10-05) |
| Condicionals i bucles | Sí: lògica arbitrària | No: components és el més semblant |
| Rollout en canviar la configuració | Manual (checksum/config) |
Automàtic (hash al nom) |
| Validació amb eines estàndard | No fins a renderitzar | Sí |
| Canvis que l'autor no va preveure | Difícil: cal bifurcar el chart | Fàcil: un pedaç arriba a qualsevol camp |
La regla pràctica del sector
flowchart TB
Q{"De qui és<br/>aquest programari?"}
Q -->|"De tercers:<br/>cert-manager, Prometheus,<br/>ingress-nginx, KEDA"| H["**Helm**<br/>consumir el chart oficial<br/>amb un values.yaml versionat"]
Q -->|"Nostre:<br/>botiga-web, api-reserves,<br/>worker-notificacions"| K["**Kustomize**<br/>base + superposicions"]
H --> G["Tots dos versionats a Git<br/>i desplegats per GitOps (10-05)"]
K --> G
style H fill:#fff4e8
style K fill:#e8f4ff
Helm per consumir, Kustomize per produir. És la decisió de Rutas Norte i la de la majoria d'equips madurs: ningú vol mantenir a mà els 60 objectes de kube-prometheus-stack, i ningú vol convertir el seu propi Deployment en una plantilla il·legible. L'excepció: si distribueixes el teu programari a clients que l'instal·len als seus clústers, necessites un paquet versionat i configurable, i aquí Helm no té rival.
- Combinar Helm i Kustomize
Hi ha dues formes de fer servir les dues eines juntes, i serveixen per al mateix: modificar un chart de tercers en un camp que el seu autor no va parametritzar.
Forma A: helm template i kustomitzar la sortida
helm template monitoratge prometheus-community/kube-prometheus-stack \
--version 65.1.1 --namespace monitoratge \
-f plataforma/valors-pro.yaml --include-crds \
> plataforma/generat/kube-prometheus-stack.yaml# plataforma/generat/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: monitoratge
resources: [kube-prometheus-stack.yaml]
patches:
# El que el chart NO permet configurar, ho pedacem aquí
- target: { kind: Deployment, name: monitoratge-grafana }
patch: |-
- op: add
path: /spec/template/spec/containers/0/env/-
value: { name: GF_FEATURE_TOGGLES_ENABLE, value: "traceToMetrics" }| Avantatge | Inconvenient |
|---|---|
| El YAML generat va a Git: saps exactament què es desplega | Cal regenerar a mà en actualitzar el chart |
| Pots pedaçar qualsevol camp | El fitxer generat és enorme i els diffs són sorollosos |
| Revisable en una petició de canvi, reproduïble bit a bit | Perds helm rollback i helm history |
Forma B: el generador helmCharts
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: monitoratge
helmCharts:
- name: kube-prometheus-stack
repo: https://prometheus-community.github.io/helm-charts
version: 65.1.1
releaseName: monitoratge
valuesFile: valors-pro.yaml
includeCRDs: true
patches:
- target: { kind: Deployment, name: monitoratge-grafana }
patch: |-
- op: add
path: /spec/template/spec/containers/0/env/-
value: { name: GF_FEATURE_TOGGLES_ENABLE, value: "traceToMetrics" }# Requereix permís explícit perquè executa un binari extern
kustomize build --enable-helm plataforma/monitoratgeUn sol fitxer ho declara tot i actualitzar és canviar el número de versió, però necessita --enable-helm i tenir helm instal·lat, el resultat no és a Git, i el suport és desigual (kubectl -k no l'admet bé; Argo CD i Flux requereixen configuració extra).
Recomanació per a Rutas Norte: la forma A. Tenir el YAML generat a Git és exactament el que fa que GitOps funcioni bé: qualsevol pot llegir el repositori i saber què hi ha desplegat.
Errors Comuns i Consells
1. Fer servir commonLabels (o labels amb includeSelectors: true) sobre una cosa ja desplegada. Modifica spec.selector, que és immutable, i l'apply falla. L'única sortida és esborrar i recrear, amb tall de servei. Fes servir sempre includeSelectors: false.
2. Declarar replicas en un component amb HPA. Cada apply desfà l'escalat automàtic fins que l'HPA reacciona. Per al que tingui HPA, no declaris replicas enlloc.
3. Secrets a secretGenerator amb literals. Base64 no és xifratge. Si escrius la contrasenya de postgres-reserves en un fitxer del repositori, està en clar a l'historial de Git per sempre. Fes servir SOPS, Sealed Secrets o External Secrets Operator.
4. Desactivar disableNameSuffixHash sense necessitat. Perds el rollout automàtic en canviar la configuració, que és la millor característica de Kustomize.
5. Recursos remots sense ?ref=. El teu desplegament canvia quan un desconegut fa un commit. Fixa sempre etiqueta o commit.
6. Posar massa coses a la base. Si la base conté coses que la meitat dels entorns han de pedaçar per treure, està mal dissenyada. La base és el mínim comú; l'opcional va en components.
7. Cadenes de superposicions molt profundes. Base → comú → regió → entorn → client és tècnicament possible i humanament impracticable: ningú sap d'on surt un valor. Dos nivells, tres com a molt.
8. Pedaç que no s'aplica i ningú se n'assabenta. Si el target no casa amb res, Kustomize no sempre avisa. Verifica sempre amb kubectl kustomize que el canvi apareix a la sortida.
9. replace en un JSON Patch sobre un camí que no existeix. Falla amb "missing value". Fes servir add, que crea o substitueix.
10. Oblidar escapar / a les claus d'un JSON Patch. rutasnorte.example/revisio s'escriu rutasnorte.example~1revisio.
11. Aplicar sense kubectl diff -k abans. És gratis, triga dos segons i t'ensenya exactament què canviarà. En producció hauria de ser obligatori.
12. Suposar que les llistes es fusionen sempre. A la fusió estratègica, les llistes amb clau de correlació coneguda (containers per name) es fusionen; les que no en tenen (args, command) se substitueixen senceres.
13. Editar objectes a mà amb kubectl edit i oblidar-ho. Kustomize no reconcilia: només actua quan algú executa apply. El següent desplegament trepitjarà el canvi manual sense avisar. Aquest és exactament el problema que resol GitOps.
Exercicis
Exercici 1: superposició de preproducció completa
Partint de la base descrita a la lliçó, escriu k8s/entorns/pre/kustomization.yaml que: faci servir el namespace rutas-norte-pre, etiqueti tot amb entorn: pre sense tocar els selectors, fixi les imatges a rc-2.4.0, activi els components de polítiques de xarxa i observabilitat, fusioni un ConfigMap amb LOG_LEVEL=info i una URL de passarel·la de proves, i apliqui un pedaç que pugi els recursos a la meitat dels de producció. Verifica el resultat sense aplicar res.
Exercici 2: pedaç JSON dirigit per etiqueta
Escriu un pedaç que afegeixi a tots els Deployments etiquetats amb app.kubernetes.io/part-of: rutas-norte un initContainer que esperi que postgres-reserves sigui accessible abans d'arrencar (06-04), sense modificar cap fitxer de la base. Explica per què necessites JSON Patch i no fusió estratègica.
Exercici 3: demostrar el rollout automàtic per hash
Amb la superposició de dev desplegada, demostra en tres passos que canviar una línia de config.env provoca un rollout automàtic: captura el nom del ConfigMap i la generació del Deployment abans, canvia el valor, aplica, i compara. Explica per què això no passaria amb un ConfigMap de nom fix.
Solucions
Solució 1
# k8s/entorns/pre/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: rutas-norte-pre
resources: [../../base]
components:
- ../../components/politiques-xarxa
- ../../components/observabilitat
labels:
- includeSelectors: false # CLAU: no tocar els selectors immutables
pairs: { entorn: pre }
images:
- { name: registry.rutasnorte.example/api-reserves, newTag: rc-2.4.0 }
- { name: registry.rutasnorte.example/botiga-web, newTag: rc-3.1.2 }
configMapGenerator:
- name: api-reserves-config
behavior: merge
envs: [config.env]
patches:
- path: recursos-patch.yaml# k8s/entorns/pre/config.env
LOG_LEVEL=info
PASSARELLA_URL=https://pagos-sandbox.proveedorexterno.example/v2# k8s/entorns/pre/recursos-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata: { name: api-reserves }
spec:
template:
spec:
containers:
- name: api
resources:
requests: { cpu: 125m, memory: 256Mi }
limits: { memory: 512Mi }kubectl kustomize k8s/entorns/pre | grep -E 'namespace:|entorn:|image:|memory:'
kubectl kustomize k8s/entorns/pre | kubectl apply --dry-run=server -f -
kubectl diff -k k8s/entorns/preSolució 2
patches:
- target:
kind: Deployment
labelSelector: "app.kubernetes.io/part-of=rutas-norte"
patch: |-
- op: add
path: /spec/template/spec/initContainers
value: []
- op: add
path: /spec/template/spec/initContainers/-
value:
name: esperar-bd
image: postgres:16.4
command: ["sh","-c","until pg_isready -h postgres-reserves -p 5432; do sleep 2; done"]
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 10001
capabilities: { drop: ["ALL"] }
resources:
requests: { cpu: 10m, memory: 32Mi }
limits: { memory: 64Mi }Nota: la primera operació buidaria una llista initContainers ja existent. Si alguns Deployments ja tenen init containers que cal conservar, la solució robusta és separar en dos pedaços amb target diferents.
Per què JSON Patch: la fusió estratègica exigeix un fitxer per objecte amb el seu metadata.name exacte, així que no es pot dirigir a "tots els que casin amb una etiqueta". JSON Patch combinat amb labelSelector a target arriba a N objectes amb una sola declaració, i /- permet afegir al final sense saber quants elements hi havia.
Solució 3
# --- ABANS ---
kubectl get deploy api-reserves -n rutas-norte-dev \
-o jsonpath='{.metadata.generation}{"\n"}{.spec.template.spec.containers[0].envFrom[0].configMapRef.name}{"\n"}'
# 4
# api-reserves-config-9t2hmf6b4d
# --- CANVI ---
sed -i 's/LOG_LEVEL=debug/LOG_LEVEL=info/' k8s/entorns/dev/config.env
kubectl diff -k k8s/entorns/dev # es veu el canvi de nom del ConfigMap
kubectl apply -k k8s/entorns/dev
# --- DESPRÉS --- -> 5 i api-reserves-config-c7d4k9m2t8
kubectl rollout status deploy/api-reserves -n rutas-norte-devExplicació: el hash forma part del nom del ConfigMap, i aquest nom apareix dins de spec.template del Deployment. En canviar la plantilla del pod, el controlador crea un ReplicaSet nou i executa el desplegament gradual (02-03).
Per què no passaria amb nom fix: spec.template continuaria sent byte a byte idèntic, metadata.generation no canviaria, i no hi hauria ReplicaSet nou. Els pods continuarien amb les variables velles fins que algú executés kubectl rollout restart a mà. Aquest és justament l'apedaçament de l'anotació checksum/config de 03-03, que aquí no cal.
Conclusió
Kustomize ha reduït el k8s/ de Rutas Norte de 114 fitxers duplicats a 43, amb cada valor definit una sola vegada. L'essencial:
- Sense plantilles: els manifestos de
k8s/base/continuen sent YAML de Kubernetes vàlid, aplicable ambkubectl apply -fi validable per l'editor. El que canvia entre entorns es declara com a transformacions a la superposició. - Ve dins de kubectl (
apply -k,diff -k,kubectl kustomize), tot i que el binari propi va més al dia i és el que convé fixar a la canalització. - El model base + superposicions + components cobreix els tres eixos reals: el comú, el propi de cada entorn, i les capacitats opcionals que només volen alguns entorns.
namespace,images,replicasilabelsresolen la majoria de diferències sense escriure un pedaç. Ilabelsha de portarincludeSelectors: false:commonLabelstoca els selectors immutables i trenca desplegaments en marxa.- Per a la resta hi ha
patches, amb fusió estratègica o JSON Patch, i untargetdirigible per tipus, nom, etiqueta o anotació.patchesStrategicMergeipatchesJson6902són obsolets. - Els generadors amb el seu sufix de hash són la joia: canviar una línia de configuració canvia el nom del ConfigMap, canvia la plantilla del pod i provoca un rollout automàtic. Resol de forma nativa el que a 03-03 arreglàvem a mà, i a més fa que el rollback torni codi i configuració alhora.
- I en la comparació honesta amb Helm, la regla és clara: Helm per consumir programari de tercers, Kustomize per a les aplicacions pròpies, amb dues formes de combinar-los quan cal pedaçar un chart aliè.
Però queda un problema que ni Helm ni Kustomize resolen, i que ha aparegut a les dues lliçons. Totes dues eines només actuen quan algú executa una ordre. Si algú fa kubectl edit en producció, cap se n'assabenta. Si el portàtil de qui desplega s'espatlla, ningú sap desplegar. Si la canalització ci-rutasnorte necessita credencials d'administrador del clúster per executar kubectl apply, tenim un problema de seguretat seriós que contradiu el RBAC mínim que vam definir a 08-01.
A la lliçó següent, GitOps amb Argo CD i Flux, fem el pas final: un agent que viu dins del clúster, estira de Git contínuament i reconcilia la realitat amb el que està declarat. Veurem el recurs Application d'Argo CD, els ApplicationSet per desplegar els tres entorns des d'una sola definició, l'equivalent a Flux, les tres solucions al problema dels secrets en un repositori, i per fi tancarem el conflicte entre l'HPA i el camp replicas que vam deixar anunciat a 09-01.
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
