Les dues lliçons anteriors van construir els magatzems: un ConfigMap amb la configuració de botiga-web i un Secret amb les credencials de postgres-reserves. En totes dues vas veure una sola forma de portar-los al contenidor, el fitxer muntat, i en totes dues va quedar ajornada l'altra: la variable d'entorn, que és la que apareix a la immensa majoria dels manifests reals i l'única que entenen les imatges de tercers com postgres:16 o redis:7.2-alpine. Aquesta lliçó la cobreix sencera i unifica les dues anteriors: com es declaren, les quatre fonts de les quals poden venir, com un pod pot consultar dades sobre si mateix amb la Downward API, què passa quan la mateixa clau arriba per dos camins, per què el shell no expandeix el que et penses, i sobretot la limitació que ho condiciona tot: una variable d'entorn no canvia mentre el procés viu. Al final tindràs el catàleg complet de variables dels sis components de Rutas Norte als tres entorns.

Contingut

  1. Què és realment una variable d'entorn en un contenidor
  2. env amb valor literal
  3. envFrom: abocar un ConfigMap o un Secret sencer
  4. valueFrom: prendre una clau concreta
  5. La Downward API: fieldRef
  6. La Downward API: resourceFieldRef
  7. Expansió de variables amb $(VAR)
  8. Interacció amb command i args
  9. Precedència i col·lisions
  10. La limitació fonamental: no es refresquen
  11. El hash de la configuració en una anotació
  12. Variable d'entorn davant de fitxer muntat
  13. El catàleg de variables de Rutas Norte

  1. Què és realment una variable d'entorn en un contenidor

Abans de la sintaxi convé entendre el mecanisme, perquè explica gairebé totes les sorpreses.

Quan el kubelet arrenca un contenidor, construeix una llista de parells CLAU=valor i la lliura al runtime (containerd), que al seu torn l'entrega al kernel a la crida execve() que llança el procés principal. A partir d'aquí:

  • El procés desa aquesta llista a la seva pròpia memòria (environ).
  • Ningú des de fora no la pot modificar. No hi ha cap crida al sistema per canviar l'entorn d'un altre procés. Ni Kubernetes, ni el kubelet, ni kubectl.
  • Els processos fills l'hereten en el moment de crear-se.
flowchart LR
    A["Manifest del pod<br/>env / envFrom"] --> B["kubelet<br/>resol ConfigMaps,<br/>Secrets i Downward API"]
    B --> C["containerd<br/>llista CLAU=valor"]
    C --> D["execve()<br/>proces principal"]
    D --> E["environ del proces<br/>CONGELAT de per vida"]
    E -.->|"herencia"| F["processos fills"]

D'aquí es dedueix tot el que ve després: per què cal reiniciar el pod en canviar un ConfigMap, per què les variables són visibles a /proc/<pid>/environ, i per què el contingut d'una variable és sempre una cadena de text (no existeixen números ni booleans a l'entorn d'un procés).

Comprovem-ho:

kubectl exec -n rutas-norte-dev deploy/api-reserves -- env | sort | head -12
DB_HOST=postgres-reserves
DB_PORT=5432
HOME=/root
HOSTNAME=api-reserves-7f4b8c9d6-2xkqp
KUBERNETES_PORT=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
KUBERNETES_SERVICE_HOST=10.96.0.1
KUBERNETES_SERVICE_PORT=443
NIVELL_LOG=debug
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
REDIS_CACHE_SERVICE_HOST=10.96.184.22
TEMPS_ESPERA_MS=5000

Hi ha tres orígens barrejats en aquesta llista:

  1. Les que vénen de la imatge (PATH, HOME): les defineix el Dockerfile o la imatge base.
  2. Les que hem declarat nosaltres (DB_HOST, NIVELL_LOG).
  3. Les que injecta Kubernetes automàticament: HOSTNAME (el nom del pod) i les variables de descobriment de Services (KUBERNETES_SERVICE_HOST, REDIS_CACHE_SERVICE_HOST...).

Aquestes últimes són un vestigi dels inicis de Kubernetes, heretat dels Docker links. Tenen dues limitacions serioses: només apareixen els Services que ja existien quan va arrencar el pod, i només els del mateix namespace. Per això ningú no les fa servir avui: el descobriment es fa per DNS, com veuràs a DNS Intern. Si tens molts Services i vols una llista d'entorn neta, es poden desactivar amb enableServiceLinks: false a spec del pod.

  1. env amb valor literal

La forma més simple. És una llista, no un mapa, i aquest és el primer detall que confon:

    spec:
      containers:
        - name: api
          image: registry.rutasnorte.example/api-reserves:2.5.0
          env:
            - name: DB_HOST                 # cada entrada es un objecte amb name i value
              value: postgres-reserves
            - name: DB_PORT
              value: "5432"                 # COMPTE: cometes obligatories
            - name: MODE_DEPURACIO
              value: "false"                # tambe els booleans

Les cometes als valors numèrics i booleans no són opcionals. El camp value és de tipus string a l'esquema de l'API. Si escrius value: 5432, l'analitzador de YAML produeix un enter i la validació falla:

error: error validating data: ValidationError(Deployment.spec.template.spec.containers[0].env[1].value):
  invalid type for io.k8s.api.core.v1.EnvVar.value: got "number", expected "string"

El mateix problema amb value: true, amb value: no (YAML 1.1 ho interpreta com a booleà) i amb value: 08:00 (ho interpreta com a sexagesimal). Regla pràctica: posa cometes a tots els valors d'env que no siguin clarament text.

Quan fer servir un valor literal en lloc d'un ConfigMap:

Fes servir value literal Fes servir ConfigMap o Secret
El valor és el mateix als tres entorns El valor canvia segons l'entorn
És una ruta interna del contenidor (/etc/secrets/postgres/password) És una URL, un host o un temps d'espera
És una constant estructural (PGDATA) És alguna cosa que un operador voldrà canviar sense tocar el Deployment
Mai, mai de la vida, per a una credencial Sempre per a una credencial (Secret)

  1. envFrom: abocar un ConfigMap o un Secret sencer

Quan el ConfigMap té deu claus i les vols totes, escriure deu blocs valueFrom és absurd. envFrom les aboca de cop:

          envFrom:
            - configMapRef:
                name: api-reserves-config       # TOTES les seves claus es tornen variables
            - secretRef:
                name: postgres-reserves-credencials

Amb aquest ConfigMap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: api-reserves-config
  namespace: rutas-norte-dev
data:
  NIVELL_LOG: debug
  TEMPS_ESPERA_MS: "5000"
  MAX_PLACES_PER_RESERVA: "9"

El contenidor rep NIVELL_LOG, TEMPS_ESPERA_MS i MAX_PLACES_PER_RESERVA amb aquests valors. El nom de la clau es converteix literalment en el nom de la variable, cosa que imposa una condició: les claus han de ser noms vàlids de variable d'entorn (lletres, dígits i _, sense començar per dígit).

Què passa si una clau no és vàlida? Kubernetes la ignora en silenci i ho registra com un esdeveniment:

