StackPractices
intermediate Por Mathias Paulenko

Construir funciones serverless

Crea y despliega funciones serverless con AWS Lambda, Google Cloud Functions y Azure Functions para computación event-driven y pago por uso.

Visión General

La computación serverless ejecuta tu código sin que aprovisiones ni parchees un solo servidor. Escribes funciones, las subes a un proveedor de nube y la plataforma se encarga del escalado y la disponibilidad por su cuenta. Pagas solo por el tiempo de ejecución — por eso encaja tan bien con cargas esporádicas y sistemas por eventos.

Esta receta recorre la creación y el despliegue de funciones en las tres grandes nubes — AWS Lambda, Google Cloud Functions y Azure Functions — con código que corre tal cual.

Cuándo Usar

Usa este recurso cuando:

  • Tienes cargas de trabajo por eventos (webhooks, procesamiento de archivos, jobs programados). Consulta Microservicios Event-Driven para patrones por eventos.
  • Quieres escalado automático de cero a miles de peticiones. Consulta Optimización de Cold Start para minimizar la latencia de arranque.
  • Necesitas evitar el mantenimiento de servidores y la sobrecarga de infraestructura.
  • Tu tráfico es esporádico y aprovisionar servidores sería un desperdicio. Consulta API Gateway Serverless para construir APIs de pago por uso.

Solución

Python (AWS Lambda)

import json

def handler(event, context):
    # Extraer parámetro de query
    name = event.get("queryStringParameters", {}).get("name", "World")

    return {
        "statusCode": 200,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps({"message": f"Hello, {name}!"})
    }

JavaScript (Google Cloud Functions)

const functions = require('@google-cloud/functions-framework');

functions.http('helloHttp', (req, res) => {
  const name = req.query.name || 'World';
  res.json({ message: `Hello, ${name}!` });
});

Java (Azure Functions)

import com.microsoft.azure.functions.*;
import java.util.Optional;

public class HelloFunction {
    @FunctionName("hello")
    public HttpResponseMessage run(
        @HttpTrigger(name = "req", methods = {HttpMethod.GET}, authLevel = AuthorizationLevel.ANONYMOUS)
        HttpRequestMessage<Optional<String>> request,
        final ExecutionContext context) {

        String name = request.getQueryParameters().getOrDefault("name", "World");
        return request.createResponseBuilder(HttpStatus.OK)
            .body("Hello, " + name + "!")
            .build();
    }
}

Explicación

Las plataformas serverless abstraen la gestión de infraestructura:

  • Triggers de eventos: peticiones HTTP, subidas de archivos, cambios en base de datos, temporizadores.
  • Escalado automático: cada invocación corre en un contenedor fresco; la plataforma sube y baja contenedores según demanda.
  • Pago por uso: se factura por milisegundos de ejecución y número de invocaciones.

Los cold starts ocurren cuando una función lleva tiempo sin invocarse: un contenedor nuevo tiene que arrancar primero, lo que suma entre 100ms y 3s según el runtime y la memoria asignada.

Variantes

PlataformaRuntimeTipos de triggerCold Start
AWS LambdaPython, Node, Java, Go, RubyHTTP, S3, SNS, SQS, EventBridge, Cron100ms–1s
Cloud FunctionsNode, Python, Go, JavaHTTP, Pub/Sub, Storage, Firestore, Cron200ms–2s
Azure FunctionsNode, Python, Java, C#HTTP, Blob, Queue, Event Grid, Timer200ms–3s

Lo que funciona

  • Mantén las funciones pequeñas y enfocadas: una función por responsabilidad; compón workflows complejos con step functions.
  • Minimiza el tamaño del paquete de despliegue: quita dependencias innecesarias para reducir el cold start.
  • Usa concurrencia provisionada en rutas sensibles a latencia: contenedores precalentados para el camino crítico.
  • Guarda el estado fuera: las funciones son stateless; DynamoDB, Redis o Firestore para persistencia.
  • Configura memoria y timeout con criterio: poca memoria causa OOM; demasiada desperdicia dinero (y Lambda escala la CPU con la memoria).

Errores Comunes

  • Guardar estado en el contenedor: las variables locales se pierden entre invocaciones; el contenedor puede reutilizarse, pero no cuentes con ello.
  • Ignorar los cold starts: las APIs síncronas de cara al usuario sufren esa latencia extra.
  • Sobreaprovisionar memoria: Lambda asigna CPU proporcional a la memoria; busca el punto justo.
  • No manejar fallos parciales: el procesamiento por lotes debe reintentar sin duplicar trabajo.
  • Amarrarse a un vendor: capas de abstracción (Serverless Framework, SAM) facilitan la portabilidad.

Avanzado: Mitigación de Cold Start

