Estándares de Configuración y Variables de Entorno
La configuración — y en particular las variables de entorno — son el contrato invisible entre el código y el entorno donde corre. Un nombre mal elegido no rompe nada el día que se escribe, pero genera deuda operacional compuesta: bugs latentes de shell, secretos filtrados en git, credenciales imposibles de rotar, manifiestos inmantenibles, auditorías fallidas. Este documento define cómo HERA nombra, agrupa, almacena y consume configuración a lo largo de cinco capas coordinadas — desde el proceso dentro del pod hasta el secreto global en GCP Secret Manager.
La regla de oro del estándar es Context-Aware Naming: cuanto más arriba en la jerarquía de scope vive el recurso, más largo y calificado debe ser su nombre; cuanto más abajo (más cerca del código), más corto y limpio, porque el contexto ya está implícito en dónde vive.
Referencia rápida para revisión de MR
Sección titulada «Referencia rápida para revisión de MR»Si estás revisando un Merge Request que toca variables de entorno, manifiestos K8s o .env.*, verifica estos 12 puntos antes de aprobar. Es la destilación del estándar — todo lo demás en esta página es contexto, justificación y ejemplos. El checklist completo con casillas marcables está al final.
| Regla | Qué verificar | Impacto si se omite |
|---|---|---|
| R1 | Toda env var en SCREAMING_SNAKE_CASE estricto, solo [A-Z0-9_] | Rompe parsers, valida POSIX |
| R2 | Sin guiones - en nombres de env vars | echo $VAR-NAME falla en bash |
| R3 | Longitud máxima de 63 caracteres por nombre | No cabe en labels K8s |
| R4 | Ningún secreto en value: — todos via valueFrom.secretKeyRef con External Secrets Operator | Credenciales filtradas en git |
| R5 | Todo nombre termina en un sufijo de la lista cerrada (18 sufijos: _URL, _HOST, _PORT, _PASSWORD, _SECRET, etc.) | Tipo ambiguo, impredecible |
| R6 | Booleanos literal true / false (lowercase) | Ambiguo con enteros o yes/no |
| R7 | Duraciones con unidad en el nombre: _TIMEOUT_MS, _TTL_SECONDS, _INTERVAL_MINUTES | Bug clásico “¿ms o segundos?” |
| R8 | Listas con separador , (coma), nunca ; | CORS y parsers estándar usan coma |
| R9 | URLs con esquema https:// y sin trailing slash | Evita // al concatenar path |
| R10 | Inter-servicio: nombrada por el servicio destino (USER_SERVICE_URL), no por el consumidor | Claridad del contrato |
| R11 | Env vars agrupadas con el orden canónico (Runtime → Database → Cache → Messaging → Storage → Auth → External APIs → Inter-service → Observability → Feature Flags → CORS / Security) | Legibilidad + review + diffing |
| R12 | Sin prefijos de framework (NEXT_PUBLIC_*, VITE_*) fuera de su scope | Mezcla client/server concerns |
Principios Fundacionales
Sección titulada «Principios Fundacionales»El estándar se construye sobre tres cuerpos de conocimiento externos y la taxonomía HERA definida en el portal:
| Fundamento | Origen | Aportación al estándar |
|---|---|---|
| 12-Factor App — Factor III (Config) | 12factor.net/config | La configuración vive en el entorno, no en el código. Un mismo binario funciona en DEV/QA/PRD sin recompilar |
| POSIX IEEE Std 1003.1 — Environment Variables | pubs.opengroup.org | Nombres válidos: [A-Z_][A-Z0-9_]*. Los guiones rompen bash, sh y scripts de CI/CD |
| OWASP ASVS 4.0 — Secrets Management | owasp.org/ASVS | Cero credenciales en código fuente ni en manifiestos versionados |
| Taxonomía HERA — Estructura de Repositorios + Clasificación de Servicios | Convenciones propias del portal | El producto es la identidad permanente. El K8s namespace es el producto. El deployment es el repo |
Los principios operativos que se derivan:
- Inmutabilidad del binario — el mismo artefacto (imagen Docker) se promueve de DEV a QA a PRD sin rebuild. Solo cambia qué GCP project sirve los secretos.
- Secretos fuera de git — ningún secreto (credencial, token, API key, clave criptográfica) vive en texto plano en un manifiesto versionado. Siempre se referencia vía
valueFrom.secretKeyRef. - Contexto implícito — el pod sabe en qué namespace vive y qué deployment es. Las env vars no repiten esa información.
- Trazabilidad total — dado cualquier nombre en cualquiera de las 5 capas, un ingeniero debe poder reconstruir las otras 4 sin documentación externa.
- Portabilidad — el manifiesto de un servicio es idéntico entre entornos. Las diferencias viven en la fuente de secretos (GCP project distinto por entorno).
Modelo Context-Aware Naming — 5 capas alineadas con la taxonomía HERA
Sección titulada «Modelo Context-Aware Naming — 5 capas alineadas con la taxonomía HERA»Este estándar no inventa jerarquía: se apoya en la Estructura de Repositorios GitLab y la extiende hacia la capa de configuración y secretos. El producto sigue siendo la identidad permanente; el namespace K8s sigue siendo el producto; el deployment sigue siendo el repo. Las capas 0, 1 y 2 son datos observados de la infraestructura existente — las capas 3, 4 y 5 son lo que este estándar define.
| Capa | Elemento | Scope | Formato canónico | Definido por |
|---|---|---|---|---|
| 0 | Jerarquía GitLab (source of truth) | Global organizacional | grupo-herdez/products/<dominio>/<producto>/<version>/<repo> | Estructura de Repositorios |
| 1 | GCP Project | Por entorno (DEV/QA/PRD) | ghdz-<tenant>-<capability>-<env> | Convención pre-existente de Grupo Herdez |
| 2a | GKE Cluster | Por entorno + capability | Nombre del cluster del GCP project del entorno | Infraestructura existente |
| 2b | K8s Namespace | Por producto (invariante entre entornos) | <producto> — ej. tienda-herdez, sitio-donamaria, cms-corporativo | Producto como Entidad Primaria |
| 2c | K8s Deployment | Por servicio dentro del producto | <tipo>-<detalle> — ej. backend-checkout-service, frontend-web-store, bff-mobile-app | Clasificación de Servicios — una de las 13 categorías oficiales |
| 3 | K8s Secret | Local al namespace | <deployment>-<resource-type> — ej. backend-checkout-service-db-users | Este estándar |
| 4 | GCP Secret Manager | Global en el GCP project | <producto>-<deployment>-<resource-type>-<attribute> — kebab-case | Este estándar |
| 5 | Pod Environment Variable | Local al proceso | <DOMAIN>_<RESOURCE>_<ATTRIBUTE> en SCREAMING_SNAKE_CASE | Este estándar (reglas R1–R12) |
A partir de las capas fijas, las tres reglas de derivación son:
- K8s namespace = nombre del producto — invariante entre entornos, versiones, partners y marcas.
tienda-herdezen DEV estienda-herdezen PRD. Cuando un producto migra dev1-partner-alphaav2-partner-beta, el namespace no cambia: la URL, los certificados TLS, los ingress y los secretos permanecen intactos. - K8s deployment = nombre del repo —
backend-checkout-serviceen el repositorio se refleja 1:1 como deployment K8s en el cluster, sin transformaciones ni abreviaciones. - GSM secret prefix =
<producto>-<deployment>— para unicidad global dentro del GCP project, que típicamente contiene N productos del mismo tenant/capability.
Ejemplo end-to-end con datos reales de HERA
Sección titulada «Ejemplo end-to-end con datos reales de HERA»process.env.DB_USERS_PASSWORDDado cualquier punto de la cadena, se reconstruyen los otros siete:
- Un SRE que ve rotarse
tienda-herdez-backend-checkout-service-db-users-passworden GCP Secret Manager sabe inmediatamente que afecta al productotienda-herdez, al deploymentbackend-checkout-service, a la base de datos lógicausers, y al pod environmentDB_USERS_PASSWORD. - Un desarrollador que ve
DB_USERS_PASSWORDen el código sabe que viene del K8s Secret del deployment en el namespace del producto, que a su vez se sincroniza desde GCP Secret Manager del GCP project del entorno actual.
Schema de Variables de Entorno — Gramática
Sección titulada «Schema de Variables de Entorno — Gramática»La gramática general de un nombre de variable de entorno es:
<DOMAIN>_<RESOURCE>_<ATTRIBUTE> │ │ │ │ │ └─ Qué atributo específico: HOST, PORT, PASSWORD, CLIENT_ID, ... │ └──────────── Qué recurso concreto: USERS, GOOGLE, PAYMENTS, CHECKOUT, ... └─────────────────────── Qué dominio funcional: DB, JWT, OAUTH, MSG, BUCKET, SMTP, OBS, FEATURE, ...Ejemplos del schema aplicado
Sección titulada «Ejemplos del schema aplicado»| Dominio | Recurso | Atributo | Variable resultante |
|---|---|---|---|
DB | USERS | HOST | DB_USERS_HOST |
DB | USERS | PASSWORD | DB_USERS_PASSWORD |
DB | CHECKOUT | HOST | DB_CHECKOUT_HOST |
JWT | (omitido — único) | ISSUER | JWT_ISSUER |
JWT | (omitido — único) | SECRET | JWT_SECRET |
OAUTH | GOOGLE | CLIENT_ID | OAUTH_GOOGLE_CLIENT_ID |
OAUTH | GOOGLE | CLIENT_SECRET | OAUTH_GOOGLE_CLIENT_SECRET |
OAUTH | GOOGLE | REDIRECT_URI | OAUTH_GOOGLE_REDIRECT_URI |
OAUTH | FACEBOOK | APP_ID | OAUTH_FACEBOOK_APP_ID |
MSG | PUBSUB | PROJECT_ID | MSG_PUBSUB_PROJECT_ID |
MSG | PUBSUB | TOPIC_MAIL | MSG_PUBSUB_TOPIC_MAIL |
BUCKET | UPLOADS | NAME | BUCKET_UPLOADS_NAME |
SMTP | (omitido — único) | HOST | SMTP_HOST |
SMTP | (omitido — único) | USERNAME | SMTP_USERNAME |
FEATURE | NEW_CHECKOUT | ENABLED | FEATURE_NEW_CHECKOUT_ENABLED |
| (implícito) | USER_SERVICE | URL | USER_SERVICE_URL |
| (implícito) | CATALOG_SERVICE | URL | CATALOG_SERVICE_URL |
Reglas Duras — R1 a R12
Sección titulada «Reglas Duras — R1 a R12»Las siguientes reglas son mandatorias y se verifican en code review. El incumplimiento bloquea el merge.
R1 — Casing SCREAMING_SNAKE_CASE estricto
Sección titulada «R1 — Casing SCREAMING_SNAKE_CASE estricto»✅ DB_USERS_PASSWORD✅ OAUTH_GOOGLE_CLIENT_ID✅ FEATURE_NEW_CHECKOUT_ENABLED
❌ db_users_password (minúsculas)❌ DbUsersPassword (camelCase)❌ DB-USERS-PASSWORD (guiones — ver R2)Solo se permiten caracteres [A-Z0-9_]. El primer carácter debe ser letra o underscore.
R2 — Prohibido el uso de guiones (-)
Sección titulada «R2 — Prohibido el uso de guiones (-)»Los guiones rompen la expansión de variables en bash, sh, y en cualquier script POSIX:
# Con RECIPE-JWT-SECRET="abc123" exportado, esto falla:echo $RECIPE-JWT-SECRET# Bash lo interpreta como: echo ${RECIPE}-JWT-SECRET → imprime "-JWT-SECRET"Node.js y otros lenguajes de aplicación toleran guiones via process.env['RECIPE-JWT-SECRET'], pero los scripts de CI/CD, entrypoints de Docker y ConfigMaps de K8s que usen expansión de shell fallan silenciosamente. El estándar elimina el riesgo por construcción.
R3 — Longitud máxima: 63 caracteres
Sección titulada «R3 — Longitud máxima: 63 caracteres»63 caracteres es el límite de un label DNS de K8s (RFC 1123). Aunque las env vars pueden ser más largas, mantenerlas bajo este límite garantiza que siempre se puedan usar como labels en métricas, dashboards y consultas.
✅ TIENDA_HERDEZ_BACKEND_CHECKOUT_SERVICE_DB_USERS_PASSWORD (56 caracteres — válido)❌ TIENDA_HERDEZ_BACKEND_CHECKOUT_SERVICE_DB_USERS_PASSWORD_LONG (60 — prefiere refactorizar)Estrategia de mitigación: si un nombre excede 63, probablemente viola la regla de oro (repite contexto que ya vive en el namespace/deployment). Acortar eliminando el prefijo de producto.
R4 — Secretos NUNCA en value: plano
Sección titulada «R4 — Secretos NUNCA en value: plano»Los secretos (credenciales, tokens, API keys, claves criptográficas, certificados) se prohíben en manifiestos versionados. Se referencian desde K8s Secrets gestionados por External Secrets Operator que sincroniza desde GCP Secret Manager.
# ❌ PROHIBIDOenv: - name: DB_USERS_PASSWORD value: "MiP4ssw0rd!"
# ✅ CORRECTO — referencia a K8s Secret sincronizado desde GSMenv: - name: DB_USERS_PASSWORD valueFrom: secretKeyRef: name: backend-checkout-service-db-users key: passwordVer la sección Patrón ExternalSecret CRD más abajo para la cadena completa GSM → K8s Secret → env var.
R5 — Sufijo obligatorio por tipo (lista cerrada)
Sección titulada «R5 — Sufijo obligatorio por tipo (lista cerrada)»Todo nombre termina en uno de los 18 sufijos estándar, que comunican inequívocamente el tipo del valor:
| Sufijo | Tipo del valor | Ejemplo |
|---|---|---|
_URL | URL completa con esquema | USER_SERVICE_URL=https://users.cloudherdez.com |
_HOST | Hostname o IP (sin puerto) | DB_USERS_HOST=10.95.128.78 |
_PORT | Puerto numérico | DB_USERS_PORT=5432 |
_NAME | Identificador lógico | DB_USERS_NAME=db_users_prd |
_USERNAME | Usuario de autenticación (nunca _USER) | DB_USERS_USERNAME=svc_users |
_PASSWORD | Contraseña (nunca _PASS, _PWD) | DB_USERS_PASSWORD (secretKeyRef) |
_SECRET | Clave criptográfica o de firma | JWT_SECRET (secretKeyRef) |
_API_KEY | API key de servicio externo | PAYMENTS_API_KEY (secretKeyRef) |
_CLIENT_ID | OAuth/OIDC client id público | OAUTH_GOOGLE_CLIENT_ID |
_CLIENT_SECRET | OAuth/OIDC client secret | OAUTH_GOOGLE_CLIENT_SECRET (secretKeyRef) |
_TOKEN | Token estático (webhook, service) | SLACK_WEBHOOK_TOKEN (secretKeyRef) |
_ENABLED | Boolean feature flag | FEATURE_NEW_CHECKOUT_ENABLED=true |
_TIMEOUT_MS / _TIMEOUT_SECONDS | Duración con unidad | DB_USERS_TIMEOUT_MS=5000 |
_TTL_SECONDS / _TTL_MINUTES / _TTL_HOURS | Time-to-live | CACHE_SESSION_TTL_MINUTES=30 |
_MAX_RETRIES / _RETRY_COUNT | Contador de reintentos | HTTP_MAX_RETRIES=3 |
_BATCH_SIZE | Cardinalidad de lote | MSG_PUBSUB_BATCH_SIZE=100 |
_REGION | Región cloud | BUCKET_UPLOADS_REGION=us-central1 |
_PROJECT_ID | GCP project id | MSG_PUBSUB_PROJECT_ID=ghdz-grupo-ext-sites-prd |
R6 — Booleanos literal true / false
Sección titulada «R6 — Booleanos literal true / false»✅ FEATURE_NEW_CHECKOUT_ENABLED=true✅ OBS_TRACING_ENABLED=false
❌ FEATURE_NEW_CHECKOUT_ENABLED=1 (ambiguo — ¿entero?)❌ FEATURE_NEW_CHECKOUT_ENABLED=yes (variante regional)❌ FEATURE_NEW_CHECKOUT_ENABLED=True (casing incorrecto)true y false en minúsculas coinciden con la representación nativa de YAML, JSON, Kubernetes y la mayoría de lenguajes. No hay ambigüedad de parsing.
R7 — Duraciones con unidad explícita en el nombre
Sección titulada «R7 — Duraciones con unidad explícita en el nombre»El error recurrente “¿eran milisegundos o segundos?” se elimina incluyendo la unidad en el nombre de la variable:
✅ DB_USERS_TIMEOUT_MS=5000✅ CACHE_SESSION_TTL_MINUTES=30✅ JOB_CLEANUP_INTERVAL_SECONDS=300
❌ DB_TIMEOUT=5000 (¿ms o segundos?)❌ CACHE_TTL=1800 (¿minutos? ¿segundos?)Unidades permitidas en nombres: _MS, _SECONDS, _MINUTES, _HOURS, _DAYS.
R8 — Listas con separador , (coma)
Sección titulada «R8 — Listas con separador , (coma)»✅ CORS_ALLOWED_ORIGINS=https://a.com,https://b.com,https://c.com
❌ CORS_ALLOWED_ORIGINS=https://a.com;https://b.com;https://c.com❌ CORS_ALLOWED_ORIGINS=https://a.com https://b.comLa coma es el separador estándar de listas en HTTP (CORS, Accept, Allow), en CSV, en la mayoría de parsers de configuración. ; es usado por sistemas legacy y no es portable.
Si un valor individual contiene una coma, se usa encoding URL o un formato de configuración estructurado (YAML/JSON), no una cadena delimitada.
R9 — URLs completas con esquema, sin trailing slash
Sección titulada «R9 — URLs completas con esquema, sin trailing slash»✅ USER_SERVICE_URL=https://users.cloudherdez.com✅ PAYMENTS_API_URL=https://api.stripe.com/v1
❌ USER_SERVICE_URL=users.cloudherdez.com (sin esquema)❌ USER_SERVICE_URL=https://users.cloudherdez.com/ (trailing slash — causa doble slash al concatenar)El trailing slash produce URLs con doble slash como https://host.com//api/v1/users cuando el código concatena la URL base con un path que empieza en /. Estandarizar sin trailing slash y obligar al código a anteponer / al path elimina la ambigüedad.
R10 — Inter-servicio: nombrar por el servicio destino
Sección titulada «R10 — Inter-servicio: nombrar por el servicio destino»Cuando un servicio consume a otro, la variable lleva el nombre del servicio destino, no del consumidor:
✅ USER_SERVICE_URL (apunta al User Service — consumido desde cualquier servicio)✅ CATALOG_SERVICE_URL✅ PAYMENTS_SERVICE_URL
❌ MY_USER_URL (¿"mi" qué?)❌ DEPENDENCIES_USER (anticuado, prefijo vacío de significado)❌ EXTERNAL_URL (no dice qué servicio)Si dos servicios externos tienen nombres similares, se usa el prefijo de capability del GCP project: APIHUB_ORDERS_SERVICE_URL vs INTEGRATIONHUB_ORDERS_SERVICE_URL.
R11 — Secciones de agrupación en manifiestos con orden fijo
Sección titulada «R11 — Secciones de agrupación en manifiestos con orden fijo»Los manifiestos K8s agrupan variables en bloques comentados con un orden canónico. Facilita code review, diffing entre entornos y detección de faltantes:
env: # ── Runtime ────────────────────────────────────────────────────── - name: NODE_ENV value: "production" - name: TZ value: "America/Mexico_City" - name: PORT value: "3000"
# ── Database ───────────────────────────────────────────────────── - name: DB_USERS_HOST valueFrom: secretKeyRef: { name: backend-checkout-service-db-users, key: host } - name: DB_USERS_PORT valueFrom: secretKeyRef: { name: backend-checkout-service-db-users, key: port } - name: DB_USERS_USERNAME valueFrom: secretKeyRef: { name: backend-checkout-service-db-users, key: username } - name: DB_USERS_PASSWORD valueFrom: secretKeyRef: { name: backend-checkout-service-db-users, key: password } - name: DB_USERS_NAME valueFrom: secretKeyRef: { name: backend-checkout-service-db-users, key: name }
# ── Cache ──────────────────────────────────────────────────────── - name: CACHE_SESSION_HOST value: "redis.tienda-herdez.svc.cluster.local" - name: CACHE_SESSION_PORT value: "6379" - name: CACHE_SESSION_TTL_MINUTES value: "30"
# ── Messaging ──────────────────────────────────────────────────── - name: MSG_PUBSUB_PROJECT_ID value: "ghdz-grupo-ext-sites-prd" - name: MSG_PUBSUB_TOPIC_ORDERS value: "orders-events"
# ── Storage ────────────────────────────────────────────────────── - name: BUCKET_UPLOADS_NAME value: "ghdz-tienda-herdez-uploads-prd" - name: BUCKET_UPLOADS_REGION value: "us-central1"
# ── Auth / Identity ────────────────────────────────────────────── - name: JWT_ISSUER value: "https://auth.cloudherdez.com" - name: JWT_AUDIENCE value: "tienda.clientes" - name: JWT_SECRET valueFrom: secretKeyRef: { name: backend-checkout-service-jwt, key: secret } - name: OAUTH_GOOGLE_CLIENT_ID value: "70096587311-xyz.apps.googleusercontent.com" - name: OAUTH_GOOGLE_CLIENT_SECRET valueFrom: secretKeyRef: { name: backend-checkout-service-oauth-google, key: client-secret } - name: OAUTH_GOOGLE_REDIRECT_URI value: "https://tienda.herdez.com/api/auth/google/callback"
# ── External APIs ──────────────────────────────────────────────── - name: PAYMENTS_SERVICE_URL value: "https://api.stripe.com/v1" - name: PAYMENTS_API_KEY valueFrom: secretKeyRef: { name: backend-checkout-service-payments, key: api-key }
# ── Inter-service Communication ────────────────────────────────── - name: USER_SERVICE_URL value: "http://platform-auth-service.platform.svc.cluster.local" - name: CATALOG_SERVICE_URL value: "http://backend-catalog-service.tienda-herdez.svc.cluster.local"
# ── Observability ──────────────────────────────────────────────── - name: OBS_LOG_LEVEL value: "info" - name: OBS_TRACING_ENABLED value: "true" - name: OBS_METRICS_PORT value: "9090"
# ── Feature Flags ──────────────────────────────────────────────── - name: FEATURE_NEW_CHECKOUT_ENABLED value: "true" - name: FEATURE_LEGACY_PAYMENTS_ENABLED value: "false"
# ── CORS / Security ────────────────────────────────────────────── - name: CORS_ALLOWED_ORIGINS value: "https://tienda.herdez.com,https://tienda.dev.hera.cloudherdez.com"Orden canónico: Runtime → Database → Cache → Messaging → Storage → Auth / Identity → External APIs → Inter-service → Observability → Feature Flags → CORS / Security.
R12 — Sin prefijos de framework en servicios que no son de ese framework
Sección titulada «R12 — Sin prefijos de framework en servicios que no son de ese framework»Los prefijos específicos de framework solo se usan dentro de ese framework:
| Prefijo | Permitido solo en | Razón |
|---|---|---|
NEXT_PUBLIC_* | Servicios Next.js | Next.js inyecta estas variables al bundle cliente. Usarlas en backends confunde el origen |
VITE_* | Servicios Vite | Vite expone estas variables al cliente. Igual que Next |
REACT_APP_* | Create React App | Legacy CRA; no usar en servicios que no son CRA |
ASTRO_* | Servicios Astro | Astro las reserva |
Si un servicio backend consume un frontend que usa NEXT_PUBLIC_API_URL, el backend no debe replicar la variable con el mismo prefijo. El backend tiene su propia variable PUBLIC_API_URL o, mejor, no necesita conocer esa URL en absoluto (el frontend le hace las requests).
¿env var, ConfigMap o Secret?
Sección titulada «¿env var, ConfigMap o Secret?»Kubernetes ofrece tres mecanismos para inyectar configuración a un pod. No son intercambiables — cada uno tiene un propósito distinto y elegir el incorrecto degrada la observabilidad, la seguridad o la mantenibilidad del servicio. El siguiente árbol de decisión es mandatorio:
| Criterio | env var directa (en el Deployment) | ConfigMap | K8s Secret (+ ExternalSecret) |
|---|---|---|---|
| Sensibilidad | Pública o interna (no sensible) | Pública o interna | Sensible (credenciales, tokens, PII) |
| Tamaño del valor | Pequeño (hasta ~1 KB) | Pequeño o mediano (hasta ~1 MB) | Pequeño a mediano |
| Frecuencia de cambio | Estable por entorno | Ocasional | Ocasional (rotación periódica) |
| Consumo multi-servicio | No (por servicio) | Sí (un ConfigMap para N deployments) | Sí (un Secret para N deployments del mismo namespace) |
| Forma de montaje | env: | envFrom: o volumeMounts | valueFrom.secretKeyRef o volumeMounts |
| Dónde vive el valor | En el manifiesto del Deployment | En un objeto ConfigMap del namespace | En GCP Secret Manager (via ExternalSecret) |
| Versionado | Git (manifiesto) | Git (manifiesto del ConfigMap) | GSM (versiones nativas del secreto) |
| Rotación | Requiere redeploy del manifiesto | Requiere apply + pod restart | Automática via ESO + pod restart |
Cuándo usar cada uno
Sección titulada «Cuándo usar cada uno»env var directa en el Deployment — usa esto para configuración específica del servicio que:
- NO es sensible
- Es relativamente estable pero puede variar entre entornos (DEV/QA/PRD)
- No se comparte con otros deployments
env: - name: OBS_LOG_LEVEL value: "info" - name: HTTP_TIMEOUT_MS value: "5000" - name: FEATURE_NEW_CHECKOUT_ENABLED value: "true"ConfigMap — usa esto cuando la configuración:
- NO es sensible
- Se comparte entre múltiples deployments del mismo namespace (ej. configuración de logging común a todo el producto)
- Es grande o estructurada (archivos de configuración YAML/JSON/properties)
- Cambia ocasionalmente sin requerir un redeploy del manifiesto del servicio
# ConfigMap compartido en el namespace tienda-herdezapiVersion: v1kind: ConfigMapmetadata: name: shared-observability-config namespace: tienda-herdezdata: log-level: "info" tracing-sampling-rate: "0.1" metrics-port: "9090"
---# Deployment que lo consumespec: containers: - name: app envFrom: - configMapRef: name: shared-observability-configK8s Secret + ExternalSecret — usa esto para cualquier valor sensible:
- Credenciales (DB, SMTP, API keys de terceros)
- Tokens de firma (JWT secrets, webhook secrets)
- Certificados TLS gestionados manualmente
- Cualquier cosa que requiera auditoría de acceso
Ver las secciones siguientes (Naming en GCP Secret Manager y Patrón ExternalSecret CRD) para el detalle operacional.
Naming en GCP Secret Manager
Sección titulada «Naming en GCP Secret Manager»Los nombres de secretos en GCP Secret Manager (GSM) son globalmente únicos dentro del GCP project. Como el project contiene N productos del mismo tenant/capability, el prefijo completo es obligatorio para evitar colisiones.
Formato
Sección titulada «Formato»<producto>-<deployment>-<resource-type>-<attribute>Reglas específicas de GSM
Sección titulada «Reglas específicas de GSM»- Kebab-case obligatorio — GSM solo acepta
[a-z0-9-]. El underscore y las mayúsculas no están permitidos. - Producto y deployment son los nombres exactos del K8s namespace y del K8s deployment respectivamente (ambos derivados de la taxonomía HERA).
- Resource-type agrupa secretos relacionados:
db-<lógico>,jwt,oauth-<provider>,payments,smtp, etc. - Attribute es el campo concreto:
password,secret,client-secret,api-key,token,private-key. - Longitud máxima 255 caracteres (límite de GSM), pero se recomienda no superar 100 para legibilidad.
Ejemplos
Sección titulada «Ejemplos»tienda-herdez-backend-checkout-service-db-users-hosttienda-herdez-backend-checkout-service-db-users-porttienda-herdez-backend-checkout-service-db-users-usernametienda-herdez-backend-checkout-service-db-users-passwordtienda-herdez-backend-checkout-service-db-users-name
tienda-herdez-backend-checkout-service-jwt-secrettienda-herdez-backend-checkout-service-payments-api-key
tienda-herdez-bff-mobile-app-oauth-google-client-secrettienda-herdez-bff-mobile-app-oauth-facebook-app-secret
sitio-donamaria-backend-forms-service-smtp-passwordsitio-barcel-frontend-web-store-cdn-api-keycms-corporativo-backend-strapi-admin-jwt-secretSeparación por entorno
Sección titulada «Separación por entorno»Cada entorno (DEV/QA/PRD) vive en un GCP project separado. Los nombres de los secretos no llevan prefijo de entorno porque el entorno es implícito en el GCP project:
ghdz-grupo-ext-sites-dev → tienda-herdez-backend-checkout-service-db-users-passwordghdz-grupo-ext-sites-qa → tienda-herdez-backend-checkout-service-db-users-passwordghdz-grupo-ext-sites-prd → tienda-herdez-backend-checkout-service-db-users-passwordMismo nombre lógico, valor distinto por entorno. El manifiesto K8s es idéntico entre entornos — cambia solo qué GCP project apunta el ClusterSecretStore.
Patrón ExternalSecret CRD
Sección titulada «Patrón ExternalSecret CRD»External Secrets Operator (ESO) es el mecanismo oficial HERA para sincronizar secretos de GCP Secret Manager a Kubernetes Secrets. Un ClusterSecretStore define la conexión al GCP project del entorno; los ExternalSecret declaran qué secretos sincronizar y cómo mapearlos.
Configuración del ClusterSecretStore (una vez por cluster)
Sección titulada «Configuración del ClusterSecretStore (una vez por cluster)»apiVersion: external-secrets.io/v1kind: ClusterSecretStoremetadata: name: gcp-secret-managerspec: provider: gcpsm: # Project que corresponde al entorno actual del cluster projectID: ghdz-grupo-ext-sites-prd auth: workloadIdentity: clusterLocation: us-central1 clusterName: <cluster-name> serviceAccountRef: name: external-secrets-sa namespace: external-secretsExternalSecret por servicio y por recurso
Sección titulada «ExternalSecret por servicio y por recurso»Un ExternalSecret sincroniza un único K8s Secret. Si un deployment necesita acceder a DB + JWT + OAuth Google, crea tres ExternalSecret:
# 1. Credenciales de base de datos "users"apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata: name: backend-checkout-service-db-users namespace: tienda-herdezspec: refreshInterval: 1h secretStoreRef: name: gcp-secret-manager kind: ClusterSecretStore target: name: backend-checkout-service-db-users # ← K8s Secret resultante creationPolicy: Owner data: - secretKey: host remoteRef: key: tienda-herdez-backend-checkout-service-db-users-host - secretKey: port remoteRef: key: tienda-herdez-backend-checkout-service-db-users-port - secretKey: username remoteRef: key: tienda-herdez-backend-checkout-service-db-users-username - secretKey: password remoteRef: key: tienda-herdez-backend-checkout-service-db-users-password - secretKey: name remoteRef: key: tienda-herdez-backend-checkout-service-db-users-name
---# 2. Firma JWT del servicioapiVersion: external-secrets.io/v1kind: ExternalSecretmetadata: name: backend-checkout-service-jwt namespace: tienda-herdezspec: refreshInterval: 1h secretStoreRef: name: gcp-secret-manager kind: ClusterSecretStore target: name: backend-checkout-service-jwt creationPolicy: Owner data: - secretKey: secret remoteRef: key: tienda-herdez-backend-checkout-service-jwt-secret
---# 3. OAuth Google (client secret sensible, client ID público)apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata: name: backend-checkout-service-oauth-google namespace: tienda-herdezspec: refreshInterval: 1h secretStoreRef: name: gcp-secret-manager kind: ClusterSecretStore target: name: backend-checkout-service-oauth-google creationPolicy: Owner data: - secretKey: client-secret remoteRef: key: tienda-herdez-backend-checkout-service-oauth-google-client-secretConsumo en el Deployment
Sección titulada «Consumo en el Deployment»El Deployment referencia los K8s Secrets creados por ESO via valueFrom.secretKeyRef:
apiVersion: apps/v1kind: Deploymentmetadata: name: backend-checkout-service namespace: tienda-herdezspec: template: spec: containers: - name: app image: <artifact-registry>/backend-checkout-service:<sha> env: # ── Database ── - name: DB_USERS_HOST valueFrom: secretKeyRef: { name: backend-checkout-service-db-users, key: host } - name: DB_USERS_PORT valueFrom: secretKeyRef: { name: backend-checkout-service-db-users, key: port } - name: DB_USERS_USERNAME valueFrom: secretKeyRef: { name: backend-checkout-service-db-users, key: username } - name: DB_USERS_PASSWORD valueFrom: secretKeyRef: { name: backend-checkout-service-db-users, key: password } - name: DB_USERS_NAME valueFrom: secretKeyRef: { name: backend-checkout-service-db-users, key: name }
# ── JWT ── - name: JWT_SECRET valueFrom: secretKeyRef: { name: backend-checkout-service-jwt, key: secret } - name: JWT_ISSUER value: "https://auth.cloudherdez.com" - name: JWT_AUDIENCE value: "tienda.clientes"
# ── OAuth ── - name: OAUTH_GOOGLE_CLIENT_ID value: "70096587311-xyz.apps.googleusercontent.com" - name: OAUTH_GOOGLE_CLIENT_SECRET valueFrom: secretKeyRef: { name: backend-checkout-service-oauth-google, key: client-secret }Helm Charts — cómo se aplica el estándar
Sección titulada «Helm Charts — cómo se aplica el estándar»HERA despliega sus servicios en GKE usando Helm charts. El estándar de env vars y secretos sigue siendo mandatorio dentro del chart — solo cambia dónde se parametriza cada capa: los valores estables por servicio viven en values.yaml, los valores que varían por entorno viven en values-<env>.yaml (overrides), y los secretos siguen siendo ExternalSecret referenciados desde el template del Deployment.
Estructura recomendada de un chart
Sección titulada «Estructura recomendada de un chart»charts/backend-checkout-service/├── Chart.yaml├── values.yaml # Valores por defecto + configuración inter-entorno estable├── values-dev.yaml # Overrides DEV (URLs, timeouts, feature flags)├── values-qa.yaml # Overrides QA├── values-prd.yaml # Overrides PRD└── templates/ ├── deployment.yaml # Consume .Values.env.* con el orden canónico R11 ├── service.yaml ├── external-secret.yaml # ExternalSecret CRDs (se mantiene el naming GSM) └── _helpers.tpl # Funciones reusables (labels, selectors)Patrón en values.yaml — agrupación canónica R11
Sección titulada «Patrón en values.yaml — agrupación canónica R11»Las env vars se estructuran por sección dentro de .Values.env, manteniendo el orden canónico del estándar. Esto hace que values-dev.yaml pueda sobrescribir solo las claves que cambian por entorno sin tocar el resto:
image: repository: ghdz-grupo-ext-sites-prd/backend-checkout-service tag: "" # sobrescrito por CI con el SHA del commit
env: runtime: NODE_ENV: "production" TZ: "America/Mexico_City" PORT: "3000"
database: # Los valores no-sensibles viven aqui; los sensibles vienen del Secret (ver abajo) DB_USERS_PORT: "5432" DB_USERS_NAME: "db_users"
cache: CACHE_SESSION_HOST: "redis.tienda-herdez.svc.cluster.local" CACHE_SESSION_PORT: "6379" CACHE_SESSION_TTL_MINUTES: "30"
messaging: MSG_PUBSUB_PROJECT_ID: "ghdz-grupo-ext-sites-prd" MSG_PUBSUB_TOPIC_ORDERS: "orders-events"
interService: USER_SERVICE_URL: "http://platform-auth-service.platform.svc.cluster.local" USER_SERVICE_TIMEOUT_MS: "3000" USER_SERVICE_MAX_RETRIES: "3" CATALOG_SERVICE_URL: "http://backend-catalog-service.tienda-herdez.svc.cluster.local" CATALOG_SERVICE_TIMEOUT_MS: "2000"
observability: OBS_LOG_LEVEL: "info" OBS_TRACING_ENABLED: "true" OBS_METRICS_PORT: "9090"
featureFlags: FEATURE_NEW_CHECKOUT_ENABLED: "true" FEATURE_LEGACY_PAYMENTS_ENABLED: "false"
# Nombres de los K8s Secrets que el ExternalSecret poblara.# Referenciados desde templates/deployment.yaml y templates/external-secret.yamlsecrets: dbUsers: k8sSecretName: backend-checkout-service-db-users gsmPrefix: tienda-herdez-backend-checkout-service-db-users jwt: k8sSecretName: backend-checkout-service-jwt gsmPrefix: tienda-herdez-backend-checkout-service-jwt oauthGoogle: k8sSecretName: backend-checkout-service-oauth-google gsmPrefix: tienda-herdez-backend-checkout-service-oauth-googleOverride por entorno
Sección titulada «Override por entorno»image: repository: ghdz-grupo-ext-sites-dev/backend-checkout-service
env: runtime: NODE_ENV: "development"
database: DB_USERS_NAME: "db_users_dev"
messaging: MSG_PUBSUB_PROJECT_ID: "ghdz-grupo-ext-sites-dev"
observability: OBS_LOG_LEVEL: "debug" OBS_TRACING_ENABLED: "false"
featureFlags: FEATURE_NEW_CHECKOUT_ENABLED: "true" FEATURE_LEGACY_PAYMENTS_ENABLED: "true" # permite validar el legacy en DEVTemplate del Deployment — iterar los grupos
Sección titulada «Template del Deployment — iterar los grupos»El template de deployment.yaml itera sobre los grupos de .Values.env preservando el orden canónico. El helper mezcla las env vars no-sensibles con las referencias a secrets:
apiVersion: apps/v1kind: Deploymentmetadata: name: backend-checkout-service namespace: {{ .Release.Namespace }}spec: template: spec: containers: - name: app image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" env: {{- /* ── Runtime ── */ -}} {{- range $k, $v := .Values.env.runtime }} - name: {{ $k }} value: {{ $v | quote }} {{- end }}
{{- /* ── Database (no-sensibles + secretKeyRef) ── */ -}} {{- range $k, $v := .Values.env.database }} - name: {{ $k }} value: {{ $v | quote }} {{- end }} - name: DB_USERS_HOST valueFrom: secretKeyRef: name: {{ .Values.secrets.dbUsers.k8sSecretName }} key: host - name: DB_USERS_USERNAME valueFrom: secretKeyRef: name: {{ .Values.secrets.dbUsers.k8sSecretName }} key: username - name: DB_USERS_PASSWORD valueFrom: secretKeyRef: name: {{ .Values.secrets.dbUsers.k8sSecretName }} key: password
{{- /* ── Cache ── */ -}} {{- range $k, $v := .Values.env.cache }} - name: {{ $k }} value: {{ $v | quote }} {{- end }}
{{- /* ── Inter-service ── */ -}} {{- range $k, $v := .Values.env.interService }} - name: {{ $k }} value: {{ $v | quote }} {{- end }}
{{- /* ── Observability ── */ -}} {{- range $k, $v := .Values.env.observability }} - name: {{ $k }} value: {{ $v | quote }} {{- end }}
{{- /* ── Feature flags ── */ -}} {{- range $k, $v := .Values.env.featureFlags }} - name: {{ $k }} value: {{ $v | quote }} {{- end }}ExternalSecret parametrizado
Sección titulada «ExternalSecret parametrizado»apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata: name: {{ .Values.secrets.dbUsers.k8sSecretName }} namespace: {{ .Release.Namespace }}spec: refreshInterval: 1h secretStoreRef: name: gcp-secret-manager kind: ClusterSecretStore target: name: {{ .Values.secrets.dbUsers.k8sSecretName }} creationPolicy: Owner data: - secretKey: host remoteRef: key: {{ .Values.secrets.dbUsers.gsmPrefix }}-host - secretKey: username remoteRef: key: {{ .Values.secrets.dbUsers.gsmPrefix }}-username - secretKey: password remoteRef: key: {{ .Values.secrets.dbUsers.gsmPrefix }}-passwordDespliegue por entorno
Sección titulada «Despliegue por entorno»# DEVhelm upgrade --install backend-checkout-service ./charts/backend-checkout-service \ --namespace tienda-herdez \ --values ./charts/backend-checkout-service/values.yaml \ --values ./charts/backend-checkout-service/values-dev.yaml \ --set image.tag=$CI_COMMIT_SHORT_SHA
# PRD (con aprobación)helm upgrade --install backend-checkout-service ./charts/backend-checkout-service \ --namespace tienda-herdez \ --values ./charts/backend-checkout-service/values.yaml \ --values ./charts/backend-checkout-service/values-prd.yaml \ --set image.tag=$CI_COMMIT_SHORT_SHACatálogo de Dominios Estándar
Sección titulada «Catálogo de Dominios Estándar»Los dominios (DOMAIN en el schema <DOMAIN>_<RESOURCE>_<ATTRIBUTE>) forman una lista cerrada. Agregar un nuevo dominio requiere aprobación del equipo de Arquitectura HERA.
| Dominio | Propósito | Ejemplos de variables |
|---|---|---|
DB | Bases de datos relacionales y NoSQL | DB_USERS_HOST, DB_CHECKOUT_PASSWORD, DB_CATALOG_NAME |
CACHE | Redis, Memcache, capas de cache | CACHE_SESSION_HOST, CACHE_SESSION_TTL_MINUTES |
JWT | Tokens JWT emitidos por el servicio | JWT_ISSUER, JWT_SECRET, JWT_AUDIENCE, JWT_TTL_MINUTES |
OAUTH | Proveedores OAuth/OIDC (client side) | OAUTH_GOOGLE_CLIENT_ID, OAUTH_FACEBOOK_APP_SECRET |
MSG | Messaging — PubSub, Kafka, RabbitMQ | MSG_PUBSUB_PROJECT_ID, MSG_PUBSUB_TOPIC_ORDERS |
BUCKET | Cloud Storage, S3-compatible | BUCKET_UPLOADS_NAME, BUCKET_UPLOADS_REGION |
SMTP | Envío de email | SMTP_HOST, SMTP_USERNAME, SMTP_PASSWORD |
OBS | Observabilidad — logs, traces, metrics | OBS_LOG_LEVEL, OBS_TRACING_ENABLED, OBS_METRICS_PORT |
FEATURE | Feature flags booleanos | FEATURE_NEW_CHECKOUT_ENABLED |
HTTP | Configuración del servidor HTTP | HTTP_MAX_RETRIES, HTTP_TIMEOUT_MS, HTTP_KEEPALIVE_SECONDS |
CORS | Cross-Origin Resource Sharing | CORS_ALLOWED_ORIGINS, CORS_ALLOWED_METHODS |
| (sin dominio) | Inter-servicio: <SERVICE>_SERVICE_URL | USER_SERVICE_URL, CATALOG_SERVICE_URL |
| (sin dominio) | APIs externas: <PROVIDER>_API_KEY/URL | PAYMENTS_API_KEY, STRIPE_API_URL |
Inter-comunicación de Microservicios
Sección titulada «Inter-comunicación de Microservicios»Cuando un servicio consume a otro (HTTP, gRPC, messaging), la variable sigue la regla R10 — nombrar por el servicio destino:
Dentro del mismo cluster (service discovery de K8s)
Sección titulada «Dentro del mismo cluster (service discovery de K8s)»# backend-checkout-service consume a platform-auth-serviceenv: - name: USER_SERVICE_URL value: "http://platform-auth-service.platform.svc.cluster.local"
- name: CATALOG_SERVICE_URL value: "http://backend-catalog-service.tienda-herdez.svc.cluster.local"
- name: PAYMENTS_SERVICE_URL value: "http://backend-payments-service.tienda-herdez.svc.cluster.local"Los DNS internos de K8s siguen el patrón <deployment>.<namespace>.svc.cluster.local. Para servicios del mismo namespace se puede abreviar a <deployment>, pero el estándar recomienda la forma completa para claridad.
Entre clusters o hacia servicios externos
Sección titulada «Entre clusters o hacia servicios externos»env: - name: APIHUB_ORDERS_SERVICE_URL value: "https://apihub.cloudherdez.com/orders/v1"
- name: INTEGRATIONHUB_SAP_SERVICE_URL value: "https://integrationhub.cloudherdez.com/sap/v2"
- name: PAYMENTS_SERVICE_URL value: "https://api.stripe.com/v1"Timeouts y reintentos obligatorios
Sección titulada «Timeouts y reintentos obligatorios»Toda variable *_SERVICE_URL debe acompañarse de timeouts y política de reintentos explícitos:
env: - name: USER_SERVICE_URL value: "http://platform-auth-service.platform.svc.cluster.local" - name: USER_SERVICE_TIMEOUT_MS value: "3000" - name: USER_SERVICE_MAX_RETRIES value: "3" - name: USER_SERVICE_RETRY_BACKOFF_MS value: "500"Un servicio que consume otro sin timeout explícito representa un riesgo operacional silencioso: el primer incidente transitorio en el servicio dependiente cascadea a todos sus consumidores.
Workers, Jobs y CronJobs
Sección titulada «Workers, Jobs y CronJobs»Todo el contenido anterior asume Deployment — servicios HTTP de larga vida con un Service asociado. Los workloads de tipo Job, CronJob y StatefulSet siguen exactamente las mismas reglas R1–R12, pero tienen tres diferencias operacionales que afectan cómo consumen secretos e env vars:
1. Lifecycle corto vs. ExternalSecret refresh
Sección titulada «1. Lifecycle corto vs. ExternalSecret refresh»El refreshInterval por defecto de un ExternalSecret es 1 hora. Un CronJob que corre cada 5 minutos puede leer el mismo K8s Secret durante muchas ejecuciones antes de que ESO reconcilie un cambio en GSM. En la práctica esto es aceptable (los secretos no rotan con frecuencia), pero si un operador rota un secreto en GSM y necesita que el siguiente job lo lea inmediatamente, debe forzar la resincronización:
# Forzar refresh inmediato del ExternalSecretkubectl annotate externalsecret backend-cleanup-job-db \ force-sync=$(date +%s) \ --overwrite \ -n tienda-herdez2. Sin Service asociado → URLs internas siguen funcionando
Sección titulada «2. Sin Service asociado → URLs internas siguen funcionando»Un Job/CronJob no expone puertos ni recibe tráfico, pero sí puede consumir servicios via *_SERVICE_URL. El DNS interno de K8s (<deployment>.<namespace>.svc.cluster.local) funciona igual dentro del pod del job. La regla R10 aplica idéntica: se nombra por el servicio destino.
# CronJob que consume el backend-catalog-serviceapiVersion: batch/v1kind: CronJobmetadata: name: worker-catalog-reindex namespace: tienda-herdezspec: schedule: "0 2 * * *" jobTemplate: spec: template: spec: restartPolicy: OnFailure containers: - name: reindexer image: <artifact-registry>/worker-catalog-reindex:<sha> env: # ── Inter-service ── - name: CATALOG_SERVICE_URL value: "http://backend-catalog-service.tienda-herdez.svc.cluster.local" - name: CATALOG_SERVICE_TIMEOUT_MS value: "30000" # jobs pueden permitirse timeouts más largos que APIs sincronas - name: CATALOG_SERVICE_MAX_RETRIES value: "5" # ── Database ── - name: DB_CATALOG_PASSWORD valueFrom: secretKeyRef: name: worker-catalog-reindex-db-catalog key: password3. Naming del job incluye frecuencia o propósito
Sección titulada «3. Naming del job incluye frecuencia o propósito»Los deployments siguen <tipo>-<detalle>. Para jobs y cronjobs, el <tipo> es worker (una de las 13 categorías oficiales del portal, ver Clasificación de Servicios) y el <detalle> describe la frecuencia o la función:
| Tipo | Ejemplo | Scope |
|---|---|---|
worker-<función> | worker-catalog-reindex, worker-loyalty-points | CronJob o consumer de queue de larga vida |
worker-<función>-<frecuencia> | worker-emails-hourly, worker-reports-daily | Opcional si la frecuencia distingue entre jobs con función similar |
El K8s Secret y el GSM prefix siguen el mismo patrón: worker-catalog-reindex-db-catalog en K8s, tienda-herdez-worker-catalog-reindex-db-catalog-password en GSM.
Validación de Variables de Entorno en el Código
Sección titulada «Validación de Variables de Entorno en el Código»El estándar regula cómo se nombran las variables, pero también requiere que el código aplicativo valide el parsing al arranque del servicio. Una variable mal configurada, mal tipada o ausente debe producir un error explícito antes de que el servicio empiece a procesar requests — y no un valor undefined que haga fallar el primer request en producción.
Cada equipo implementa la validación en el lenguaje del servicio usando la librería idiomática:
| Stack | Librería recomendada | Patrón |
|---|---|---|
| TypeScript / Node.js | envalid | Validación + tipado estático |
| Java / Spring Boot | @ConfigurationProperties + @Validated | Beans fuertemente tipados |
| Python | pydantic-settings | Model-based validation |
| Go | envconfig | Struct tags |
Ejemplo de referencia en TypeScript (stack principal HERA)
Sección titulada «Ejemplo de referencia en TypeScript (stack principal HERA)»// src/env.ts — Validación y tipado de env vars al arranque del servicio.// El servicio falla al iniciar si alguna variable requerida no esta presente// o no cumple el tipo declarado. Nunca usar process.env directamente en el resto// del codigo — siempre importar desde este modulo.
import { cleanEnv, str, num, bool, url, port } from 'envalid';
export const env = cleanEnv(process.env, { // ── Runtime ── NODE_ENV: str({ choices: ['development', 'qa', 'production'] }), TZ: str({ default: 'America/Mexico_City' }), PORT: port({ default: 3000 }),
// ── Database (users) ── DB_USERS_HOST: str(), DB_USERS_PORT: port({ default: 5432 }), DB_USERS_NAME: str(), DB_USERS_USERNAME: str(), DB_USERS_PASSWORD: str(), DB_USERS_TIMEOUT_MS: num({ default: 5000 }),
// ── JWT ── JWT_ISSUER: url(), JWT_AUDIENCE: str(), JWT_SECRET: str(), JWT_TTL_MINUTES: num({ default: 60 }),
// ── OAuth Google ── OAUTH_GOOGLE_CLIENT_ID: str(), OAUTH_GOOGLE_CLIENT_SECRET: str(), OAUTH_GOOGLE_REDIRECT_URI: url(),
// ── Inter-service ── USER_SERVICE_URL: url(), USER_SERVICE_TIMEOUT_MS: num({ default: 3000 }), USER_SERVICE_MAX_RETRIES: num({ default: 3 }),
CATALOG_SERVICE_URL: url(), CATALOG_SERVICE_TIMEOUT_MS: num({ default: 2000 }),
// ── Observability ── OBS_LOG_LEVEL: str({ choices: ['debug', 'info', 'warn', 'error'], default: 'info' }), OBS_TRACING_ENABLED: bool({ default: true }), OBS_METRICS_PORT: port({ default: 9090 }),
// ── Feature flags ── FEATURE_NEW_CHECKOUT_ENABLED: bool({ default: false }), FEATURE_LEGACY_PAYMENTS_ENABLED: bool({ default: false }),});
// Resto del codigo importa desde aqui — nunca process.env directamente// import { env } from './env';// const host = env.DB_USERS_HOST; // tipo: string, garantizado no-undefinedEste patrón da tres ventajas inmediatas:
- Fail-fast al arrancar — si falta
DB_USERS_PASSWORD, el proceso termina con un error claro en vez de fallar en el primer query a la base de datos. - Tipado estático —
env.DB_USERS_PORTesnumber, nostring | undefined. El compilador TypeScript garantiza que el código no useparseInten todos lados. - Defaults explícitos — los valores opcionales declaran su fallback en un solo lugar, no repartido por el código.
Equivalente en Java/Spring Boot
Sección titulada «Equivalente en Java/Spring Boot»// application.yaml (o application.properties)// db.users.host: ${DB_USERS_HOST}// db.users.port: ${DB_USERS_PORT:5432}// db.users.password: ${DB_USERS_PASSWORD}
@ConfigurationProperties(prefix = "db.users")@Validatedpublic class DbUsersProperties { @NotBlank private String host; @Min(1) @Max(65535) private int port = 5432; @NotBlank private String name; @NotBlank private String username; @NotBlank private String password; @Min(1000) private int timeoutMs = 5000; // getters/setters}Spring falla al iniciar con un error de validación descriptivo si cualquier campo @NotBlank viene vacío.
Equivalente en Python (pydantic-settings)
Sección titulada «Equivalente en Python (pydantic-settings)»from pydantic_settings import BaseSettingsfrom pydantic import HttpUrl, PositiveInt
class Settings(BaseSettings): db_users_host: str db_users_port: PositiveInt = 5432 db_users_name: str db_users_username: str db_users_password: str db_users_timeout_ms: PositiveInt = 5000
jwt_issuer: HttpUrl jwt_secret: str
user_service_url: HttpUrl user_service_timeout_ms: PositiveInt = 3000
obs_log_level: str = "info" obs_tracing_enabled: bool = True
model_config = {"case_sensitive": False}
settings = Settings() # falla al importar si falta algo requeridoDesarrollo Local
Sección titulada «Desarrollo Local»Los servicios HERA corren en producción con secretos servidos por GCP Secret Manager vía External Secrets Operator. Para desarrollo local — donde el developer no tiene (ni debe tener) credenciales de producción — el flujo recomendado es docker-compose con un .env.local ignorado por git:
Estructura de archivos del repo
Sección titulada «Estructura de archivos del repo»backend-checkout-service/├── .env.example # Plantilla versionada, todos los nombres de vars con placeholders├── .env.local # ← IGNORADO por git, valores reales para dev local├── .gitignore # Contiene .env.local, .env.*.local, .env├── docker-compose.yml # Levanta el servicio + dependencias locales (DB, Redis, etc.)├── Dockerfile└── src/ └── env.ts # Validacion con envalid (ver seccion anterior).env.example — plantilla versionada
Sección titulada «.env.example — plantilla versionada»# === Runtime ===NODE_ENV=developmentTZ=America/Mexico_CityPORT=3000
# === Database (users) ===# Para dev local: apunta al Postgres del docker-composeDB_USERS_HOST=postgresDB_USERS_PORT=5432DB_USERS_NAME=db_users_localDB_USERS_USERNAME=devDB_USERS_PASSWORD=<rellenar en .env.local>
# === JWT ===JWT_ISSUER=http://localhost:3000JWT_AUDIENCE=local.clientesJWT_SECRET=<rellenar en .env.local — usar valor aleatorio de 32+ chars>
# === Inter-service ===USER_SERVICE_URL=http://host.docker.internal:3001USER_SERVICE_TIMEOUT_MS=3000USER_SERVICE_MAX_RETRIES=3
# === Observability ===OBS_LOG_LEVEL=debugOBS_TRACING_ENABLED=falseOBS_METRICS_PORT=9090
# === Feature flags ===FEATURE_NEW_CHECKOUT_ENABLED=trueFEATURE_LEGACY_PAYMENTS_ENABLED=true.gitignore — proteger secretos
Sección titulada «.gitignore — proteger secretos»# Env files con valores reales.env.env.local.env.*.localdocker-compose.yml — servicio + dependencias
Sección titulada «docker-compose.yml — servicio + dependencias»version: '3.9'
services: app: build: . env_file: - .env.local # ← lee las vars desde aqui, no las declara inline ports: - "3000:3000" depends_on: - postgres
postgres: image: postgres:16-alpine environment: POSTGRES_DB: db_users_local POSTGRES_USER: dev POSTGRES_PASSWORD: dev # solo para local, nunca en prod ports: - "5432:5432" volumes: - postgres-data:/var/lib/postgresql/data
volumes: postgres-data:Flujo para el developer
Sección titulada «Flujo para el developer»# 1. Clonar el repogit clone <repo-url>cd backend-checkout-service
# 2. Copiar la plantilla y rellenarlacp .env.example .env.local$EDITOR .env.local # rellena DB_USERS_PASSWORD, JWT_SECRET, etc.
# 3. Levantar el stackdocker compose up -d
# 4. Verificar que el servicio arrancó (la validación de env.ts pasó)curl http://localhost:3000/healthExcepciones Permitidas
Sección titulada «Excepciones Permitidas»El estándar aplica a todo código escrito para HERA. Frameworks y herramientas de terceros que tienen convenciones propias establecidas se mantienen intactas dentro de su scope:
| Excepción | Ámbito permitido | Razón |
|---|---|---|
NEXT_PUBLIC_* | Solo dentro de servicios Next.js | Next.js las inyecta al bundle cliente — renombrarlas rompe el build |
VITE_* | Solo dentro de servicios Vite | Vite las expone al cliente via import.meta.env |
REACT_APP_* | Solo dentro de servicios Create React App (legacy) | Convención de CRA, aún en uso en componentes legacy |
ASTRO_* | Solo dentro de servicios Astro (como este portal) | Astro las reserva internamente |
STRAPI_*, STRAPI_HEALTH_CHECK_* | Solo dentro de instancias Strapi CMS | Strapi las documenta como configuración oficial |
DATABASE_URL, REDIS_URL | Formato connection string oficial | 12-Factor App las codifica como estándar — se permiten pero se prefiere el schema HERA descompuesto (DB_*_HOST, DB_*_PORT, etc.) |
NODE_ENV, TZ, PORT, HOST, HOME, PATH, PWD | Todos los servicios | Estándar POSIX/Node — se heredan sin renombrar |
KUBERNETES_* | Inyectadas automáticamente por K8s | El kubelet las inyecta, no son configurables |
GOOGLE_APPLICATION_CREDENTIALS, GOOGLE_CLOUD_PROJECT | Servicios que usan GCP SDKs | Estándar Google Cloud SDK — se mantiene |
Ejemplos Antes / Después
Sección titulada «Ejemplos Antes / Después»Casos reales detectados en manifiestos del grupo, transformados al estándar HERA:
Ejemplo 1 — Credenciales de base de datos
Sección titulada «Ejemplo 1 — Credenciales de base de datos»env:- name: DB_HOST value: "10.95.128.78"- name: DB_NAME value: "db_tensai_hoy_toca_2025"- name: DB_USER value: "usr_tensai_hoytoca2025"- name: DB_PASS value: "MiP4ssw0rd!2025"# ── Database ──- name: DB_USERS_HOST valueFrom: secretKeyRef: { name: backend-users-service-db-users, key: host }- name: DB_USERS_PORT valueFrom: secretKeyRef: { name: backend-users-service-db-users, key: port }- name: DB_USERS_USERNAME valueFrom: secretKeyRef: { name: backend-users-service-db-users, key: username }- name: DB_USERS_PASSWORD valueFrom: secretKeyRef: { name: backend-users-service-db-users, key: password }- name: DB_USERS_NAME valueFrom: secretKeyRef: { name: backend-users-service-db-users, key: name }Cambios aplicados: R4 (secretos fuera de value:), R5 (sufijos explícitos _USERNAME en vez de _USER, _PASSWORD en vez de _PASS), R11 (sección Database explícita), añadido _PORT que faltaba.
Ejemplo 2 — JWT con guiones (bug POSIX latente)
Sección titulada «Ejemplo 2 — JWT con guiones (bug POSIX latente)»env:- name: RECIPE-JWT-SECRET value: "super-secret-jwt-key"- name: RECIPE-JWT-ISSUER value: "hoyToca-api-auth"- name: RECIPE-JWT-AUDIENCE value: "hoytoca-clientes"# ── Auth / Identity ──- name: JWT_ISSUER value: "https://auth.cloudherdez.com"- name: JWT_AUDIENCE value: "hoy-toca.clientes"- name: JWT_SECRET valueFrom: secretKeyRef: { name: backend-users-service-jwt, key: secret }Cambios aplicados: R1/R2 (SCREAMING_SNAKE_CASE sin guiones), R4 (secreto fuera de value:), eliminado el prefijo RECIPE- que no correspondía al valor (hoyToca-api-auth), URL completa con esquema en JWT_ISSUER (R9).
Ejemplo 3 — OAuth Google con secretos filtrados
Sección titulada «Ejemplo 3 — OAuth Google con secretos filtrados»env:- name: AUTH-GOOGLE-CLIENT-ID value: "70096587311-0ndm012ctrkh3o0590bsulj879aistts.apps.googleusercontent.com"- name: AUTH-GOOGLE-CLIENTSECRET value: "MiP4ssw0rd!2025"- name: AUTH-GOOGLE-CLIENT-REDIRECT-URI value: "https://..."- name: OAUTH_GOOGLE_CLIENT_ID value: "70096587311-0ndm012ctrkh3o0590bsulj879aistts.apps.googleusercontent.com"- name: OAUTH_GOOGLE_CLIENT_SECRET valueFrom: secretKeyRef: { name: backend-users-service-oauth-google, key: client-secret }- name: OAUTH_GOOGLE_REDIRECT_URI value: "https://users.cloudherdez.com/api/auth/google/callback"Cambios aplicados: R1 (SCREAMING_SNAKE_CASE), R2 (sin guiones), R4 (client secret via secretKeyRef), R5 (_CLIENT_SECRET en vez de _CLIENTSECRET), R9 (URL completa).
Ejemplo 4 — Lista de orígenes CORS con separador incorrecto
Sección titulada «Ejemplo 4 — Lista de orígenes CORS con separador incorrecto»env:- name: ALLOWED_ORIGINS value: "https://a.dev.cloudherdez.com;https://b.dev.cloudherdez.com;http://c.dev.cloudherdez.com"# ── CORS / Security ──- name: CORS_ALLOWED_ORIGINS value: "https://a.dev.cloudherdez.com,https://b.dev.cloudherdez.com,https://c.dev.cloudherdez.com"Cambios aplicados: R8 (separador , estándar CORS en vez de ;), añadido dominio CORS_ para agrupar, eliminada mezcla http/https (ahora todos https).
Ejemplo 5 — Messaging con secretos y URLs mezcladas
Sección titulada «Ejemplo 5 — Messaging con secretos y URLs mezcladas»env:- name: PROJECT_ID value: "ghdz-grupo-ext-sites-dev"- name: TOPIC_ID value: "mail-send"- name: AUTH-PUBSUB-TOPIC value: "mail-send"# ── Messaging ──- name: MSG_PUBSUB_PROJECT_ID value: "ghdz-grupo-ext-sites-dev"- name: MSG_PUBSUB_TOPIC_MAIL value: "mail-send"Cambios aplicados: prefijo MSG_PUBSUB_ para agrupar, eliminado duplicado, R11 (sección Messaging explícita).
Checklist de Cumplimiento — Code Review
Sección titulada «Checklist de Cumplimiento — Code Review»Esta checklist es obligatoria en todo MR que toque manifiestos K8s, ConfigMaps, o archivos .env.*. Todo item debe estar marcado ✅ antes del merge a develop.
Nomenclatura
Sección titulada «Nomenclatura»- Toda env var cumple
SCREAMING_SNAKE_CASEestricto (R1) - Ninguna env var contiene guiones
-(R2) - Ninguna env var excede 63 caracteres (R3)
- Toda env var termina en uno de los 18 sufijos del catálogo cerrado (R5)
- Las duraciones incluyen unidad en el nombre (
_MS,_SECONDS,_MINUTES) (R7) - Las URLs incluyen esquema
https://y no tienen trailing slash (R9) - Las variables inter-servicio llevan el nombre del servicio destino, no del consumidor (R10)
- Ningún prefijo de framework (
NEXT_PUBLIC_*,VITE_*, etc.) aparece fuera de su scope (R12)
Secretos
Sección titulada «Secretos»- Ningún secreto aparece en
value:— todos víavalueFrom.secretKeyRef(R4) - Existe un
ExternalSecretpor cada K8s Secret referenciado, conremoteRef.keyen GSM - El nombre del K8s Secret sigue el patrón
<deployment>-<resource-type> - El nombre del secreto en GSM sigue el patrón
<producto>-<deployment>-<resource-type>-<attribute>en kebab-case - Ningún
.env,.env.production,.env.prdestá siendo commitado (verificar.gitignore)
Formato de valores
Sección titulada «Formato de valores»- Booleanos son literal
true/false(lowercase) (R6) - Listas usan separador
,(coma), no;(R8) - Las variables están agrupadas en secciones con el orden canónico
Runtime → Database → Cache → Messaging → Storage → Auth → External APIs → Inter-service → Observability → Feature Flags → CORS / Security(R11)
Inter-servicio
Sección titulada «Inter-servicio»- Toda variable
*_SERVICE_URLva acompañada de*_TIMEOUT_MSy*_MAX_RETRIESexplícitos - Los servicios del mismo cluster usan DNS interno
<deployment>.<namespace>.svc.cluster.local - Los servicios externos usan URL pública completa con
https://
Documentación
Sección titulada «Documentación»- Toda variable nueva está documentada en el
README.mddel servicio con: nombre, tipo, obligatoria/opcional, valor por defecto, ejemplo - El
.env.exampledel repo lista todas las variables sin valores reales (usar placeholders<...>)
Integración con el Ecosistema HERA
Sección titulada «Integración con el Ecosistema HERA»Portal HERA (este sitio)
Sección titulada «Portal HERA (este sitio)»Este portal mismo aplica el estándar. El objeto ECOSYSTEM_LINKS en astro.config.mjs consume variables como:
// Carga .env al process.env usando el built-in de Node 20.6+ (zero-deps).try { process.loadEnvFile('.env');} catch { // .env ausente o Node < 20.6: fallbacks hardcoded se aplican}
const ECOSYSTEM_LINKS = { calcK8s: process.env.CALC_K8S_SERVICE_URL ?? 'http://localhost:5173/', // Futuras herramientas: añadir aquí + en .env.example};Siguiendo R10 (nombrar por servicio destino): CALC_K8S_SERVICE_URL apunta al servicio Calculadora K8s. Sin prefijo HERA_ porque el portal consume el servicio — el consumidor no debe calificarse a sí mismo.
Inicialización de un servicio nuevo
Sección titulada «Inicialización de un servicio nuevo»Cuando un equipo inicia un nuevo servicio, los entregables obligatorios del primer MR incluyen:
src/env.ts(o equivalente) — tipado/validación de las env vars (ej. con envalid).env.example— plantilla con todas las variables del servicio, sin valores realesk8s/deployment.yaml— manifiesto con agrupación R11k8s/external-secrets.yaml— ExternalSecrets por recursoREADME.md— sección Configuración con tabla de variables
Rotación de secretos
Sección titulada «Rotación de secretos»El flujo de rotación es:
- Operador genera el nuevo valor en GCP Secret Manager (nueva versión del secreto existente)
- ESO resincroniza en la próxima ventana (o se fuerza via kubectl)
- El K8s Secret se actualiza
- Los pods del deployment se reinician (manualmente o via argo rollout)
- Los pods leen el nuevo valor de la env var al arrancar
Ningún código de aplicación contiene lógica de reloading de secretos — es responsabilidad del runtime (K8s + ESO).
Referencias
Sección titulada «Referencias»Internas
Sección titulada «Internas»- Estructura de Repositorios — taxonomía HERA fuente de verdad
- Clasificación de Servicios — 13 categorías oficiales + naming
<tipo>-<detalle> - Gestión de Secretos — políticas de seguridad y rotación
- GKE Deep Dive — configuración del cluster + ESO
- GitLab Workflow — flujo de ambientes DEV/QA/PRD
Externas
Sección titulada «Externas»- The Twelve-Factor App — Config
- POSIX IEEE Std 1003.1 — Environment Variables
- OWASP ASVS 4.0 — V10 Malicious Code
- External Secrets Operator Documentation
- Google Secret Manager — Best Practices
- Kubernetes Documentation — Secrets
Evolución del Estándar
Sección titulada «Evolución del Estándar»| Versión | Fecha | Cambio |
|---|---|---|
| 1.0 | 2026-04-13 | Versión inicial — R1 a R12, 18 sufijos, 10 dominios, modelo Context-Aware Naming de 5 capas |
Este estándar es la primera pieza de una serie más amplia de convenciones de configuración y naming del ecosistema HERA. Las páginas hermanas, que complementan sin contradecir lo establecido aquí, son:
- Estándares de Etiquetado (K8s Labels) — mapeo formal desde
product-manifest.ymlaapp.kubernetes.io/*y labels customhera.grupoherdez.com/* - Estándares de Nombramiento en GCP — Service Accounts, Cloud Storage buckets, Artifact Registry, Cloud SQL, Pub/Sub