Tancàvem el mòdul 2 amb una llista de deutes i el primer de tots era aquest: la URL de l'API va incrustada dins de la imatge de botiga-web. Cada vegada que volem desplegar la botiga a rutas-norte-pre en lloc de a rutas-norte-pro cal construir una imatge diferent, amb el mateix codi i un sol caràcter canviat en un fitxer de configuració. Això trenca la promesa més bàsica del contenidor: construir una vegada, desplegar a qualsevol lloc. L'objecte que ho arregla és el ConfigMap, un magatzem de configuració no confidencial que viu al clúster, es versiona a Git juntament amb la resta de manifests i s'injecta al contenidor en el moment de l'arrencada. En aquesta lliçó construiràs el ConfigMap de botiga-web amb el seu nginx.conf real i el muntaràs com un fitxer dins del contenidor, que és la forma de consum més potent i la que dona menys sorpreses. La injecció com a variables d'entorn la veuràs a fons a Variables d'Entorn; aquí només l'esmentarem per situar-la.

Contingut

  1. Per què se separa la configuració del codi
  2. Anatomia de l'objecte ConfigMap
  3. Les quatre formes de crear un ConfigMap
  4. Les tres formes de consumir-lo (i quina veurem aquí)
  5. Muntar un ConfigMap com a volum
  6. Seleccionar claus concretes amb items i defaultMode
  7. subPath: l'ús i els seus dos perills
  8. El cas central: el nginx.conf de botiga-web
  9. Actualitzar un ConfigMap i la propagació als volums
  10. ConfigMaps immutables
  11. El límit d'1 MiB i què fer si se supera
  12. ConfigMaps per entorn a Rutas Norte

  1. Per què se separa la configuració del codi

La metodologia dels dotze factors (The Twelve-Factor App), escrita el 2011 per a aplicacions al núvol, dedica el seu tercer factor exactament a aquest problema i el seu enunciat és una sola frase:

Desa la configuració a l'entorn. La configuració és tot allò que pot variar entre desplegaments; el codi és el que no varia.

La prova pràctica que proposa és taxativa i val la pena aplicar-la a Rutas Norte: podries fer públic el repositori de codi ara mateix, sense filtrar cap credencial? Avui la resposta és no, perquè la contrasenya de postgres-reserves està escrita en un manifest del repositori. Aquesta part la salda la lliçó de Secrets. Però hi ha una segona prova, més silenciosa: és exactament la mateixa imatge la que corre a rutas-norte-dev, a rutas-norte-pre i a rutas-norte-pro? Avui tampoc.

Això no és purisme. Té conseqüències molt concretes:

Amb la configuració dins de la imatge Amb la configuració fora de la imatge
Una imatge per entorn: botiga-web:2.5.0-dev, -pre, -pro Una sola imatge botiga-web:2.5.0 per als tres entorns
El que es prova a pre no és el que surt a pro El validat a pre és l'artefacte exacte que es promociona
Canviar un temps d'espera exigeix reconstruir i tornar a passar el pipeline (minuts o hores) Canviar un temps d'espera és editar un objecte del clúster (segons)
La configuració de producció viu al registre d'imatges, illegible La configuració viu en YAML, revisable en una pull request
Un rollback de configuració obliga a rebobinar la imatge Un rollback de configuració és independent del codi

Els dos casos concrets que arrossega Rutas Norte:

  1. La URL de l'API a botiga-web. La SPA necessita saber a quina adreça ha de trucar: http://api-reserves dins del clúster, https://api.rutasnorte.example des del navegador del client. Avui està incrustada al JavaScript compilat dins de la imatge.
  2. El nginx.conf de botiga-web. La configuració del servidor —compressió, capçaleres de seguretat, la regla de fallback a index.html que tota SPA necessita, el proxy cap a api-reserves— està cuita a la imatge. Ajustar la memòria cau dels fitxers estàtics requereix reconstruir-la.

Tots dos es resolen avui, en aquesta lliçó.

  1. Anatomia de l'objecte ConfigMap

Un ConfigMap és un dels objectes més simples de Kubernetes. No té spec ni status: només metadata i dades.

apiVersion: v1                       # ConfigMap viu al grup core, sense prefix
kind: ConfigMap
metadata:
  name: botiga-web-config
  namespace: rutas-norte-dev
  labels:
    app: botiga-web
    app.kubernetes.io/part-of: rutas-norte
    entorn: dev
data:                                # parells clau -> valor de TEXT (UTF-8)
  API_URL: "http://api-reserves:8080"
  NIVELL_LOG: "debug"
  nginx.conf: |                      # el valor pot ser un fitxer sencer
    server {
      listen 80;
    }
binaryData:                          # parells clau -> valor BINARI (base64)
  favicon.ico: AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAA...

Els punts que defineixen l'objecte:

  • apiVersion: v1: pertany al grup core, com Pod, Service o Namespace. No porta grup al davant.
  • No té spec. Un ConfigMap no descriu un estat desitjat que un controlador hagi de reconciliar; és un contenidor de dades passiu. Per això tampoc té status.
  • data conté text vàlid en UTF-8. Les claus són noms de fitxer potencials, així que s'han de limitar a caràcters alfanumèrics, -, _ i ..
  • binaryData conté dades que no són text, codificades en base64 en escriure-les al manifest. Kubernetes les descodifica en muntar-les. Serveix per a un certificat en format DER, un .ico, una font, un petit .tar.gz de plantilles.
  • Una mateixa clau no pot aparèixer a data i a binaryData: l'apply falla.
  • Té namespace. Un pod només pot muntar ConfigMaps del seu propi namespace. No hi ha cap forma nativa de referenciar-ne un d'un altre namespace, i aquesta restricció és la que fa natural el patró d'"un ConfigMap per entorn" que veurem a l'apartat 12.

Un detall important sobre les claus multilínia. El | de YAML és un bloc literal: conserva els salts de línia tal qual. És el que vols per a un fitxer de configuració. El seu cosí > (bloc plegat) converteix els salts de línia simples en espais, i arruïnaria un nginx.conf. Per a fitxers, fes servir sempre |.

  1. Les quatre formes de crear un ConfigMap

3.1. Manifest YAML

És la forma canònica i l'única que es versiona bé a Git. Ja l'has vista a dalt. S'aplica com qualsevol altre objecte:

