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

  1. La filosofia sense plantilles
  2. Kustomize dins de kubectl i com a binari propi
  3. El model base + superposicions
  4. kustomization.yaml camp a camp
  5. Pedaços: fusió estratègica i JSON Patch
  6. Generadors de ConfigMaps i Secrets
  7. Components: trossos opcionals reutilitzables
  8. Flux de treball: kustomize, diff i apply
  9. Migració completa de Rutas Norte
  10. Helm davant de Kustomize
  11. Combinar Helm i Kustomize
  12. Errors comuns i consells
  13. Exercicis
  14. Conclusió

  1. 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

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.

  1. 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/pro

Avantatge 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.

  1. 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

  1. kustomization.yaml camp a camp

resources

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.3

Sobre 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-norte

commonLabels (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 immutable

L'ú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 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

replicas:
  - { name: informes-ocupacio-llancador, count: 1 }

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.

  1. 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: 30

Els 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.

  1. 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

kubectl kustomize k8s/entorns/pro | grep -A3 'kind: ConfigMap'
kind: ConfigMap
metadata:
  name: api-reserves-config-9t2hmf6b4d
  namespace: rutas-norte-pro

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

generatorOptions:
  disableNameSuffixHash: true
  labels: { generat-per: kustomize }

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: secretGenerator no xifra res. Base64 no és xifratge (03-02). Si poses la contrasenya de postgres-reserves en un literals o 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.

  1. 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.

  1. 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: 2Gi

Aquesta 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=300s

Poda 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

  1. 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 fitxers

I 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.

diff k8s/dev/api-reserves-deployment.yaml k8s/pro/api-reserves-deployment.yaml
<   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.

kubectl diff -k k8s/entorns/pro

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

  1. 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 : rollback, uninstall No: poda manual i empipadora
Hooks i ordenació No (ho cobreixen les onades d'Argo CD, 10-05)
Condicionals i bucles : 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
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.

  1. 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/monitoratge

Un 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/pre

Solució 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-dev

Explicació: 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 amb kubectl apply -f i 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, replicas i labels resolen la majoria de diferències sense escriure un pedaç. I labels ha de portar includeSelectors: false: commonLabels toca els selectors immutables i trenca desplegaments en marxa.
  • Per a la resta hi ha patches, amb fusió estratègica o JSON Patch, i un target dirigible per tipus, nom, etiqueta o anotació. patchesStrategicMerge i patchesJson6902 só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

Mòdul 2: Components Principals de Kubernetes

Mòdul 3: Gestió de Configuració i Secrets

Mòdul 4: Xarxes a Kubernetes

Mòdul 5: Emmagatzematge a Kubernetes

Mòdul 6: Conceptes Avançats de Kubernetes

Mòdul 7: Monitoratge i Registre

Mòdul 8: Seguretat a Kubernetes

Mòdul 9: Escalat i Rendiment

Mòdul 10: Ecosistema i Eines de Kubernetes

Mòdul 11: Estudis de Cas i Aplicacions del Món Real

Mòdul 12: Preparació per a la Certificació de Kubernetes

© Copyright 2026. Tots els drets reservats