Queda la última de las cuatro cargas que diagnosticamos en 06-01, y es la más absurda de todas. El catálogo de MercadoFresco recibe 4.100 consultas por minuto en hora punta y, según las trazas de X-Ray de 05-02, el 94 % devuelven exactamente el mismo resultado que la anterior. Los precios y las descripciones cambian una vez al día, a las 06:00, cuando llega la carga de la lonja.

Aurora resuelve cada una de esas consultas correctamente en unos 6 milisegundos. El problema no es que sea lenta: es que hacerlo 4.100 veces por minuto para no aportar ninguna información nueva es trabajo desperdiciado, y ese desperdicio se paga tres veces —en latencia para el cliente, en capacidad de Aurora que hay que pagar aunque no produzca nada, y en riesgo, porque esa carga constante deja al clúster sin margen justo cuando llega el pico del viernes—.

Amazon ElastiCache es el servicio de caché en memoria gestionado de AWS, compatible con Redis (y su bifurcación de código abierto Valkey) y con Memcached. Aquí el catálogo se sirve desde memoria en microsegundos, las sesiones dejan de necesitar sesiones pegajosas en el ALB —el cabo suelto que 03-03 dejó abierto— y el módulo se cierra con la capa de datos de MercadoFresco completa.

Aviso de coste. Un nodo de ElastiCache se paga por hora exista o no tráfico, igual que una instancia EC2. Un cache.r7g.large olvidado cuesta unos 130 USD al mes. Borra los grupos de réplica que crees para practicar. Todos los datos son ficticios.

Contenido

  1. Por qué cachear, y qué no se debe cachear
  2. Redis frente a Memcached
  3. Arquitectura: nodos, grupos de réplica y modo clúster
  4. Particionamiento, ranuras hash, conmutación por error y puntos de enlace
  5. Patrones de caché: cache-aside, write-through, write-behind y read-through
  6. El catálogo de MercadoFresco con cache-aside
  7. TTL, invalidación y coherencia
  8. Estampida de caché y claves calientes
  9. Estructuras de Redis aplicadas a la tienda
  10. Contadores atómicos y el límite del stock
  11. Sesiones en Redis: adiós a las sesiones pegajosas
  12. Seguridad: red, cifrado y control de acceso
  13. Métricas, alarmas y la medición del antes y el después
  14. MemoryDB y DAX: cuándo no es ElastiCache
  15. Costes y ahorro medido
  16. Errores comunes y consejos
  17. Ejercicios
  18. La capa de datos completa de MercadoFresco
  19. Conclusión

Por qué cachear, y qué no se debe cachear

Una caché guarda en memoria el resultado de una operación cara para no repetirla. Aporta tres cosas distintas y conviene separarlas:

Beneficio Antes Después
Latencia 6 ms por consulta a Aurora 0,3 ms desde memoria
Coste por consulta Capacidad de Aurora (ACU) Nodo fijo, coste marginal casi nulo
Protección Todo el tráfico llega a la base de datos La base solo ve los fallos de caché

El tercero es el que más se subestima. Una caché con un 95 % de aciertos convierte 4.100 consultas por minuto en 205: la base de datos deja de estar al límite y recupera el margen que necesita para el pico.

Qué se debe cachear, en orden de rentabilidad: datos que se leen mucho más de lo que se escriben (el catálogo, 4.100:1), resultados de cálculos caros, datos que toleran estar ligeramente desactualizados y objetos que se reconstruyen a partir de varias fuentes. Qué no se debe cachear: datos que cambian en cada lectura, datos cuya exactitud momentánea es crítica —el stock al cobrar—, datos que solo se leen una vez (una búsqueda con filtros muy específicos que nadie repetirá) y datos personales sensibles sin una razón clara, porque una caché es una copia más que proteger y que borrar bajo el RGPD.

Redis frente a Memcached

Redis / Valkey Memcached
Estructuras de datos Cadenas, hashes, listas, conjuntos, conjuntos ordenados, flujos Solo cadenas
Persistencia Sí (instantáneas y AOF) No
Réplicas Sí, con conmutación automática No
Alta disponibilidad Multi-AZ con conmutación Ninguna
Particionamiento Nativo, con modo clúster En el cliente
Operaciones atómicas Muchas (INCR, ZADD, transacciones, scripts Lua) Pocas
Publicación y suscripción No
Modelo de hilos Un hilo por núcleo lógico (mayormente monohilo) Multihilo
Casos ideales Casi todos Caché pura, objetos grandes, multihilo

La elección para MercadoFresco es Redis, por tres razones concretas y no por popularidad. Necesitamos estructuras y no solo cadenas: conjuntos ordenados para el ranking del viernes, hashes para el carrito rápido, contadores atómicos. Necesitamos alta disponibilidad: si la caché de sesiones cae, todos los clientes pierden la sesión a la vez, y con Memcached no hay réplica posible. Y necesitamos operaciones atómicas para los contadores, que en Memcached son limitadas.

Memcached sigue teniendo su nicho —caché puramente volátil, muy simple, con objetos grandes y donde el multihilo aporta más rendimiento por nodo—, pero no es este caso. Sobre Valkey: es la bifurcación de código abierto de Redis tras su cambio de licencia, compatible a nivel de protocolo, con soporte de ElastiCache y a menor precio. Para un despliegue nuevo es la opción por defecto razonable, y lo que se explica aquí aplica igual.

Arquitectura: nodos, grupos de réplica y modo clúster

Un nodo es la unidad mínima: una instancia con memoria y un proceso de Redis. Un fragmento (shard) es un nodo primario más de 0 a 5 réplicas con los mismos datos. Y un grupo de réplica es el conjunto de fragmentos que forman el despliegue. De ahí las dos topologías posibles:

graph TD
    subgraph MCD["Modo clúster DESACTIVADO · 1 fragmento"]
        P1["Primario<br/>todo el conjunto de datos"] --> R1["Réplica AZ b"]
        P1 --> R2["Réplica AZ a"]
    end
    subgraph MCA["Modo clúster ACTIVADO · 3 fragmentos"]
        F1["Fragmento 1<br/>ranuras 0-5460"] --> FR1["Réplica"]
        F2["Fragmento 2<br/>ranuras 5461-10922"] --> FR2["Réplica"]
        F3["Fragmento 3<br/>ranuras 10923-16383"] --> FR3["Réplica"]
    end
