Skip to content
StackPractices
advanced Por Mathias Paulenko

Referencia Detallada de Arquitectura Serverless

Decidir cuando ir serverless y cuando no. Cubre patrones FaaS, diseno event-driven, cold starts, modelos de costo, vendor lock-in y estrategias de migracion para aplicaciones serverless de produccion.

Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.

Introducción

La arquitectura serverless promete escalado infinito, cero mantenimiento, y pricing pay-per-use. La realidad es mas matizada. Serverless destaca para ciertos workloads y lucha con otros. Elegir serverless por las razones equivocadas lleva a costos inesperados, latencia de cold start, y pesadillas de debugging. Esta guia te ayuda a decidir cuando serverless es la opcion correcta y como arquitectar aplicaciones serverless que funcionen en produccion.

Qué Significa Realmente Serverless

Serverless no significa “sin servidores.” Significa que tu no gestionas servidores. El cloud provider maneja provisioning, escalado, patching, y disponibilidad. Tu escribes funciones y configuras triggers.

Tradicional:    Tu gestionas VMs → Tu deployas app → Tu escalas → Tu patcheas
Contenedor:     Tu builds images → Orchestrator deploya → Tu configuras escalado
Serverless:     Tu escribes funciones → Provider maneja todo lo demas

Características Clave

  • Sin gestion de servidores: Sin patching de OS, sin capacity provisioning
  • Auto-escalado: Escala de 0 a miles de ejecuciones concurrentes
  • Pay-per-use: Facturado por tiempo de ejecucion y memoria, no por idle time
  • Event-driven: Funciones triggeradas por HTTP requests, queue messages, timers, o cloud events
  • Stateless: Cada invocacion es independiente; sin estado en memoria entre llamadas

Cuándo Ir Serverless

Buen Fit: Workloads Event-Driven

# AWS Lambda triggerada por S3 upload
import json
import boto3

def lambda_handler(event, context):
    for record in event["Records"]:
        bucket = record["s3"]["bucket"]["name"]
        key = record["s3"]["object"]["key"]
        
        # Procesar archivo subido
        process_file(bucket, key)
    
    return {"statusCode": 200, "body": json.dumps({"processed": len(event["Records"])})}

Serverless brilla para workloads que son:

  • Intermitentes: El trafico sube y baja impredeciblemente
  • Event-driven: Triggerados por uploads, messages, o webhooks
  • Short-lived: Cada ejecucion se completa en segundos, no horas
  • Stateless: Sin necesidad de conexiones long-running o estado en memoria

Buen Fit: HTTP APIs con Trafico Variable

# API Gateway + Lambda para endpoints REST
def lambda_handler(event, context):
    http_method = event["httpMethod"]
    path = event["path"]
    
    if path == "/users" and http_method == "GET":
        users = db.users.list(limit=100)
        return {"statusCode": 200, "body": json.dumps(users)}
    
    if path == "/users" and http_method == "POST":
        body = json.loads(event["body"])
        user = db.users.create(body)
        return {"statusCode": 201, "body": json.dumps(user)}
    
    return {"statusCode": 404, "body": json.dumps({"error": "Not found"})}

Buen Fit: Tareas Programadas

# CloudWatch Events + Lambda para cron jobs
def lambda_handler(event, context):
    # Ejecutar limpieza nocturna
    delete_expired_sessions()
    send_daily_reports()
    update_aggregates()
    
    return {"status": "complete"}

Buen Fit: Stream Processing

# Lambda procesando DynamoDB streams
def lambda_handler(event, context):
    for record in event["Records"]:
        if record["eventName"] == "INSERT":
            new_item = record["dynamodb"]["NewImage"]
            # Enviar email de bienvenida para nuevos usuarios
            if new_item.get("type", {}).get("S") == "user":
                send_welcome_email(new_item["email"]["S"])
    
    return {"status": "processed"}

Cuándo NO Ir Serverless

Mal Fit: Tareas Long-Running

AWS Lambda tiene un timeout de 15 minutos. Para tareas que duran mas (video encoding, procesamiento de datos grande), usa contenedores o VMs.

Mal Fit: Aplicaciones Tiempo Real

WebSockets, streaming, y long-polling requieren conexiones persistentes. Las funciones serverless son stateless y short-lived. Usa servidores dedicados o managed WebSocket services.

Mal Fit: High-Performance Computing

Las funciones serverless tienen CPU y memoria limitados (hasta 10GB RAM en Lambda). Para workloads CPU-intensive (ML training, simulaciones), usa GPU instances o HPC clusters.

Mal Fit: Trafico Constante y Predecible

Si tu trafico es constante 24/7, serverless es mas caro que reserved instances. Un container service con auto-scaling te da el mismo escalado a menor costo.

