Ilustración de arquitectura serverless en AWS con Lambda y DynamoDB — flujo automatizado en la nube.

Arquitectura serverless en AWS: tutorial completo con Lambda y DynamoDB

Construye tu primera API serverless production-ready paso a paso: desde configuración hasta deployment y monitoreo

Introducción

La arquitectura serverless ha transformado radicalmente cómo construimos y desplegamos aplicaciones en 2025: sin servidores que gestionar, escalado automático instantáneo y pago solo por uso real. AWS Lambda procesa trillones de requests mensuales para empresas desde startups hasta Netflix, mientras DynamoDB maneja petabytes de datos con latencia de milisegundos sin que desarrolladores toquen un solo servidor.

Para equipos pequeños, serverless significa velocidad de desarrollo 10x mayor: en lugar de configurar EC2, instalar dependencias, gestionar auto-scaling, balanceadores de carga y monitoreo, escribes código de tu función Lambda, defines tu tabla DynamoDB y despliegas todo en minutos. Para empresas establecidas, serverless reduce costos operativos 60-80% al eliminar servidores idle y pagar solo por ejecuciones reales, no por capacidad provisionada.

Este tutorial te lleva desde cero hasta una API REST serverless completa funcionando en producción: qué es serverless y por qué importa, arquitectura de tres capas (Presentation, Logic, Data), configuración inicial de AWS, implementación paso a paso de Lambda functions con código real, diseño de tablas DynamoDB optimizadas, integración con API Gateway, monitoreo con CloudWatch, optimización de costos y mejores prácticas de seguridad. Al finalizar, tendrás una aplicación desplegada que escala automáticamente de 10 a 10,000 requests/segundo sin intervención manual.

Qué es Serverless y Ventajas vs Arquitectura Tradicional

Serverless no significa “sin servidores”; significa que no gestionas servidores. AWS opera la infraestructura subyacente, gestiona escalado, alta disponibilidad, parches de seguridad y actualizaciones del sistema operativo; tú solo despliegas código y pagas por ejecuciones.

Comparativa arquitectura tradicional vs serverless:

AspectoTradicional (EC2)Serverless (Lambda)
ProvisiónLanzar instancias, configurar OSSubir código, listo
EscaladoAuto-scaling groups manualAutomático, instantáneo
DisponibilidadMulti-AZ, load balancersBuilt-in, 99.95% SLA
MantenimientoParches, updates, seguridad OSAWS gestiona todo
Costo24/7 running ($50-500/mes)Por invocación ($0.20/millón)
Tiempo a producciónDías/semanasHoras
Idle time costPagas aunque no usesZero costo en idle
Arquitectura serverless en AWS

Ventajas clave de serverless:

1. Zero gestión de servidores
No provisionar, parchear, monitorear servidores; enfoque 100% en lógica de negocio.

2. Escalado automático infinito
Lambda escala de 0 a 10,000+ ejecuciones concurrentes en segundos sin configuración.

3. Pago por uso real
Solo pagas por tiempo de ejecución (milisegundos × RAM); cero costo cuando no hay requests.

4. Alta disponibilidad nativa
AWS distribuye funciones en múltiples zonas automáticamente; no configuras multi-AZ.

5. Velocidad de desarrollo
De idea a producción en horas; ideal para MVPs, prototipos, microservicios.

Casos de uso ideales para serverless:

  • APIs REST/GraphQL con tráfico variable
  • Webhooks y event processing
  • Procesamiento de imágenes/archivos subidos a S3
  • Scheduled jobs (cron-like tasks)
  • Backends móviles y web apps
  • Microservicios event-driven

Cuándo NO usar serverless:

  • Aplicaciones con ejecuciones >15 minutos (límite Lambda)
  • Workloads con tráfico constante 24/7 (EC2 puede ser más barato)
  • Aplicaciones que requieren estado persistente en memoria
  • Procesamiento que necesita GPU/hardware especializado

Arquitectura de Tres Capas Serverless

La arquitectura serverless típica sigue patrón de tres capas:

Capa 1: Presentation (Frontend)

text[Usuario] → [CloudFront CDN] → [S3 Static Website]
  • React, Vue, Next.js en S3 bucket
  • CloudFront distribuye contenido globalmente con latencia <50ms
  • Costo: ~$1-5/mes para sitios pequeños

Capa 2: Logic (Backend)