Modo clúster desactivado Modo clúster activado
Fragmentos 1 Hasta 500
Límite de datos La memoria de un nodo La suma de todos los fragmentos
Escalado Vertical (nodo mayor) Horizontal (más fragmentos)
Cliente Cualquiera Debe soportar el protocolo de clúster
Operaciones multiclave Sin restricción Solo dentro del mismo fragmento

MercadoFresco empieza con el modo clúster desactivado. El catálogo son 1,2 GB y las sesiones no llegan a 2 GB: todo cabe holgadamente en un nodo cache.t4g.medium de 3,09 GiB, con una réplica en la otra AZ. Activar el modo clúster añadiría complejidad —restricciones en operaciones multiclave, cliente compatible— sin resolver ningún problema que hoy exista. Es exactamente el criterio de 06-01: no añadas complejidad que no puedas justificar con una medición.

Particionamiento, ranuras hash, conmutación por error y puntos de enlace

Con el modo clúster activado, Redis divide el espacio de claves en 16.384 ranuras hash. Cada clave se asigna a una ranura calculando CRC16(clave) mod 16384, y cada fragmento posee un rango de ranuras. Añadir un fragmento redistribuye ranuras sin parar el servicio.

Las etiquetas hash fuerzan que varias claves caigan en la misma ranura poniendo entre llaves la parte que se usa para el cálculo: carrito:{4471}:lineas y carrito:{4471}:total van juntas porque ambas hashean solo 4471. Es lo que permite operar sobre varias claves relacionadas en un clúster particionado.

Multi-AZ con conmutación automática promueve una réplica si el primario falla, en 15-60 segundos y actualizando el punto de enlace principal solo. Hay que configurarlo explícitamente y tener claro qué implica: si la persistencia no está activada, lo que hubiera solo en el primario se pierde. Para una caché es aceptable —se repuebla desde Aurora—; para sesiones no lo es tanto, y por eso MercadoFresco activa réplicas y persistencia en el grupo de sesiones.

Los puntos de enlace disponibles son cuatro:

Punto de enlace Cuándo existe Uso
Principal Modo clúster desactivado Escrituras y lecturas que exigen el último dato
De lectura Modo clúster desactivado Reparte lecturas entre réplicas
De configuración Modo clúster activado El único que usa la aplicación; el cliente descubre la topología
De nodo Siempre Diagnóstico; nunca en la aplicación

Igual que con Aurora, usar el punto de enlace de nodo en la aplicación es el error que se paga en la primera conmutación por error.

Patrones de caché: cache-aside, write-through, write-behind y read-through

Patrón Quién escribe en la caché Ventaja Inconveniente
Cache-aside (carga diferida) La aplicación, tras un fallo de lectura Solo se cachea lo que se usa; resistente a fallos de caché Primer acceso lento; puede haber datos obsoletos
Write-through La aplicación, al escribir La caché siempre está al día Se cachea todo, se use o no; encarece la escritura
Write-behind La caché, de forma diferida Escrituras muy rápidas Riesgo de pérdida de datos; complejo
Read-through La propia caché, transparente Código de aplicación limpio Requiere una capa que lo implemente

MercadoFresco usa cache-aside para el catálogo —el patrón por defecto y el más robusto: si la caché desaparece, la aplicación sigue funcionando, solo que más lenta— combinado con write-through en la carga de la lonja de las 06:00, que actualiza la caché a la vez que la base de datos para que el primer cliente de la mañana no la encuentre vacía. Write-behind se descarta explícitamente: escribir primero en memoria y volcar después a Aurora significa que un fallo del nodo pierde pedidos.

El catálogo de MercadoFresco con cache-aside

import json, hashlib, random
import redis
from redis.exceptions import RedisError

# decode_responses=True devuelve str en lugar de bytes: más cómodo con JSON.
# socket_timeout bajo es esencial: si la caché no responde, hay que ir a la base
# de datos rápido, no bloquear la petición del cliente.
cache = redis.Redis(
    host="mercadofresco-catalogo.abc123.ng.0001.euw1.cache.amazonaws.com",
    port=6379, ssl=True, decode_responses=True,
    socket_timeout=0.15, socket_connect_timeout=0.15,
    health_check_interval=30,
)

TTL_BASE = 3600  # 1 hora; los precios cambian una vez al día

def clave_producto(sku: str) -> str:
    # Un prefijo con versión permite invalidar todo el catálogo cambiando "v3".
    return f"catalogo:v3:producto:{sku}"

def obtener_producto(sku: str, conexion_aurora) -> dict:
    clave = clave_producto(sku)

    # 1. Intentar la caché. Cualquier fallo de Redis NO debe romper la tienda.
    try:
        crudo = cache.get(clave)
        if crudo is not None:
            return json.loads(crudo)          # acierto de caché: ~0,3 ms
    except RedisError:
        pass                                   # se registra y se sigue

    # 2. Fallo de caché: leer de Aurora (la fuente de verdad).
    with conexion_aurora.cursor() as cur:
        cur.execute("""
            SELECT p.sku, p.nombre, p.descripcion, p.precio, p.categoria,
                   p.origen, p.alergenos, p.unidad_venta
            FROM   productos p
            WHERE  p.sku = %s AND p.activo = true
        """, (sku,))
        fila = cur.fetchone()
    if fila is None:
        return None
    producto = dict(zip(
        ["sku", "nombre", "descripcion", "precio", "categoria",
         "origen", "alergenos", "unidad_venta"], fila))

    # 3. Poblar la caché con TTL aleatorizado (ver estampida más abajo).
    try:
        ttl = TTL_BASE + random.randint(-300, 300)
        cache.setex(clave, ttl, json.dumps(producto, default=str))
    except RedisError:
        pass

    return producto

Cinco decisiones deliberadas en ese código:

  1. Los fallos de Redis nunca rompen la petición. Un try/except alrededor de cada operación y un tiempo de espera de 150 ms. Una caché caída debe degradar el rendimiento, jamás la disponibilidad: es el error más grave que se comete al introducir una caché.
  2. La clave lleva versión (catalogo:v3:). Cambiar el número invalida todo el catálogo de golpe sin recorrer claves, que es lo que hay que hacer cuando cambia el formato del objeto cacheado.
  3. setex en vez de set + expire. Una sola operación atómica; con dos, un fallo intermedio deja una clave sin caducidad, y esas claves inmortales llenan la caché.
  4. El TTL se aleatoriza ±5 minutos para evitar que todo caduque a la vez.
  5. Se serializa a JSON, que es legible y depurable. Con volumen alto, los formatos binarios ahorran memoria y CPU, pero conviene medirlo antes de complicarlo.