Mal Fit: Workflows Stateful Complejos

Las funciones serverless son stateless. Los workflows stateful complejos (shopping carts, multi-step forms) requieren gestion de estado externa (base de datos, Redis), anadiendo latencia y complejidad.

Patrones de Diseno Serverless

Composición de Funciones

Divide operaciones complejas en funciones mas pequenas que se encadenan.

# Step Functions state machine para procesamiento de ordenes
{
  "StartAt": "ValidateOrder",
  "States": {
    "ValidateOrder": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123:function:validate-order",
      "Next": "ChargePayment"
    },
    "ChargePayment": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123:function:charge-payment",
      "Next": "CheckResult"
    },
    "CheckResult": {
      "Type": "Choice",
      "Choices": [
        {"Variable": "$.paymentStatus", "StringEquals": "success", "Next": "ShipOrder"},
        {"Variable": "$.paymentStatus", "StringEquals": "failed", "Next": "NotifyFailure"}
      ]
    },
    "ShipOrder": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123:function:ship-order",
      "End": true
    },
    "NotifyFailure": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123:function:notify-failure",
      "End": true
    }
  }
}

Fan-Out / Fan-In

Una funcion triggera multiples funciones paralelas, luego agrega resultados.

import boto3
import json
from concurrent.futures import ThreadPoolExecutor

lambda_client = boto3.client("lambda")

def fan_out_handler(event, context):
    items = event["items"]
    
    # Invocar processor para cada item en paralelo
    with ThreadPoolExecutor(max_workers=10) as executor:
        futures = []
        for item in items:
            futures.append(
                executor.submit(
                    lambda_client.invoke,
                    FunctionName="process-item",
                    Payload=json.dumps({"item": item})
                )
            )
        
        results = [json.loads(f.result()["Payload"].read()) for f in futures]
    
    # Agregar resultados
    return {"processed": len(results), "results": results}

Event Sourcing

Almacena events como la fuente de verdad. Las funciones reaccionan a events y proyectan estado a read models.

# Funcion 1: Guardar event
def save_event_handler(event, context):
    event_store.append({
        "aggregate_id": event["aggregateId"],
        "event_type": event["type"],
        "data": event["data"],
        "timestamp": event["timestamp"]
    })
    return {"status": "saved"}

# Funcion 2: Proyectar a read model (triggerada por event store)
def project_handler(event, context):
    for record in event["Records"]:
        event_data = json.loads(record["body"])
        
        if event_data["event_type"] == "UserCreated":
            db.users.upsert({
                "id": event_data["aggregate_id"],
                "name": event_data["data"]["name"],
                "email": event_data["data"]["email"]
            })

Cold Starts

Los cold starts ocurren cuando una funcion no ha sido invocada recientemente. El provider necesita provisionar un nuevo container, cargar el runtime, y ejecutar tu codigo. Esto anade 1-10 segundos de latencia.

Factores de Cold Start

FactorImpactoMitigacion
RuntimeJava > .NET > Python > Node.js > GoElegir runtimes de inicio rapido (Go, Node.js)
Tamano de paqueteMas grande = cold start mas lentoMinimizar dependencias
Configuracion VPCVPC anade 1-2s cold startUsar VPC-less si es posible
Asignacion de memoriaMas memoria = inicio mas rapidoAsignar 512MB+ para funciones CPU-heavy
Provisioned concurrencyElimina cold startPagar por instancias always-warm

Mitigando Cold Starts

# Lazy initialization: cargar dependencias pesadas fuera del handler
import boto3
import json

# Estos cargan solo en cold start
s3 = boto3.client("s3")
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("users")

def lambda_handler(event, context):
    # Handler es rapido — dependencias ya cargadas
    user_id = event["pathParameters"]["userId"]
    response = table.get_item(Key={"id": user_id})
    
    return {
        "statusCode": 200,
        "body": json.dumps(response.get("Item", {}))
    }

Provisioned Concurrency

Para funciones sensibles a latencia, usa provisioned concurrency para mantener instancias warm.

# AWS CLI: asignar provisioned concurrency
aws lambda put-provisioned-concurrency-config \
  --function-name my-api \
  --qualifier live \
  --provisioned-concurrent-executions 10

Modelo de Costo

Cómo Funciona el Pricing Serverless

Costo = (Numero de requests) × (Duracion de ejecucion en ms) × (Memoria asignada) × (Precio por GB-ms)

Pricing de AWS Lambda (ejemplo):

  • $0.20 por 1 millon de requests
  • $0.0000166667 por GB-segundo
  • Free tier: 1 millon de requests + 400,000 GB-segundos por mes

Ejemplo de Comparacion de Costo