text[Frontend] → [API Gateway] → [Lambda Functions] → [Step Functions (opcional)]
  • API Gateway expone REST/HTTP APIs públicas
  • Lambda ejecuta lógica de negocio (Node.js, Python, Go, etc.)
  • Step Functions orquestan workflows complejos multi-step

Capa 3: Data (Persistencia)

text[Lambda] → [DynamoDB / RDS / S3]
  • DynamoDB: NoSQL serverless, latencia <10ms, escala automática
  • S3: Almacenamiento de objetos (imágenes, archivos)
  • RDS Serverless: SQL si necesitas relaciones complejas

Proyecto Tutorial: API de Gestión de Tareas (TODO API)

Vamos a construir una API REST completa con:

  • POST /tasks – Crear tarea
  • GET /tasks – Listar todas las tareas
  • GET /tasks/{id} – Obtener tarea específica
  • PUT /tasks/{id} – Actualizar tarea
  • DELETE /tasks/{id} – Eliminar tarea

Stack tecnológico:

  • Lambda: Node.js 20.x
  • DynamoDB: tabla de tareas
  • API Gateway: REST API
  • CloudWatch: logs y métricas
  • SAM/Serverless Framework: deployment

Paso 1: Configuración Inicial de AWS

Prerequisitos:

bash# Instalar AWS CLI
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install

# Verificar instalación
aws --version

# Configurar credenciales
aws configure
# AWS Access Key ID: [tu_access_key]
# AWS Secret Access Key: [tu_secret_key]
# Default region: us-east-1
# Default output format: json

Crear usuario IAM con permisos:

bash# En AWS Console → IAM → Users → Create User
# Adjuntar políticas:
- AWSLambda_FullAccess
- AmazonDynamoDBFullAccess
- AmazonAPIGatewayAdministrator
- CloudWatchLogsFullAccess

Estructura del proyecto:

textserverless-todo-api/
├── functions/
│   ├── createTask.js
│   ├── getTasks.js
│   ├── getTask.js
│   ├── updateTask.js
│   └── deleteTask.js
├── config/
│   └── dynamodb.js
├── template.yaml (SAM)
├── package.json
└── .env

Paso 2: Crear Tabla DynamoDB

Diseño de tabla:

textTableName: Tasks
Primary Key: taskId (String)
Attributes:
  - taskId: UUID único
  - title: String
  - description: String
  - status: String (pending, in_progress, completed)
  - priority: String (low, medium, high)
  - createdAt: Timestamp
  - updatedAt: Timestamp

Crear tabla vía AWS CLI:

bashaws dynamodb create-table \
  --table-name Tasks \
  --attribute-definitions \
    AttributeName=taskId,AttributeType=S \
  --key-schema \
    AttributeName=taskId,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST \
  --region us-east-1

Verificar creación:

bashaws dynamodb describe-table --table-name Tasks

Configuración de DynamoDB en código (config/dynamodb.js):

javascriptconst { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
const { DynamoDBDocumentClient } = require('@aws-sdk/lib-dynamodb');

const client = new DynamoDBClient({ region: process.env.AWS_REGION || 'us-east-1' });
const dynamoDb = DynamoDBDocumentClient.from(client);

const TABLE_NAME = process.env.TABLE_NAME || 'Tasks';

module.exports = { dynamoDb, TABLE_NAME };

Paso 3: Implementar Lambda Functions

Instalar dependencias:

bashnpm init -y
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb uuid

Function 1: Crear Tarea (functions/createTask.js)

javascriptconst { PutCommand } = require('@aws-sdk/lib-dynamodb');
const { dynamoDb, TABLE_NAME } = require('../config/dynamodb');
const { v4: uuidv4 } = require('uuid');

exports.handler = async (event) => {
  try {
    // Parse request body
    const body = JSON.parse(event.body);
    
    // Validar campos requeridos
    if (!body.title) {
      return {
        statusCode: 400,
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ error: 'title es requerido' })
      };
    }

    // Crear objeto tarea
    const task = {
      taskId: uuidv4(),
      title: body.title,
      description: body.description || '',
      status: body.status || 'pending',
      priority: body.priority || 'medium',
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString()
    };

    // Guardar en DynamoDB
    await dynamoDb.send(new PutCommand({
      TableName: TABLE_NAME,
      Item: task
    }));

    return {
      statusCode: 201,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*' // CORS
      },
      body: JSON.stringify({
        message: 'Tarea creada exitosamente',
        task
      })
    };

  } catch (error) {
    console.error('Error creando tarea:', error);
    return {
      statusCode: 500,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ error: 'Error interno del servidor' })
    };
  }
};