TTL, invalidación y coherencia

Toda caché tiene el mismo problema de fondo: el dato cacheado puede diferir del real. Hay tres formas de gestionarlo y se combinan: la caducidad por TTL (la clave expira sola, incoherencia hasta el TTL, complejidad mínima), la invalidación explícita (al escribir se borra la clave, incoherencia casi cero) y el write-through (al escribir se actualiza la clave, incoherencia cero). En MercadoFresco la política es explícita y por tipo de dato:

Dato TTL Invalidación Justificación
Ficha de producto 1 h ± 5 min Sí, al cambiar precio Cambia una vez al día
Listado de categoría 15 min No Cambia poco, tolera desfase
Stock aproximado 30 s No «Quedan pocas unidades» tolera desfase
Ranking de más vendidos 5 min No Es informativo
Sesión de usuario 30 min deslizante Sí, al cerrar sesión Seguridad

La invalidación explícita en la carga de la lonja:

def actualizar_precio(sku: str, nuevo_precio, conexion_aurora):
    # 1. La fuente de verdad SIEMPRE se escribe primero. Si falla, no se toca la caché.
    with conexion_aurora.cursor() as cur:
        cur.execute("UPDATE productos SET precio = %s WHERE sku = %s",
                    (nuevo_precio, sku))
    conexion_aurora.commit()

    # 2. Después se invalida. Se BORRA, no se actualiza: así el siguiente lector
    #    recarga el objeto completo y no hay riesgo de dejar un objeto a medias.
    try:
        cache.delete(clave_producto(sku))
    except RedisError:
        pass   # el TTL lo arreglará en menos de una hora

El orden importa: base de datos primero, caché después. Al revés, un fallo entre ambas operaciones deja la caché con un dato que la base de datos nunca llegó a tener, y ese error persiste hasta que alguien lo note. Y se borra en lugar de actualizar porque una escritura parcial en la caché es un dato corrupto que se sirve como bueno.

Estampida de caché y claves calientes

La estampida de caché (thundering herd) ocurre cuando una clave muy consultada caduca y todas las peticiones simultáneas fallan a la vez y van juntas a la base de datos. Con 4.100 consultas por minuto y una clave popular, eso son decenas de consultas idénticas en el mismo instante.

Tres mitigaciones, de menor a mayor complejidad: TTL aleatorizado, ya aplicado arriba, que impide que muchas claves caduquen en el mismo segundo y resuelve barato la mayor parte del problema; bloqueo de repoblación, con un solo proceso recargando la clave mientras los demás sirven el valor antiguo o esperan brevemente; y refresco anticipado, que recarga antes de caducar con probabilidad creciente según se acerca el vencimiento.

def obtener_con_bloqueo(clave: str, cargar, ttl: int = 3600):
    valor = cache.get(clave)
    if valor is not None:
        return json.loads(valor)

    # SET NX EX: solo un proceso consigue el bloqueo. El EX de 10 s garantiza
    # que el bloqueo se libera aunque el proceso que lo tomó muera.
    bloqueo = f"lock:{clave}"
    if cache.set(bloqueo, "1", nx=True, ex=10):
        try:
            dato = cargar()                        # única consulta a Aurora
            cache.setex(clave, ttl + random.randint(-300, 300),
                        json.dumps(dato, default=str))
            return dato
        finally:
            cache.delete(bloqueo)

    # No obtuvimos el bloqueo: esperar un poco y reintentar la caché.
    time.sleep(0.05)
    valor = cache.get(clave)
    return json.loads(valor) if valor else cargar()

Una clave caliente es el otro problema: una sola clave que concentra tanto tráfico que satura el nodo que la contiene. En modo clúster no se puede repartir, porque una clave vive en una ranura. Las soluciones son replicar la clave con sufijos (ranking:viernes:0:4, eligiendo uno al azar en la lectura) o cachearla también en la memoria del proceso de la aplicación con un TTL de pocos segundos. Es el mismo concepto que la clave caliente de DynamoDB en 06-02.

Estructuras de Redis aplicadas a la tienda

Aquí es donde Redis se distancia de una caché genérica: las estructuras permiten resolver problemas que con cadenas exigirían leer, modificar y reescribir el objeto entero.

# 1. CADENAS · la ficha de producto serializada, con caducidad.
cache.setex("catalogo:v3:producto:PESC-SALM-001", 3600, json.dumps(producto))

# 2. HASHES · el carrito rápido. Cada campo es un SKU y cada valor, las unidades.
#    Cambiar una línea NO exige reescribir el carrito entero: HINCRBY es atómico.
cache.hincrby("carrito:rapido:4471", "PESC-SALM-001", 2)
cache.hincrby("carrito:rapido:4471", "VERD-TOMA-014", -1)
cache.expire("carrito:rapido:4471", 1800)
carrito = cache.hgetall("carrito:rapido:4471")   # {'PESC-SALM-001': '2', ...}

# 3. LISTAS · las últimas búsquedas del cliente, con tope de 10.
cache.lpush("busquedas:4471", "salmón noruego")
cache.ltrim("busquedas:4471", 0, 9)              # descarta lo que sobra
ultimas = cache.lrange("busquedas:4471", 0, 9)

# 4. CONJUNTOS ORDENADOS · el ranking de más vendidos del viernes.
#    ZINCRBY suma al marcador de forma atómica; el orden se mantiene solo.
cache.zincrby("ranking:viernes:2026-08-07", 2, "PESC-SALM-001")
top10 = cache.zrevrange("ranking:viernes:2026-08-07", 0, 9, withscores=True)
cache.expire("ranking:viernes:2026-08-07", 604800)   # una semana
Estructura Operaciones clave Uso en MercadoFresco Alternativa en Aurora
Cadena SET, GET, SETEX Ficha de producto SELECT por clave
Hash HSET, HINCRBY, HGETALL Carrito rápido Tabla de líneas
Lista LPUSH, LTRIM, LRANGE Últimas búsquedas Tabla con LIMIT 10
Conjunto ordenado ZINCRBY, ZREVRANGE Ranking del viernes GROUP BY + ORDER BY
Contador INCR, DECR, INCRBY Vistas, límites de tasa UPDATE ... SET n = n + 1

