tutoriales.com

Construyendo Aplicaciones Multitenant Serverless con AWS Lambda y DynamoDB

Este tutorial práctico detalla cómo construir una arquitectura multitenant (multiusuario) utilizando un modelo serverless con AWS Lambda y Amazon DynamoDB, implementando aislamiento de datos y seguridad avanzada.

Avanzado12 min de lectura8 views
Reportar error

🚀 Introducción al Desarrollo Multitenant Serverless

El diseño de aplicaciones multitenant (multiusuario) permite que una sola instancia de una aplicación sirva a múltiples organizaciones o clientes (tenants), manteniendo sus datos y configuraciones estrictamente aislados. Al combinar este patrón arquitectónico con la tecnología serverless de AWS, logramos una infraestructura capaz de escalar automáticamente, reducir costos operativos al mínimo y eliminar la necesidad de aprovisionar servidores dedicados por cliente.

En este tutorial completo, exploraremos cómo diseñar una estrategia de aislamiento de datos utilizando particionado lógico en Amazon DynamoDB y cómo validar tokens de autenticación en AWS Lambda para identificar dinámicamente al tenant en cada petición HTTP.

💡 Consejo: Este enfoque es ideal para empresas SaaS (Software as a Service) que buscan maximizar su margen de beneficio reduciendo los costos de infraestructura inactiva.

🛠️ Arquitectura y Estrategias de Aislamiento

Antes de escribir código, es fundamental comprender las diferentes estrategias disponibles para separar los datos y los recursos entre los distintos tenants en un entorno serverless.

Estrategia de AislamientoVentajasDesventajasCosto Operativo
------------
Silos (Aislamiento Total)Máxima seguridad y cumplimiento normativoCostoso, despliegues complejos por clienteAlto
Pools (Aislamiento Lógico)Muy económico, fácil mantenimientoRequiere lógica estricta de filtrado en códigoBajo
------------
Modelo HíbridoFlexibilidad para clientes VIP vs EstándarComplejidad arquitectónica moderadaMedio

Para este tutorial, utilizaremos el modelo de pools (aislamiento lógico) utilizando una tabla única en Amazon DynamoDB, donde la clave de partición incluirá el identificador del tenant.

HTTP Req JSON Payload API Gateway REST Endpoint Lambda Authorizer Lambda Lógica Principal DynamoDB PK: TenantID SK: ResourceID Arquitectura Serverless Multi-tenant

📋 Prerrequisitos y Configuración del Entorno

Para seguir este tutorial paso a paso, necesitarás tener instalado y configurado lo siguiente:

  • Una cuenta activa de AWS con permisos para crear funciones Lambda, tablas DynamoDB y roles IAM.
  • Node.js (versión 18.x o superior) instalado en tu máquina local.
  • AWS CLI configurado con tus credenciales (aws configure).
  • El framework Serverless o AWS SAM para el despliegue de la infraestructura.
📌 Nota: Asegúrate de que tu usuario de IAM tenga permisos de administrador o suficientes políticas para administrar recursos serverless.

🗄️ Diseño del Modelo de Datos en DynamoDB

El núcleo de nuestra aplicación multitenant recae en cómo estructuramos las claves en Amazon DynamoDB. Utilizaremos una tabla llamada SaaSAppData con el siguiente esquema:

  • Partition Key (PK): TENANT#<tenant_id> (Ejemplo: TENANT#empresa_a)
  • Sort Key (SK): METADATA#<resource_id> o USER#<user_id>

Esta estructura nos permite realizar consultas rápidas y eficientes garantizando que ningún tenant pueda acceder accidentalmente a los datos de otro.

A continuación, definimos la infraestructura usando una plantilla de despliegue en formato YAML (Serverless Framework):

service: serverless-multitenant-app

provider:
  name: aws
  runtime: nodejs18.x
  region: us-east-1
  environment:
    DYNAMODB_TABLE: ${self:custom.tableName}
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - dynamodb:Query
            - dynamodb:PutItem
            - dynamodb:GetItem
            - dynamodb:DeleteItem
          Resource:
            - arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/${self:custom.tableName}

custom:
  tableName: SaaSAppData

resources:
  Resources:
    SaasDataTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:custom.tableName}
        BillingMode: PAY_PER_REQUEST
        AttributeDefinitions:
          - AttributeName: PK
            AttributeType: S
          - AttributeName: SK
            AttributeType: S
        KeySchema:
          - AttributeName: PK
            KeyType: HASH
          - AttributeName: SK
            KeyType: RANGE

🔐 Autenticación y Extracción del Tenant ID

Cada petición que llegue a nuestras funciones Lambda debe incluir un token JWT (JSON Web Token) emitido por nuestro servicio de autenticación (por ejemplo, AWS Cognito). El token contendrá una reclamación (claim) llamada custom:tenant_id.

Crearemos una función middleware o un decorador en Node.js que extraiga este valor y lo inyecte en el contexto de ejecución de la función Lambda.