# AWS Lambda: inicializa fuera del handler para reutilizar conexiones
import json
import boto3

# Esto corre solo en cold start — se reutiliza entre invocaciones
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('users')

def handler(event, context):
    # El handler se mantiene delgado — las conexiones ya están calientes
    user_id = event['pathParameters']['id']
    response = table.get_item(Key={'id': user_id})
    return {
        'statusCode': 200,
        'body': json.dumps(response.get('Item', {}))
    }

Inicializa conexiones de base de datos, clientes HTTP e imports pesados fuera del handler. La plataforma reutiliza el contenedor entre invocaciones, así que esas inicializaciones corren una sola vez en cold start y persisten. El cuerpo del handler queda mínimo: procesa el evento y devuelve.

Avanzado: Orquestación con Step Functions

{
  "StartAt": "ValidateOrder",
  "States": {
    "ValidateOrder": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123:function:ValidateOrder",
      "Next": "CheckInventory"
    },
    "CheckInventory": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123:function:CheckInventory",
      "Next": "ProcessPayment"
    },
    "ProcessPayment": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123:function:ProcessPayment",
      "End": true
    }
  }
}

Step Functions encadena invocaciones Lambda con reintentos, manejo de errores y gestión de estado integrados. Cada paso se reintenta de forma independiente. Usa estados Choice para branching condicional, Parallel para ejecución concurrente y Map para fan-out.

Avanzado: Concurrencia Provisionada

# AWS CLI: configurar concurrencia provisionada
aws lambda put-provisioned-concurrency-config \
  --function-name my-api \
  --qualifier production \
  --provisioned-concurrent-executions 10

La concurrencia provisionada mantiene entornos calientes listos para responder. Configúrala en endpoints sensibles a latencia donde el cold start es inaceptable, y vigila la utilización — si la capacidad queda ociosa, redúcela. Para tráfico con picos, combínala con escalado on-demand.

Avanzado: Arquitectura por Eventos