El conjunto ordenado merece un comentario: calcular el ranking de más vendidos en SQL exige agregar y ordenar todas las líneas del día, mientras que con ZINCRBY el orden se mantiene incrementalmente en cada venta y consultar el top 10 cuesta tiempo logarítmico. Es la diferencia entre un cuadro de mando que se actualiza cada cinco minutos y uno que va en vivo.

Y una advertencia sobre el carrito rápido: el carrito autoritativo sigue estando en DynamoDB (06-02). El hash de Redis es una copia caliente para pintar la cabecera. Si el nodo se reinicia, el carrito no se pierde porque la verdad está en otro sitio; si se usara Redis como fuente única, un reinicio sería una pérdida de ventas.

Contadores atómicos y el límite del stock

INCR y DECR son atómicos: mil procesos concurrentes producen el resultado correcto sin bloqueos. Eso los hace ideales para contadores de vistas, limitación de tasa por IP y stock aproximado.

# Reservar unidades en la caché ANTES de tocar la base de datos:
# descarta rápidamente la mayoría de intentos cuando ya no queda producto.
restante = cache.decrby("stock:aprox:PESC-SALM-001", unidades)
if restante < 0:
    cache.incrby("stock:aprox:PESC-SALM-001", unidades)   # deshacer
    raise SinStock("Producto agotado")

Advertencia que no admite matices. Esto no es el control de stock. Es un filtro previo, una optimización que evita que miles de peticiones lleguen a Aurora cuando un producto ya está agotado. El stock real sigue siendo transaccional en Aurora y se descuenta dentro de la transacción de confirmación del pedido, con `UPDATE stock SET unidades = unidades - :n WHERE sku = :s AND unidades

= :n` comprobando que afectó a una fila. Si esa comprobación falla, el pedido se rechaza aunque la caché dijera que había existencias.

La razón es la de siempre: una caché puede perder datos. Un reinicio del nodo, una conmutación por error o una expulsión por falta de memoria pueden dejar el contador en un valor incorrecto, y con producto fresco eso significa vender cajas de fresas que no existen: llamar al cliente, reembolsar y perderlo. Es exactamente la distinción de 06-01: eventual para mostrar, fuerte para decidir.

Sesiones en Redis: adiós a las sesiones pegajosas

En 03-03 dejamos un cabo suelto. El ALB alb-mercadofresco-tienda tiene stickiness.enabled=false porque la aplicación es sin estado, y dijimos entonces que «MercadoFresco acabará usando ElastiCache». Este es ese momento.

El problema que resuelve: si la sesión vive en la memoria de la instancia EC2 que atendió al usuario, ese usuario tiene que volver siempre a la misma instancia. Eso obliga a activar sesiones pegajosas en el ALB, y las sesiones pegajosas rompen tres cosas: el reparto de carga se desequilibra, al reducirse el ASG los usuarios de la instancia terminada pierden la sesión, y los despliegues continuos expulsan usuarios. Con las sesiones en Redis, cualquier instancia puede atender a cualquier usuario:

# La sesión vive en Redis; la instancia EC2 no guarda nada del usuario.
def cargar_sesion(id_sesion: str) -> dict:
    clave = f"sesion:{id_sesion}"
    datos = cache.get(clave)
    if datos is None:
        return {}
    cache.expire(clave, 1800)     # TTL deslizante: se renueva con la actividad
    return json.loads(datos)

def guardar_sesion(id_sesion: str, datos: dict):
    cache.setex(f"sesion:{id_sesion}", 1800, json.dumps(datos))
Con sesión local Con sesión en Redis
Sesiones pegajosas en el ALB Necesarias Innecesarias
Reparto de carga Desequilibrado Uniforme
Reducir el ASG Expulsa usuarios Transparente
Despliegue continuo Expulsa usuarios Transparente
Instancia sustituible No del todo Sí, completamente

Este es el cierre del hilo que viene de 02-01: el ASG asg-mercadofresco-tienda puede por fin escalar, reducirse y reemplazar instancias con total libertad, porque ninguna instancia contiene nada que le pertenezca a un cliente. Con dos precauciones: el grupo de sesiones usa réplicas y persistencia, porque perder todas las sesiones a la vez es un incidente visible; y los datos de sesión se limitan a lo imprescindible —identificador de cliente y preferencias—, nunca datos de pago ni información personal que obligaría a tratar la caché como un almacén sujeto al RGPD.

Seguridad: red, cifrado y control de acceso

aws elasticache create-replication-group \
  --replication-group-id mercadofresco-catalogo \
  --replication-group-description "Cache de catalogo y sesiones" \
  --engine valkey --engine-version 7.2 \
  --cache-node-type cache.t4g.medium \
  --num-cache-clusters 2 \
  --automatic-failover-enabled --multi-az-enabled \
  --cache-subnet-group-name sng-mercadofresco-datos \
  --security-group-ids sg-mercadofresco-cache \
  --at-rest-encryption-enabled \
  --transit-encryption-enabled --transit-encryption-mode required \
  --kms-key-id alias/mercadofresco-datos \
  --snapshot-retention-limit 5 --snapshot-window "03:00-04:00" \
  --tags Key=Proyecto,Value=mercadofresco Key=Entorno,Value=produccion \
         Key=Componente,Value=cache Key=Propietario,Value=luis \
         Key=CentroCoste,Value=plataforma \
  --region eu-west-1 --profile mercadofresco-dev

Las cinco medidas que hacen que esto sea seguro. Subredes privadas: sng-mercadofresco-datos no tiene ruta a internet, y una caché nunca debe ser accesible desde fuera —los incidentes históricos de Redis expuesto son numerosos y graves—. Grupo de seguridad dedicado: sg-mercadofresco-cache permite el puerto 6379 solo desde sg-mercadofresco-tienda y desde el rol de las Lambda, referenciando grupos de seguridad y no rangos de IP, como en 03-02. Cifrado en reposo y en tránsito con alias/mercadofresco-datos, en modo required, porque preferred aceptaría conexiones sin TLS y eso convierte el cifrado en opcional de hecho. RBAC: la tienda recibe un usuario que puede leer y escribir catalogo:* y sesion:* pero no puede ejecutar FLUSHALL, KEYS ni CONFIG, que es el mínimo privilegio de 04-01 aplicado a la caché. Y credenciales en Secrets Manager, como todo desde 04-03, nunca en variables de entorno en claro.

Métricas, alarmas y la medición del antes y el después