Function 2: Listar Tareas (functions/getTasks.js)

javascriptconst { ScanCommand } = require('@aws-sdk/lib-dynamodb');
const { dynamoDb, TABLE_NAME } = require('../config/dynamodb');

exports.handler = async (event) => {
  try {
    // Query parameters para filtrado opcional
    const queryParams = event.queryStringParameters || {};
    const status = queryParams.status;

    let scanParams = {
      TableName: TABLE_NAME
    };

    // Filtro opcional por status
    if (status) {
      scanParams.FilterExpression = '#status = :status';
      scanParams.ExpressionAttributeNames = { '#status': 'status' };
      scanParams.ExpressionAttributeValues = { ':status': status };
    }

    const result = await dynamoDb.send(new ScanCommand(scanParams));

    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*'
      },
      body: JSON.stringify({
        count: result.Count,
        tasks: result.Items
      })
    };

  } catch (error) {
    console.error('Error obteniendo tareas:', error);
    return {
      statusCode: 500,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ error: 'Error interno del servidor' })
    };
  }
};

Function 3: Obtener Tarea por ID (functions/getTask.js)

javascriptconst { GetCommand } = require('@aws-sdk/lib-dynamodb');
const { dynamoDb, TABLE_NAME } = require('../config/dynamodb');

exports.handler = async (event) => {
  try {
    const taskId = event.pathParameters.id;

    const result = await dynamoDb.send(new GetCommand({
      TableName: TABLE_NAME,
      Key: { taskId }
    }));

    if (!result.Item) {
      return {
        statusCode: 404,
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ error: 'Tarea no encontrada' })
      };
    }

    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*'
      },
      body: JSON.stringify(result.Item)
    };

  } catch (error) {
    console.error('Error obteniendo tarea:', error);
    return {
      statusCode: 500,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ error: 'Error interno del servidor' })
    };
  }
};

Function 4: Actualizar Tarea (functions/updateTask.js)

javascriptconst { UpdateCommand } = require('@aws-sdk/lib-dynamodb');
const { dynamoDb, TABLE_NAME } = require('../config/dynamodb');

exports.handler = async (event) => {
  try {
    const taskId = event.pathParameters.id;
    const body = JSON.parse(event.body);

    // Construir expresión de actualización dinámica
    let updateExpression = 'SET updatedAt = :updatedAt';
    let expressionAttributeValues = {
      ':updatedAt': new Date().toISOString()
    };
    let expressionAttributeNames = {};

    if (body.title) {
      updateExpression += ', title = :title';
      expressionAttributeValues[':title'] = body.title;
    }
    if (body.description !== undefined) {
      updateExpression += ', description = :description';
      expressionAttributeValues[':description'] = body.description;
    }
    if (body.status) {
      updateExpression += ', #status = :status';
      expressionAttributeNames['#status'] = 'status';
      expressionAttributeValues[':status'] = body.status;
    }
    if (body.priority) {
      updateExpression += ', priority = :priority';
      expressionAttributeValues[':priority'] = body.priority;
    }

    const result = await dynamoDb.send(new UpdateCommand({
      TableName: TABLE_NAME,
      Key: { taskId },
      UpdateExpression: updateExpression,
      ExpressionAttributeValues: expressionAttributeValues,
      ExpressionAttributeNames: Object.keys(expressionAttributeNames).length > 0 
        ? expressionAttributeNames 
        : undefined,
      ReturnValues: 'ALL_NEW'
    }));

    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*'
      },
      body: JSON.stringify({
        message: 'Tarea actualizada exitosamente',
        task: result.Attributes
      })
    };

  } catch (error) {
    console.error('Error actualizando tarea:', error);
    return {
      statusCode: 500,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ error: 'Error interno del servidor' })
    };
  }
};

Function 5: Eliminar Tarea (functions/deleteTask.js)

javascriptconst { DeleteCommand } = require('@aws-sdk/lib-dynamodb');
const { dynamoDb, TABLE_NAME } = require('../config/dynamodb');

exports.handler = async (event) => {
  try {
    const taskId = event.pathParameters.id;

    await dynamoDb.send(new DeleteCommand({
      TableName: TABLE_NAME,
      Key: { taskId }
    }));

    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*'
      },
      body: JSON.stringify({
        message: 'Tarea eliminada exitosamente'
      })
    };

  } catch (error) {
    console.error('Error eliminando tarea:', error);
    return {
      statusCode: 500,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ error: 'Error interno del servidor' })
    };
  }
};