kubectl apply -f k8s/base/botiga-web/configmap.yaml
configmap/botiga-web-config created

3.2. --from-literal

Per a valors curts, sobretot quan estàs explorant o generant el YAML amb --dry-run.

kubectl create configmap botiga-web-config \
  --from-literal=API_URL=http://api-reserves:8080 \
  --from-literal=NIVELL_LOG=debug \
  -n rutas-norte-dev
configmap/botiga-web-config created

Comprovem què ha creat exactament:

kubectl get configmap botiga-web-config -n rutas-norte-dev -o yaml
apiVersion: v1
data:
  API_URL: http://api-reserves:8080
  NIVELL_LOG: debug
kind: ConfigMap
metadata:
  creationTimestamp: "2026-08-05T09:14:22Z"
  name: botiga-web-config
  namespace: rutas-norte-dev
  resourceVersion: "184213"
  uid: 6f2b1c4a-3d9e-4a71-9f0c-2b8e5d417a63

Fixa't que no porta cap etiqueta. Els objectes creats amb kubectl create no segueixen l'esquema d'etiquetatge de Rutas Norte, i això els deixa fora de totes les consultes amb -l app.kubernetes.io/part-of=rutas-norte. Per això l'ús recomanat de l'ordre imperativa és com a generador de YAML, no com a forma de crear objectes:

kubectl create configmap botiga-web-config \
  --from-literal=API_URL=http://api-reserves:8080 \
  --dry-run=client -o yaml > k8s/base/botiga-web/configmap.yaml

Després s'editen les etiquetes a mà i s'aplica el fitxer. És el mateix flux --dry-run=client -o yaml del mòdul 1, ara aplicat a la configuració.

3.3. --from-file amb un fitxer