Métrica Qué indica Alarma sugerida
CacheHitRate Porcentaje de aciertos < 80 % durante 15 min
Evictions Claves expulsadas por falta de memoria > 0 sostenido
DatabaseMemoryUsagePercentage Memoria usada frente a maxmemory > 80 %
CPUUtilization / EngineCPUUtilization Carga del proceso Redis > 70 %
CurrConnections Conexiones abiertas Subida anómala
ReplicationLag Retardo de la réplica > 1 s

Dos merecen explicación. Evictions mayor que cero de forma sostenida es siempre un problema: Redis está borrando claves que aún no habían caducado porque no le cabe más, la tasa de aciertos cae y la carga vuelve a Aurora; la solución es más memoria o menos datos, no ignorarlo. Y EngineCPUUtilization es más informativa que CPUUtilization porque el proceso es fundamentalmente monohilo: un nodo con 4 vCPU puede mostrar 25 % de CPU total con el hilo del motor al 100 %.

La medición del antes y el después, con los paneles del módulo 5:

Métrica Origen Antes Después
TiempoConfirmacionPedido p99 MercadoFresco/Tienda 880 ms 310 ms
Latencia de la ficha de producto X-Ray 240 ms 28 ms
Consultas/min a Aurora (catálogo) AWS/RDS 4.100 215
ACU medias de Aurora AWS/RDS 2,4 1,5
CacheHitRate AWS/ElastiCache 94,8 %

Este es el momento de recordar por qué el módulo 5 iba antes que el 6: sin esas métricas, ninguna de las cuatro decisiones de este módulo se podría defender ante dirección con datos. La latencia de la ficha es lo que el cliente nota; el resto es lo que convence al que firma la factura.

MemoryDB y DAX: cuándo no es ElastiCache

Servicio Qué es Cuándo
ElastiCache Caché en memoria; los datos pueden perderse Cachear una fuente de verdad que está en otro sitio
MemoryDB Base de datos en memoria duradera, compatible con Redis Cuando Redis es la fuente de verdad y no puede perder datos
DAX Caché específica de DynamoDB, misma API Lecturas repetitivas muy intensas sobre DynamoDB

MemoryDB replica cada escritura en un registro transaccional distribuido en varias AZ antes de confirmarla, con durabilidad comparable a la de una base de datos, y cuesta bastante más. MercadoFresco no lo necesita: el catálogo está en Aurora y el carrito en DynamoDB, así que la caché siempre puede repoblarse. DAX ya se descartó en 06-02: el carrito no tiene lecturas repetitivas —cada cliente lee el suyo—, así que la tasa de aciertos sería baja, y además ElastiCache sirve datos de varias fuentes mientras que DAX solo entiende de DynamoDB.

Costes y ahorro medido

Concepto Cantidad Coste mensual
cache.t4g.medium primario 730 h × 0,073 53,29 USD
cache.t4g.medium réplica 730 h × 0,073 53,29 USD
Copias de seguridad (5 días) ~3 GB 0,25 USD
Transferencia entre AZ Réplica ~2 USD
Total ElastiCache ≈108,83 USD

Y lo que devuelve, medido en la factura de Aurora del mes siguiente:

Concepto Antes Después Diferencia
ACU medias de Aurora 2,4 1,5 −0,9 ACU
Coste de cómputo de Aurora 210 USD 131 USD −79 USD
E/S de Aurora 36 USD 22 USD −14 USD
Coste de ElastiCache 0 109 USD +109 USD
Neto +16 USD/mes

Conviene ser honesto: la caché no se paga sola en euros, cuesta 16 USD netos al mes. Lo que compra es la latencia de la ficha dividida por ocho —de 240 ms a 28 ms, que el cliente percibe—, un margen de capacidad en Aurora que antes no existía para absorber el pico del viernes, y las sesiones distribuidas que permiten al ASG escalar y reducirse sin expulsar a nadie. Como en Aurora, la justificación no es el ahorro: es lo que se compra con el gasto.

Limpieza. Al terminar de practicar: aws elasticache delete-replication-group --replication-group-id <id>, con --final-snapshot-identifier si quieres conservar los datos. Un grupo olvidado con dos nodos son más de 100 USD al mes por nada. Comprueba con Cost Explorer que la etiqueta Componente=cache desaparece al mes siguiente.

Errores Comunes y Consejos

Dejar que un fallo de la caché rompa la tienda. El error más grave y el más común. Toda operación de caché va envuelta en try/except y con tiempo de espera corto. Una caché caída degrada el rendimiento; nunca la disponibilidad.

Cachear el resultado antes de confirmarlo en la base de datos. Base de datos primero, caché después. Al revés, un fallo intermedio deja en la caché un dato que nunca existió.

Actualizar la caché en vez de borrarla al invalidar. Borrar es idempotente y sencillo; actualizar puede dejar un objeto parcialmente escrito que se sirve como bueno durante una hora.

Usar la caché como fuente de verdad del stock. Ya está dicho con toda claridad: el stock real es transaccional en Aurora. La caché solo filtra intentos evidentes.

Ignorar Evictions. Un valor sostenido por encima de cero significa que Redis borra claves vivas por falta de memoria. La tasa de aciertos cae y la carga vuelve a la base de datos, con lo que la caché deja de cumplir su función mientras se sigue pagando.

Ejecutar KEYS * en producción. Recorre todo el espacio de claves y, como Redis es esencialmente monohilo, bloquea el servidor mientras lo hace. Se usa SCAN, que itera por lotes. Lo mejor es que el RBAC prohíba KEYS directamente.

Poner claves sin TTL. Una clave sin caducidad no se va nunca. Suficientes de ellas y la memoria se llena, con lo que empiezan las expulsiones. La regla es simple: toda clave lleva TTL, salvo excepción justificada y documentada.

Cachear datos personales sin pensarlo. Una caché es una copia más de datos personales sujeta al RGPD: hay que cifrarla, restringir el acceso y poder borrarla ante una solicitud de supresión. Cachea identificadores y preferencias, no direcciones ni datos de pago.

Consejo: mide la tasa de aciertos por tipo de clave, no solo la global. Un 94 % global puede esconder un 99 % en el catálogo y un 40 % en los listados, y ese 40 % indica un TTL mal elegido o un patrón que no se repite lo suficiente para merecer caché.

Consejo: prueba el modo degradado. Apaga la caché en preproducción con carga y comprueba que la tienda sigue funcionando, más lenta pero funcionando. Si se cae, la caché se ha convertido en una dependencia crítica sin que nadie lo decidiera.

Ejercicios

Ejercicio 1: diseñar la política de caché de una funcionalidad nueva