'use strict';

const extractTenantId = (event) => {
  try {
    const claims = event.requestContext.authorizer.jwt.claims;
    const tenantId = claims['custom:tenant_id'];
    
    if (!tenantId) {
      throw new Error('Tenant ID no encontrado en el token de autorización');
    }
    
    return tenantId;
  } catch (error) {
    throw new Error(`Error de autenticación multitenant: ${error.message}`);
  }
};

module.exports = { extractTenantId };
⚠️ Advertencia: Nunca confíes en los parámetros enviados por el cuerpo de la petición (body o query params) para identificar al tenant. Utiliza siempre el token de autenticación verificado en el backend.

⚡ Implementación de la Función Lambda Principal

Ahora desarrollaremos la lógica de negocio para crear y consultar registros asegurando el aislamiento estricto de los datos. La función utilizará el SDK de AWS para JavaScript (v3).

'use strict';

const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
const { DynamoDBDocumentClient, PutCommand, QueryCommand } = require("@aws-sdk/lib-dynamodb");
const { extractTenantId } = require("./auth");

const client = new DynamoDBClient({});
const dynamo = DynamoDBDocumentClient.from(client);
const TABLE_NAME = process.env.DYNAMODB_TABLE;

exports.handler = async (event) => {
  console.log("Evento recibido:", JSON.stringify(event, null, 2));
  
  try {
    const tenantId = extractTenantId(event);
    const httpMethod = event.requestContext.http.method;
    const pk = `TENANT#${tenantId}`;

    if (httpMethod === 'POST') {
      const body = JSON.parse(event.body);
      const sk = `RESOURCE#${body.resourceId}`;

      const putParams = {
        TableName: TABLE_NAME,
        Item: {
          PK: pk,
          SK: sk,
          data: body.data,
          createdAt: new Date().toISOString(),
        },
      };

      await dynamo.send(new PutCommand(putParams));

      return {
        statusCode: 201,
        body: JSON.stringify({ message: 'Recurso creado exitosamente para el tenant', tenantId }),
      };
    } 
    
    if (httpMethod === 'GET') {
      const queryParams = {
        TableName: TABLE_NAME,
        KeyConditionExpression: "PK = :pk",
        ExpressionAttributeValues: {
          ":pk": pk,
        },
      };

      const result = await dynamo.send(new QueryCommand(queryParams));

      return {
        statusCode: 200,
        body: JSON.stringify({ tenantId, items: result.Items }),
      };
    }

    return {
      statusCode: 405,
      body: JSON.stringify({ message: 'Método no permitido' }),
    };

  } catch (error) {
    console.error("Error procesando la solicitud:", error);
    return {
      statusCode: 500,
      body: JSON.stringify({ message: 'Error interno del servidor', error: error.message }),
    };
  }
};

📊 Pruebas y Validación del Aislamiento

Para verificar que nuestra arquitectura multitenant funciona correctamente y que ningún cliente puede ver los datos de otro, realizaremos un proceso de validación paso a paso:

Paso 1: Desplegar la infraestructura utilizando serverless deploy.
Paso 2: Generar tokens de prueba JWT para dos clientes distintos: empresa_a y empresa_b.
Paso 3: Realizar peticiones POST con el token de empresa_a para insertar registros.
Paso 4: Realizar peticiones GET con el token de empresa_b y comprobar que la respuesta devuelve una lista vacía o exclusivamente sus propios datos.

🔍 Preguntas Frecuentes (FAQ)

¿Qué sucede si un tenant crece exponencialmente y consume todos los recursos de la tabla DynamoDB? Al utilizar el modo de facturación On-Demand (Pay-Per-Request), DynamoDB escala automáticamente el rendimiento de lectura y escritura sin necesidad de aprovisionamiento manual, mitigando el problema del 'noisy neighbor' a nivel de almacenamiento y capacidad base.
¿Es posible migrar un tenant del modelo Pool al modelo Silo en el futuro? Sí, es posible. Diseñar la capa de acceso a datos utilizando un patrón repositorio o funciones abstractas facilita la migración de datos hacia una tabla o base de datos dedicada si un cliente enterprise requiere aislamiento físico total.

✨ Conclusión y Próximos Pasos

¡Felicidades! Has implementado con éxito los fundamentos de una arquitectura multitenant altamente segura y escalable utilizando un enfoque serverless con AWS Lambda y Amazon DynamoDB. Esta configuración te permite crecer de manera ilimitada mientras mantienes costos operativos bajos y un aislamiento de datos estricto.

Como siguientes pasos recomendados, puedes explorar la integración de AWS WAF para protección contra ataques web y la implementación de límites de velocidad (Rate Limiting) por tenant utilizando Amazon API Gateway.

🔥 Importante: Recuerda siempre realizar auditorías de seguridad periódicas sobre las políticas IAM y las reclamaciones de tus tokens JWT para garantizar la integridad del sistema multitenant.

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!