flowchart diagram: S3[(Subida a S3)
import json
import boto3

s3 = boto3.client('s3')
sns = boto3.client('sns')

def handler(event, context):
    # Disparada por una subida a S3
    for record in event['Records']:
        bucket = record['s3']['bucket']['name']
        key = record['s3']['object']['key']

        # Procesar el archivo subido
        response = s3.get_object(Bucket=bucket, Key=key)
        content = response['Body'].read().decode('utf-8')

        # Publicar el resultado a SNS
        sns.publish(
            TopicArn='arn:aws:sns:us-east-1:123:processing-complete',
            Message=json.dumps({
                'file': key,
                'size': len(content),
                'lines': content.count('\n')
            })
        )

    return {'statusCode': 200, 'body': json.dumps({'processed': len(event['Records'])})}

Las funciones por eventos responden a eventos de la nube: subidas a S3, DynamoDB streams, mensajes de SQS, notificaciones SNS o schedules de EventBridge. Cada evento trae registros que tu handler procesa de forma independiente. Diseña handlers idempotentes porque las fuentes pueden entregar duplicados, y usa partial batch responses (Lambda) para reportar qué registros fallaron y deben reintentarse.

Avanzado: Respuestas Parciales por Lotes

SQS entrega mensajes por lotes — si un registro falla, el lote entero se reintenta salvo que indiques lo contrario. Reportar los fallos por registro le dice a Lambda que reintente solo los rotos:

import json

def handler(event, context):
    failed = []

    for record in event['Records']:
        try:
            process(record)  # tu lógica de negocio por mensaje
        except Exception:
            # Acumula el messageId fallido — el resto queda consumido
            failed.append({"itemIdentifier": record['messageId']})

    # Solo los items listados vuelven a la cola (y luego a la DLQ)
    return {"batchItemFailures": failed}

El contrato: el handler devuelve batchItemFailures con los messageId que deben reintentarse; todo lo no listado cuenta como consumido. Sin esto, un registro envenenado arrastra el lote completo — hasta maxReceiveCount veces antes de que la DLQ lo vea.

Avanzado: Desarrollo Local

# template.yaml — AWS SAM desarrollo local
Resources:
  ProcessUpload:
    Type: AWS::Serverless::Function
    Properties:
      Handler: app.handler
      Runtime: python3.11
      Events:
        Upload:
          Type: S3
          Properties:
            Bucket: uploads
            Events: s3:ObjectCreated:*

# Ejecutar en local:
# sam local start-api
# sam local invoke ProcessUpload --event events/s3-upload.json
# sam local generate-event s3 --bucket uploads --key data.csv > events/s3-upload.json

AWS SAM CLI, Azure Functions Core Tools y el emulador de Cloud Functions ofrecen entornos de ejecución locales. Genera eventos de muestra con sam local generate-event para probar la lógica del handler sin desplegar, y usa sam local start-api para probar funciones HTTP con curl o Postman. Aun así, corre un test de integración final en la nube — los emuladores no replican todos los comportamientos de la plataforma.

Cuándo Evitar

  • Computaciones de larga duración: tareas que superan los 15 minutos (Lambda) necesitan cómputo por contenedores (ECS, Cloud Run, EKS).
  • Aplicaciones con estado: juegos en tiempo real, servidores WebSocket o streaming necesitan conexiones persistentes.
  • Invocaciones de alta frecuencia: si tu función corre más de 1000 veces por segundo, un servidor siempre activo puede salir más barato.
  • Dependencias complejas: librerías nativas grandes o runtimes propios aumentan el cold start y la complejidad del despliegue.

Troubleshooting

  • Cold start alto: sube la concurrencia provisionada, reduce el paquete y evita inicializar clientes pesados por invocación.
  • La función hace timeout: revisa dependencias downstream, asignación de memoria y lógica de reintentos. Sube el timeout solo después de optimizar el código.
  • Estado perdido entre invocaciones: las funciones serverless son stateless — persiste el estado en una base de datos, caché o cola durable.
  • Paquete de despliegue demasiado grande: excluye dependencias de desarrollo y assets sin uso.
  • Problemas de orden de eventos: muchas fuentes son at-least-once y sin orden. Diseña para idempotencia y secuenciación explícita.

Véase También

Preguntas frecuentes

¿Cómo reduzco la latencia de cold start?

Usa runtimes ligeros (Node.js, Python) en lugar de Java, recorta el tamaño del paquete e inicializa los clientes pesados fuera del handler para que los contenedores calientes los reutilicen. En rutas de cara al usuario donde eso no basta, la concurrencia provisionada mantiene entornos precalentados.

¿Las funciones serverless soportan tareas de larga duración?

AWS Lambda corta a los 15 minutos, Cloud Functions a los 60 y Azure es configurable según el plan. Para trabajo más largo, divídelo: pon la tarea en SQS y deja que un worker la procese en chunks escribiendo progreso a DynamoDB, u orquesta funciones cortas con Step Functions. Pasada la hora, Fargate o Cloud Run son mejor casa.

¿Cómo depuro funciones serverless en local?

AWS SAM CLI, Azure Functions Core Tools y el Functions Framework traen emuladores locales — sam local invoke con un evento generado cubre la mayoría de los bugs del handler. Reprueba siempre en la nube: los permisos IAM, los timeouts y la forma de los eventos difieren del emulador.

¿Cómo monitorizo funciones serverless en producción?

Emite logs JSON estructurados (caen en CloudWatch/Stackdriver), sigue conteo de invocaciones, duración, tasa de errores y porcentaje de cold starts, y alarma sobre tasa de errores y duración p99. El tracing distribuido (X-Ray, Cloud Trace) muestra el recorrido de la petición entre funciones y llamadas downstream.

¿Cómo manejo reintentos e idempotencia?

Haz los handlers idempotentes — procesar el mismo evento dos veces debe dar el mismo resultado — usando una clave de idempotencia como el ID del evento más un almacén de deduplicación. Configura una DLQ para eventos que agoten los reintentos y, en Lambda, ajusta MaximumRetryAttempts y DestinationConfig para las invocaciones fallidas.

¿Cuál es el modelo de costes de serverless?

AWS Lambda cobra por invocación (~$0.20/millón) más por GB-segundo de ejecución, y las demás nubes tienen precios similares. Memoria, duración y conteo de invocaciones alimentan la factura — con tráfico alto sostenido, compara contra un servidor siempre activo antes de asumir que serverless sale más barato.

¿Qué diferencia hay entre Lambda layers e imágenes de contenedor?

Las layers son paquetes de código compartido (hasta 5 por función) que reducen el paquete de despliegue y permiten compartir código entre funciones. Las imágenes de contenedor (hasta 10GB) empaquetan todo incluido el runtime — úsalas cuando el paquete supera 250MB o necesitas un runtime propio. Mantén las layers delgadas; las grandes suben el cold start.

¿API Gateway o invocación directa de Lambda?

API Gateway cuando el endpoint necesita autenticación, rate limiting, validación de requests o dominio propio. Invocación directa (EventBridge, SQS, SNS) para llamadas internas donde controlas ambos lados — API Gateway añade coste por request y ~10-30ms de latencia que no necesitas dentro.

¿Cómo gestiono variables de entorno por entorno?

Mantén la config por stage (serverless.dev.yml, serverless.prod.yml) y nunca pongas secretos ahí — los secretos viven en Parameter Store o Secrets Manager y se leen en runtime vía el execution role de la función. Valores normales como URLs de API van bien como variables de entorno de la función.