MercadoFresco lanza «mi lista habitual»: una pantalla que muestra los 20 productos que un cliente más ha comprado en los últimos 6 meses, con su precio actual, su disponibilidad y una marca si están en oferta. Calcularla en Aurora exige agregar el historial del cliente (1,2 s) y consultar precio y stock de 20 productos.

Diseña la estrategia: qué se cachea y con qué clave, qué TTL para cada pieza, qué patrón usas para cada una, qué se invalida y cuándo, cómo evitas la estampida el lunes por la mañana, y qué estructura de Redis eliges para cada elemento. Justifica por qué no cacheas la respuesta completa de la pantalla en una sola clave.

Ejercicio 2: diagnosticar una caché que no ayuda

Dos semanas después de desplegar la caché, las métricas son estas: CacheHitRate 41 %, DatabaseMemoryUsagePercentage 97 %, Evictions 8.400 por hora, EngineCPUUtilization 88 %, CurrConnections 2.900 y creciendo, y la latencia p99 de la tienda ha empeorado respecto a antes de la caché. Se descubre además que el equipo cachea los resultados de búsqueda con clave busqueda:<texto completo>:<filtros> y TTL de 24 horas.

Responde: (a) cuál es la causa raíz y cómo la deduces de las métricas; (b) por qué la latencia ha empeorado en vez de mejorar; (c) qué papel juega CurrConnections; (d) cuatro medidas ordenadas por impacto; (e) qué política habría evitado el problema desde el diseño.

Ejercicio 3: cerrar el círculo del stock

Escribe el flujo completo de «añadir al carrito y confirmar pedido» para una caja de fresas de la que quedan 3 unidades, indicando en cada paso qué almacén interviene (ElastiCache, DynamoDB, Aurora), qué tipo de consistencia se usa y por qué. Debe cubrir: mostrar la ficha con «últimas unidades», añadir al carrito, el paso a pago, la confirmación con descuento de stock, y qué se le muestra a un cliente que llega tarde. Indica además qué pasa en cada paso si ElastiCache se cae en ese momento.

Soluciones

Solución 1

No se cachea la pantalla completa en una sola clave porque mezcla piezas con ritmos de cambio radicalmente distintos: la lista de productos habituales cambia como mucho una vez al día, pero el precio y la disponibilidad cambian cada pocos minutos. Con una sola clave hay que elegir entre un TTL corto —que desperdicia la agregación cara recalculándola constantemente— o uno largo —que muestra precios obsoletos—. La regla general es cachear por ritmo de cambio, no por pantalla.

Pieza Clave Estructura TTL Patrón Invalidación
Lista de SKU habituales habitual:v1:<id_cliente> Conjunto ordenado (puntuación = compras) 24 h ± 1 h Cache-aside con bloqueo Al confirmar un pedido, ZINCRBY incremental
Ficha de producto catalogo:v3:producto:<sku> Cadena JSON 1 h ± 5 min Cache-aside Al cambiar precio
Stock aproximado stock:aprox:<sku> Contador 30 s Write-through en la carga No
Marca de oferta ofertas:v1:activas Conjunto 10 min Cache-aside Al publicar campaña

La pantalla se compone leyendo el conjunto ordenado (una operación) y luego las 20 fichas con una sola llamada MGET, no veinte GET: la reducción de viajes de red es lo que convierte 20 × 0,3 ms en 0,4 ms.

Estampida del lunes por la mañana: el riesgo es que miles de claves habitual:* caduquen a la vez. Se mitiga con las tres técnicas combinadas: TTL aleatorizado ±1 hora, bloqueo de repoblación con SET NX EX para que solo un proceso agregue por cliente, y —dado que la agregación cuesta 1,2 s— un refresco anticipado programado de madrugada para los clientes más activos, que llegan por la mañana con la clave ya caliente.

Incremental en vez de recalcular: al confirmar un pedido, en lugar de invalidar la lista se hace ZINCRBY habitual:v1:<cliente> <unidades> <sku>. La lista se mantiene actualizada sin volver a ejecutar nunca la agregación de 1,2 s, salvo cuando la clave caduque de verdad. Es el mejor uso de un conjunto ordenado en toda la lección.

Solución 2

(a) Causa raíz: la caché de resultados de búsqueda. Las métricas lo dicen en cadena. La clave busqueda:<texto completo>:<filtros> tiene cardinalidad prácticamente infinita: cada combinación de texto libre y filtros genera una clave nueva que casi nadie repetirá. Con TTL de 24 horas, esas claves se acumulan sin parar hasta llenar la memoria (DatabaseMemoryUsagePercentage 97 %), y entonces Redis empieza a expulsar (Evictions 8.400/h) —y expulsa también las claves del catálogo, que sí eran útiles—. De ahí el CacheHitRate del 41 %: se cachea muchísimo que no se reutiliza y se pierde lo que sí. Es el caso de manual de cachear datos que se leen una sola vez.

(b) Por qué la latencia empeoró. Ahora cada petición paga dos costes en vez de uno: primero consulta Redis, falla el 59 % de las veces, y luego consulta Aurora igualmente. A eso se suma que Aurora recibe casi la misma carga que antes —porque los aciertos útiles se han desplomado— con lo que no ha mejorado su latencia, y que el nodo de Redis está saturado (EngineCPUUtilization 88 %), así que incluso las operaciones de caché son lentas. Una caché con mala tasa de aciertos es peor que no tener caché: añade latencia y no quita carga.

(c) CurrConnections creciendo. Indica que las conexiones no se están reutilizando: o falta un pool de conexiones, o cada invocación Lambda abre una nueva y no la cierra. Cada conexión consume memoria del nodo —agravando el problema de memoria— y CPU en el hilo del motor. Con 2.900 conexiones y creciendo, el nodo acabará rechazando conexiones nuevas.

(d) Cuatro medidas por impacto.

  1. Dejar de cachear las búsquedas de texto libre. Es la causa raíz y su corrección libera memoria de inmediato. Si se quiere cachear búsqueda, solo las consultas más frecuentes —el top 100 medido en los registros— con TTL de minutos, no las 400.000 combinaciones posibles.
  2. Configurar la política de expulsión adecuada (allkeys-lru o volatile-lru) y revisar maxmemory. Con LRU, al menos se expulsa lo menos usado en lugar de lo que toque.
  3. Introducir un pool de conexiones con límite máximo en la aplicación y en las Lambda, reutilizando el cliente entre invocaciones.
  4. Revisar el dimensionamiento una vez corregido lo anterior: si con solo el catálogo y las sesiones la memoria sigue por encima del 80 %, hay que subir de tipo de nodo.