Paso 4: Configurar API Gateway y Deployment con SAM

AWS SAM Template (template.yaml):

textAWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Serverless TODO API con Lambda y DynamoDB

Globals:
  Function:
    Timeout: 10
    Runtime: nodejs20.x
    Environment:
      Variables:
        TABLE_NAME: !Ref TasksTable
        AWS_REGION: !Ref AWS::Region

Resources:
  # DynamoDB Table
  TasksTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: Tasks
      AttributeDefinitions:
        - AttributeName: taskId
          AttributeType: S
      KeySchema:
        - AttributeName: taskId
          KeyType: HASH
      BillingMode: PAY_PER_REQUEST

  # API Gateway
  TasksApi:
    Type: AWS::Serverless::Api
    Properties:
      Name: tasks-api
      StageName: prod
      Cors:
        AllowMethods: "'GET, POST, PUT, DELETE, OPTIONS'"
        AllowHeaders: "'Content-Type,Authorization'"
        AllowOrigin: "'*'"

  # Lambda Functions
  CreateTaskFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: functions/
      Handler: createTask.handler
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref TasksTable
      Events:
        CreateTask:
          Type: Api
          Properties:
            RestApiId: !Ref TasksApi
            Path: /tasks
            Method: POST

  GetTasksFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: functions/
      Handler: getTasks.handler
      Policies:
        - DynamoDBReadPolicy:
            TableName: !Ref TasksTable
      Events:
        GetTasks:
          Type: Api
          Properties:
            RestApiId: !Ref TasksApi
            Path: /tasks
            Method: GET

  GetTaskFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: functions/
      Handler: getTask.handler
      Policies:
        - DynamoDBReadPolicy:
            TableName: !Ref TasksTable
      Events:
        GetTask:
          Type: Api
          Properties:
            RestApiId: !Ref TasksApi
            Path: /tasks/{id}
            Method: GET

  UpdateTaskFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: functions/
      Handler: updateTask.handler
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref TasksTable
      Events:
        UpdateTask:
          Type: Api
          Properties:
            RestApiId: !Ref TasksApi
            Path: /tasks/{id}
            Method: PUT

  DeleteTaskFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: functions/
      Handler: deleteTask.handler
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref TasksTable
      Events:
        DeleteTask:
          Type: Api
          Properties:
            RestApiId: !Ref TasksApi
            Path: /tasks/{id}
            Method: DELETE

Outputs:
  ApiUrl:
    Description: URL del API Gateway
    Value: !Sub "https://${TasksApi}.execute-api.${AWS::Region}.amazonaws.com/prod"

Desplegar con SAM:

bash# Instalar SAM CLI
brew install aws-sam-cli  # macOS
# o pip install aws-sam-cli

# Build
sam build

# Deploy
sam deploy --guided
# Stack Name: serverless-todo-api
# AWS Region: us-east-1
# Confirm changes: Y
# Allow SAM CLI IAM role creation: Y
# CreateTaskFunction may not have authorization: Y
# Save arguments to config: Y

# Output mostrará URL del API
# Outputs:
# ApiUrl: https://abc123xyz.execute-api.us-east-1.amazonaws.com/prod

Paso 5: Probar la API

Crear tarea:

bashcurl -X POST https://tu-api-url/prod/tasks \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Implementar autenticación",
    "description": "Agregar JWT auth a la API",
    "status": "pending",
    "priority": "high"
  }'

Listar tareas:

bashcurl https://tu-api-url/prod/tasks

Filtrar por status:

bashcurl "https://tu-api-url/prod/tasks?status=pending"

Obtener tarea específica:

bashcurl https://tu-api-url/prod/tasks/uuid-de-la-tarea

Actualizar tarea:

bashcurl -X PUT https://tu-api-url/prod/tasks/uuid-de-la-tarea \
  -H "Content-Type: application/json" \
  -d '{"status": "completed"}'

Eliminar tarea:

bashcurl -X DELETE https://tu-api-url/prod/tasks/uuid-de-la-tarea

Paso 6: Monitoreo con CloudWatch

Ver logs de Lambda:

bash# Listar log groups
aws logs describe-log-groups --log-group-name-prefix /aws/lambda/