kubectl describe pod api-reserves-7f4b8c9d6-2xkqp -n rutas-norte-dev | grep -A3 Events
Events:
  Type     Reason              Age   From     Message
  ----     ------              ----  -------  -------
  Warning  InvalidEnvironmentVariableNames  12s  kubelet
    Keys [nginx.conf, api-url] from the EnvFrom list in the container "api" are invalid

Aquest és exactament el motiu pel qual a la lliçó de ConfigMaps vaig insistir a separar el ConfigMap de fitxers del ConfigMap de variables. Un nginx.conf no pot ser una variable d'entorn, però conviu perfectament al mateix objecte i espatlla l'envFrom.

El camp prefix

envFrom admet un prefix, que resol el problema de les col·lisions entre fonts:

          envFrom:
            - configMapRef:
                name: api-reserves-config
              prefix: APP_                      # NIVELL_LOG -> APP_NIVELL_LOG
            - secretRef:
                name: postgres-reserves-credencials
              prefix: DB_                       # password    -> DB_password

Resultat dins del contenidor:

kubectl exec -n rutas-norte-dev deploy/api-reserves -- env | grep -E "^(APP_|DB_)" | sort
APP_MAX_PLACES_PER_RESERVA=9
APP_NIVELL_LOG=debug
APP_TEMPS_ESPERA_MS=5000
DB_database=reserves
DB_password=d3v-C4nvi4m3-2026
DB_username=rutasnorte

Fixa't en DB_password en minúscules: el prefix s'anteposa tal qual, sense canviar la resta. Si vols DB_PASSWORD, la clau del Secret s'ha d'anomenar PASSWORD. És un motiu raonable per anomenar les claus dels Secrets en majúscules quan saps que es consumiran amb envFrom.

optional

Per defecte, si el ConfigMap o el Secret referenciat no existeix, el pod no arrenca: es queda en CreateContainerConfigError.

kubectl get pods -n rutas-norte-dev
kubectl describe pod api-reserves-6c8d7f9b5-lmnop -n rutas-norte-dev | grep -A2 "Warning"
NAME                            READY   STATUS                       RESTARTS   AGE
api-reserves-6c8d7f9b5-lmnop    0/1     CreateContainerConfigError   0          34s

  Warning  Failed  5s (x4 over 33s)  kubelet
    Error: configmap "api-reserves-config" not found

Amb optional: true el pod arrenca sense aquestes variables:

          envFrom:
            - configMapRef:
                name: api-reserves-experimental
                optional: true                  # si no existeix, s'ignora

Fes-lo servir amb compte. És apropiat per a configuració veritablement opcional (un ConfigMap de banderes de funcionalitat que només existeix a dev), però és perillós per a l'essencial: una fallada silenciosa on el pod arrenca amb la configuració a mitges és molt pitjor de diagnosticar que un pod que no arrenca. A Rutas Norte, optional: true només es fa servir per al ConfigMap de banderes experimentals.

  1. valueFrom: prendre una clau concreta

Quan vols una sola clau, o quan el nom de la variable ha de ser diferent del de la clau, es fa servir valueFrom:

          env:
            # D'un ConfigMap
            - name: LOG_LEVEL                  # nom que espera l'aplicacio
              valueFrom:
                configMapKeyRef:
                  name: api-reserves-config
                  key: NIVELL_LOG              # nom de la clau al ConfigMap
            # D'un Secret
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-reserves-credencials
                  key: password
            # Opcional: si falta, la variable simplement no existeix
            - name: NEW_RELIC_KEY
              valueFrom:
                secretKeyRef:
                  name: apm-credencials
                  key: llicencia
                  optional: true

Aquesta és la forma recomanada per defecte davant d'envFrom, i per tres raons sòlides:

  1. És explícita. En llegir el Deployment saps exactament quines variables rep el contenidor. Amb envFrom cal anar a mirar el ConfigMap.
  2. Desacobla noms. L'aplicació espera LOG_LEVEL i el nostre ConfigMap es diu NIVELL_LOG perquè l'equip treballa en català. valueFrom ho tradueix sense reanomenar res.
  3. Mínim privilegi. Al pod només hi arriben les claus que necessita, igual que fèiem amb items als volums.
envFrom valueFrom
Verbositat Baixa Alta
Traçabilitat en llegir el YAML Dolenta Excel·lent
Reanomenar claus Només amb prefix Sí, lliurement
Claus no vàlides S'ignoren en silenci Error explícit
Recomanació Moltes claus, noms ja correctes Per defecte

I un advertiment sobre secretKeyRef: encara que la referència sigui neta, el valor acaba a l'entorn del procés. És visible a /proc/<pid>/environ per a qualsevol procés del contenidor, i molts frameworks aboquen l'entorn complet a les pàgines d'error de depuració. Per a credencials d'alt valor, el fitxer muntat de la lliçó anterior continua sent millor opció.

  1. La Downward API: fieldRef

Fins ara la configuració venia de fora. La Downward API ("API cap avall") permet el contrari: que el contenidor conegui dades sobre si mateix i sobre el pod que el conté, sense parlar amb l'API de Kubernetes ni necessitar permisos.

El cas de Rutas Norte: quan un client informa que una reserva va fallar a les 22:47, volem poder buscar a les traces quin pod i quin node van atendre aquella petició. Amb 6 rèpliques d'api-reserves repartides per 3 nodes, sense aquesta informació la investigació és impossible.

          env:
            - name: POD_NOM
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
            - name: POD_IP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
            - name: NODE_NOM
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
            - name: COMPTE_SERVEI
              valueFrom:
                fieldRef:
                  fieldPath: spec.serviceAccountName
            - name: ENTORN
              valueFrom:
                fieldRef:
                  fieldPath: metadata.labels['entorn']        # etiqueta concreta

Camps disponibles a fieldRef:

fieldPath Què retorna Ús típic
metadata.name Nom del pod Traces, identificar la rèplica
metadata.namespace Namespace Compondre noms DNS, etiquetar mètriques
metadata.uid UID del pod Correlació en sistemes externs
metadata.labels['clau'] Valor d'una etiqueta Llegir entorn sense duplicar-lo
metadata.annotations['clau'] Valor d'una anotació Configuració injectada per un operador
spec.nodeName Node on corre Diagnòstic de problemes de node
spec.serviceAccountName ServiceAccount Auditoria
status.podIP IP del pod Registrar-se en un servei de descobriment
status.podIPs Llista d'IPs (doble pila) Xarxes IPv4/IPv6
status.hostIP IP del node Enviar mètriques a un agent del node

Dues restriccions que cal conèixer:

  • Només s'admeten aquests camps. No pots demanar spec.containers[0].image ni un camp arbitrari del pod. Per a això cal parlar amb l'API, que és el que veurem a ServiceAccounts.
  • metadata.labels i metadata.annotations sense especificar clau només funcionen en un volum downwardAPI, no a env. A env cal indicar la clau concreta entre claudàtors.

Vegem el resultat i el seu valor real:

kubectl exec -n rutas-norte-pro deploy/api-reserves -- env | grep -E "^(POD_|NODE_|ENTORN)"
POD_NOM=api-reserves-7f4b8c9d6-qh4nc
POD_NAMESPACE=rutas-norte-pro
POD_IP=10.244.2.37
NODE_NOM=rutas-norte-m02
ENTORN=pro