(e) La política de diseño que lo evita. Dos reglas explícitas, escritas antes de la primera línea de código. Primera: solo se cachea lo que se lee muchas más veces de las que se escribe, y esa proporción se mide antes de cachear —el catálogo era 4.100:1; una búsqueda de texto libre es aproximadamente 1:1—. Segunda: toda clave tiene TTL acotado y toda familia de claves tiene un límite de cardinalidad conocido; si no se puede acotar cuántas claves distintas va a generar un patrón, no se cachea con ese patrón. Añadir una alarma sobre Evictions > 0 desde el primer día habría avisado en horas en lugar de en semanas.

Solución 3

1. Mostrar la ficha con «últimas unidades». ElastiCache, consistencia eventual. Se lee catalogo:v3:producto:PESC-FRES-002 y stock:aprox:PESC-FRES-002 (TTL 30 s). Si el contador es 3, se pinta «quedan pocas unidades». Un desfase de 30 segundos aquí no daña a nadie. Si ElastiCache se cae: fallo de caché, se lee de Aurora, la ficha tarda 240 ms en vez de 28. Funciona, más lento.

2. Añadir al carrito. DynamoDB, escritura en mercadofresco-carritos con UpdateItem, más una actualización del hash carrito:rapido:<cliente> en ElastiCache para pintar la cabecera. No se descuenta stock: añadir al carrito no reserva nada, porque reservar al añadir provoca que carritos abandonados bloqueen producto real. Se hace un DECRBY sobre stock:aprox solo como filtro orientativo si la política de negocio lo pide, aceptando que es aproximado. Si ElastiCache se cae: el carrito se guarda igual en DynamoDB —que es la verdad— y la cabecera se pinta leyendo de DynamoDB. No se pierde nada.

3. Paso a pago. DynamoDB con lectura fuertemente consistente (ConsistentRead=True) del carrito: de esta lectura depende lo que se cobra, así que no vale la eventual. Se recalculan los precios leyendo de Aurora, no de la caché, porque el importe que se cobra no puede basarse en un precio de hasta una hora de antigüedad. Si ElastiCache se cae: no afecta, este paso no la usa.

4. Confirmación y descuento de stock. Aurora, transacción con consistencia fuerte, y es el único paso que decide:

BEGIN;
UPDATE stock SET unidades = unidades - 3
 WHERE sku = 'PESC-FRES-002' AND unidades >= 3;   -- debe afectar a 1 fila
-- si afecta a 0 filas: ROLLBACK y rechazar el pedido
INSERT INTO pedidos (...) VALUES (...);
INSERT INTO lineas_pedido (...) VALUES (...);
COMMIT;

Después del COMMIT, y solo después, se invalida stock:aprox:PESC-FRES-002 en la caché y se hace ZINCRBY en el ranking del viernes. El orden es el de siempre: fuente de verdad primero, caché después. Si ElastiCache se cae: el pedido se confirma correctamente; el contador aproximado quedará desfasado hasta que caduque a los 30 segundos, sin ninguna consecuencia.

5. El cliente que llega tarde. Vio «quedan pocas unidades» porque leyó la caché, añadió al carrito y, al confirmar, el UPDATE afecta a 0 filas porque otro cliente se llevó las tres cajas. La respuesta correcta es no confirmar el pedido y mostrar un mensaje claro —«las fresas se han agotado mientras completabas el pedido»— con la sugerencia de un producto equivalente y la opción de continuar sin ese artículo. Nunca se confirma un pedido basándose en una lectura de caché.

El principio que recorre los cinco pasos, y que es la síntesis del módulo entero: eventual para mostrar, fuerte para decidir. La caché acelera lo que el cliente ve; la transacción de Aurora decide lo que el cliente compra. Y cada almacén tiene un modo de fallo acotado: si la caché cae, todo sigue funcionando más lento; si DynamoDB cae, se pierden carritos pero no pedidos; solo si Aurora cae se para la venta, y por eso Aurora es la única de las tres con seis copias en tres zonas de disponibilidad.

La capa de datos completa de MercadoFresco

Las cuatro cargas que diagnosticamos en 06-01 tienen ya su motor, y cada asignación se sostiene sobre una medición:

Carga Patrón medido Motor Resultado medido
Pedidos y stock OLTP transaccional, SQL ad hoc Aurora PostgreSQL Conmutación de 96 s a 20 s; réplica de 8 s a 15 ms
Carrito y sesiones Clave-valor, 180.000 escrituras/día DynamoDB 78 GB → 8 GB; 51 → 8 USD/mes; sin VACUUM
Informes OLAP, 6-40 M de filas agregadas Redshift Serverless 4-6 min → 2-6 s; fuera de producción
Catálogo 4.100 lecturas/min, 94 % idénticas ElastiCache (Valkey) 240 ms → 28 ms; 4.100 → 215 consultas/min
graph TD
    CF["CloudFront E2QWERTY123ABC"] --> ALB["alb-mercadofresco-tienda<br/>sin sesiones pegajosas"]
    ALB --> ASG["asg-mercadofresco-tienda<br/>instancias sin estado"]
    ASG --> EC["ElastiCache · mercadofresco-catalogo<br/>catálogo, sesiones, ranking, contadores"]
    ASG --> DDB["DynamoDB · mercadofresco-carritos<br/>carrito y sesiones · TTL 30 días"]
    ASG --> AUR["Aurora · aurora-mercadofresco-pedidos<br/>pedidos, stock, catálogo · fuente de verdad"]
    EC -.->|fallo de caché| AUR
    AUR -->|canalización nocturna e integración sin ETL| RS["Redshift · mercadofresco-analitica<br/>hechos_pedidos y dimensiones"]
    DDB -.->|Streams: carritos abandonados| S3["S3 · mercadofresco-informes-analitica"]
    S3 --> RS
    RS --> SARA["Informes de Sara<br/>ticket medio, cohortes, ranking del viernes"]

Y el balance económico completo del módulo, que conviene mirar de frente:

Componente Antes Después
RDS mercadofresco-pedidos + réplica 118 USD
Aurora Serverless v2 153 USD
DynamoDB mercadofresco-carritos 8 USD
Redshift Serverless 16 USD
ElastiCache 109 USD
Total capa de datos 118 USD 286 USD