Aquí hi ha el cas interessant. Tens el nginx.conf real al teu repositori i el vols ficar dins d'un ConfigMap sense copiar-lo a mà dins d'un YAML (amb el risc d'espatllar la indentació).

kubectl create configmap botiga-web-nginx \
  --from-file=k8s/base/botiga-web/nginx.conf \
  --dry-run=client -o yaml -n rutas-norte-dev
apiVersion: v1
data:
  nginx.conf: |
    server {
      listen 80;
      server_name www.rutasnorte.example;
      ...
    }
kind: ConfigMap
metadata:
  creationTimestamp: null
  name: botiga-web-nginx
  namespace: rutas-norte-dev

La clau és el nom base del fitxer (nginx.conf), no la ruta completa. Si vols que la clau s'anomeni d'una altra manera, s'anteposa amb un =:

kubectl create configmap botiga-web-nginx \
  --from-file=default.conf=k8s/base/botiga-web/nginx-produccio.conf \
  --dry-run=client -o yaml -n rutas-norte-dev

La clau serà default.conf encara que el fitxer del disc s'anomeni nginx-produccio.conf. Aquest truc és imprescindible quan el nom que espera l'aplicació no coincideix amb el que et resulta còmode al repositori.

3.4. --from-file amb un directori

Si apuntes a un directori, cada fitxer de dins es converteix en una clau. No és recursiu: els subdirectoris s'ignoren.

ls k8s/base/botiga-web/conf.d/
compressio.conf  seguretat.conf  spa.conf
kubectl create configmap botiga-web-nginx \
  --from-file=k8s/base/botiga-web/conf.d/ \
  --dry-run=client -o yaml -n rutas-norte-dev
apiVersion: v1
data:
  compressio.conf: |
    gzip on;
    gzip_types text/css application/javascript application/json;
  seguretat.conf: |
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
  spa.conf: |
    location / {
      try_files $uri $uri/ /index.html;
    }
kind: ConfigMap
metadata:
  creationTimestamp: null
  name: botiga-web-nginx

Tres fitxers, tres claus. Quan muntem aquest ConfigMap a /etc/nginx/conf.d apareixeran els tres fitxers amb aquests mateixos noms, que és just el que nginx espera.

3.5. --from-env-file

Aquesta és la que més confon. Pren un fitxer amb format CLAU=valor i crea una clau per línia, no una clau amb el fitxer sencer.

cat k8s/entorns/dev/botiga-web.env
API_URL=http://api-reserves:8080
NIVELL_LOG=debug
TEMPS_ESPERA_MS=5000
kubectl create configmap botiga-web-config \
  --from-env-file=k8s/entorns/dev/botiga-web.env \
  --dry-run=client -o yaml -n rutas-norte-dev
apiVersion: v1
data:
  API_URL: http://api-reserves:8080
  NIVELL_LOG: debug
  TEMPS_ESPERA_MS: "5000"
kind: ConfigMap
metadata:
  creationTimestamp: null
  name: botiga-web-config

Compara-ho amb el que hauria fet --from-file sobre el mateix fitxer: una sola clau anomenada botiga-web.env el valor de la qual seria el text complet. La diferència és enorme i és la font d'un error clàssic.

Opció Sobre app.env produeix Ús típic
--from-file=app.env 1 clau: app.env → contingut sencer Muntar el fitxer tal qual
--from-env-file=app.env N claus: una per línia Alimentar variables d'entorn amb envFrom

Regles del format de --from-env-file: les línies buides i les que comencen per # s'ignoren; no s'admeten cometes al voltant del valor (es desarien com a part del valor) ni continuacions de línia; i les claus han de ser noms vàlids de variable d'entorn.

3.6. Taula resum

Forma Ordre Quan fer-la servir
Manifest kubectl apply -f cm.yaml Sempre en producció. És el que es versiona
Literal --from-literal=K=V Proves ràpides i generació de YAML
Fitxer --from-file=ruta/f.conf Fitxers de configuració llargs
Directori --from-file=ruta/dir/ Un conjunt de fitxers que van junts
Env file --from-env-file=app.env Moltes variables simples de cop

  1. Les tres formes de consumir-lo (i quina veurem aquí)

flowchart LR
    CM["ConfigMap<br/>botiga-web-config"] --> A["Variables d'entorn<br/>env / envFrom"]
    CM --> B["Fitxer en un volum<br/>volumeMounts"]
    CM --> C["Arguments de línia d'ordres<br/>args amb $(VAR)"]
    A --> P["Contenidor"]
    B --> P
    C --> P
Forma Avantatge Inconvenient On s'estudia
Variable d'entorn Simple, universal No es refresca sense reiniciar el pod 03-03
Fitxer en volum S'actualitza en calent, admet fitxers sencers L'app ha de llegir del disc Aquesta lliçó
Argument (args) Explícit al manifest Fix des de l'arrencada 03-03

Aquesta lliçó se centra en la segona perquè és la que resol el cas del nginx.conf —un fitxer de 40 línies no cap còmodament en una variable d'entorn— i perquè és l'única que permet canviar la configuració sense recrear pods.

  1. Muntar un ConfigMap com a volum

El mecanisme té sempre dues meitats que han de quadrar: un volum a nivell de pod que declara d'on surten les dades, i un volumeMount a nivell de contenidor que declara on apareixen. El pont entre tots dos és el camp name.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: botiga-web
  namespace: rutas-norte-dev
spec:
  replicas: 3
  selector:
    matchLabels:
      app: botiga-web                        # només app i entorn al selector
      entorn: dev
  template:
    metadata:
      labels:
        app: botiga-web
        app.kubernetes.io/name: botiga-web
        app.kubernetes.io/part-of: rutas-norte
        entorn: dev
    spec:
      containers:
        - name: nginx
          image: nginx:1.27.1-alpine          # etiqueta immutable, mai latest
          ports:
            - containerPort: 80
          volumeMounts:
            - name: config-nginx               # (2) ha de coincidir amb volumes[].name
              mountPath: /etc/nginx/conf.d     # on apareix dins del contenidor
              readOnly: true                   # bona pràctica: l'app no ha d'escriure
      volumes:
        - name: config-nginx                   # (1) nom del volum dins del pod
          configMap:
            name: botiga-web-nginx             # ConfigMap del MATEIX namespace

Què passa exactament quan arrenca aquest pod:

  1. El kubelet llegeix el pod i veu un volum de tipus configMap.
  2. Demana a l'API el ConfigMap botiga-web-nginx del namespace rutas-norte-dev. Si no existeix, el pod es queda en ContainerCreating fins que aparegui, amb un esdeveniment FailedMount. No falla per sempre: tan bon punt creïs el ConfigMap, el pod arrenca.
  3. Crea al node un directori i hi escriu a dins un fitxer per cada clau del ConfigMap.
  4. Munta aquest directori a /etc/nginx/conf.d dins del contenidor.

I aquí hi ha el punt que sorprèn tothom la primera vegada: el muntatge substitueix el contingut del directori del contenidor. El que la imatge nginx portava a /etc/nginx/conf.d (el fitxer default.conf per defecte) deixa de veure's. No s'esborra de la imatge; simplement queda ocult sota el punt de muntatge, igual que a Linux en muntar un disc sobre un directori que ja tenia fitxers.

Anem a veure-ho:

kubectl exec -n rutas-norte-dev deploy/botiga-web -- ls -la /etc/nginx/conf.d
total 12
drwxrwxrwt 3 root root  120 Aug  5 09:41 .
drwxr-xr-x 3 root root 4096 Aug  5 09:41 ..
drwxr-xr-x 2 root root   80 Aug  5 09:41 ..2026_08_05_09_41_18.1042318874
lrwxrwxrwx 1 root root   32 Aug  5 09:41 ..data -> ..2026_08_05_09_41_18.1042318874
lrwxrwxrwx 1 root root   17 Aug  5 09:41 compressio.conf -> ..data/compressio.conf
lrwxrwxrwx 1 root root   17 Aug  5 09:41 seguretat.conf -> ..data/seguretat.conf
lrwxrwxrwx 1 root root   11 Aug  5 09:41 spa.conf -> ..data/spa.conf

Aquesta sortida conté tota la màgia del mecanisme, així que convé llegir-la amb calma:

  • Els fitxers que veu l'aplicació (compressio.conf, etc.) són enllaços simbòlics a ..data/.
  • ..data és al seu torn un enllaç simbòlic a un directori amb marca de temps.
  • El directori real conté els fitxers de veritat.

Per què tanta indirecció? Perquè permet una actualització atòmica. Quan el ConfigMap canvia, el kubelet escriu un directori nou amb marca de temps nova i després mou l'enllaç ..data de cop. L'aplicació no veu mai un estat a mitges amb la meitat dels fitxers vells i la meitat nous. És un detall d'implementació, però explica dos comportaments que veuràs més endavant: per què l'actualització funciona i per què amb subPath no funciona.

  1. Seleccionar claus concretes amb items i defaultMode

Per defecte es projecten totes les claus. Sovint no és el que vols: un mateix ConfigMap pot tenir claus destinades a variables d'entorn (API_URL) i claus que són fitxers (nginx.conf), i muntar-les totes juntes ompliria /etc/nginx/conf.d de brossa que nginx intentaria interpretar.

Amb items tries quines claus es projecten i amb quin nom de fitxer:

      volumes:
        - name: config-nginx
          configMap:
            name: botiga-web-config
            defaultMode: 0444                  # permisos per defecte: només lectura
            items:
              - key: nginx.conf                # clau del ConfigMap
                path: default.conf             # nom del fitxer resultant
                mode: 0400                     # permisos NOMÉS d'aquest fitxer
              - key: mapa-mime.types
                path: mime.types

Detalls que importen:

  • path és relatiu al mountPath. Amb mountPath: /etc/nginx/conf.d i path: default.conf, el fitxer acaba a /etc/nginx/conf.d/default.conf.
  • path admet subdirectoris: path: extra/capceleres.conf crea el subdirectori. El que no admet és començar per / ni contenir ...
  • Si fas servir items, només es projecta el que llistis. Les claus no esmentades desapareixen del volum.
  • Si una clau llistada a items no existeix al ConfigMap, el pod no arrenca. És un comportament desitjable: falla aviat i amb un missatge clar en lloc d'arrencar nginx sense configuració.
  • defaultMode s'escriu en octal i s'ha de posar amb el 0 al davant (0444). Si escrius 444 en decimal, YAML ho interpreta com el número 444, que en octal és 0674: permisos absurds. És un error real i freqüent.
  • El mode d'un item concret preval sobre defaultMode.

Un advertiment sobre els permisos: si el contenidor corre amb un usuari no root (l'habitual i el recomanable, com veuràs a Contextos de Seguretat), un defaultMode: 0400 significa "només lectura per al propietari", i el propietari del fitxer és root. El procés no el podria llegir. Per a aquest cas, 0444 és l'elecció segura.

  1. subPath: l'ús i els seus dos perills

Torna al problema de l'apartat 5: muntar un volum sobre un directori amaga el que ja hi havia. I si només vols afegir o substituir un únic fitxer dins d'un directori que ha de conservar la resta del seu contingut?

El cas canònic a Rutas Norte és /etc/nginx/nginx.conf. Aquest fitxer és a /etc/nginx/, un directori que també conté mime.types, fastcgi_params, el directori conf.d... Si muntem el volum a /etc/nginx/, nginx es queda sense tota la resta i no arrenca.

La solució és subPath:

          volumeMounts:
            - name: config-nginx
              mountPath: /etc/nginx/nginx.conf   # ruta a un FITXER, no a un directori
              subPath: nginx.conf                # quina clau del volum es munta aquí
              readOnly: true

Amb això, només el fitxer /etc/nginx/nginx.conf queda substituït; la resta de /etc/nginx/ continua intacta.

kubectl exec -n rutas-norte-dev deploy/botiga-web -- ls /etc/nginx/
conf.d
fastcgi.conf
fastcgi_params
mime.types
modules
nginx.conf
scgi_params
uwsgi_params

Tot al seu lloc. Ara els dos perills, que cal conèixer abans de fer-lo servir:

Perill 1: amb subPath l'actualització NO es propaga. Quan muntes amb subPath, el kubelet copia el fitxer directament en lloc de crear la cadena d'enllaços simbòlics de l'apartat 5. En no haver-hi enllaç ..data per moure, el fitxer queda congelat en el valor que tenia en arrencar el pod. Pots editar el ConfigMap cent vegades: el contenidor continuarà veient l'original fins que el recreïs. És, de bon tros, la causa número u de "he canviat la configuració i no passa res".

Perill 2: és fàcil equivocar-se de sentit. mountPath ha d'apuntar al fitxer de destinació i subPath a la clau d'origen. Si els intercanvies, o si mountPath apunta a un directori existent, obtindràs un directori on esperaves un fitxer i l'aplicació fallarà amb un missatge confús.

Sense subPath Amb subPath
mountPath és un directori mountPath és un fitxer
Amaga el contingut previ del directori Conserva la resta del directori
Fitxers com a enllaços simbòlics a ..data Fitxer copiat directament
S'actualitza en calent No s'actualitza mai
Preferible sempre que sigui possible Només si no hi ha alternativa

La recomanació pràctica: evita subPath si pots. En el cas de nginx tens una alternativa millor, que és la que farem servir: muntar a /etc/nginx/conf.d/, el directori que la mateixa imatge reserva per a configuració afegida, sense tocar /etc/nginx/nginx.conf. Aquest directori està pensat per ser substituït sencer.

  1. El cas central: el nginx.conf de botiga-web

Anem a resoldre el deute del mòdul 2 amb tot l'anterior. Creem el fitxer k8s/base/botiga-web/configmap-nginx.yaml:

apiVersion: v1
kind: ConfigMap
metadata:
  name: botiga-web-nginx
  namespace: rutas-norte-dev
  labels:
    app: botiga-web
    app.kubernetes.io/name: botiga-web
    app.kubernetes.io/component: frontend
    app.kubernetes.io/part-of: rutas-norte
    entorn: dev
data:
  default.conf: |
    server {
      listen 80;
      server_name www.rutasnorte.example;
      root /usr/share/nginx/html;
      index index.html;

      # Capceleres de seguretat per al portal de venda de bitllets
      add_header X-Content-Type-Options "nosniff" always;
      add_header X-Frame-Options "SAMEORIGIN" always;
      add_header Referrer-Policy "strict-origin-when-cross-origin" always;

      # Compressio dels estatics de la SPA
      gzip on;
      gzip_types text/css application/javascript application/json image/svg+xml;
      gzip_min_length 1024;

      # Els estatics amb hash al nom es guarden a la cache un any
      location ~* \.(js|css|woff2|png|svg)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
      }

      # Regla imprescindible en una SPA: qualsevol ruta desconeguda
      # retorna index.html perque l'enrutador del navegador la resolgui
      location / {
        try_files $uri $uri/ /index.html;
      }

      # Sonda de vida per al modul 7
      location = /healthz {
        access_log off;
        return 200 "ok\n";
      }
    }

I modifiquem el Deployment de botiga-web que ja teníem:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: botiga-web
  namespace: rutas-norte-dev
  labels:
    app: botiga-web
    app.kubernetes.io/name: botiga-web
    app.kubernetes.io/part-of: rutas-norte
    entorn: dev
spec:
  replicas: 3
  selector:
    matchLabels:
      app: botiga-web
      entorn: dev
  template:
    metadata:
      labels:
        app: botiga-web
        app.kubernetes.io/name: botiga-web
        app.kubernetes.io/component: frontend
        app.kubernetes.io/part-of: rutas-norte
        entorn: dev
    spec:
      containers:
        - name: nginx
          image: nginx:1.27.1-alpine
          ports:
            - name: http
              containerPort: 80
          volumeMounts:
            - name: config-nginx
              mountPath: /etc/nginx/conf.d      # directori sencer: sense subPath
              readOnly: true
          resources:                             # els afinarem a 03-04
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 200m
              memory: 128Mi
      volumes:
        - name: config-nginx
          configMap:
            name: botiga-web-nginx
            defaultMode: 0444

Apliquem i comprovem:

kubectl apply -f k8s/base/botiga-web/configmap-nginx.yaml
kubectl apply -f k8s/base/botiga-web/deployment.yaml
kubectl rollout status deploy/botiga-web -n rutas-norte-dev
configmap/botiga-web-nginx created
deployment.apps/botiga-web configured
Waiting for deployment "botiga-web" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "botiga-web" rollout to finish: 2 out of 3 new replicas have been updated...
deployment "botiga-web" successfully rolled out

Verifiquem que nginx està llegint la nostra configuració i no la de la imatge:

kubectl exec -n rutas-norte-dev deploy/botiga-web -- cat /etc/nginx/conf.d/default.conf | head -5
kubectl exec -n rutas-norte-dev deploy/botiga-web -- nginx -T 2>/dev/null | grep -c "try_files"
server {
  listen 80;
  server_name www.rutasnorte.example;
  root /usr/share/nginx/html;
  index index.html;
1

nginx -T bolca la configuració efectiva després de resoldre tots els include. Que aparegui el nostre try_files confirma que la regla de la SPA està activa. I la prova definitiva, des d'un pod efímer:

kubectl run prova --rm -it --restart=Never --image=curlimages/curl:8.10.1 \
  -n rutas-norte-dev -- curl -si http://botiga-web/healthz
HTTP/1.1 200 OK
Server: nginx/1.27.1
Content-Type: text/plain
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin

ok

Les tres capçaleres de seguretat hi són. La configuració de nginx ja no viu a la imatge: viu en un YAML revisable en una pull request.

  1. Actualitzar un ConfigMap i la propagació als volums

Ara el pagament de l'esforç. Afegim una capçalera més sense reconstruir la imatge i sense reiniciar res.

# Editem el YAML al repositori i apliquem
kubectl apply -f k8s/base/botiga-web/configmap-nginx.yaml
configmap/botiga-web-nginx configured

Esperem i mirem dins del contenidor:

kubectl exec -n rutas-norte-dev deploy/botiga-web -- grep Permissions-Policy /etc/nginx/conf.d/default.conf
add_header Permissions-Policy "geolocation=(), microphone=()" always;

El fitxer ha canviat dins d'un contenidor que no s'ha reiniciat. Els tres pods continuen tenint RESTARTS 0.

Sobre els temps i les condicions:

  • La propagació no és instantània. El kubelet refresca els volums al seu cicle de sincronització, governat per --sync-frequency (per defecte 1 minut), més el temps de memòria cau del seu client de l'API (configMapAndSecretChangeDetectionStrategy, per defecte Watch). A la pràctica compta amb fins a un parell de minuts, no amb segons.
  • Es propaga només si el volum no fa servir subPath (apartat 7).
  • Es propaga només si el ConfigMap no és immutable (apartat 10).
  • I, sobretot: que el fitxer canviï no vol dir que l'aplicació se n'assabenti. nginx va llegir la seva configuració en arrencar i la té a memòria.

Aquest darrer punt és fonamental. Hi ha tres tipus d'aplicació:

Tipus d'aplicació Comportament en canviar el fitxer Què fer
Rellegeix el fitxer a cada ús S'aplica sola Res
Recarrega amb un senyal (nginx, HAProxy) Ignora el canvi Enviar el senyal o reiniciar
Llegeix només en arrencar (la majoria) Ignora el canvi Reiniciar els pods

Per a nginx podem enviar-li el senyal de recàrrega, que no talla cap connexió en curs:

kubectl exec -n rutas-norte-dev deploy/botiga-web -- nginx -s reload
2026/08/05 10:22:41 [notice] 47#47: signal process started

Per al cas general, la solució neta és forçar un desplegament nou:

kubectl rollout restart deploy/botiga-web -n rutas-norte-dev

Això substitueix els pods amb l'estratègia RollingUpdate del mòdul 2, sense tall de servei. Existeix una tècnica més elegant i automàtica —desar un hash del ConfigMap en una anotació del template, de manera que canviar la configuració dispari el desplegament tot sol— que veuràs a Variables d'Entorn, on encara és més necessària.

  1. ConfigMaps immutables

Des de Kubernetes 1.21, un ConfigMap es pot declarar immutable:

apiVersion: v1
kind: ConfigMap
metadata:
  name: botiga-web-nginx-v3
  namespace: rutas-norte-pro
data:
  default.conf: |
    server { listen 80; }
immutable: true                    # a partir d'aqui, ni data ni binaryData es poden tocar

Què hi guanya:

  1. Rendiment i escala. Aquest és el motiu principal. Cada kubelet manté un watch obert contra l'apiserver per cada ConfigMap que munta. En un clúster gran això són milers de connexions vigilant canvis. Si l'objecte és immutable, el kubelet tanca el watch: no hi ha res a vigilar. La càrrega sobre l'apiserver baixa de manera notable.
  2. Protecció contra el canvi accidental. Ningú no pot modificar per error la configuració de producció d'una tirada.
  3. Determinisme. Un pod que arrenqui avui i un altre que arrenqui d'aquí a un mes amb el mateix ConfigMap tindran exactament la mateixa configuració. Amb un ConfigMap mutable, dues rèpliques del mateix Deployment podrien tenir continguts diferents si el ConfigMap va canviar entre les seves arrencades.

Què s'hi perd: l'actualització en calent de l'apartat 9. I hi ha dues regles dures:

  • La immutabilitat no es pot revertir. Un cop immutable: true, no es pot tornar a false. Només queda esborrar l'objecte i recrear-lo.
  • metadata sí que es pot canviar (etiquetes, anotacions). El que queda congelat és data i binaryData.

Intentar modificar-lo produeix un error molt clar:

kubectl apply -f configmap-nginx-v3.yaml
The ConfigMap "botiga-web-nginx-v3" is invalid: data: Forbidden: field is immutable when `immutable` is set

El patró que acompanya la immutabilitat és versionar el nom: botiga-web-nginx-v3, -v4, etc. El Deployment apunta a la versió concreta, i canviar de configuració significa crear un ConfigMap nou i actualitzar la referència al Deployment, cosa que dispara un desplegament amb historial i rollback. És exactament el que fan els generadors de Kustomize afegint un sufix amb el hash del contingut.

Recomanació per a Rutas Norte: mutable a dev (iterar ràpid amb nginx -s reload), immutable amb nom versionat a pro (determinisme i rollback).

  1. El límit d'1 MiB i què fer si se supera

Un ConfigMap no pot superar 1 MiB, comptant la suma de data i binaryData més les metadades. El límit no és capriciós: els ConfigMaps es desen a etcd, que imposa per defecte una mida màxima d'1,5 MiB per valor, i etcd és el cor del clúster.

En passar-te'n, l'error és immediat:

Error from server (RequestEntityTooLarge): Request entity too large: limit is 3145728

O bé, quan la validació la fa el mateix objecte:

The ConfigMap "cataleg-rutes" is invalid: []: Too long: must have at most 1048576 bytes

Tingues present a més que base64 infla un 33 %: un .ico de 800 KiB ocupa uns 1,07 MiB a binaryData i no hi cap.

Què fer si t'acostes al límit:

Situació Solució
Molts fitxers que van junts Dividir en diversos ConfigMaps i muntar diversos volums
Un sol fitxer enorme (dataset, catàleg) No és configuració. Va en un volum persistent (mòdul 5) o es descarrega en arrencar
Binaris grans (fonts, imatges) Han d'anar a la imatge del contenidor
Plantilles comprimides .tar.gz a binaryData i descompressió amb un init container (06-04)

La regla d'olfacte: si t'acostes al megabyte, gairebé segur que estàs ficant en un ConfigMap alguna cosa que no és configuració. A Rutas Norte això aplicaria al catàleg complet de rutes i parades, que són dades de negoci i viuen a postgres-reserves, no en un ConfigMap.

  1. ConfigMaps per entorn a Rutas Norte

Recorda la restricció de l'apartat 2: un ConfigMap té namespace i un pod només pot muntar els del seu propi namespace. Com que els entorns de Rutas Norte són namespaces, això encaixa de manera natural: un ConfigMap per entorn, amb el mateix nom a cadascun.

flowchart TB
    subgraph dev["rutas-norte-dev"]
        CMD["ConfigMap botiga-web-config<br/>API_URL: http://api-reserves:8080<br/>NIVELL_LOG: debug"]
        DD["Deployment botiga-web<br/>configMap: botiga-web-config"]
        CMD --- DD
    end
    subgraph pre["rutas-norte-pre"]
        CMP["ConfigMap botiga-web-config<br/>API_URL: https://api-pre.rutasnorte.example<br/>NIVELL_LOG: info"]
        DP["Deployment botiga-web<br/>configMap: botiga-web-config"]
        CMP --- DP
    end
    subgraph pro["rutas-norte-pro"]
        CMR["ConfigMap botiga-web-config<br/>API_URL: https://api.rutasnorte.example<br/>NIVELL_LOG: warn"]
        DR["Deployment botiga-web<br/>configMap: botiga-web-config"]
        CMR --- DR
    end
    IMG["Imatge unica<br/>botiga-web:2.5.0"] --> DD
    IMG --> DP
    IMG --> DR

El nom del ConfigMap és idèntic als tres namespaces. Això permet que el Deployment sigui literalment el mateix fitxer a k8s/base/, i que l'única cosa que canviï entre entorns sigui el ConfigMap de k8s/entorns/<entorn>/. L'estructura del repositori queda així:

k8s/
├── base/
│   ├── botiga-web/
│   │   ├── deployment.yaml          # identic per als tres entorns
│   │   ├── service.yaml
│   │   └── configmap-nginx.yaml     # nginx.conf comu
│   └── api-reserves/
│       ├── deployment.yaml
│       └── service.yaml
└── entorns/
    ├── dev/
    │   ├── namespace.yaml
    │   ├── botiga-web-config.yaml   # API_URL de dev
    │   └── api-reserves-config.yaml
    ├── pre/
    │   └── ...
    └── pro/
        └── ...

I aquest és el contingut concret de la configuració de Rutas Norte per entorn. Pren aquesta taula com a referència; a 03-03 l'ampliarem al catàleg complet de tots els components.

Clau dev pre pro
API_URL http://api-reserves:8080 https://api-pre.rutasnorte.example https://api.rutasnorte.example
NIVELL_LOG debug info warn
TEMPS_ESPERA_MS 5000 3000 2000
REDIS_HOST redis-cache redis-cache redis-cache
REDIS_TTL_SEGONS 30 120 300
MAX_PLACES_PER_RESERVA 9 9 9
MODE_MANTENIMENT false false false

Observa dues decisions de disseny:

  • REDIS_TTL_SEGONS puja en acostar-se a producció. En desenvolupament interessa una memòria cau curta per veure els canvis; en producció, amb els pics de trànsit de ponts i vacances, interessa una memòria cau llarga que descarregui postgres-reserves.
  • MODE_MANTENIMENT és un interruptor operatiu. Posar-lo a true i recarregar nginx mostra la pàgina d'"estem actualitzant" sense desplegar res. És exactament el tipus de palanca que justifica separar configuració de codi.

I una cosa que no hi ha en aquesta taula i mai no hi ha de ser: la contrasenya de postgres-reserves. Un ConfigMap no ofereix cap protecció: qualsevol que pugui llistar ConfigMaps al namespace en llegeix el contingut en clar. Aquest deute el salda la lliçó següent.

Errors Comuns i Consells

Error Símptoma Solució
ConfigMap en un altre namespace Pod en ContainerCreating, esdeveniment FailedMount Un pod només munta ConfigMaps del seu namespace. Crea'l al correcte
ConfigMap inexistent configmap "x" not found als esdeveniments kubectl get cm -n <ns> i revisa el nom
Muntar sobre un directori amb contingut L'aplicació no troba els seus fitxers Munta en un subdirectori previst (conf.d) o fes servir subPath
Esperar actualització amb subPath "He canviat el ConfigMap i no passa res" subPath no propaga. Treu-lo o fes rollout restart
Esperar que l'app recarregui sola El fitxer canvia però el comportament no Envia el senyal de recàrrega o reinicia els pods
defaultMode: 644 sense el zero Permisos estranys (--w----r-T) És octal: escriu 0644
0400 amb usuari no root Permission denied en llegir Fes servir 0444 o ajusta fsGroup
Fer servir > en lloc de ` ` a YAML El fitxer arriba amb les línies enganxades
Confondre --from-file i --from-env-file Una clau amb tot el .env a dins Revisa la taula de l'apartat 3.5
Ficar secrets en un ConfigMap Credencial llegible per qualsevol Fes servir Secrets i RBAC
Objectes creats amb kubectl create No apareixen a les consultes amb -l Genera el YAML amb --dry-run=client -o yaml i afegeix etiquetes
Superar 1 MiB Too long: must have at most 1048576 bytes Divideix, o admet que no és configuració

Cinc consells que estalvien hores:

  1. Etiqueta els ConfigMaps amb el mateix esquema que la resta. kubectl get cm -l app=botiga-web -A ha de funcionar.
  2. Comprova sempre des de dins del contenidor, no des del YAML: kubectl exec ... -- cat <ruta>. És l'única veritat.
  3. Documenta cada clau amb un comentari al YAML. El manifest és la documentació de la configuració.
  4. Un ConfigMap per propòsit, no un de gegant per aplicació. Separa el que es munta com a fitxer del que alimenta variables d'entorn.
  5. En producció, immutable i versionat. El determinisme compensa la comoditat.

Exercicis

Exercici 1: De la imatge al ConfigMap

api-reserves necessita un fitxer /app/config/regles-tarifes.json amb les regles de descompte, que avui és dins de la seva imatge.

  1. Crea a la teva màquina el fitxer regles-tarifes.json amb aquest contingut:
{
  "descompte_anada_tornada": 0.12,
  "descompte_jove": 0.20,
  "edat_maxima_jove": 26,
  "recarrec_pont": 0.15
}
  1. Genera amb kubectl (sense escriure YAML a mà) el manifest d'un ConfigMap anomenat api-reserves-tarifes a rutas-norte-dev a partir d'aquest fitxer, i desa'l a k8s/entorns/dev/.
  2. Afegeix-li les etiquetes de l'esquema de Rutas Norte i aplica'l.
  3. Escriu el fragment de volumes i volumeMounts que el munta a /app/config de manera que només es pugui llegir.
  4. Verifica des de dins del pod que el fitxer existeix i té el contingut correcte.

Exercici 2: subPath i el parany de l'actualització

  1. Desplega un pod de botiga-web que munti la seva configuració amb subPath sobre /etc/nginx/nginx.conf.
  2. Comprova que /etc/nginx/ conserva mime.types i el directori conf.d.
  3. Modifica el ConfigMap i espera tres minuts. Comprova si el fitxer ha canviat dins del contenidor.
  4. Repeteix l'experiment muntant el mateix ConfigMap sense subPath a /etc/nginx/conf.d/. Es propaga ara?
  5. Explica en dues línies la diferència i què es veu amb ls -la en cada cas.

Exercici 3: Configuració dels tres entorns

  1. Crea el ConfigMap botiga-web-config als tres namespaces amb els valors de la taula de l'apartat 12.
  2. Demostra amb una sola ordre que existeixen tres objectes amb aquest nom i que els seus valors de NIVELL_LOG són diferents.
  3. Fes immutable únicament el de rutas-norte-pro i intenta modificar-lo. Copia l'error.
  4. Actualitza el REDIS_TTL_SEGONS de producció sabent que el ConfigMap és immutable. Descriu la seqüència completa de passos.
  5. Respon: si demà cal activar el mode manteniment a producció a les 3 de la matinada, quina ordre exacta executaries?

Solucions

Solució 1

mkdir -p k8s/entorns/dev
cat > /tmp/regles-tarifes.json <<'EOF'
{
  "descompte_anada_tornada": 0.12,
  "descompte_jove": 0.20,
  "edat_maxima_jove": 26,
  "recarrec_pont": 0.15
}
EOF

kubectl create configmap api-reserves-tarifes \
  --from-file=/tmp/regles-tarifes.json \
  -n rutas-norte-dev \
  --dry-run=client -o yaml > k8s/entorns/dev/api-reserves-tarifes.yaml

El YAML generat, ja amb les etiquetes afegides a mà:

apiVersion: v1
kind: ConfigMap
metadata:
  name: api-reserves-tarifes
  namespace: rutas-norte-dev
  labels:
    app: api-reserves
    app.kubernetes.io/name: api-reserves
    app.kubernetes.io/component: backend
    app.kubernetes.io/part-of: rutas-norte
    entorn: dev
data:
  regles-tarifes.json: |
    {
      "descompte_anada_tornada": 0.12,
      "descompte_jove": 0.20,
      "edat_maxima_jove": 26,
      "recarrec_pont": 0.15
    }
kubectl apply -f k8s/entorns/dev/api-reserves-tarifes.yaml
configmap/api-reserves-tarifes created

El fragment del Deployment:

          volumeMounts:
            - name: tarifes
              mountPath: /app/config
              readOnly: true
      volumes:
        - name: tarifes
          configMap:
            name: api-reserves-tarifes
            defaultMode: 0444

I la verificació:

kubectl exec -n rutas-norte-dev deploy/api-reserves -- cat /app/config/regles-tarifes.json
{
  "descompte_anada_tornada": 0.12,
  "descompte_jove": 0.20,
  "edat_maxima_jove": 26,
  "recarrec_pont": 0.15
}

Un apunt: la clau del ConfigMap és regles-tarifes.json perquè --from-file pren el nom base del fitxer, i el mountPath és el directori /app/config. El resultat és /app/config/regles-tarifes.json, la ruta demanada.

Solució 2

Amb subPath:

          volumeMounts:
            - name: config-nginx
              mountPath: /etc/nginx/nginx.conf
              subPath: nginx.conf
              readOnly: true
kubectl exec -n rutas-norte-dev deploy/botiga-web -- ls /etc/nginx/
conf.d
fastcgi_params
mime.types
modules
nginx.conf
scgi_params
uwsgi_params

El directori ho conserva tot. Després de modificar el ConfigMap i esperar:

kubectl apply -f configmap-nginx.yaml
sleep 180
kubectl exec -n rutas-norte-dev deploy/botiga-web -- grep -c "worker_connections 2048" /etc/nginx/nginx.conf
configmap/botiga-web-nginx configured
0

No ha canviat. El fitxer continua amb el valor de l'arrencada.

Sense subPath, muntant a /etc/nginx/conf.d/:

kubectl apply -f configmap-nginx.yaml
sleep 90
kubectl exec -n rutas-norte-dev deploy/botiga-web -- grep -c "Permissions-Policy" /etc/nginx/conf.d/default.conf
configmap/botiga-web-nginx configured
1

Ara sí. La diferència es veu amb ls -la:

# SENSE subPath: enllacos simbolics, l'actualitzacio mou ..data de forma atomica
lrwxrwxrwx 1 root root 15 Aug  5 10:41 default.conf -> ..data/default.conf
lrwxrwxrwx 1 root root 32 Aug  5 10:41 ..data -> ..2026_08_05_10_41_02.418822910

# AMB subPath: fitxer real copiat en arrencar, sense enllac que actualitzar
-rw-r--r-- 1 root root 648 Aug  5 09:12 nginx.conf

Sense subPath, el kubelet actualitza escrivint un directori nou i movent l'enllaç ..data. Amb subPath no hi ha enllaç: el fitxer es va copiar una vegada i allà es queda fins que es recreï el pod.

Solució 3

for E in dev pre pro; do
  kubectl create namespace rutas-norte-$E --dry-run=client -o yaml | kubectl apply -f -
done

kubectl create configmap botiga-web-config -n rutas-norte-dev \
  --from-literal=API_URL=http://api-reserves:8080 \
  --from-literal=NIVELL_LOG=debug \
  --from-literal=TEMPS_ESPERA_MS=5000 \
  --from-literal=REDIS_TTL_SEGONS=30

kubectl create configmap botiga-web-config -n rutas-norte-pre \
  --from-literal=API_URL=https://api-pre.rutasnorte.example \
  --from-literal=NIVELL_LOG=info \
  --from-literal=TEMPS_ESPERA_MS=3000 \
  --from-literal=REDIS_TTL_SEGONS=120

kubectl create configmap botiga-web-config -n rutas-norte-pro \
  --from-literal=API_URL=https://api.rutasnorte.example \
  --from-literal=NIVELL_LOG=warn \
  --from-literal=TEMPS_ESPERA_MS=2000 \
  --from-literal=REDIS_TTL_SEGONS=300

La comprovació en una ordre:

kubectl get cm botiga-web-config -A -o custom-columns=\
NS:.metadata.namespace,LOG:.data.NIVELL_LOG,TTL:.data.REDIS_TTL_SEGONS,API:.data.API_URL
NS                 LOG     TTL   API
rutas-norte-dev    debug   30    http://api-reserves:8080
rutas-norte-pre    info    120   https://api-pre.rutasnorte.example
rutas-norte-pro    warn    300   https://api.rutasnorte.example

Tres objectes amb el mateix nom i continguts diferents, exactament el que permet que el Deployment sigui idèntic.

Ara la immutabilitat de producció:

kubectl patch configmap botiga-web-config -n rutas-norte-pro \
  --type merge -p '{"immutable": true}'
kubectl patch configmap botiga-web-config -n rutas-norte-pro \
  --type merge -p '{"data": {"NIVELL_LOG": "info"}}'
configmap/botiga-web-config patched
Error from server: ConfigMap "botiga-web-config" is invalid: data: Forbidden: field is immutable when `immutable` is set

Per actualitzar REDIS_TTL_SEGONS a producció essent immutable, la seqüència correcta és:

  1. Crear un ConfigMap nou amb nom versionat, botiga-web-config-v2, amb el valor nou i immutable: true.
  2. Aplicar-lo.
  3. Editar el Deployment de rutas-norte-pro perquè apunti a botiga-web-config-v2, amb kubernetes.io/change-cause documentant el motiu.
  4. kubectl rollout status per seguir el desplegament progressiu del mòdul 2.
  5. Verificar i conservar el -v1 uns dies: si cal fer kubectl rollout undo, el Deployment el tornarà a referenciar i ha d'existir.
  6. Esborrar el -v1 passada la finestra de rollback.

Fixa't en l'avantatge: la configuració de producció ara té historial i rollback, igual que el codi.

Per al mode manteniment a les 3 de la matinada, amb el ConfigMap mutable d'nginx:

kubectl patch configmap botiga-web-nginx -n rutas-norte-pro \
  --type merge -p '{"data": {"manteniment.conf": "return 503;"}}'
kubectl exec -n rutas-norte-pro deploy/botiga-web -- nginx -s reload

Sense reconstruir imatge, sense desplegar, sense tallar connexions. Aquest és exactament el valor de tenir la configuració fora del codi.

Conclusió

Has saldat el primer dels deutes del mòdul 2. Saps per què el tercer factor dels dotze factors exigeix que la configuració visqui fora del codi, i has vist el cost concret de no fer-ho: una imatge per entorn, el provat a pre diferent del desplegat a pro, i un canvi de temps d'espera convertit en un pipeline complet. Coneixes l'anatomia del ConfigMap —sense spec ni status, amb data per a text i binaryData per a la resta, amb namespace i amb les claus com a noms de fitxer potencials— i les seves quatre formes de creació, sabent distingir el parany entre --from-file i --from-env-file.

Domines el consum com a volum, que és el que dona més joc: la parella volumes/volumeMounts, el fet que el muntatge amaga el contingut previ del directori, la cadena d'enllaços simbòlics cap a ..data que fa atòmica l'actualització, la selecció de claus amb items, els permisos amb defaultMode en octal i les seves interaccions amb els usuaris no root, i els dos perills de subPath, començant pel més car: amb subPath la configuració no es propaga mai. Saps que la propagació triga fins a un parell de minuts i que canviar el fitxer no equival que l'aplicació se n'assabenti, amb les tres estratègies segons el tipus d'aplicació. I coneixes els ConfigMaps immutables, el que guanyen en càrrega sobre l'apiserver i determinisme, el patró de nom versionat que els acompanya, i el límit d'1 MiB amb el senyal que emet: si t'hi acostes, no és configuració.

Sobre el terreny, botiga-web ja serveix el seu nginx.conf des d'un ConfigMap muntat a /etc/nginx/conf.d, amb capçaleres de seguretat, compressió, memòria cau d'estàtics, la regla try_files de la SPA i un endpoint /healthz que la lliçó de sondes aprofitarà. Els tres entorns tenen el seu propi ConfigMap amb el mateix nom, cosa que ha permès que el Deployment sigui un únic fitxer a k8s/base/.

Queda el deute gros. Tot el que hem desat avui és públic: qualsevol amb permís de lectura al namespace veu el contingut íntegre d'un ConfigMap. La contrasenya de postgres-reserves —la base de dades que desa el nom, el DNI, el telèfon i el correu de cada client de Rutas Norte— continua en clar en un manifest de Git. La lliçó següent, Secrets, la treu d'allà: veuràs en què es diferencia realment un Secret d'un ConfigMap, per què base64 no és xifratge i ho demostraràs descodificant-ne un en tres segons, els tipus de Secret i els seus usos, el xifratge en repòs a etcd que per defecte no està activat, com es descarreguen imatges privades de registry.rutasnorte.example i quines solucions reals fa servir l'ecosistema quan el Secret natiu no n'hi ha prou.

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