I així queda una traça d'api-reserves que faci servir aquestes variables:

2026-08-05T22:47:13.412Z INFO  [pod=api-reserves-7f4b8c9d6-qh4nc node=rutas-norte-m02 entorn=pro]
  reserva_creada id=RN-2026-084412 origen=Bilbao desti=Santander places=2 ms=184
2026-08-05T22:47:19.887Z ERROR [pod=api-reserves-7f4b8c9d6-qh4nc node=rutas-norte-m02 entorn=pro]
  reserva_fallida motiu=timeout_bd ms=5001

Amb aquesta línia, el diagnòstic és immediat: tots els errors vénen del mateix pod i del mateix node, així que el problema no és l'aplicació sinó aquell node concret (o la seva ruta de xarxa fins a postgres-reserves). Sense la Downward API, aquesta correlació no existeix.

Un detall important sobre ENTORN: podríem haver-lo posat com a literal value: "pro", però aleshores tindríem la dada duplicada (a l'etiqueta i a la variable) amb risc que es desincronitzin. Llegir-lo de metadata.labels['entorn'] garanteix que sempre coincideix amb l'etiqueta real del pod. És una aplicació directa de l'esquema d'etiquetatge del mòdul 2.

La Downward API com a volum

Existeix també la variant de fitxer, útil quan vols totes les etiquetes o anotacions:

      volumes:
        - name: info-pod
          downwardAPI:
            items:
              - path: etiquetes
                fieldRef:
                  fieldPath: metadata.labels        # sense clau: totes
              - path: anotacions
                fieldRef:
                  fieldPath: metadata.annotations
kubectl exec -n rutas-norte-pro deploy/api-reserves -- cat /etc/pod-info/etiquetes
app="api-reserves"
app.kubernetes.io/component="backend"
app.kubernetes.io/name="api-reserves"
app.kubernetes.io/part-of="rutas-norte"
entorn="pro"
pod-template-hash="7f4b8c9d6"

I amb un avantatge: a diferència de les variables, aquest fitxer sí que s'actualitza si canvien les etiquetes del pod, amb el mateix mecanisme de la lliçó de ConfigMaps.

  1. La Downward API: resourceFieldRef

La segona meitat de la Downward API exposa els recursos declarats del contenidor, que estudiarem a fons a Quotes i Límits:

          resources:
            requests:
              cpu: 250m
              memory: 256Mi
            limits:
              cpu: "1"
              memory: 512Mi
          env:
            - name: CPU_LIMIT_MILICORES
              valueFrom:
                resourceFieldRef:
                  containerName: api           # obligatori si hi ha diversos contenidors
                  resource: limits.cpu
                  divisor: 1m                  # unitat de sortida
            - name: MEMORIA_LIMIT_MB
              valueFrom:
                resourceFieldRef:
                  containerName: api
                  resource: limits.memory
                  divisor: 1Mi
            - name: MEMORIA_SOLLICITADA_MB
              valueFrom:
                resourceFieldRef:
                  containerName: api
                  resource: requests.memory
                  divisor: 1Mi
kubectl exec -n rutas-norte-pro deploy/api-reserves -- env | grep -E "^(CPU_|MEMORIA_)"
CPU_LIMIT_MILICORES=1000
MEMORIA_LIMIT_MB=512
MEMORIA_SOLLICITADA_MB=256

Recursos disponibles: limits.cpu, requests.cpu, limits.memory, requests.memory, limits.ephemeral-storage, requests.ephemeral-storage.

El divisor decideix la unitat: 1m dona milicores, 1 dona cores sencers (arrodonint cap amunt), 1Mi dona mebibytes, 1Gi gibibytes. Si l'omets, el valor per defecte és 1, cosa que per a memòria significa bytes i produeix números incòmodes com 536870912.

Per a què serveix això a la pràctica? Perquè el procés es dimensioni a si mateix. És un problema real i molt comú: molts entorns d'execució no veuen els límits de cgroups i creuen que disposen de tota la memòria i totes les CPU del node, amb la qual cosa dimensionen els seus pools de fils i els seus munts de memòria a lo gran i acaben en OOMKilled.

Aplicat a api-reserves (Node.js):

          env:
            - name: MEMORIA_LIMIT_MB
              valueFrom:
                resourceFieldRef:
                  containerName: api
                  resource: limits.memory
                  divisor: 1Mi
            # Al munt de V8 se li dona el 75% del limit del contenidor,
            # deixant marge per a la resta del proces i evitar l'OOMKill
            - name: NODE_OPTIONS
              value: "--max-old-space-size=384"

Aquest 384 és el 75 % de 512Mi. L'ideal seria calcular-lo, però l'expansió $(VAR) de l'apartat següent no fa aritmètica, així que a la pràctica es calcula a l'entrypoint del contenidor llegint MEMORIA_LIMIT_MB:

# entrypoint.sh d'api-reserves
MAX_HEAP=$(( MEMORIA_LIMIT_MB * 75 / 100 ))
exec node --max-old-space-size=${MAX_HEAP} servidor.js

Per a la JVM existeix l'equivalent automàtic (-XX:MaxRAMPercentage=75), que és l'opció preferible quan el llenguatge l'ofereix.

  1. Expansió de variables amb $(VAR)

Kubernetes permet referenciar unes variables des d'unes altres fent servir la sintaxi $(NOM):

          env:
            - name: DB_HOST
              value: postgres-reserves
            - name: DB_PORT
              value: "5432"
            - name: DB_NOM
              value: reserves
            - name: DB_URL
              value: "postgresql://$(DB_HOST):$(DB_PORT)/$(DB_NOM)"
kubectl exec -n rutas-norte-dev deploy/api-reserves -- printenv DB_URL
postgresql://postgres-reserves:5432/reserves

Les regles, que cal conèixer amb precisió:

Regla 1: la sintaxi és $(VAR), amb parèntesis. No ${VAR} ni $VAR. Aquestes dues són sintaxi de shell i Kubernetes les deixa intactes.

Regla 2: només s'expandeixen variables definides ABANS a la mateixa llista env. L'ordre importa:

          env:
            - name: MAL
              value: "$(DEFINIDA_DESPRES)"    # no s'expandeix: encara no existeix
            - name: DEFINIDA_DESPRES
              value: "hola"
MAL=$(DEFINIDA_DESPRES)

Quan una referència no es pot resoldre, queda literal. No hi ha error ni avís, i el valor $(DEFINIDA_DESPRES) arriba tal qual a l'aplicació. És una font d'errors silenciosos.

Regla 3: no s'expandeixen les variables que vénen d'envFrom. Les d'envFrom es processen com un bloc i no participen en l'expansió de les d'env. Si necessites compondre amb una clau d'un ConfigMap, porta-la primer amb valueFrom:

          env:
            - name: DB_HOST                        # primer la portem explicitament
              valueFrom:
                configMapKeyRef:
                  name: api-reserves-config
                  key: DB_HOST
            - name: DB_URL                         # ara si que es pot referenciar
              value: "postgresql://$(DB_HOST):5432/reserves"

Regla 4: no s'expandeixen les variables de la imatge. $(PATH) o $(HOME) no es resolen: Kubernetes només coneix les que ell mateix defineix.

Regla 5: $$ escapa el dòlar. Si la teva contrasenya conté $( literal, escriu-ho $$(.

            - name: PLANTILLA
              value: "El cost es $$(preu) euros"
El cost es $(preu) euros

  1. Interacció amb command i args

Aquí hi ha el malentès més freqüent del tema, i mereix un apartat propi.

        - name: worker
          image: registry.rutasnorte.example/worker-notificacions:1.8.0
          env:
            - name: LOT_MAXIM
              value: "50"
          command: ["/app/worker"]
          args: ["--lot=$(LOT_MAXIM)", "--cua=notificacions"]

Això funciona: Kubernetes expandeix $(LOT_MAXIM) a args amb les mateixes regles de l'apartat anterior, i el procés rep --lot=50.

Però això no funciona:

          command: ["/app/worker"]
          args: ["--lot=${LOT_MAXIM}"]           # sintaxi de shell
# El proces rep literalment:
--lot=${LOT_MAXIM}

I això tampoc:

          command: ["sh", "-c", "echo $LOT_MAXIM"]
          args: ["--extra=$LOT_MAXIM"]           # un sol dolar

La causa de la confusió: no hi ha shell. Quan escrius command: ["/app/worker"], containerd executa aquest binari directament amb execve(). No s'invoca /bin/sh, així que ningú no expandeix $VAR ni ${VAR}: aquesta expansió és una feina del shell, i el shell no és a la cadena.

Les tres opcions i quan fer servir cadascuna:

Forma Qui expandeix Quan fer-la servir
args: ["--lot=$(VAR)"] Kubernetes, abans d'arrencar Preferida. Simple i sense shell
command: ["sh","-c","/app/worker --lot=$VAR"] El shell del contenidor Si necessites canonades, condicionals o aritmètica
El mateix programa llegeix os.environ L'aplicació El més net de tot

Si fas servir la segona forma, dos avisos importants:

Avís 1: el shell es converteix en el PID 1 i molts shells no reenvien els senyals als seus fills. Això trenca la terminació ordenada amb SIGTERM que vas estudiar a la lliçó de Pods: en esborrar el pod, el procés real no rep el senyal i mor de cop en esgotar-se el període de gràcia. La solució és exec:

          command: ["sh", "-c", "exec /app/worker --lot=$LOT_MAXIM"]

Amb exec, el shell es reemplaça pel programa, que hereta el PID 1 i rep els senyals.

Avís 2: és una porta d'entrada a la injecció d'ordres. Si una variable ve d'una font poc fiable i la interpretes en un sh -c, un valor com ; rm -rf / s'executa. Amb $(VAR) de Kubernetes això no passa: el valor es passa com a argument, no s'interpreta.

I un recordatori de la lliçó anterior: mai no posis una credencial a args, ni tan sols expandida des d'un Secret. kubectl describe pod mostra Args amb el valor ja resolt.

  1. Precedència i col·lisions

Quan MATEIXA_CLAU arriba per diverses vies, quina guanya? L'ordre de resolució és estricte:

flowchart TB
    A["1. Variables de la IMATGE (Dockerfile ENV)"] --> B["2. Variables de serveis de Kubernetes"]
    B --> C["3. envFrom, en l'ORDRE de la llista<br/>(cadascuna trepitja l'anterior)"]
    C --> D["4. env, en l'ORDRE de la llista<br/>(cadascuna trepitja l'anterior)"]
    D --> E["VALOR FINAL<br/>guanya l'ultim aplicat"]

La regla en una frase: env sempre guanya sobre envFrom, i dins de cada bloc guanya l'última entrada.

Exemple complet per veure els quatre nivells:

apiVersion: v1
kind: ConfigMap
metadata:
  name: config-a
  namespace: rutas-norte-dev
data:
  NIVELL_LOG: "info"
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: config-b
  namespace: rutas-norte-dev
data:
  NIVELL_LOG: "warn"
---
apiVersion: v1
kind: Pod
metadata:
  name: prova-precedencia
  namespace: rutas-norte-dev
spec:
  containers:
    - name: prova
      image: busybox:1.36
      command: ["sh", "-c", "env | grep NIVELL_LOG; sleep 3600"]
      envFrom:
        - configMapRef:
            name: config-a          # info
        - configMapRef:
            name: config-b          # warn  <- trepitja config-a
      env:
        - name: NIVELL_LOG
          value: "debug"            # <- trepitja tot l'anterior
kubectl apply -f prova-precedencia.yaml
kubectl logs prova-precedencia -n rutas-norte-dev
pod/prova-precedencia created
NIVELL_LOG=debug

Guanya debug, el d'env. I si eliminem aquest bloc env, guanyaria warn, el de l'últim configMapRef.

Consells per no patir amb això:

  1. Evita les col·lisions en lloc de gestionar-les. Un ConfigMap per propòsit i sense claus repetides.
  2. Fes servir prefix quan abocuis diverses fonts amb envFrom.
  3. Davant del dubte, comprova el valor efectiu, que és l'única veritat:
kubectl exec -n rutas-norte-dev deploy/api-reserves -- printenv NIVELL_LOG
  1. Compte amb les variables de la imatge. Una imatge base pot definir NODE_ENV=production al seu Dockerfile, i si no la sobreescrius explícitament hi serà sense que aparegui en cap manifest teu. kubectl exec ... -- env la revela.

  1. La limitació fonamental: no es refresquen

Tornem al mecanisme de l'apartat 1. Les variables d'entorn es fixen a execve() i ningú no les pot canviar després. La conseqüència és taxativa:

Si canvies un ConfigMap o un Secret, els pods que ja corren no veuran el canvi mai, per moltes hores que esperis.

Demostració:

# El valor actual
kubectl exec -n rutas-norte-dev deploy/api-reserves -- printenv NIVELL_LOG

# Canviem el ConfigMap
kubectl patch configmap api-reserves-config -n rutas-norte-dev \
  --type merge -p '{"data":{"NIVELL_LOG":"trace"}}'

# Comprovem que l'objecte SI que ha canviat
kubectl get cm api-reserves-config -n rutas-norte-dev -o jsonpath='{.data.NIVELL_LOG}'; echo

# I ara, cinc minuts despres, dins del pod
sleep 300
kubectl exec -n rutas-norte-dev deploy/api-reserves -- printenv NIVELL_LOG
debug
configmap/api-reserves-config patched
trace
debug

L'objecte val trace i el procés continua veient debug. I continuarà així fins a la fi dels temps.

Comparativa amb el que sí que es refresca:

Mecanisme Es refresca en canviar la font?
ConfigMap muntat com a volum , en 1-2 minuts
Secret muntat com a volum , en 1-2 minuts
Volum amb subPath No
Volum d'un objecte immutable No
Downward API en volum (etiquetes)
Variable d'entorn (qualsevol origen) Mai

Les tres sortides possibles:

  1. Reiniciar els pods a mà. Funciona i és el que vam fer en rotar credencials:
kubectl rollout restart deploy/api-reserves -n rutas-norte-dev

El problema és que cal recordar-se'n. Si algú canvia el ConfigMap en una pull request i oblida el reinici, la plataforma queda en un estat en què el manifest diu una cosa i els pods en fan una altra. Aquest desfasament silenciós és perillós: es descobreix setmanes després, quan un pod es reinicia per un altre motiu i de sobte canvia de comportament sense que ningú hagi tocat res.

  1. Fer servir fitxer muntat per al que hagi de canviar en calent.

  2. Automatitzar el reinici amb un hash, que és la solució elegant i l'apartat següent.

  1. El hash de la configuració en una anotació

La tècnica és senzilla i s'ha convertit en un patró estàndard: desar un resum (hash) del contingut del ConfigMap i del Secret en una anotació del template del pod.

La clau és que Kubernetes dispara un desplegament nou quan canvia qualsevol cosa dins de spec.template, incloses les seves anotacions. Com que el hash depèn del contingut de la configuració, canviar la configuració canvia el hash, canviar el hash canvia el template, i canviar el template dispara el desplegament progressiu.

flowchart LR
    A["Canvies el ConfigMap"] --> B["Recalcules el hash<br/>sha256 del contingut"]
    B --> C["Canvia l'anotacio<br/>del spec.template"]
    C --> D["El Deployment detecta<br/>que el template ha canviat"]
    D --> E["RollingUpdate automatic<br/>sense tall de servei"]

Al manifest:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-reserves
  namespace: rutas-norte-pro
spec:
  replicas: 4
  selector:
    matchLabels:
      app: api-reserves
      entorn: pro
  template:
    metadata:
      labels:
        app: api-reserves
        app.kubernetes.io/name: api-reserves
        app.kubernetes.io/part-of: rutas-norte
        entorn: pro
      annotations:
        # Aquests valors els calcula la canalitzacio abans d'aplicar
        rutasnorte.example/config-hash: "a3f5b81c9d2e4770"
        rutasnorte.example/secret-hash: "7e21c40ab6f39185"
    spec:
      containers:
        - name: api
          image: registry.rutasnorte.example/api-reserves:2.5.0
          envFrom:
            - configMapRef:
                name: api-reserves-config
            - secretRef:
                name: postgres-reserves-credencials

Com es calcula el hash a la canalització de desplegament:

#!/usr/bin/env bash
set -euo pipefail
NS=rutas-norte-pro

# Hash del contingut del ConfigMap (nomes el camp data, no metadades volatils)
CONFIG_HASH=$(kubectl get cm api-reserves-config -n "$NS" -o jsonpath='{.data}' \
  | sha256sum | cut -c1-16)

SECRET_HASH=$(kubectl get secret postgres-reserves-credencials -n "$NS" -o jsonpath='{.data}' \
  | sha256sum | cut -c1-16)

kubectl patch deployment api-reserves -n "$NS" -p "$(cat <<EOF
{"spec":{"template":{"metadata":{"annotations":{
  "rutasnorte.example/config-hash":"${CONFIG_HASH}",
  "rutasnorte.example/secret-hash":"${SECRET_HASH}"
}}}}}
EOF
)"

kubectl rollout status deployment/api-reserves -n "$NS" --timeout=5m
deployment.apps/api-reserves patched
Waiting for deployment "api-reserves" rollout to finish: 2 out of 4 new replicas have been updated...
deployment "api-reserves" successfully rolled out

És important fer el hash només de .data: si el fessis de l'objecte complet, el resourceVersion canviaria a cada actualització encara que el contingut fos idèntic, i tindries desplegaments espuris.

Quatre avantatges d'aquest patró:

  1. Canviar la configuració desplega sol. No cal recordar-se de res.
  2. Queda historial. Apareix a kubectl rollout history i es pot desfer amb rollout undo, igual que un canvi de codi.
  3. No hi ha desfasament. El pod que corre sempre correspon al ConfigMap vigent.
  4. És auditable. L'anotació diu a quina versió exacta de la configuració correspon cada revisió.

Val la pena saber que les eines de l'ecosistema fan això per tu:

Eina Com ho resol
Kustomize configMapGenerator afegeix un sufix de hash al nom del ConfigMap; el Deployment el referencia i canvia sol
Helm L'anotació checksum/config amb include, que és el patró canònic de les seves plantilles
Reloader (Stakater) Un controlador que vigila ConfigMaps i Secrets i fa rollout restart automàticament en canviar
Argo CD Detecta la deriva entre Git i el clúster i reconcilia

Per a Rutas Norte, mentre treballem amb YAML pla, l'script de dalt a la canalització és suficient. Al mòdul 10 el substituirem per Kustomize.

  1. Variable d'entorn davant de fitxer muntat

Ja tens tots els elements per decidir. Aquesta és la taula de decisió:

Criteri Variable d'entorn Fitxer muntat
Valor curt i simple Excessiu
Fitxer de configuració complet Impossible a la pràctica
Contingut binari No (binaryData)
Ha de refrescar-se sense reiniciar No pot
Compatible amb imatges de tercers Gairebé sempre Només si admeten *_FILE
Visible a /proc/<pid>/environ (risc) No
Pot filtrar-se en un bolcat d'error (risc) Poc probable
S'hereta a processos fills (de vegades indesitjable) No
Visible a kubectl describe pod La referència, no el valor La referència
Apareix a docker inspect / crictl inspect Sí, amb el valor No
Cost d'arrencada Nul Un muntatge a preparar
Es pot compondre amb $(VAR) No
Límit de mida pràctic Uns pocs KB 1 MiB

Regles de decisió per a Rutas Norte:

  1. Configuració no sensible i estable durant la vida del pod → variable d'entorn. NIVELL_LOG, TEMPS_ESPERA_MS, REDIS_HOST. És el més simple i ho entén tothom.
  2. Fitxers de configuració → volum. El nginx.conf de botiga-web, el regles-tarifes.json d'api-reserves.
  3. Credencials d'alt valor → volum, amb *_FILE si la imatge ho admet. La contrasenya de postgres-reserves per a api-reserves.
  4. Credencials exigides per imatges de tercers → variable amb secretKeyRef, sense alternativa. POSTGRES_PASSWORD a la imatge oficial de PostgreSQL.
  5. Configuració que ha de canviar-se en calent → volum. El MODE_MANTENIMENT de nginx.
  6. Identitat del pod → Downward API en variables. És informació immutable durant la vida del pod, així que la limitació no molesta.

  1. El catàleg de variables de Rutas Norte

Tanquem amb l'inventari complet, que és també l'estat de la plataforma en acabar aquesta lliçó. Marco en cada cas l'origen: L literal, CM ConfigMap, S Secret, DA Downward API.

botiga-web (nginx servint la SPA)

Variable Origen dev pre pro
NGINX_ENTRYPOINT_QUIET_LOGS L 1 1 1
POD_NOM DA metadata.name ídem ídem

botiga-web gairebé no fa servir variables: la seva configuració és el nginx.conf del ConfigMap muntat, perquè nginx llegeix un fitxer, no l'entorn. És l'exemple perfecte de la regla 2.

api-reserves (API REST en Node.js)

Variable Origen dev pre pro
NODE_ENV L development production production
PORT L 8080 8080 8080
LOG_LEVEL CM NIVELL_LOG debug info warn
DB_HOST CM postgres-reserves ídem ídem
DB_PORT CM 5432 5432 5432
DB_NOM CM reserves reserves reserves
DB_USUARI S username (secret) (secret) (secret)
DB_PASSWORD_FILE L /etc/secrets/postgres/password ídem ídem
DB_POOL_MAX CM 5 10 25
REDIS_HOST CM redis-cache ídem ídem
REDIS_TTL_SEGONS CM 30 120 300
MAX_PLACES_PER_RESERVA CM 9 9 9
TEMPS_ESPERA_MS CM 5000 3000 2000
POD_NOM DA metadata.name ídem ídem
NODE_NOM DA spec.nodeName ídem ídem
ENTORN DA metadata.labels['entorn'] ídem ídem
MEMORIA_LIMIT_MB DA limits.memory / 1Mi ídem ídem

Observa DB_POOL_MAX: creix amb l'entorn perquè en producció hi ha més rèpliques i més trànsit en ponts i vacances, però no pot créixer sense límit, perquè max_connections de PostgreSQL és finit i rèpliques × DB_POOL_MAX no l'ha de superar. Amb 4 rèpliques i DB_POOL_MAX: 25 són 100 connexions. És exactament el tipus de càlcul que cal documentar al YAML amb un comentari.

postgres-reserves

Variable Origen Valor
POSTGRES_USER S username (secret)
POSTGRES_PASSWORD S password (secret)
POSTGRES_DB S database (secret)
PGDATA L /var/lib/postgresql/data/pgdata

Aquí no hi ha elecció: la imatge oficial de PostgreSQL llegeix aquestes variables. És la regla 4. El PGDATA en un subdirectori és una precaució imprescindible quan arribi el volum persistent del mòdul 5: el punt de muntatge sol contenir un lost+found que impedeix inicialitzar la base de dades.

redis-cache

Variable Origen dev pre pro
REDIS_MAXMEMORY CM 64mb 256mb 1gb
REDIS_MAXMEMORY_POLICY CM allkeys-lru ídem ídem

allkeys-lru és una decisió de negoci: redis-cache és la memòria cau de disponibilitat de places i és prescindible, així que preferim que descarti les claus menys usades abans que rebutjar escriptures.

worker-notificacions

Variable Origen dev pre pro
LOG_LEVEL CM debug info warn
DB_HOST CM postgres-reserves ídem ídem
DB_PASSWORD_FILE L /etc/secrets/postgres/password ídem ídem
SMTP_HOST CM mailhog smtp-pre.rutasnorte.example smtp.rutasnorte.example
SMTP_PORT CM 1025 587 587
SMTP_USUARI S (secret) (secret) (secret)
SMTP_PASSWORD S (secret) (secret) (secret)
REMITENT CM [email protected] no-reply@pre... [email protected]
LOT_MAXIM CM 10 50 200
INTERVAL_SONDEIG_S CM 30 15 5
POD_NOM DA metadata.name ídem ídem

A dev l'SMTP apunta a mailhog, un capturador de correu local: en desenvolupament no s'envien correus reals a clients. És una decisió de protecció de dades, no de comoditat, i és exactament el tipus de cosa que la separació de configuració fa possible.

informes-ocupacio (arribarà al mòdul 6)

Variable Origen Valor previst
DB_HOST CM postgres-reserves
DATA_INFORME L $(date -d yesterday) des del CronJob
DESTI_S3 CM s3://informes-rutasnorte/<entorn>/

El Deployment complet d'api-reserves

Reunim tot l'après en un únic manifest, que és l'estat real del component en tancar aquesta lliçó:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-reserves
  namespace: rutas-norte-pro
  labels:
    app: api-reserves
    app.kubernetes.io/name: api-reserves
    app.kubernetes.io/component: backend
    app.kubernetes.io/part-of: rutas-norte
    entorn: pro
spec:
  replicas: 4
  selector:
    matchLabels:
      app: api-reserves
      entorn: pro
  template:
    metadata:
      labels:
        app: api-reserves
        app.kubernetes.io/name: api-reserves
        app.kubernetes.io/component: backend
        app.kubernetes.io/part-of: rutas-norte
        entorn: pro
      annotations:
        rutasnorte.example/config-hash: "a3f5b81c9d2e4770"
        rutasnorte.example/secret-hash: "7e21c40ab6f39185"
    spec:
      imagePullSecrets:
        - name: registry-rutasnorte
      containers:
        - name: api
          image: registry.rutasnorte.example/api-reserves:2.5.0
          ports:
            - name: http
              containerPort: 8080

          env:
            # --- Literals: iguals als tres entorns ---
            - name: NODE_ENV
              value: "production"
            - name: PORT
              value: "8080"
            - name: DB_PASSWORD_FILE
              value: "/etc/secrets/postgres/password"

            # --- Del ConfigMap, amb reanomenat ---
            - name: LOG_LEVEL
              valueFrom:
                configMapKeyRef:
                  name: api-reserves-config
                  key: NIVELL_LOG
            - name: DB_HOST
              valueFrom:
                configMapKeyRef:
                  name: api-reserves-config
                  key: DB_HOST
            - name: REDIS_TTL_SEGONS
              valueFrom:
                configMapKeyRef:
                  name: api-reserves-config
                  key: REDIS_TTL_SEGONS

            # --- Del Secret: nomes l'usuari; la clau va per fitxer ---
            - name: DB_USUARI
              valueFrom:
                secretKeyRef:
                  name: postgres-reserves-credencials
                  key: username

            # --- Downward API: identitat per a les traces ---
            - name: POD_NOM
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: NODE_NOM
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
            - name: ENTORN
              valueFrom:
                fieldRef:
                  fieldPath: metadata.labels['entorn']
            - name: MEMORIA_LIMIT_MB
              valueFrom:
                resourceFieldRef:
                  containerName: api
                  resource: limits.memory
                  divisor: 1Mi

            # --- Composta: DB_HOST ja esta definida a dalt ---
            - name: DB_URL_BASE
              value: "postgresql://$(DB_HOST):5432/reserves"

          volumeMounts:
            - name: credencials-bd
              mountPath: /etc/secrets/postgres
              readOnly: true

          resources:
            requests:
              cpu: 250m
              memory: 256Mi
            limits:
              cpu: "1"
              memory: 512Mi

      volumes:
        - name: credencials-bd
          secret:
            secretName: postgres-reserves-credencials
            defaultMode: 0400
            items:
              - key: password
                path: password

Aquest manifest es pot publicar sense filtrar res. Conté referències, no valors. És exactament el llistó que ens vam posar en començar el mòdul.

Errors Comuns i Consells

Error Símptoma Solució
Valor numèric sense cometes invalid type ... got "number" value: "5432"
value: no sense cometes Arriba false Cometes sempre
ConfigMap amb claus no vàlides i envFrom Variables que falten, esdeveniment InvalidEnvironmentVariableNames Separa el ConfigMap de fitxers del de variables
ConfigMap o Secret inexistent CreateContainerConfigError Crea'l, o optional: true si de debò és opcional
optional: true en l'essencial Arrencada silenciosa amb configuració incompleta Reserva'l per al veritablement opcional
${VAR} o $VAR a args Arriba el text literal Fes servir $(VAR), o sh -c
$(VAR) referenciant alguna cosa definida després Arriba el text literal, sense avís Defineix primer, referencia després
$(VAR) sobre una clau d'envFrom No s'expandeix Porta-la amb valueFrom primer
sh -c sense exec El SIGTERM no arriba al procés real command: ["sh","-c","exec ..."]
Credencial a args Visible a describe pod Mai. Fes servir secretKeyRef o un volum
Esperar que un canvi de ConfigMap arribi sol "He canviat el ConfigMap i no passa res" Les variables no es refresquen. rollout restart o hash
Col·lisió entre envFrom i la imatge Un valor inesperat kubectl exec -- printenv VAR
resourceFieldRef sense divisor Números en bytes divisor: 1Mi
metadata.labels sense clau a env Error de validació A env cal posar metadata.labels['clau']

Consells:

  1. valueFrom per defecte, envFrom només quan siguin moltes i ja s'anomenin bé. La traçabilitat del manifest val més que estalviar línies.
  2. Afegeix la Downward API a tot component des del primer dia. El cost és de sis línies i el dia de l'incident val el seu pes en or.
  3. Documenta cada variable amb un comentari al manifest: què fa, quin rang és vàlid, qui la consumeix.
  4. Ordena env per blocs —literals, ConfigMap, Secret, Downward API, compostes— com al manifest final de l'apartat 13. Es llegeix molt millor.
  5. Davant de qualsevol dubte, kubectl exec -- env. El manifest és la intenció; l'entorn del procés és la realitat.

Exercicis

Exercici 1: Les quatre fonts en un sol pod

Crea un pod de diagnòstic anomenat inspector-config a rutas-norte-dev que rebi variables pels quatre camins i les imprimeixi:

  1. Un literal COMPONENT=inspector.
  2. Totes les claus del ConfigMap api-reserves-config amb el prefix CFG_.
  3. Únicament la clau password del Secret postgres-reserves-credencials, amb el nom CLAU_BD.
  4. El nom del pod, el node i l'etiqueta entorn per Downward API.
  5. Una variable RESUM composta amb $(VAR) que contingui <component>@<node>:<entorn>.

Aplica el manifest, mostra la sortida i explica per què el punt 5 només funciona si les variables estan en l'ordre correcte.

Exercici 2: Demostrar que les variables no es refresquen

  1. Desplega api-reserves amb LOG_LEVEL provinent del ConfigMap.
  2. Comprova el valor efectiu dins del pod.
  3. Canvia el ConfigMap a trace i espera tres minuts. Comprova de nou el valor al pod i el valor de l'objecte.
  4. Munta a més el mateix ConfigMap com a volum a /etc/config i repeteix l'experiment. Quina diferència observes?
  5. Implementa la tècnica del hash: calcula el sha256 del .data del ConfigMap, posa'l en una anotació del template i demostra que en canviar el ConfigMap i recalcular el hash es dispara un desplegament.

Exercici 3: Depurar un args que no expandeix

Un company ha desplegat worker-notificacions amb aquest fragment i es queixa que el treballador processa lots de mida ${LOT_MAXIM} en lloc de 200:

        - name: worker
          image: busybox:1.36
          env:
            - name: LOT_MAXIM
              valueFrom:
                configMapKeyRef:
                  name: worker-config
                  key: LOT_MAXIM
          command: ["sh", "-c", "echo Processant lot de ${LOT_MAXIM}; sleep 3600"]
          args: ["--cua=$LOT_MAXIM"]
  1. Explica què està malament a args i què està bé a command, i per què són casos diferents.
  2. Corregeix el manifest fent servir la sintaxi de Kubernetes.
  3. Explica per què aquest command amb sh -c és problemàtic per a la terminació ordenada del mòdul 2 i corregeix-lo.
  4. Reescriu el fragment sense fer servir shell en absolut.
  5. Si LOT_MAXIM vingués d'un envFrom en lloc d'un valueFrom, funcionaria l'expansió $(LOT_MAXIM) a args? Justifica-ho.

Solucions

Solució 1

apiVersion: v1
kind: Pod
metadata:
  name: inspector-config
  namespace: rutas-norte-dev
  labels:
    app: inspector-config
    app.kubernetes.io/part-of: rutas-norte
    entorn: dev
spec:
  restartPolicy: Never
  containers:
    - name: inspector
      image: busybox:1.36
      command: ["sh", "-c", "env | sort; echo '---'; echo \"RESUM=$RESUM\""]
      envFrom:
        - configMapRef:
            name: api-reserves-config
          prefix: CFG_
      env:
        # L'ordre importa per al punt 5
        - name: COMPONENT
          value: "inspector"
        - name: CLAU_BD
          valueFrom:
            secretKeyRef:
              name: postgres-reserves-credencials
              key: password
        - name: POD_NOM
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: NODE_NOM
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName
        - name: ENTORN
          valueFrom:
            fieldRef:
              fieldPath: metadata.labels['entorn']
        # Composta: totes les seves referencies estan definides A DALT
        - name: RESUM
          value: "$(COMPONENT)@$(NODE_NOM):$(ENTORN)"
kubectl apply -f inspector-config.yaml
kubectl logs inspector-config -n rutas-norte-dev
pod/inspector-config created

CFG_DB_HOST=postgres-reserves
CFG_MAX_PLACES_PER_RESERVA=9
CFG_NIVELL_LOG=debug
CFG_REDIS_TTL_SEGONS=30
CFG_TEMPS_ESPERA_MS=5000
CLAU_BD=d3v-C4nvi4m3-2026
COMPONENT=inspector
ENTORN=dev
HOSTNAME=inspector-config
NODE_NOM=rutas-norte
POD_NOM=inspector-config
RESUM=inspector@rutas-norte:dev
---
RESUM=inspector@rutas-norte:dev

El punt 5 funciona perquè COMPONENT, NODE_NOM i ENTORN estan definides abans que RESUM a la llista env. Kubernetes resol la llista en ordre i només pot substituir el que ja ha processat. Si RESUM estigués en primera posició, el valor seria literalment $(COMPONENT)@$(NODE_NOM):$(ENTORN), sense cap error ni avís.

I observa l'efecte col·lateral que serveix d'advertiment: CLAU_BD amb la contrasenya apareix al log del pod, perquè l'ordre fa env. Qualsevol amb kubectl logs la veu, i en producció aquest log aniria al sistema centralitzat del mòdul 7. No aboquis mai l'entorn en un contenidor que consumeixi secrets.

Solució 2

kubectl exec -n rutas-norte-dev deploy/api-reserves -- printenv LOG_LEVEL
kubectl patch cm api-reserves-config -n rutas-norte-dev --type merge -p '{"data":{"NIVELL_LOG":"trace"}}'
sleep 180
kubectl get cm api-reserves-config -n rutas-norte-dev -o jsonpath='{.data.NIVELL_LOG}'; echo
kubectl exec -n rutas-norte-dev deploy/api-reserves -- printenv LOG_LEVEL
debug
configmap/api-reserves-config patched
trace
debug

L'objecte val trace, el procés continua en debug. Amb el volum afegit:

          volumeMounts:
            - name: config
              mountPath: /etc/config
              readOnly: true
      volumes:
        - name: config
          configMap:
            name: api-reserves-config
kubectl exec -n rutas-norte-dev deploy/api-reserves -- cat /etc/config/NIVELL_LOG; echo
kubectl exec -n rutas-norte-dev deploy/api-reserves -- printenv LOG_LEVEL
trace
debug

El mateix ConfigMap, el mateix pod, el mateix instant, dos valors diferents. El fitxer es va refrescar i la variable no. Aquesta és la diferència en una sola pantalla.

El hash:

NS=rutas-norte-dev
H=$(kubectl get cm api-reserves-config -n $NS -o jsonpath='{.data}' | sha256sum | cut -c1-16)
echo "hash: $H"
kubectl patch deployment api-reserves -n $NS \
  -p "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"rutasnorte.example/config-hash\":\"$H\"}}}}}"
kubectl rollout status deployment/api-reserves -n $NS
kubectl exec -n $NS deploy/api-reserves -- printenv LOG_LEVEL
hash: c81f4a9e2b7d0356
deployment.apps/api-reserves patched
Waiting for deployment "api-reserves" rollout to finish: 1 out of 3 new replicas have been updated...
deployment "api-reserves" successfully rolled out
trace

Ara sí. I amb historial:

kubectl rollout history deployment/api-reserves -n rutas-norte-dev
REVISION  CHANGE-CAUSE
1         desplegament inicial
2         <none>
3         canvi de configuracio: NIVELL_LOG=trace

Solució 3

  1. args està malament, command està bé. A args s'ha escrit $LOT_MAXIM amb la sintaxi del shell; Kubernetes només entén $(VAR), així que deixa el text tal qual i el programa rep --cua=$LOT_MAXIM. A command, en canvi, l'expansió ${LOT_MAXIM} sí que funciona, però no la fa Kubernetes: la fa el sh -c que s'està executant. Són dues expansions diferents fetes per dos actors diferents, i confondre-les és l'origen de l'error.

A més hi ha un segon problema ocult: quan es defineix command amb sh -c "...", el contingut d'args s'afegeix com a arguments posicionals del shell ($0, $1...), no del programa. Aquell --cua=... no arriba a cap lloc útil.

  1. Corregit amb sintaxi de Kubernetes:
        - name: worker
          image: busybox:1.36
          env:
            - name: LOT_MAXIM
              valueFrom:
                configMapKeyRef:
                  name: worker-config
                  key: LOT_MAXIM
          command: ["/app/worker"]
          args:
            - "--lot=$(LOT_MAXIM)"
            - "--cua=notificacions"
# El proces rep:
/app/worker --lot=200 --cua=notificacions
  1. El problema del sh -c sense exec: el shell és el PID 1 del contenidor i /app/worker és el seu fill. Quan Kubernetes esborra el pod envia SIGTERM al PID 1, és a dir, al shell. La majoria de shells no reenvien senyals als seus fills, així que el treballador no se n'assabenta i continua processant correus fins que expira el terminationGracePeriodSeconds (30 s per defecte) i arriba el SIGKILL, que el mata en sec. Si estava enviant un correu de confirmació, es perd.
          command: ["sh", "-c", "exec /app/worker --lot=$LOT_MAXIM"]

Amb exec, el shell es reemplaça a si mateix pel treballador, que passa a ser el PID 1 i rep el SIGTERM directament.

  1. Sense shell en absolut, que és el correcte:
        - name: worker
          image: registry.rutasnorte.example/worker-notificacions:1.8.0
          env:
            - name: LOT_MAXIM
              valueFrom:
                configMapKeyRef:
                  name: worker-config
                  key: LOT_MAXIM
          command: ["/app/worker"]
          args: ["--lot=$(LOT_MAXIM)", "--cua=notificacions"]

Sense shell no hi ha problema de senyals, no hi ha risc d'injecció d'ordres, i l'expansió la fa Kubernetes abans d'arrencar res. El més net de tot seria que /app/worker llegís directament LOT_MAXIM del seu entorn i prescindir d'args.

  1. No, no funcionaria. L'expansió $(VAR) només abasta les variables definides a la llista env anterior al seu ús. Les que vénen per envFrom es resolen com un bloc separat i no participen en l'expansió ni d'env ni d'args. --cua=$(LOT_MAXIM) arribaria literal. La solució és portar-la explícitament amb valueFrom abans de fer-la servir, encara que també vingui a l'envFrom.

Conclusió

Amb aquesta lliçó tanques el bloc de configuració del mòdul. Entens el mecanisme real —la llista CLAU=valor que el kubelet lliura a execve() i que queda congelada al procés— i d'aquí es dedueix tota la resta. Domines les quatre fonts: el literal amb les seves cometes obligatòries, envFrom amb prefix i optional i el seu parany de les claus no vàlides que s'ignoren en silenci, valueFrom amb configMapKeyRef i secretKeyRef com a opció recomanada per explícita i per permetre reanomenar, i la Downward API en les seves dues formes, fieldRef per a la identitat del pod i resourceFieldRef perquè el procés es dimensioni a si mateix i no acabi en OOMKilled.

Saps que api-reserves registra ara a cada traça quin pod i quin node la van atendre, cosa que converteix un incident inabordable en un diagnòstic de dos minuts. Coneixes les cinc regles de l'expansió $(VAR) —parèntesis, ordre, res d'envFrom, res de la imatge, $$ per escapar— i per què a command/args no hi ha shell llevat que el demanis, amb les conseqüències que això arrossega: ${VAR} no s'expandeix, i si fas servir sh -c sense exec trenques la terminació ordenada del mòdul 2. Tens clara la precedència (env guanya a envFrom, i dins de cada bloc guanya l'últim) i la manera de comprovar la veritat, que sempre és kubectl exec -- printenv.

I sobretot tens interioritzada la limitació que governa les decisions de disseny: les variables d'entorn no es refresquen mai, amb la solució del hash de la configuració en una anotació del template que converteix un canvi de ConfigMap en un desplegament progressiu amb historial i rollback. Amb la taula de decisió "variable davant de fitxer" ja no dubtes on posar cada cosa, i el catàleg complet dels sis components de Rutas Norte per entorn és el mapa de la plataforma tal com està ara: una sola imatge per component, zero credencials a Git i tota la variabilitat concentrada a k8s/entorns/.

Queden dos deutes del mòdul 2, i tots dos són de recursos. Cap namespace no té quota, així que un desplegament equivocat a rutas-norte-dev —una rèplica de més, un bucle de reinici, un pod que demana 16 GiB— pot consumir la capacitat del clúster i deixar sense lloc rutas-norte-pro. I els requests i limits que anem escrivint des del mòdul 2 estan posats a ull, sense cap criteri. La lliçó següent, Quotes i Límits de Recursos, ho aborda: veuràs qui fa servir les requests (el planificador) i qui els limits (el kubelet a través de cgroups), la diferència crucial entre la CPU, que s'estrangula, i la memòria, que mata el procés amb OOMKilled, què passa quan la suma de límits supera la capacitat del clúster, i posaràs una ResourceQuota a cadascun dels tres entorns de Rutas Norte perquè dev no pugui tornar a espantar pro.

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