ADR-023: cada servicio tiene su espacio de llaves en Redis, con prefijo y usuario propio
Aceptado el 11/11/2026 por el agente, con aviso a Andrés (`docs/plan.md`, "Qué puede decidir el agente"). Complementa ADR-019 (las dos llaves `product:{sku}` de pricing-service e inventory-service). Implementado en compose y en los servicios el 15/11/2026; pendiente en EKS (ver la nota de implementación).
Contexto
La regla 6 de CLAUDE.md dice "una base por servicio, sin tablas compartidas". Para PostgreSQL está resuelto. Para Redis, el diseño no lo dice.
ADR-019 hizo que pricing-service e inventory-service guarden los dos product:{sku} (docs/to-be/05-modelo-de-lectura.md), cada uno con su propio formato. En compose comparten una instancia: en la misma base lógica, cada uno pisaría la llave del otro. El carril f3-surtido lo evitó en local con la base lógica 1 para inventory. Pero en la nube (docs/plataforma/04-stack.md: Amazon ElastiCache) un Redis en modo clúster solo admite la base 0, así que ese arreglo no sirve allá.
La práctica habitual con un Redis compartido es un prefijo de llave por dueño más control de acceso por patrón de llave. Redis lo trae desde la versión 6 (ACL con ~prefijo:*) y ElastiCache lo ofrece con sus usuarios y cadenas de acceso.
Opciones
| Opción | A favor | En contra |
|---|---|---|
A. Una instancia compartida; prefijo {servicio}: en toda llave y un usuario de Redis por servicio que solo accede a su prefijo |
Cumple la regla 6: ningún servicio lee ni pisa llaves de otro, y Redis lo impide, no solo la convención; una sola instancia que operar y pagar; funciona en modo clúster | Un servicio ruidoso afecta la latencia de los demás; hay que dar de alta un usuario por servicio |
| B. Una instancia por servicio | Aislamiento total, también de rendimiento | Doble costo y operación para dos servicios de lectura con datos pequeños |
| C. Bases lógicas numeradas por servicio | Ya funciona en local | ElastiCache en modo clúster solo tiene la base 0; sin control de acceso por base |
| D. Llaves sin prefijo y formato acordado entre servicios | Sin trabajo | Es una tabla compartida en Redis: rompe la regla 6 y acopla los formatos |
Decisión
Opción A.
- Prefijo: toda llave lleva el nombre del servicio:
pricing:price:{sku}:{channel}:{priceList},pricing:product:{sku},inventory:stock:{sku}:{locationId},inventory:product:{sku}. El resto de la llave es el dedocs/to-be/05-modelo-de-lectura.md. - Usuario por servicio: cada servicio entra con su propio usuario de Redis, cuya cadena de acceso solo permite su prefijo (
~pricing:*,~inventory:*) y los comandos que usa. La contraseña es un secreto por carga, leído de archivo (regla 8, ADR-009). La lista exacta de comandos por usuario vive en un solo archivo versionado,platform/redis/users.acl.tmpl, con marcadores para las contraseñas. Lo usan compose y el clúster, y la prueba de permisos lo valida. El usuariodefaultqueda apagado. - Local: compose crea los usuarios con un archivo de ACL generado por
scripts/secrets.sh, y se retira la base lógica 1 de inventory. - Nube: el demo usa Redis dentro del clúster, no ElastiCache, por costo (carril
infra, ADR-022). La ACL se monta como archivo, y cada contraseña vive en el secreto de su carga (redis_password) y en el de Redis. En producción, con ElastiCache, son usuarios y un grupo de usuarios con la misma cadena de acceso. - Lecturas por lote: en modo clúster, un
MGETde llaves de ranuras distintas falla. Las lecturas por lote usan un cliente que reparte por ranura (Lettuce en modo clúster, el de Spring Data Redis) o una tubería. Las operaciones de escritura son de una sola llave y no cambian.
Verificación
- Prueba de cada servicio: con su usuario, escribir o leer una llave del otro prefijo falla con error de permiso.
make f3-e2een verde con los usuarios activos.- Una prueba de punta a punta con Redis en modo clúster queda para F7.
Qué se sacrifica
- Llaves más largas, unos bytes por llave.
- Un usuario y un secreto más por servicio que usa Redis.
- Sin aislamiento de rendimiento: si un servicio satura Redis, el otro lo nota. Si pasa, la salida es la opción B para ese servicio.
Nota de implementación (15/11/2026)
Aplicado en el código y en compose por el carril pagos-y-adaptadores (hallazgo R10 de docs/revision-final.md). La decisión no cambia. Lo aplicado cubre los dos servicios, compose y la plantilla platform/redis/users.acl.tmpl. Detalles que fijó la aplicación: la clave de cada servicio es el secreto redis_password de su carga; si el archivo no existe, el servicio no se autentica (así siguen funcionando kind y EKS mientras Redis tenga la ACL apagada); además de los usuarios de servicio hay un usuario healthcheck que solo puede PING; TTL está permitido para que las pruebas lean el TTL de seguridad. En Kubernetes falta, para el carril que lo tome:
- Secretos: el catálogo de OpenTofu ya los define (
infra/tofu/modules/catalogo/main.tf,redis-pricingyredis-inventory):redis_passworden pricing-service e inventory-service, yredis_pricing-service_passwordyredis_inventory-service_passworden Redis. Falta montarlos:redis_passwordenkeysdeplatform/k8s/values/pricing-service.yamleinventory-service.yaml, y en Redis las dos claves más una del usuariohealthcheck, que el catálogo todavía no tiene (redis_healthcheck_password, o el healthcheck con otro mecanismo). - Redis (
platform/k8s/values/redis.yaml): uninitContainerllamadoacl(es el que buscaplatform/k8s/scripts/check-catalog.mjs) que genere el archivo de usuarios desdeplatform/redis/users.acl.tmplcon el SHA-256 de cada clave, comoscripts/secrets.d/redis-acl.sh(los marcadores se llaman como los secretos de compose:redis_pricing_passwordyredis_inventory_password; en el clúster se leen de los archivos con los nombres del catálogo),redis-server --aclfiley el healthcheck con el usuariohealthcheck. - Al encender la ACL, el aviso de
platform/k8s/scripts/check-catalog.mjsdeja de aplicar.