El mòdul 10 va acabar amb una constatació incòmoda: Escena Viva funciona. Reparteix càrrega entre treballadors, guarda el catàleg a la memòria cau de Redis, processa les cues d'emissió d'entrades en un procés a part, mesura el seu propi percentil 99 i exposa una API REST i una altra de GraphQL. I tot això continua executant-se en un portàtil, amb els secrets en un fitxer .env que només existeix al teu disc, sense registres centralitzats, sense supervisor, sense contenidor i sense desplegament automàtic. Aquest mòdul tanca aquesta distància. I comença per on cal començar: per la configuració. No perquè sigui el més vistós, sinó perquè és el primer que es trenca quan una aplicació surt del portàtil. La mateixa base de codi ha d'arrencar a la teva màquina apuntant a una base de dades local, al servidor d'integració contínua apuntant a contenidors efímers, en preproducció apuntant a una còpia de les dades reals i en producció apuntant a la base de dades on la Lucía i en Marc compren de veritat les seves entrades per al Festival de Jazz de Primavera. Una sola base de codi, quatre comportaments. La diferència està sencera a la configuració.
Contingut
- Els dotze factors aplicats a la configuració
process.envde veritatdotenvi els seus límits- Validar en arrencar i fallar de pressa
- Configuració per entorn sense duplicar fitxers
- Què no va mai en una variable d'entorn
- Gestió de secrets de debò
- Rotació de secrets sense parada
- Quan un secret es filtra
- Configuració en calent enfront de reinici
- Taula de referència de les variables d'Escena Viva
- Els dotze factors aplicats a la configuració
The Twelve-Factor App és un manifest del 2011 que descriu com s'hauria de construir una aplicació que es desplega en serveis al núvol. El seu factor III diu, en una frase: desa la configuració a l'entorn. La idea de fons és separar allò que canvia entre desplegaments d'allò que no. El codi d'Escena Viva és idèntic al teu portàtil i en producció: el mateix crearAplicacio, els mateixos repositoris, el mateix càlcul d'aforament. El que canvia és a quina base de dades es connecta, amb quina clau signa els JWT, en quin port escolta i quants treballadors arrenca. Això és configuració. La prova pràctica per distingir una cosa de l'altra és la que els dotze factors anomenen la prova del repositori públic:
¿Podries fer públic el repositori d'Escena Viva ara mateix, sense canviar res, sense comprometre cap credencial?
Si la resposta és no, tens configuració dins del codi. Tant se val si és en un config.json, en un objecte JavaScript o en un comentari: si està versionat i és secret, has suspès la prova. Compte amb el matís: la prova parla de credencials, no de qualsevol valor que variï. L'aforament màxim de la Sala Bóveda no és configuració per entorn: és una regla de negoci, i viu al domini o a la base de dades. Un error molt habitual és convertir en variable d'entorn tot allò que en algun moment podria canviar, i acabar amb quaranta variables que ningú sap per a què serveixen.
| És configuració | No és configuració |
|---|---|
| URL de PostgreSQL, MongoDB, Redis | Nom de les taules i col·leccions |
JWT_SECRET, claus d'API de tercers |
Durada de negoci d'un token de refresc decidida per producte |
| Port d'escolta, nombre de treballadors | Regles d'aforament, preus base, tipus de sala |
| Origen permès per CORS | Capçaleres de seguretat que sempre són les mateixes |
| Nivell de registre | Format de l'identificador d'entrada EV-<any>-<6 dígits> |
La regla mental: si dos desplegaments del mateix codi necessiten valors diferents, és configuració. Si tots els desplegaments necessiten el mateix valor, és codi.
process.env de veritat
process.env de veritatNode exposa les variables d'entorn del procés a process.env. És un objecte normal, però té tres característiques que causen la majoria dels errors. Tot són cadenes. Sempre. No hi ha nombres, ni booleans, ni nuls.
// Suposant: PORT=3000 DEPURAR=false TREBALLADORS=0
typeof process.env.PORT; // 'string'
process.env.PORT + 1; // '30001' (concatenacio, no suma)
Boolean(process.env.DEPURAR); // true (la cadena 'false' es certa)
Number(process.env.TREBALLADORS) || 4; // 4 (el 0 es perd)Les tres línies són errors reals que es veuen en producció. L'última és especialment traïdora: || 4 sembla un valor per defecte raonable fins que algú demana explícitament zero treballadors i n'obté quatre. Per això més endavant convertirem tipus amb un esquema, no a mà. undefined no és el mateix que cadena buida. Una variable que no existeix dóna undefined; una variable declarada sense valor dóna ''. Totes dues són falses en un if, però signifiquen coses diferents: la primera és «no m'ho has dit», la segona és «et dic explícitament que està buit».
node -e "console.log(process.env.ORIGEN_CORS)" # undefined
ORIGEN_CORS= node -e "console.log(JSON.stringify(process.env.ORIGEN_CORS))" # ""Majúscules per convenció. No és una regla de Node, és una convenció d'Unix heretada: les variables d'entorn van en MAJUSCULES_AMB_GUIO_BAIX. Respecta-la encara que la resta del projecte faci servir camelCase; qui llegeixi un docker-compose.yml o un tauler de Heroku espera aquest format.
D'on surten les variables
Això és el que sol quedar borrós. Les variables no vénen d'un sol lloc: vénen del procés pare, sigui quin sigui.
| Origen | Quan s'usa | Exemple |
|---|---|---|
| Intèrpret d'ordres interactiu | Proves puntuals | PORT=4000 npm start |
Fitxer .env + dotenv |
Desenvolupament local | .env a l'arrel del projecte |
| Unitat de systemd | VPS o servidor propi | Environment= o EnvironmentFile= |
| Ecosistema de PM2 / motor de contenidors | Supervisor o Docker | env_production (11-03), ENV/-e (11-04) |
| Tauler del proveïdor PaaS | Heroku, Render, Fly | Variables de configuració (lliçó 11-05) |
| Secrets de CI | Canonades | secrets de GitHub Actions (lliçó 11-06) |
Tots acaben al mateix lloc: process.env. Per això l'aplicació no ha de saber quin d'ells l'ha arrencada. Aquest desacoblament és justament el que fa que el mateix codi funcioni en els set escenaris.
dotenv i els seus límits
dotenv i els seus límitsEn desenvolupament, exportar quinze variables a mà a cada terminal és insuportable. dotenv llegeix un fitxer .env i n'aboca el contingut a process.env. El fragment inicial de src/config/index.js, ja existent des del mòdul 6, és simplement require('dotenv').config();. Dos límits que cal tenir claríssims:
dotenvno sobreescriu el que ja existeix aprocess.env. Si l'entorn real ja defineixPORT, el.envs'ignora per a aquesta variable. És el comportament correcte: l'entorn real mana sobre el fitxer de comoditat.dotenvno és un gestor de secrets. És un fitxer de text pla al teu disc. En producció no hi ha d'existir. El que hi ha en producció són variables injectades per l'orquestrador, el supervisor o la plataforma.
Des de Node 20 existeix a més node --env-file=.env, que fa el mateix sense dependència. Mantenim dotenv a Escena Viva perquè el projecte ja el fa servir i perquè el seu comportament és idèntic en totes les versions que suportem, però convé saber que l'alternativa nativa existeix.
.env fora, .env.example dins
El .gitignore d'Escena Viva ignora .env i totes les seves variants locals. El que sí que es versiona és .env.example: la llista completa de variables que l'aplicació necessita, amb valors falsos o buits. És documentació executable, i és el primer que mira algú que s'incorpora a l'equip.
# .env.example — copia'l a .env i omple els valors.
# MAI posis valors reals aqui: aquest fitxer SI que esta versionat.
NODE_ENV=development
PORT=3000
NIVELL_REGISTRE=debug
# Bases de dades
URL_POSTGRES=postgres://escena:escena@localhost:5432/escena_viva
URL_MONGO=mongodb://localhost:27017/escena_viva
URL_REDIS=redis://localhost:6379
# Seguretat — genera valors amb: openssl rand -hex 32
JWT_SECRET=
JWT_SECRET_ANTERIOR=
SESSIO_SECRET=
CSRF_SECRET=
# Xarxa, processos i observabilitat (11-02)
ORIGEN_CORS=http://localhost:5173
CONFIAR_EN_PROXY=false
NOMBRE_TREBALLADORS=0
CONCURRENCIA_CUA=5
TOKEN_METRIQUES=Regla operativa: cada vegada que algú afegeix una variable al codi, l'afegeix a .env.example al mateix commit. Si no, el següent que cloni el repositori descobrirà la variable que falta quan l'aplicació peti.
- Validar en arrencar i fallar de pressa
Aquí hi ha el cor de la lliçó. La fallada clàssica de configuració no és que una variable falti: és quan t'assabentes que falta. Imagina que JWT_SECRET no està definit en producció. Sense validació, l'aplicació arrenca perfectament. Serveix el catàleg, mostra les set sessions del Festival de Jazz, deixa navegar. Tres hores després, la Lucía intenta iniciar sessió per comprar dues entrades i jsonwebtoken llança secretOrPrivateKey must have a value. Un error 500, una clienta perduda i un registre que no diu res útil. Amb validació en arrencar, l'aplicació no arrenca. El desplegament falla al primer segon, la plataforma no envia trànsit a la instància trencada i el missatge diu exactament què falta. Això és fallar de pressa. Ampliem src/config/index.js amb un esquema zod (la mateixa llibreria que ja fem servir per validar peticions al mòdul 6, així que no afegim dependències).
// src/config/index.js
'use strict';
require('dotenv').config();
const { z } = require('zod');
// Ajudes de conversio: recorda que TOT arriba com a cadena.
const comEnter = (perDefecte) =>
z.coerce.number().int().nonnegative().default(perDefecte);
const comBoolea = (perDefecte) =>
z.enum(['true', 'false']).default(String(perDefecte)).transform((v) => v === 'true');
// Un secret util te longitud suficient: 32 caracters hex com a minim.
const secretFort = z.string().min(32, 'minim 32 caracters: openssl rand -hex 32');
const esquemaConfiguracio = z.object({
NODE_ENV: z.enum(['development', 'test', 'staging', 'production']).default('development'),
PORT: comEnter(3000),
NIVELL_REGISTRE: z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal']).default('info'),
URL_POSTGRES: z.string().url(),
URL_MONGO: z.string().url(),
URL_REDIS: z.string().url(),
JWT_SECRET: secretFort,
// Opcional: nomes existeix durant una rotacio (apartat 8).
JWT_SECRET_ANTERIOR: secretFort.optional(),
SESSIO_SECRET: secretFort,
CSRF_SECRET: secretFort,
ORIGEN_CORS: z.string().default('http://localhost:5173'),
CONFIAR_EN_PROXY: comBoolea(false),
NOMBRE_TREBALLADORS: comEnter(0),
CONCURRENCIA_CUA: comEnter(5),
TOKEN_METRIQUES: z.string().min(16).optional(),
});
const resultat = esquemaConfiguracio.safeParse(process.env);
if (!resultat.success) {
// Ni logger ni res elaborat: aqui encara no hi ha aplicacio.
const problemes = resultat.error.issues
.map((i) => ` - ${i.path.join('.')}: ${i.message}`)
.join('\n');
process.stderr.write(
`\nConfiguracio invalida. Escena Viva no pot arrencar:\n${problemes}\n\n` +
"Revisa .env.example i les variables de l'entorn de desplegament.\n\n"
);
process.exit(1);
}
const crua = resultat.data;
// La forma de l'objecte NO es la de l'entorn: la resta del codi demana
// configuracio.seguretat.jwtSecret, mai process.env.JWT_SECRET.
const configuracio = Object.freeze({
entorn: crua.NODE_ENV,
esProduccio: crua.NODE_ENV === 'production',
esProva: crua.NODE_ENV === 'test',
port: crua.PORT,
nivellRegistre: crua.NIVELL_REGISTRE,
confiarEnProxy: crua.CONFIAR_EN_PROXY,
origensCors: crua.ORIGEN_CORS.split(',').map((origen) => origen.trim()),
baseDades: Object.freeze({
postgres: crua.URL_POSTGRES, mongo: crua.URL_MONGO, redis: crua.URL_REDIS,
}),
seguretat: Object.freeze({
jwtSecret: crua.JWT_SECRET,
jwtSecretAnterior: crua.JWT_SECRET_ANTERIOR,
sessioSecret: crua.SESSIO_SECRET,
csrfSecret: crua.CSRF_SECRET,
}),
processos: Object.freeze({
nombreTreballadors: crua.NOMBRE_TREBALLADORS,
concurrenciaCua: crua.CONCURRENCIA_CUA,
}),
observabilitat: Object.freeze({ tokenMetriques: crua.TOKEN_METRIQUES }),
});
module.exports = { configuracio };Què fa cada decisió:
z.coerce.number()converteix la cadena a nombre dins de l'esquema, així que no tornes a escriure maiNumber(process.env.ALGUNA_COSA)dispers pel codi.comBooleaaccepta només'true'o'false': si algú escriuCONFIAR_EN_PROXY=si, l'arrencada falla amb un missatge clar en comptes d'interpretar-ho com a cert..default()centralitza els valors per defecte en un únic lloc. El|| 4dispers desapareix, i amb ell el problema del zero.safeParse+process.exit(1)converteix qualsevol problema en una mort immediata amb codi de sortida diferent de zero, que és el que PM2, Docker i les PaaS interpreten com a «arrencada fallida».Object.freezea tots els nivells impedeix que un mòdul modifiqui la configuració en calent i deixi la resta del procés veient una altra cosa.- La forma de l'objecte exportat no és la de l'entorn. La resta del codi demana
configuracio.seguretat.jwtSecret, noprocess.env.JWT_SECRET. Si demà reanomenem la variable, es toca un fitxer.
Amb JWT_SECRET absent, l'arrencada produeix Configuracio invalida. Escena Viva no pot arrencar: - JWT_SECRET: Required. Quaranta caràcters de sortida que estalvien una tarda sencera.
Regla d'or:
process.enves llegeix exclusivament asrc/config/index.js. En qualsevol altre fitxer està prohibit. Ho pots vigilar amb una regla d'ESLint (no-restricted-properties) o amb ungrepa la canonada de CI de la lliçó 11-06.
- Configuració per entorn sense duplicar fitxers
La temptació és crear config.development.js, config.staging.js i config.production.js. És un error, perquè duplica estructura: quan afegeixes una variable has de tocar tres fitxers i sempre n'oblides un, i perquè l'única diferència real entre entorns són els valors, no la forma. A Escena Viva hi ha un sol esquema i els valors arriben de fora:
| Entorn | NODE_ENV |
D'on surten els valors |
|---|---|---|
| Desenvolupament | development |
.env local amb dotenv |
| Prova | test |
.env.test (mòdul 9) i serveis de CI |
| Preproducció | staging |
Tauler del proveïdor o secrets de l'orquestrador |
| Producció | production |
Gestor de secrets + variables de la plataforma |
Per què NODE_ENV=production importa més del que sembla
NODE_ENV no és una variable com les altres: mig ecosistema la mira.
- Express desactiva la traça d'error a les respostes, activa la memòria cau de vistes i redueix feina per petició. Executar Express amb
NODE_ENVsense definir en producció és més lent i més filtrador. - Moltes llibreries (validadors, motors de plantilles, React al frontal) desactiven comprovacions de desenvolupament i avisos.
- npm:
npm ci --omit=devinstal·la només les dependències de producció. És el que farem a la imatge Docker de 11-04 i a la construcció de la PaaS de 11-05. I per això tot el que l'aplicació necessita en temps d'execució ha d'estar adependencies, mai adevDependencies. Unrequired'un paquet de desenvolupament peta en producció i enlloc més. - El nostre propi codi:
configuracio.esProducciodecideix sipino-prettyestà actiu (11-02), si les galetes portensecure, si s'exposen les traces.
Només valen els quatre valors de l'enum. Un NODE_ENV=prod mal escrit ja no arrenca l'aplicació en comptes de deixar-la en mode desenvolupament silenciosament.
- Què no va mai en una variable d'entorn
| Sí | No |
|---|---|
| Cadenes de connexió, claus, tokens | Fitxers grans o binaris (certificats PEM sencers) |
| Interruptors de comportament per entorn | Lògica de negoci disfressada de configuració |
| Noms de màquina, ports, orígens | Dades personals d'usuaris |
| Nivell de registre, indicadors de funcionalitat simples | Estructures complexes en JSON codificat |
Sobre l'últim punt: CONFIG_SALES={"teatro-almendra":{"aforament":800}} és un senyal d'alarma. Si la configuració necessita estructura, el que necessites és un fitxer muntat (que pot venir d'un secret de l'orquestrador) o una taula a la base de dades, no una variable d'entorn amb JSON a dins que ningú pot llegir ni depurar.
- Gestió de secrets de debò
Les variables d'entorn estan millor que el codi versionat, però no són segures. Es filtren per llocs que no esperes:
- Bolcats de procés. Un core dump conté el bloc d'entorn complet.
- Registres i traces. Un
console.log(process.env)en una depuració d'urgència, o una llibreria que aboca el context en un informe d'error, i el teuJWT_SECRETacaba al servei de registre de tercers. /proc/<pid>/environ. A Linux, qualsevol procés del mateix usuari pot llegir l'entorn d'un altre.- Imatges de contenidor. Un
ENV JWT_SECRET=...alDockerfilequeda gravat en una capa de la imatge, i la imatge es publica en un registre (11-04). - La sortida d'un error. Alguns clients de base de dades inclouen l'URL completa —amb usuari i contrasenya— al missatge d'excepció.
- Subprocessos. Tot fill hereta l'entorn del pare per defecte.
Per això existeixen els gestors de secrets. El panorama honest:
| Solució | Com funciona | A favor | En contra |
|---|---|---|---|
| Variables del proveïdor (Heroku, Render, Fly) | Tauler o CLI, injectades en arrencar | Zero infraestructura, immediat | Sense rotació automàtica ni auditoria fina |
| HashiCorp Vault | Servei dedicat; l'app demana el secret amb un token | Rotació, auditoria, secrets dinàmics i de vida curta | Operar Vault és un projecte en si mateix |
| AWS Secrets Manager / GCP Secret Manager | Servei gestionat del proveïdor, amb IAM | Integració nativa, rotació programada | Lliga al proveïdor, cost per secret i accés |
| Fitxers muntats (secrets de Kubernetes/Swarm) | El secret apareix com a fitxer al contenidor | No apareix a environ ni a la imatge |
Requereix orquestrador; cal llegir el fitxer |
| Xifratge al repositori (SOPS, git-crypt) | Secrets xifrats versionats; es desxifren en desplegar | Historial i revisió per PR dels canvis | La clau mestra continua havent de viure en algun lloc |
Escena Viva comença amb variables del proveïdor (11-05) perquè la mida del projecte no justifica operar Vault, i deixa el camí obert: com que tota la lectura passa per src/config/index.js, migrar a fitxers muntats és canviar quinze línies en un únic fitxer. Un patró intermedi molt útil és el sufix _FILE: si existeix JWT_SECRET_FILE, es llegeix el contingut d'aquell fitxer; si no, es fa servir JWT_SECRET. Així la mateixa imatge serveix per a una PaaS amb variables i per a un orquestrador amb secrets muntats.
- Rotació de secrets sense parada
Els secrets caduquen. Es roten perquè algú deixa l'equip, perquè ho exigeix una auditoria o perquè s'han filtrat. El problema és que canviar JWT_SECRET de cop invalida tots els tokens en circulació: la Lucía, en Marc i els organitzadors de les tres sales queden desconnectats a mitja compra. La solució és acceptar dues claus durant la transició. Se signa sempre amb la nova i es verifica contra totes dues.
// src/serveis/tokens.js (fragment adaptat per a la rotacio)
'use strict';
const jwt = require('jsonwebtoken');
const { configuracio } = require('../config/index.js');
const { ErrorDAutenticacio } = require('../errors.js');
const { jwtSecret, jwtSecretAnterior } = configuracio.seguretat;
// Se signa SEMPRE amb el secret vigent.
const signarAcces = (carrega) => jwt.sign(carrega, jwtSecret, { expiresIn: '15m' });
function verificarAcces(token) {
// Ordre important: primer el vigent (cas majoritari).
for (const clau of [jwtSecret, jwtSecretAnterior].filter(Boolean)) {
try {
return jwt.verify(token, clau);
} catch (error) {
// Token caducat o malformat: no el tapem.
if (error.name !== 'JsonWebTokenError') throw error;
}
}
throw new ErrorDAutenticacio('Token no valid');
}
module.exports = { signarAcces, verificarAcces };El procediment complet, amb tokens d'accés de 15 minuts: (1) generar la clau nova amb openssl rand -hex 32; (2) posar el valor actual a JWT_SECRET_ANTERIOR i el nou a JWT_SECRET; (3) reiniciar amb recàrrega sense talls (lliçó 11-03), i des d'aquell instant se signa amb la nova i s'accepta la vella; (4) esperar més que la vida del token més llarg —amb accés de 15 minuts i refresc de 7 dies, fins que caduquin els refrescos o se'n forci la rotació—; i (5) esborrar JWT_SECRET_ANTERIOR i reiniciar un altre cop. Fixa't que l'esquema ja ho suporta: JWT_SECRET_ANTERIOR és .optional(), així que l'aplicació arrenca igual amb una clau o amb dues. La rotació no requereix tocar codi.
- Quan un secret es filtra
Passa. Algú enganxa l'URL de PostgreSQL en un xat, o fa commit del .env un divendres. L'ordre de les accions importa:
- Revocar i rotar primer. El secret filtrat és vàlid fins que deixa de ser-ho; tota la resta pot esperar.
- Revisar accessos després, amb els registres d'auditoria del mòdul 8: hi va haver connexions des d'IP desconegudes?, es van emetre tokens estranys?
- Netejar l'historial al final, sabent que no n'hi ha prou. Reescriure l'historial de Git (
git filter-repo) no esborra el secret dels clons que ja existeixen, ni dels forks, ni de la memòria cau de la interfície web, ni dels rastrejadors automàtics que escanegen GitHub buscant credencials. Un secret que ha estat en un repositori remot està compromès per sempre. Esborrar el commit és higiene, no remei. - Evitar la reincidència. Un ganxo de pre-commit que detecti patrons de credencials (el projecte ja té husky des del mòdul 9) i escaneig de secrets a la canonada de CI (11-06).
- Configuració en calent enfront de reinici
Hi ha dues maneres d'aplicar un canvi de configuració: recarregar-la al procés viu, o reiniciar el procés.
| En calent | Reinici | |
|---|---|---|
| Complexitat | Alta: cada mòdul ha de reaccionar al canvi | Nul·la |
| Estat | Risc d'inconsistència entre mòduls | Tot coherent des del segon zero |
| Tall de servei | Cap | Cap si hi ha recàrrega sense talls |
| Auditoria | Difícil saber quins valors hi havia a cada moment | L'arrencada en deixa constància |
Escena Viva reinicia, i per això configuracio està congelat. La raó és que ja tenim aturada ordenada (mòdul 6) i tindrem recàrrega seqüencial de treballadors (11-03): reiniciar costa segons i no perd ni una petició. La complexitat de recarregar en calent només es justifica quan l'arrencada és caríssima, que no és el nostre cas. Nota important: un indicador de funcionalitat no és configuració de desplegament. Si vols activar la venda anticipada del Festival de Jazz a les 10:00 sense reiniciar, això va a base de dades o a un servei d'indicadors, no a process.env.
- Taula de referència de les variables d'Escena Viva
Aquesta taula és la referència que farem servir a la resta del mòdul: el .env.example, l'ecosystem.config.js de PM2, el docker-compose.yml, el tauler de la PaaS i els secrets de CI en deriven tots.
| Variable | Tipus | Obligatòria | Per defecte | On es fa servir |
|---|---|---|---|---|
NODE_ENV |
enum | No | development |
Express, npm, configuracio.esProduccio |
PORT |
enter | No | 3000 |
src/servidor.js |
NIVELL_REGISTRE |
enum | No | info |
pino (11-02) |
URL_POSTGRES |
url | Sí | — | src/db/sequelize.js |
URL_MONGO |
url | Sí | — | src/db/connexio.js |
URL_REDIS |
url | Sí | — | src/db/redis.js, sessió, límits, cues |
JWT_SECRET |
secret ≥32 | Sí | — | src/serveis/tokens.js |
JWT_SECRET_ANTERIOR |
secret ≥32 | No | — | Rotació (apartat 8) |
SESSIO_SECRET |
secret ≥32 | Sí | — | src/middleware/sessio.js |
CSRF_SECRET |
secret ≥32 | Sí | — | src/middleware/csrf.js |
ORIGEN_CORS |
llista | No | http://localhost:5173 |
src/middleware/cors.js |
CONFIAR_EN_PROXY |
boolea | No | false |
trust proxy (11-05) |
NOMBRE_TREBALLADORS |
enter | No | 0 (= nuclis) |
src/cluster.js, pool de Sequelize |
CONCURRENCIA_CUA |
enter | No | 5 |
src/processos/consumidor-entrades.js |
TOKEN_METRIQUES |
secret ≥16 | No | — | /metriques protegit (11-02) |
Errors Comuns i Consells
- Llegir
process.envfora desrc/config/index.js. És l'error que més mal fa a llarg termini: es perd la validació, els valors per defecte es dispersen i ningú sap quines variables fa servir l'aplicació. Prohibeix-ho amb lint. - Confondre «no és al codi» amb «és segur». Una variable d'entorn és visible a
/proc, en bolcats i en qualsevol abocament accidental. Tracta-la com un secret en trànsit, no com una caixa forta. - Consell: genera tots els secrets amb
openssl rand -hex 32i no reutilitzis mai el mateix valor per aJWT_SECRET,SESSIO_SECRETiCSRF_SECRET: si un es filtra, es filtren els tres. I actualitza.env.examplei la taula de l'apartat 11 al mateix commit en què afegeixis una variable.
Exercicis
Exercici 1 — Diagnòstic de configuració
Escriu un script scripts/comprovar-configuracio.js que carregui src/config/index.js i imprimeixi una taula amb totes les variables de l'esquema indicant, per a cadascuna, si ve de l'entorn o d'un valor per defecte, censurant els secrets. Ha de sortir amb codi 1 si la configuració és invàlida (aprofitant el process.exit(1) que ja fa el mòdul).
Exercici 2 — Suport de _FILE
Amplia src/config/index.js perquè qualsevol variable pugui venir d'un fitxer: si existeix <NOM>_FILE, es llegeix el contingut d'aquella ruta (retallant el salt de línia final) i es fa servir com a valor de <NOM>. Aplica-ho abans de validar amb zod.
Exercici 3 — Rotació simulada
Escriu una prova d'integració que verifiqui que un token signat amb JWT_SECRET_ANTERIOR continua sent acceptat per verificarAcces, i que un de signat amb una tercera clau desconeguda és rebutjat amb ErrorDAutenticacio.
Solucions
Exercici 1. La clau és no tornar a llegir process.env a l'script llevat que sigui per saber-ne l'origen, i censurar per nom:
// scripts/comprovar-configuracio.js
'use strict';
// Si la configuracio fos invalida, aquest require ja hauria sortit amb codi 1.
const { configuracio } = require('../src/config/index.js');
const SENSIBLES = /SECRET|TOKEN|PASSWORD|URL_POSTGRES|URL_MONGO/;
const NOMS = [
'NODE_ENV', 'PORT', 'NIVELL_REGISTRE', 'URL_POSTGRES', 'URL_MONGO',
'URL_REDIS', 'JWT_SECRET', 'JWT_SECRET_ANTERIOR', 'SESSIO_SECRET',
'CSRF_SECRET', 'ORIGEN_CORS', 'CONFIAR_EN_PROXY', 'NOMBRE_TREBALLADORS',
'CONCURRENCIA_CUA', 'TOKEN_METRIQUES',
];
const censurar = (nom, valor) =>
valor === undefined ? '(sense definir)' : SENSIBLES.test(nom) ? '********' : String(valor);
console.table(NOMS.map((nom) => ({
variable: nom,
origen: process.env[nom] === undefined ? 'per defecte' : 'entorn',
valor: censurar(nom, process.env[nom]),
})));Exercici 2. Es preprocessa process.env abans del safeParse:
const fs = require('node:fs');
function resoldreFitxers(entorn) {
const resolt = { ...entorn };
for (const [clau, valor] of Object.entries(entorn)) {
if (!clau.endsWith('_FILE') || !valor) continue;
try {
// Lectura sincrona a proposit: som a l'arrencada.
resolt[clau.slice(0, -'_FILE'.length)] = fs.readFileSync(valor, 'utf8').trimEnd();
} catch (error) {
process.stderr.write(`No s'ha pogut llegir ${clau}=${valor}: ${error.message}\n`);
process.exit(1);
}
}
return resolt;
}
const resultat = esquemaConfiguracio.safeParse(resoldreFitxers(process.env));Es llegeix de forma síncrona expressament: som a l'arrencada, abans que existeixi bucle d'esdeveniments amb feina, i volem fallar abans de continuar.
Exercici 3. Amb mocha i chai, signant a mà amb cada clau:
const { expect } = require('chai');
const jwt = require('jsonwebtoken');
const { configuracio } = require('../../src/config/index.js');
const { verificarAcces } = require('../../src/serveis/tokens.js');
describe('rotacio de JWT_SECRET', () => {
const carrega = { sub: 'usr-lucia', rol: 'assistent' };
it('accepta un token signat amb el secret anterior', () => {
const anterior = configuracio.seguretat.jwtSecretAnterior;
expect(anterior, 'defineix JWT_SECRET_ANTERIOR a .env.test').to.be.a('string');
const token = jwt.sign(carrega, anterior, { expiresIn: '15m' });
expect(verificarAcces(token)).to.include({ sub: 'usr-lucia' });
});
it('rebutja un token signat amb una clau desconeguda', () => {
const token = jwt.sign(carrega, 'x'.repeat(32), { expiresIn: '15m' });
expect(() => verificarAcces(token)).to.throw(/no valid/i);
});
});Conclusió
La configuració d'Escena Viva ha deixat de ser un .env al teu portàtil per convertir-se en un contracte explícit: un esquema zod que enumera cada variable, en converteix el tipus, aplica valors per defecte i mata el procés amb un missatge comprensible si falta un secret. Un únic punt de lectura de process.env, un .env.example que documenta el contracte, una taula de referència que servirà de base per al fitxer de PM2, el Dockerfile, el docker-compose.yml, el tauler de la PaaS i els secrets de CI. I, a més, una estratègia de rotació que permet canviar la clau de signatura dels JWT sense desconnectar ningú. Amb la configuració resolta, apareix la següent pregunta incòmoda: quan alguna cosa falli en producció a les tres de la matinada —i fallarà—, com te n'assabentes? A la propera lliçó, Registre i Monitoratge en Producció, substituïm els console.log per registre estructurat en JSON amb pino, pengem el registrador fill de l'idPeticio que arrosseguem des del mòdul 6 per poder seguir una compra fallida per tots els seus rastres, exposem mètriques amb prom-client —inclòs el retard del bucle d'esdeveniments que ja sabem mesurar— i implementem les sondes /salut/viu i /salut/preparat que la resta del mòdul donarà per suposades.
Curs de Node.js: De Principiant a Avançat
Mòdul 1: Introducció a Node.js
- Què és Node.js?
- Instal·lació i Configuració de l'Entorn
- El Teu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Modern per a Node.js
- El Projecte del Curs: la Plataforma Escena Viva
Mòdul 2: Conceptes Bàsics
- Arquitectura de Node.js
- El Bucle d'Esdeveniments (Event Loop)
- Callbacks i Programació Asíncrona
- Promeses i async/await
- Esdeveniments i EventEmitter
- Mòduls CommonJS i require()
- Mòduls ES i Interoperabilitat
Mòdul 3: Sistema de Fitxers i E/S
- Lectura i Escriptura de Fitxers
- El Mòdul fs a Fons
- Rutes Multiplataforma amb el Mòdul path
- Treballant amb Streams
- Streams de Transformació i pipeline
- Buffers i Dades Binàries
Mòdul 4: HTTP i Servidors Web
- Creant un Servidor HTTP Simple
- Gestió de Sol·licituds i Respostes
- Enrutament Manual
- Servint Fitxers Estàtics
- Rebent Dades: Cossos de Petició i JSON
- Consumint APIs Externes des de Node.js
Mòdul 5: NPM i Gestió de Paquets
- Introducció a NPM i package.json
- Instal·lació i Ús de Paquets
- Versionat Semàntic i package-lock
- Scripts d'npm i Automatització del Projecte
- Creació i Publicació de Paquets
- Seguretat i Manteniment de Dependències
Mòdul 6: Framework Express.js
- Introducció a Express.js
- Configuració d'una Aplicació Express
- Enrutament a Express
- Middleware
- Middleware de Tercers Essencials
- Validació de Dades d'Entrada
- Gestió d'Errors
Mòdul 7: Bases de Dades i ORMs
- Introducció a les Bases de Dades
- Usant MongoDB amb Mongoose
- Operacions CRUD
- Relacions, Poblat i Consultes Avançades
- Usant Bases de Dades SQL amb Sequelize
- Migracions, Transaccions i Dades de Prova
Mòdul 8: Autenticació i Autorització
- Introducció a l'Autenticació
- Registre d'Usuaris i Hash de Contrasenyes
- Sessions i Galetes amb Passport.js
- Autenticació amb JWT
- Control d'Accés Basat en Rols
- Bones Pràctiques de Seguretat en APIs
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb Mocha i Chai
- Dobles de Prova amb Sinon
- Proves d'Integració
- Cobertura i Automatització de les Proves
- Depuració d'Aplicacions Node.js
Mòdul 10: Temes Avançats
- El Mòdul Cluster
- Fils de Treball (Worker Threads)
- Memòria Cau i Cues de Treball amb Redis
- Optimització del Rendiment
- Construcció d'APIs RESTful
- GraphQL amb Node.js
Mòdul 11: Desplegament i DevOps
- Configuració i Variables d'Entorn
- Registre i Monitoratge en Producció
- Usant PM2 per a la Gestió de Processos
- Empaquetatge amb Docker
- Desplegant a Heroku i Altres PaaS
- Integració i Desplegament Continus
