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
- Què és realment una variable d'entorn en un contenidor
envamb valor literalenvFrom: abocar un ConfigMap o un Secret sencervalueFrom: prendre una clau concreta- La Downward API:
fieldRef - La Downward API:
resourceFieldRef - Expansió de variables amb
$(VAR) - Interacció amb
commandiargs - Precedència i col·lisions
- La limitació fonamental: no es refresquen
- El hash de la configuració en una anotació
- Variable d'entorn davant de fitxer muntat
- El catàleg de variables de Rutas Norte
- 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:
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=5000Hi ha tres orígens barrejats en aquesta llista:
- Les que vénen de la imatge (
PATH,HOME): les defineix elDockerfileo la imatge base. - Les que hem declarat nosaltres (
DB_HOST,NIVELL_LOG). - 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.
env amb valor literal
env amb valor literalLa 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 booleansLes 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) |
envFrom: abocar un ConfigMap o un Secret sencer
envFrom: abocar un ConfigMap o un Secret sencerQuan 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-credencialsAmb 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:
Events:
Type Reason Age From Message
---- ------ ---- ------- -------
Warning InvalidEnvironmentVariableNames 12s kubelet
Keys [nginx.conf, api-url] from the EnvFrom list in the container "api" are invalidAquest é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_passwordResultat dins del contenidor:
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=rutasnorteFixa'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 foundAmb optional: true el pod arrenca sense aquestes variables:
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.
valueFrom: prendre una clau concreta
valueFrom: prendre una clau concretaQuan 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: trueAquesta és la forma recomanada per defecte davant d'envFrom, i per tres raons sòlides:
- És explícita. En llegir el Deployment saps exactament quines variables rep el contenidor. Amb
envFromcal anar a mirar el ConfigMap. - Desacobla noms. L'aplicació espera
LOG_LEVELi el nostre ConfigMap es diuNIVELL_LOGperquè l'equip treballa en català.valueFromho tradueix sense reanomenar res. - Mínim privilegi. Al pod només hi arriben les claus que necessita, igual que fèiem amb
itemsals 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ó.
- La Downward API:
fieldRef
fieldRefFins 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 concretaCamps 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].imageni un camp arbitrari del pod. Per a això cal parlar amb l'API, que és el que veurem a ServiceAccounts. metadata.labelsimetadata.annotationssense especificar clau només funcionen en un volumdownwardAPI, no aenv. Aenvcal indicar la clau concreta entre claudàtors.
Vegem el resultat i el seu valor real:
POD_NOM=api-reserves-7f4b8c9d6-qh4nc
POD_NAMESPACE=rutas-norte-pro
POD_IP=10.244.2.37
NODE_NOM=rutas-norte-m02
ENTORN=proI 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=5001Amb 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.annotationsapp="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.
- La Downward API:
resourceFieldRef
resourceFieldRefLa 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: 1MiRecursos 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.jsPer a la JVM existeix l'equivalent automàtic (-XX:MaxRAMPercentage=75), que és l'opció preferible quan el llenguatge l'ofereix.
- Expansió de variables amb
$(VAR)
$(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)"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"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 $$(.
- Interacció amb
command i args
command i argsAquí 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:
I això tampoc:
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:
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.
- 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'anteriorGuanya debug, el d'env. I si eliminem aquest bloc env, guanyaria warn, el de l'últim configMapRef.
Consells per no patir amb això:
- Evita les col·lisions en lloc de gestionar-les. Un ConfigMap per propòsit i sense claus repetides.
- Fes servir
prefixquan abocuis diverses fonts ambenvFrom. - Davant del dubte, comprova el valor efectiu, que és l'única veritat:
- Compte amb les variables de la imatge. Una imatge base pot definir
NODE_ENV=productional seuDockerfile, i si no la sobreescrius explícitament hi serà sense que aparegui en cap manifest teu.kubectl exec ... -- envla revela.
- 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_LOGL'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 | Sí, en 1-2 minuts |
| Secret muntat com a volum | Sí, en 1-2 minuts |
Volum amb subPath |
No |
| Volum d'un objecte immutable | No |
| Downward API en volum (etiquetes) | Sí |
| Variable d'entorn (qualsevol origen) | Mai |
Les tres sortides possibles:
- Reiniciar els pods a mà. Funciona i és el que vam fer en rotar credencials:
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.
-
Fer servir fitxer muntat per al que hagi de canviar en calent.
-
Automatitzar el reinici amb un hash, que és la solució elegant i l'apartat següent.
- 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-credencialsCom 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=5mdeployment.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ó:
- Canviar la configuració desplega sol. No cal recordar-se de res.
- Queda historial. Apareix a
kubectl rollout historyi es pot desfer ambrollout undo, igual que un canvi de codi. - No hi ha desfasament. El pod que corre sempre correspon al ConfigMap vigent.
- É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.
- 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 | Sí | Excessiu |
| Fitxer de configuració complet | Impossible a la pràctica | Sí |
| Contingut binari | No | Sí (binaryData) |
| Ha de refrescar-se sense reiniciar | No pot | Sí |
| Compatible amb imatges de tercers | Gairebé sempre | Només si admeten *_FILE |
Visible a /proc/<pid>/environ |
Sí (risc) | No |
| Pot filtrar-se en un bolcat d'error | Sí (risc) | Poc probable |
| S'hereta a processos fills | Sí (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) |
Sí | No |
| Límit de mida pràctic | Uns pocs KB | 1 MiB |
Regles de decisió per a Rutas Norte:
- 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. - Fitxers de configuració → volum. El
nginx.confdebotiga-web, elregles-tarifes.jsond'api-reserves. - Credencials d'alt valor → volum, amb
*_FILEsi la imatge ho admet. La contrasenya depostgres-reservesper aapi-reserves. - Credencials exigides per imatges de tercers → variable amb
secretKeyRef, sense alternativa.POSTGRES_PASSWORDa la imatge oficial de PostgreSQL. - Configuració que ha de canviar-se en calent → volum. El
MODE_MANTENIMENTde nginx. - Identitat del pod → Downward API en variables. És informació immutable durant la vida del pod, així que la limitació no molesta.
- 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: passwordAquest 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:
valueFromper defecte,envFromnomés quan siguin moltes i ja s'anomenin bé. La traçabilitat del manifest val més que estalviar línies.- 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.
- Documenta cada variable amb un comentari al manifest: què fa, quin rang és vàlid, qui la consumeix.
- Ordena
envper blocs —literals, ConfigMap, Secret, Downward API, compostes— com al manifest final de l'apartat 13. Es llegeix molt millor. - 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:
- Un literal
COMPONENT=inspector. - Totes les claus del ConfigMap
api-reserves-configamb el prefixCFG_. - Únicament la clau
passworddel Secretpostgres-reserves-credencials, amb el nomCLAU_BD. - El nom del pod, el node i l'etiqueta
entornper Downward API. - Una variable
RESUMcomposta 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
- Desplega
api-reservesambLOG_LEVELprovinent del ConfigMap. - Comprova el valor efectiu dins del pod.
- Canvia el ConfigMap a
tracei espera tres minuts. Comprova de nou el valor al pod i el valor de l'objecte. - Munta a més el mateix ConfigMap com a volum a
/etc/configi repeteix l'experiment. Quina diferència observes? - Implementa la tècnica del hash: calcula el
sha256del.datadel ConfigMap, posa'l en una anotació deltemplatei 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"]- Explica què està malament a
argsi què està bé acommand, i per què són casos diferents. - Corregeix el manifest fent servir la sintaxi de Kubernetes.
- Explica per què aquest
commandambsh -cés problemàtic per a la terminació ordenada del mòdul 2 i corregeix-lo. - Reescriu el fragment sense fer servir shell en absolut.
- Si
LOT_MAXIMvingués d'unenvFromen lloc d'unvalueFrom, funcionaria l'expansió$(LOT_MAXIM)aargs? 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)"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:devEl 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_LEVELL'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-configkubectl 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_LEVELEl 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_LEVELhash: 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
traceAra sí. I amb historial:
Solució 3
argsestà malament,commandestà bé. Aargss'ha escrit$LOT_MAXIMamb la sintaxi del shell; Kubernetes només entén$(VAR), així que deixa el text tal qual i el programa rep--cua=$LOT_MAXIM. Acommand, en canvi, l'expansió${LOT_MAXIM}sí que funciona, però no la fa Kubernetes: la fa elsh -cque 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.
- 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 problema del
sh -csenseexec: el shell és el PID 1 del contenidor i/app/workerés el seu fill. Quan Kubernetes esborra el pod enviaSIGTERMal 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 elterminationGracePeriodSeconds(30 s per defecte) i arriba elSIGKILL, que el mata en sec. Si estava enviant un correu de confirmació, es perd.
Amb exec, el shell es reemplaça a si mateix pel treballador, que passa a ser el PID 1 i rep el SIGTERM directament.
- 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.
- No, no funcionaria. L'expansió
$(VAR)només abasta les variables definides a la llistaenvanterior al seu ús. Les que vénen perenvFromes resolen com un bloc separat i no participen en l'expansió ni d'envni d'args.--cua=$(LOT_MAXIM)arribaria literal. La solució és portar-la explícitament ambvalueFromabans 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
- Què és Kubernetes?
- Arquitectura de Kubernetes
- Conceptes i Terminologia Clau
- Configuració d'un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objectes, Manifests YAML i el Model Declaratiu
- El Projecte del Curs: la Plataforma Rutas Norte
Mòdul 2: Components Principals de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualitzacions, Rollbacks i Estratègies de Desplegament
- Serveis
- Namespaces
- Etiquetes, Selectors i Anotacions
Mòdul 3: Gestió de Configuració i Secrets
- ConfigMaps
- Secrets
- Variables d'Entorn
- Quotes i Límits de Recursos
- LimitRanges i Classes de Qualitat de Servei (QoS)
- ServiceAccounts i Accés a l'API des dels Pods
Mòdul 4: Xarxes a Kubernetes
- Xarxes de Clúster
- Tipus de Serveis
- DNS Intern i Descobriment de Serveis
- Controladors d'Ingress
- TLS i Gestió de Certificats amb cert-manager
- Polítiques de Xarxa
Mòdul 5: Emmagatzematge a Kubernetes
- Volums
- Volums Persistents
- Reclamacions de Volums Persistents
- Classes d'Emmagatzematge
- Aprovisionament Dinàmic, Expansió i Snapshots
- Còpies de Seguretat i Restauració de Dades
Mòdul 6: Conceptes Avançats de Kubernetes
- StatefulSets
- DaemonSets
- Treballs i CronJobs
- Init Containers, Sidecars i Patrons Multicontenidor
- Planificació: Afinitat, Taints i Toleracions
- Definicions de Recursos Personalitzats (CRDs)
- Operadors i el Patró Controlador
Mòdul 7: Monitoratge i Registre
- Verificacions de Salut i Sondes
- Servidor de Mètriques i kubectl top
- Monitoratge amb Prometheus
- Visualització i Alertes amb Grafana i Alertmanager
- Registre Centralitzat amb Elasticsearch, Fluentd i Kibana (EFK)
- Depuració d'Aplicacions i Esdeveniments del Clúster
Mòdul 8: Seguretat a Kubernetes
- Control d'Accés Basat en Rols (RBAC)
- Contextos de Seguretat i Enduriment del Contenidor
- Polítiques de Seguretat de Pods i Pod Security Standards
- Seguretat de Xarxa
- Seguretat d'Imatges
- Auditoria, Escaneig i Gestió de Vulnerabilitats
Mòdul 9: Escalat i Rendiment
- Autoescalat Horitzontal de Pods
- Autoescalat Vertical de Pods
- Autoescalat de Clúster
- Escalat per Esdeveniments i Mètriques Personalitzades amb KEDA
- Alta Disponibilitat: PodDisruptionBudgets i Topologia
- Ajust de Rendiment
Mòdul 10: Ecosistema i Eines de Kubernetes
- Minikube i Entorns Locals amb kind
- Kubeadm
- Helm
- Kustomize
- GitOps amb Argo CD i Flux
- Kubernetes Gestionat: EKS, AKS i GKE
Mòdul 11: Estudis de Cas i Aplicacions del Món Real
- Desplegament d'una Aplicació Web
- Execució d'Aplicacions amb Estat
- CI/CD amb Kubernetes
- Estratègies de Desplegament: Blue-Green i Canary
- Gestió Multi-Clúster
- Operació en Producció: Incidències, Runbooks i Costos
