Capa de interoperabilidad · Prueba técnica de arquitectura

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.

  1. 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 de docs/to-be/05-modelo-de-lectura.md.
  2. 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 usuario default queda apagado.
  3. 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.
  4. 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.
  5. Lecturas por lote: en modo clúster, un MGET de 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

Qué se sacrifica

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:

  1. Secretos: el catálogo de OpenTofu ya los define (infra/tofu/modules/catalogo/main.tf, redis-pricing y redis-inventory): redis_password en pricing-service e inventory-service, y redis_pricing-service_password y redis_inventory-service_password en Redis. Falta montarlos: redis_password en keys de platform/k8s/values/pricing-service.yaml e inventory-service.yaml, y en Redis las dos claves más una del usuario healthcheck, que el catálogo todavía no tiene (redis_healthcheck_password, o el healthcheck con otro mecanismo).
  2. Redis (platform/k8s/values/redis.yaml): un initContainer llamado acl (es el que busca platform/k8s/scripts/check-catalog.mjs) que genere el archivo de usuarios desde platform/redis/users.acl.tmpl con el SHA-256 de cada clave, como scripts/secrets.d/redis-acl.sh (los marcadores se llaman como los secretos de compose: redis_pricing_password y redis_inventory_password; en el clúster se leen de los archivos con los nombres del catálogo), redis-server --aclfile y el healthcheck con el usuario healthcheck.
  3. Al encender la ACL, el aviso de platform/k8s/scripts/check-catalog.mjs deja de aplicar.