# Escenario: 10 millones de requests/mes, 100ms cada uno, 512MB memoria

# Serverless (Lambda)
requests_cost = 10_000_000 / 1_000_000 * 0.20  # $2.00
compute_cost = 10_000_000 * 0.1 * 0.5 / 1024 * 0.0000166667 * 1000  # ~$8.13
total_serverless = requests_cost + compute_cost  # ~$10.13/mes

# Contenedor (ECS Fargate: 0.5 vCPU, 1GB RAM, always running)
container_cost = 730 * (0.025 * 0.5 + 0.003 * 1)  # ~$10.22/mes

# Break-even: ~10M requests/mes a 100ms cada uno
# Menos de 10M: serverless es mas barato
# Mas de 10M: contenedor es mas barato

Tips de Optimizacion de Costo

  • Right-size memory: Mas memoria puede reducir tiempo de ejecucion, bajando costo total
  • Reducir tiempo de ejecucion: Optimizar codigo para terminar mas rapido
  • Usar caching: Cachear computaciones costosas en Redis o DynamoDB DAX
  • Evitar over-provisioning: Empezar con 128MB y aumentar solo si es necesario
  • Monitorear costos: Setear billing alerts para detectar spikes inesperados

Vendor Lock-In

Las plataformas serverless difieren considerablemente. Mover de AWS Lambda a Google Cloud Functions o Azure Functions requiere reescribir handlers, triggers, y configuracion de infraestructura.

Factores de Lock-In

FactorAWS LambdaGoogle Cloud FunctionsAzure Functions
Handler signaturelambda_handler(event, context)handler(request)main(req: func.HttpRequest)
TriggersAPI Gateway, S3, SQS, DynamoDBHTTP, Pub/Sub, Cloud StorageHTTP, Blob, Queue, Event Grid
Infra as CodeCloudFormation, SAM, CDKCloud Deployment ManagerARM, Bicep
Gestion de estadoStep FunctionsWorkflowsDurable Functions

Reduciendo Lock-In

# Abstraer el handler de la logica de negocio
def process_order(order_data: dict) -> dict:
    # Logica de negocio pura — sin codigo cloud-specific
    validate(order_data)
    charge_payment(order_data)
    ship_order(order_data)
    return {"status": "complete"}

# AWS Lambda handler
def lambda_handler(event, context):
    order_data = json.loads(event["body"])
    result = process_order(order_data)
    return {"statusCode": 200, "body": json.dumps(result)}

# Google Cloud Functions handler
def handler(request):
    order_data = request.get_json()
    result = process_order(order_data)
    return json.dumps(result)

Estrategia de Migración

Patrón Strangler Fig

Reemplaza gradualmente un monolito con funciones serverless. Rutea trafico a serverless para nuevas features primero, luego reemplaza progresivamente funcionalidad existente.

# API Gateway: rutar nuevos endpoints a Lambda, viejos a monolito
{
  "routes": [
    {"path": "/api/v2/users", "target": "lambda:users-v2"},
    {"path": "/api/v1/*", "target": "http://monolith.example.com"}
  ]
}

Pasos de Migración

  1. Identificar candidatos: Encontrar endpoints intermitentes, event-driven, o stateless
  2. Empezar pequeno: Migrar un endpoint a serverless
  3. Testear exhaustivamente: Verificar performance, costo, y confiabilidad
  4. Rutar trafico: Usar API Gateway para dividir trafico entre viejo y nuevo
  5. Monitorear: Comparar metricas entre serverless y tradicional
  6. Expandir: Migrar mas endpoints a medida que crece la confianza
  7. Descommissionar: Remover infraestructura vieja cuando la migracion este completa

Observabilidad

Las funciones serverless son mas dificiles de debuggear que las aplicaciones tradicionales. No puedes SSH a una funcion o attachar un debugger.

Logging Estructurado

import json
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    request_id = context.aws_request_id
    
    logger.info(json.dumps({
        "request_id": request_id,
        "event": "function_start",
        "user_id": event.get("user_id"),
        "action": event.get("action")
    }))
    
    try:
        result = process_request(event)
        logger.info(json.dumps({
            "request_id": request_id,
            "event": "function_success",
            "duration_ms": 150
        }))
        return result
    except Exception as e:
        logger.error(json.dumps({
            "request_id": request_id,
            "event": "function_error",
            "error": str(e)
        }))
        raise

Distributed Tracing

Usa AWS X-Ray u OpenTelemetry para tracear requests across multiples funciones.

from aws_xray_sdk.core import patch_all
import boto3

patch_all()