La capa de datos cuesta 2,4 veces más. A cambio: la latencia p99 de la tienda pasa de 1.900 ms a 310 ms, la conmutación por error de 96 a 20 segundos, los informes de Sara de seis minutos a seis segundos, la tabla de sesiones deja de crecer sin control, y el ASG puede escalar y reducirse sin expulsar a nadie. Para una tienda que hace 900 pedidos por hora en el pico del viernes, con un ticket medio de decenas de euros, 168 USD adicionales al mes es una decisión que se justifica sola. Lo importante es que ahora está justificada con números, y esa es la diferencia entre arquitectura e intuición.

Conclusión

El catálogo se sirve desde memoria. Sabes por qué cachear —latencia, coste por consulta y, sobre todo, protección de la base de datos, porque un 95 % de aciertos convierte 4.100 consultas por minuto en 205 y devuelve a Aurora el margen que necesitaba para el pico— y sabes qué no cachear: lo que cambia en cada lectura, lo que exige exactitud momentánea, lo que se lee una sola vez y los datos personales sin una razón clara.

Conoces Redis/Valkey frente a Memcached y por qué MercadoFresco elige el primero: estructuras de datos más allá de las cadenas, réplicas con conmutación automática y operaciones atómicas. Dominas la arquitectura —nodo, fragmento, grupo de réplica, modo clúster activado y desactivado con sus 16.384 ranuras hash y sus etiquetas para coubicar claves, Multi-AZ con conmutación— y la decisión de empezar con el modo clúster desactivado, porque 1,2 GB de catálogo caben de sobra en un nodo y añadir complejidad sin medición es el error que este módulo lleva cinco lecciones combatiendo. Con los puntos de enlace principal, de lectura, de configuración y de nodo, y la regla de que el de nodo jamás aparece en la aplicación.

Manejas los cuatro patrones de caché y la elección razonada: cache-aside para el catálogo porque es el más robusto —si la caché desaparece, la tienda sigue—, write-through en la carga de la lonja de las 06:00 para que el primer cliente no encuentre la caché vacía, y write-behind descartado explícitamente porque perder escrituras de negocio no es aceptable. Con el código completo y sus cinco decisiones: try/except en toda operación con tiempo de espera de 150 ms, clave con versión para invalidar el catálogo entero de golpe, setex atómico para que no queden claves inmortales, TTL aleatorizado y serialización depurable. Y con el orden que no se negocia: base de datos primero, caché después, y borrar en vez de actualizar al invalidar.

Sabes qué es una estampida de caché y las tres mitigaciones —TTL aleatorizado, bloqueo de repoblación con SET NX EX, refresco anticipado— y reconoces la clave caliente como el mismo problema que en DynamoDB con otro nombre. Aplicas las estructuras de Redis a problemas reales: cadenas para la ficha, hashes para el carrito rápido con HINCRBY atómico, listas con LTRIM para las últimas búsquedas, conjuntos ordenados con ZINCRBY para que el ranking del viernes se mantenga solo en cada venta, y contadores atómicos para el stock aproximado —con la advertencia que no admite matices: el stock real sigue siendo transaccional en Aurora, porque una caché puede perder datos y vender fresas que no existen significa llamar al cliente, reembolsar y perderlo—.

Y has cerrado el cabo suelto de 03-03: con las sesiones en Redis, el ALB no necesita sesiones pegajosas, el reparto de carga es uniforme, y el ASG asg-mercadofresco-tienda puede escalar, reducirse y reemplazar instancias sin expulsar a nadie, porque ninguna instancia contiene ya nada que pertenezca a un cliente. Todo ello en subredes privadas con sg-mercadofresco-cache, cifrado en reposo y en tránsito en modo required, RBAC que prohíbe FLUSHALL y KEYS, credenciales en Secrets Manager, alarmas sobre CacheHitRate, Evictions, EngineCPUUtilization y DatabaseMemoryUsagePercentage, y la comparación honesta con MemoryDB y DAX para saber cuándo el problema no es de caché. Coste: 109 USD que devuelven 93 en Aurora, y una latencia de ficha dividida por ocho.

Con esto se cierra el módulo 6. Las cuatro cargas tienen su motor y cada decisión se sostiene sobre una medición del módulo 5: Aurora para pedidos y stock, DynamoDB para el carrito y las sesiones, Redshift Serverless para los informes de Sara, ElastiCache para el catálogo. La capa de datos cuesta 2,4 veces más y vale cada euro, y lo importante es que ahora se puede demostrar.

Pero al repartir los datos entre cuatro almacenes ha aparecido un problema nuevo, y esta vez no es de rendimiento. La tienda hace demasiadas cosas de forma síncrona dentro de la petición del cliente. Cuando alguien pulsa «Confirmar pedido», el mismo hilo cobra la tarjeta, escribe en Aurora, actualiza DynamoDB, invalida la caché, avisa al almacén para que prepare la caja, manda el correo de confirmación, notifica al repartidor y publica el evento para la analítica. Ocho cosas encadenadas, y si cualquiera de ellas falla, el pedido entero se rompe: si el proveedor de correo tarda cuatro segundos, el cliente espera cuatro segundos; si el sistema del almacén está caído, se pierde una venta que estaba pagada. Ya no hay una base de datos que consultar para arreglarlo, porque el problema no está en ningún almacén: está en que todo depende de todo, al mismo tiempo.

En el módulo 7, «Integración de aplicaciones», empezando por 07-01, «Amazon SQS», veremos cómo desacoplar esas ocho tareas: colas para que el pedido se confirme en cuanto está cobrado y lo demás ocurra después, temas para que un mismo evento llegue a varios interesados sin que la tienda sepa quiénes son, eventos para enrutar según su contenido, y flujos de trabajo para orquestar procesos largos con reintentos y compensaciones. El objetivo es que confirmar un pedido vuelva a ser una sola cosa rápida y fiable, y que el resto del mundo se entere a su ritmo.

Curso de AWS

Módulo 1: Introducción a AWS

Módulo 2: Servicios principales de AWS

Módulo 3: Redes y entrega de contenido

Módulo 4: Seguridad e identidad

Módulo 5: Monitorización y gestión

Módulo 6: Bases de datos

Módulo 7: Integración de aplicaciones

Módulo 8: Herramientas para desarrolladores

Módulo 9: Infraestructura como código y gobierno de cuentas

Módulo 10: Contenedores en AWS

Módulo 11: Mejores prácticas y gestión de costos

© Copyright 2026. Todos los derechos reservados