# Ver logs en tiempo real
aws logs tail /aws/lambda/serverless-todo-api-CreateTaskFunction --follow

Crear alarma de errores:

bashaws cloudwatch put-metric-alarm \
  --alarm-name lambda-errors \
  --alarm-description "Alerta si >5 errores en 5 minutos" \
  --metric-name Errors \
  --namespace AWS/Lambda \
  --statistic Sum \
  --period 300 \
  --threshold 5 \
  --comparison-operator GreaterThanThreshold \
  --evaluation-periods 1 \
  --dimensions Name=FunctionName,Value=CreateTaskFunction

Métricas clave a monitorear:

  • Invocations: cantidad de ejecuciones
  • Duration: tiempo de ejecución promedio
  • Errors: errores no capturados
  • Throttles: requests rechazados por límites de concurrencia
  • ConcurrentExecutions: ejecuciones simultáneas

Optimización de Costos

Cálculo de costos Lambda:

textCosto = (requests × $0.20/millón) + (GB-segundos × $0.0000166667)

Ejemplo:
- 1 millón requests/mes
- 512MB RAM por función
- 200ms duración promedio

Requests: 1M × $0.20/1M = $0.20
Compute: (0.5GB × 0.2s × 1M) × $0.0000166667 = $1.67
Total: $1.87/mes

Free tier: 1M requests + 400,000 GB-segundos gratis/mes

Optimizaciones:

  1. Ajustar memoria: más RAM = más CPU = ejecución más rápida; encuentra sweet spot
  2. Minimizar cold starts: provisioned concurrency para funciones críticas
  3. DynamoDB on-demand: perfecto para tráfico variable; no pagas por capacidad sin usar
  4. Comprimir responses: reduce data transfer costs
  5. Caché con Lambda@Edge: reduce invocaciones para contenido estático/semi-estático

Mejores Prácticas de Seguridad

1. IAM Roles con mínimos privilegios

textPolicies:
  - DynamoDBCrudPolicy:
      TableName: !Ref TasksTable  # Solo esta tabla
  # No usar FullAccess en producción

2. Variables de entorno cifradas

bashaws lambda update-function-configuration \
  --function-name CreateTaskFunction \
  --environment "Variables={DB_PASSWORD=$(aws kms encrypt --key-id alias/my-key --plaintext 'secret' --output text --query CiphertextBlob)}"

3. API Gateway con autenticación

textTasksApi:
  Type: AWS::Serverless::Api
  Properties:
    Auth:
      DefaultAuthorizer: MyCognitoAuthorizer
      Authorizers:
        MyCognitoAuthorizer:
          UserPoolArn: !GetAtt MyCognitoUserPool.Arn

4. Rate limiting

textTasksApi:
  Properties:
    ThrottleSettings:
      RateLimit: 100
      BurstLimit: 200

5. Validación de input

javascript// Usar librería de validación
const Joi = require('joi');

const taskSchema = Joi.object({
  title: Joi.string().min(3).max(100).required(),
  description: Joi.string().max(500),
  status: Joi.string().valid('pending', 'in_progress', 'completed'),
  priority: Joi.string().valid('low', 'medium', 'high')
});

const { error, value } = taskSchema.validate(body);
if (error) {
  return { statusCode: 400, body: JSON.stringify({ error: error.details[0].message }) };
}

Conclusión: Serverless como Ventaja Competitiva

Arquitectura serverless en AWS

La arquitectura serverless con AWS Lambda y DynamoDB no es solo una elección tecnológica; es una ventaja estratégica que permite iterar más rápido, escalar sin fricción y optimizar costos dramáticamente comparado con arquitecturas tradicionales. Lo que antes requería equipos de DevOps gestionando clusters Kubernetes ahora se reduce a escribir funciones y desplegar con sam deploy.

Este tutorial te dio fundamentos productivos: API REST completa, persistencia escalable, deployment automatizado y monitoreo básico. Desde aquí, expande con autenticación (Cognito), websockets (API Gateway WebSocket), procesamiento asíncrono (SQS + Lambda), CI/CD (GitHub Actions + SAM) y arquitecturas event-driven complejas (EventBridge, Step Functions).

El futuro del desarrollo backend es serverless-first: menos tiempo gestionando infraestructura, más tiempo construyendo features que usuarios aman. Empieza hoy, despliega tu primera función Lambda y experimenta la libertad de no preocuparte por servidores nunca más.

Deja un comentario