def lambda_handler(event, context):
    # X-Ray automaticamente tracea esta funcion
    # y cualquier AWS SDK call que haga
    dynamodb = boto3.resource("dynamodb")
    table = dynamodb.Table("users")
    response = table.get_item(Key={"id": event["user_id"]})
    return response.get("Item")

Seguridad

IAM Least Privilege

Otorga a las funciones solo los permisos que necesitan.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["dynamodb:GetItem", "dynamodb:PutItem"],
      "Resource": "arn:aws:dynamodb:us-east-1:123:table/users"
    }
  ]
}

Variables de Entorno para Secrets

Nunca hardcodees secrets. Usa variables de entorno con AWS Secrets Manager o Parameter Store.

import os
import boto3

def get_secret():
    secret_name = os.environ["DB_SECRET_NAME"]
    client = boto3.client("secretsmanager")
    response = client.get_secret_value(SecretId=secret_name)
    return json.loads(response["SecretString"])

Checklist de Producción

  • Funciones son stateless e idempotent
  • Mitigacion de cold start (provisioned concurrency o lazy init)
  • Asignacion de memoria right-sized
  • Timeout seteado apropiadamente (no el max por defecto)
  • Roles IAM siguen least privilege
  • Secrets en Secrets Manager o Parameter Store
  • Logging estructurado con request IDs
  • Distributed tracing habilitado
  • Dead letter queue para invocaciones async fallidas
  • Estrategia de retry configurada
  • Monitoreo de costo y billing alerts
  • Deployment via Infrastructure as Code (SAM, CDK, Serverless Framework)

Preguntas Frecuentes

¿Es serverless mas barato que contenedores?

Depende de los patrones de trafico. Para trafico intermitente o impredecible, serverless es mas barato (sin costos idle). Para trafico constante alto, contenedores son mas baratos (capacidad reservada). El break-even point es tipicamente alrededor de 60-70% de utilizacion sostenida.

¿Cómo manejo tareas long-running en serverless?

Dividelas en chunks mas pequenos usando Step Functions o un patron basado en queue. Cada funcion deberia completar dentro del limite de timeout. Para tareas verdaderamente long-running (horas), usa AWS Batch o Fargate en lugar de Lambda.

¿Puedo usar serverless para websockets?

No directamente. Usa AWS API Gateway WebSocket API con Lambda, o un managed service como AWS AppSync subscriptions o Pusher. La conexion WebSocket es gestionada por API Gateway; Lambda maneja los messages.

¿Cómo testeo funciones serverless localmente?

Usa AWS SAM CLI (sam local invoke) o Serverless Framework’s serverless invoke local. Estos emulan el runtime de Lambda localmente. Para integration tests, usa LocalStack para emular AWS services.

¿Qué es provisioned concurrency?

Provisioned concurrency mantiene un numero especificado de instancias de funcion warm y listas para responder inmediatamente. Pagas por la capacidad provisionada independientemente de las invocaciones. Usalo para endpoints sensibles a latencia donde los cold starts son inaceptables.

¿Cómo manejo conexiones de base de datos en serverless?

Cada invocacion de funcion puede abrir una nueva conexion de base de datos, agotando connection pools. Usa un connection proxy como Amazon RDS Proxy o pgBouncer. Alternativamente, usa DynamoDB (sin connection pools) o Aurora Serverless (conexiones auto-scaling).

See Also

Guide

Complete Guide to Redis Caching Strategies

Master Redis caching with cache-aside, read-through, write-through, write-behind, and refresh-ahead patterns. Covers eviction policies, TTL tuning, serialization, and production operations.

Pattern

Circuit Breaker Pattern

Prevent cascading failures by stopping requests to failing services. An architectural pattern for resilient distributed systems.

Pattern

Strangler Fig: Gradually Replace Legacy by Intercepting

How to gradually replace a legacy system by intercepting routes and routing traffic to new services. Covers strangler fig, incremental migration, and cutover.

Recipe

Orchestrate Serverless Workflows with AWS Step Functions

Build state machine workflows with AWS Step Functions using sequential, parallel, and map states for orchestrating Lambda functions and long-running processes.

Guide

Complete Guide to Apache Kafka in Production

Run Apache Kafka in production with confidence. Covers partitions, replication, consumer groups, monitoring, performance tuning, and operational best practices for high-throughput streaming pipelines.

Guide

Complete Guide to AWS Lambda in Production

Run AWS Lambda in production with confidence. Covers cold start optimization, layers, deployment patterns, observability with X-Ray, security hardening, connection pooling, and cost tuning for production workloads.

Guide

Complete Guide to Serverless Databases

Choose and operate serverless databases for event-driven applications. Covers DynamoDB, Aurora Serverless, FaunaDB, and PlanetScale with pricing, scaling, query patterns, and migration strategies.