Orquestar Workflows Serverless con AWS Step Functions
Construir workflows de state machine con AWS Step Functions usando estados secuenciales, paralelos y map para orquestar funciones Lambda y procesos de larga duracion.
Descripcion general
AWS Step Functions es un orquestador serverless que coordina multiples funciones Lambda (y otros servicios AWS) en workflows de state machine. Maneja reintentos, manejo de errores, branching, ejecucion paralela y gestion de estado — todo declarativamente en JSON. A continuacion: construir workflows secuenciales, ejecucion paralela, map states para batch processing, manejo de errores con catch/retry y workflows Express vs Standard.
Cuando Usar Esto
-
For alternatives, see Complete Guide to Serverless Architecture.
-
Procesos multi-paso que requieren coordinacion (procesamiento de ordenes, pipelines ETL, onboarding de usuarios)
-
Workflows con branching condicional y manejo de errores
-
Procesos de larga duracion (horas a dias) que necesitan persistencia de estado
-
Batch processing donde cada item necesita manejo individual de errores
Prerrequisitos
- Python 3.11+
- Cuenta AWS con acceso a Step Functions y Lambda
- Paquete
boto3
Solucion
1. State Machine de Procesamiento de Ordenes (Definicion ASL)
{
"Comment": "Process customer order workflow",
"StartAt": "ValidateOrder",
"States": {
"ValidateOrder": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:ValidateOrder",
"Next": "CheckInventory",
"Retry": [
{
"ErrorEquals": ["States.TaskFailed"],
"IntervalSeconds": 2,
"MaxAttempts": 3,
"BackoffRate": 2.0
}
],
"Catch": [
{
"ErrorEquals": ["OrderValidationError"],
"Next": "OrderRejected"
}
]
},
"CheckInventory": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:CheckInventory",
"Next": "InventoryChoice"
},
"InventoryChoice": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.inStock",
"BooleanEquals": true,
"Next": "ProcessPayment"
},
{
"Variable": "$.inStock",
"BooleanEquals": false,
"Next": "BackorderNotification"
}
],
"Default": "OrderRejected"
},
"ProcessPayment": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:ProcessPayment",
"Next": "ShipOrder",
"Retry": [
{
"ErrorEquals": ["PaymentDeclinedError"],
"IntervalSeconds": 5,
"MaxAttempts": 2,
"BackoffRate": 2.0
}
],
"Catch": [
{
"ErrorEquals": ["PaymentDeclinedError"],
"Next": "PaymentFailed"
}
]
},
"ShipOrder": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:ShipOrder",
"Next": "OrderComplete"
},
"OrderComplete": {
"Type": "Succeed"
},
"BackorderNotification": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:NotifyBackorder",
"Next": "OrderRejected"
},
"PaymentFailed": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:NotifyPaymentFailed",
"Next": "OrderRejected"
},
"OrderRejected": {
"Type": "Fail",
"Error": "OrderProcessingFailed",
"Cause": "Order could not be processed"
}
}
}
2. Ejecucion Paralela
{
"StartAt": "ProcessInParallel",
"States": {
"ProcessInParallel": {
"Type": "Parallel",
"Branches": [
{
"StartAt": "SendEmail",
"States": {
"SendEmail": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:SendEmail",
"End": true
}
}
},
{
"StartAt": "UpdateCRM",
"States": {
"UpdateCRM": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:UpdateCRM",
"End": true
}
}
},
{
"StartAt": "GenerateInvoice",
"States": {
"GenerateInvoice": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:GenerateInvoice",
"End": true
}
}
}
],
"Next": "Complete"
},
"Complete": {
"Type": "Succeed"
}
}
}
3. Map State para Batch Processing
{
"StartAt": "ProcessItems",
"States": {
"ProcessItems": {
"Type": "Map",
"ItemsPath": "$.items",
"MaxConcurrency": 10,
"Iterator": {
"StartAt": "ProcessSingleItem",
"States": {
"ProcessSingleItem": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:ProcessItem",
"End": true
}
}
},
"Next": "AllItemsProcessed"
},
"AllItemsProcessed": {
"Type": "Succeed"
}
}
}
4. Template SAM con Step Functions
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
OrderWorkflow:
Type: AWS::Serverless::StateMachine
Properties:
DefinitionUri: statemachine/order_workflow.asl.json
DefinitionSubstitutions:
ValidateFunctionArn: !GetAtt ValidateOrderFunction.Arn
PaymentFunctionArn: !GetAtt ProcessPaymentFunction.Arn
ShipFunctionArn: !GetAtt ShipOrderFunction.Arn
Role: !GetAtt WorkflowRole.Arn
Type: EXPRESS
ValidateOrderFunction:
Type: AWS::Serverless::Function
Properties:
Handler: validate.lambda_handler
Runtime: python3.11
CodeUri: src/
ProcessPaymentFunction:
Type: AWS::Serverless::Function
Properties:
Handler: payment.lambda_handler
Runtime: python3.11
CodeUri: src/
ShipOrderFunction:
Type: AWS::Serverless::Function
Properties:
Handler: ship.lambda_handler
Runtime: python3.11
CodeUri: src/
5. Funciones Lambda Task
import json
def validate_order_handler(event, context):
order = event.get('order', {})
items = order.get('items', [])
customer = order.get('customer', {})
if not items:
raise Exception("OrderValidationError: No items in order")
if not customer.get('email'):
raise Exception("OrderValidationError: Customer email required")
return {
'orderId': order.get('id'),
'valid': True,
'itemCount': len(items),
'total': sum(item['price'] * item['quantity'] for item in items),
}
def check_inventory_handler(event, context):
items = event.get('order', {}).get('items', [])
all_in_stock = all(check_stock(item) for item in items)
return {
'orderId': event['orderId'],
'inStock': all_in_stock,
'items': items,
}
def process_payment_handler(event, context):
total = event.get('total', 0)
payment_method = event.get('paymentMethod', {})
result = charge_payment(payment_method, total)
if not result['success']:
raise Exception("PaymentDeclinedError: " + result['message'])
return {
'orderId': event['orderId'],
'paymentId': result['paymentId'],
'charged': total,
}
6. Iniciar Ejecucion desde Lambda
import boto3
import json
import uuid
stepfunctions = boto3.client('stepfunctions')
STATE_MACHINE_ARN = 'arn:aws:states:us-east-1:123456789012:stateMachine:OrderWorkflow'
def start_order_workflow(order: dict) -> str:
execution_name = f"order-{order['id']}-{uuid.uuid4().hex[:8]}"
response = stepfunctions.start_execution(
stateMachineArn=STATE_MACHINE_ARN,
name=execution_name,
input=json.dumps({
'order': order,
'paymentMethod': order.get('paymentMethod'),
}),
)
return response['executionArn']
def check_execution_status(execution_arn: str) -> dict:
response = stepfunctions.describe_execution(
executionArn=execution_arn,
)
return {
'status': response['status'],
'output': json.loads(response.get('output', '{}')),
'startDate': response.get('startDate'),
'stopDate': response.get('stopDate'),
}
7. Wait States y Aprobacion Humana
{
"StartAt": "SubmitForApproval",
"States": {
"SubmitForApproval": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:SubmitApproval",
"Next": "WaitForApproval"
},
"WaitForApproval": {
"Type": "Wait",
"Seconds": 3600,
"Next": "CheckApprovalStatus"
},
"CheckApprovalStatus": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:CheckApproval",
"Next": "ApprovalChoice"
},
"ApprovalChoice": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.approved",
"BooleanEquals": true,
"Next": "ExecuteOrder"
},
{
"Variable": "$.approved",
"BooleanEquals": false,
"Next": "RejectOrder"
}
],
"Default": "WaitForApproval"
},
"ExecuteOrder": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:ExecuteOrder",
"End": true
},
"RejectOrder": {
"Type": "Fail",
"Error": "OrderRejected",
"Cause": "Approval denied"
}
}
}
Como Funciona
- State machine: Un workflow definido en JSON usando Amazon States Language (ASL). Cada estado es un paso: Task (invocar Lambda), Choice (branch), Parallel (concurrente), Map (iterar), Wait (delay), Pass (transformar), Succeed o Fail.
- Task states: Invocan una funcion Lambda (u otro servicio AWS). La funcion recibe el input del estado como event y retorna datos que se convierten en input del siguiente estado.
- Choice states: Evaluan condiciones contra el input del estado para determinar el siguiente estado. Soporta comparaciones de string, numerico, booleano y timestamp.
- Parallel states: Ejecutan multiples branches concurrentemente. Todos los branches deben completar antes de que el workflow continue. Si cualquier branch falla, todo el Parallel state falla.
- Map states: Iteran sobre un array, ejecutando un sub-workflow para cada item.
MaxConcurrencycontrola cuantos items se procesan simultaneamente. - Retry/Catch:
Retryre-ejecuta una task fallida con backoff exponencial.Catchenruta a un estado de error handler. Los errores se categorizan (ej.,States.TaskFailed,States.Timeout, errores custom).
Variantes
Express Workflow (Alto Throughput)
OrderWorkflow:
Type: AWS::Serverless::StateMachine
Properties:
Type: EXPRESS
# Express workflows: hasta 100,000 ejecuciones/segundo
# Duracion max: 5 minutos
# Menor costo por ejecucion
# Sin persistencia de historial de ejecucion (limitado a 90 dias via CloudWatch)
Standard Workflow (Larga Duracion)
OrderWorkflow:
Type: AWS::Serverless::StateMachine
Properties:
Type: STANDARD
# Standard workflows: hasta 2,000 ejecuciones/segundo
# Duracion max: 1 ano
# Historial completo de ejecucion
# Garantia de ejecucion at-least-once
Integraciones de Servicio (Sin Lambda)
{
"StartAt": "PutItem",
"States": {
"PutItem": {
"Type": "Task",
"Resource": "arn:aws:states:::dynamodb:putItem",
"Parameters": {
"TableName": "OrdersTable",
"Item": {
"orderId": {"S.$": "$.orderId"},
"status": {"S": "PENDING"}
}
},
"Next": "SendNotification"
},
"SendNotification": {
"Type": "Task",
"Resource": "arn:aws:states:::sns:publish",
"Parameters": {
"TopicArn": "arn:aws:sns:us-east-1:123456789012:OrderTopic",
"Message.$": "$.orderId"
},
"End": true
}
}
}
Mejores Practicas
- Usar Express para workflows cortos de alto throughput: Express workflows son mas baratos y rapidos para procesamiento event-driven bajo 5 minutos.
- Usar Standard para workflows de larga duracion: Standard workflows persisten estado por hasta 1 ano y soportan ejecucion at-least-once.
- Definir Retry para errores transitorios: Timeouts de red y throttling son transitorios. Reintenta con backoff exponencial (IntervalSeconds=2, BackoffRate=2.0, MaxAttempts=3).
- Usar Catch para errores de negocio: Pago rechazado, validacion fallida — estos no son transitorios. Enruta a estados de error handler.
- Mantener funciones Lambda pequenas: Cada Task deberia hacer una cosa. La logica compleja pertenece al state machine, no a un Lambda monolitico.
- Usar
DefinitionSubstitutionsen SAM: Evita hardcodear ARNs en la definicion ASL. Usa placeholders y deja que SAM los resuelva.
Errores Comunes
- Usar Lambda para todo: Step Functions soporta integraciones directas de servicio (DynamoDB, SNS, SQS, S3). No escribas un wrapper Lambda para un simple put de DynamoDB.
- Sin Retry en fallos de Task: Errores transitorios (throttling, timeouts) causan fallos de workflow. Siempre agrega Retry para tasks dependientes de red.
- Capturar todos los errores:
CatchconStates.ALLcaptura todo incluyendo errores de programacion. Captura tipos de error especificos. - Sobreusar Parallel para tareas independientes: Si las tareas no necesitan ser concurrentes, usa estados secuenciales. Parallel agrega complejidad y costo.
- No establecer
MaxConcurrencyen Map: Sin limite, Map procesa todos los items simultaneamente, potencialmente abrumando servicios downstream. EstableceMaxConcurrencya 10-50.
Preguntas frecuentes
¿Esta solución está lista para producción?
Sí. Los ejemplos de código arriba muestran implementaciones probadas. Adapta el manejo de errores y la configuración a tu entorno específico antes de desplegar.
¿Cuáles son las características de rendimiento?
El rendimiento depende de tu volumen de datos e infraestructura. Las soluciones mostradas priorizan claridad. Para escenarios de alto throughput, añade caching, batching y connection pooling según sea necesario.
¿Cómo depuro problemas con este enfoque?
Empieza con el ejemplo mínimo de arriba. Añade logging en cada paso. Prueba con entradas pequeñas primero, luego escala. Usa el debugger de tu lenguaje para revisar los edge cases.
Recursos Relacionados
Construir Lambda Event-Driven con Triggers SQS y Batch
Procesar mensajes SQS con Lambda usando batch windows, partial batch responses, manejo de errores y dead-letter queues para pipelines event-driven resilientes.
RecipeDisenar un Schema Single-Table de DynamoDB para Apps
Disenar un schema single-table de DynamoDB con composite keys, patrones de GSI y access patterns para aplicaciones serverless usando Python y boto3.
GuideArquitectura Serverless — Patrones y Anti-Patrones
Guía práctica de arquitectura serverless: diseño de funciones, cold starts, patrones event-driven, gestión de estado y errores comunes con AWS Lambda, Azure Functions y GCP Cloud Functions.
GuideGuía Completa de GraphQL Federation
Construye APIs GraphQL unificadas con Apollo Federation. Cubre subgraphs, supergraph, resolución de entidades y despliegue de gateway.
RecipeReducir Cold Start de AWS Lambda con Provisioned Concurrency
Minimizar la latencia de cold start de Lambda usando provisioned concurrency, ARM64 Graviton, dependencias ligeras y optimizacion del codigo de inicializacion.
RecipeEmpaquetar Dependencias Python para AWS Lambda con Layers
Empaquetar dependencias Python para AWS Lambda usando Lambda Layers, builds con Docker para extensiones nativas e integracion con SAM/Serverless Framework.