O blog da AWS

Otimizando fluxos de trabalho Serverless na AWS: Da orquestração com AWS Lambda para AWS Step Functions

Por Debasis Rath, Sr. Solutions Architect e Stefan Mationg, AWS WWPS Education SA.

Esta publicação discute o anti-padrão de usar AWS Lambda como orquestrador e como redesenhar soluções Serverless utilizando AWS Step Functions com integrações nativas.

O Step Functions é um serviço de fluxo de trabalho (workflow) Serverless que você pode usar para criar aplicativos distribuídos, automatizar processos, orquestrar microsserviços e criar pipelines de dados e aprendizado de máquina (ML). O Step Functions fornece integrações nativas com mais de 200 serviços da AWS, além de APIs externas de terceiros. Você pode usar essas integrações para implantar soluções prontas para produção com menos esforço, reduzindo a complexidade do código, melhorando a capacidade de manutenção a longo prazo e minimizando a dívida técnica ao operar em grande escala.

O anti-padrão de orquestração com Lambda

Vamos examinar um antipadrão comum: usar uma função Lambda como orquestrador para distribuição de mensagens em vários canais. Considere esse cenário real em que um sistema precisa enviar notificações por meio de canais de SMS ou e-mail com base nas preferências do usuário, conforme mostrado no diagrama a seguir.

Os exemplos de payload para esse cenário são:

  1. Send SMS only:
    {
        "body": {
            "channel": "sms",
            "message": "Hello from AWS Lambda!",
            "phoneNumber": "+1234567890",
            "metadata": {
                "priority": "high",
                "category": "notification"
            }
        }
    }
  2. Send email only:
    {
        "body": {
            "channel": "email",
            "message": "Hello from AWS Lambda!",
            "email": {
                "to": "recipient@example.com",
                "subject": "Test Notification",
                "from": "sender@example.com"
            },
            "metadata": {
                "priority": "normal",
                "category": "notification"
            }
        }
    }
    Python
  3. Send both SMS and email:
    {
        "body": {
            "channel": "both",
            "message": "Hello from AWS Lambda!",
            "phoneNumber": "+1234567890",
            "email": {
                "to": "recipient@example.com",
                "subject": "Test Notification",
                "from": "sender@example.com"
            },
            "metadata": {
                "priority": "high",
                "category": "notification"
            }
        }
    }
    Python

Veja como isso normalmente começa, com uma função do Lambda atuando como orquestradora:

import boto3
import json
# Initialize Lambda client
# You can specify region if needed: boto3.client('lambda', region_name='us-east-1')
lambda_client = boto3.client('lambda')
def lambda_handler(event, context):
    try:
        # Parse the incoming event
        body = json.loads(event['body'])
        
        # Validate required fields
        if 'channel' not in body:
            return {
                'statusCode': 400,
                'body': json.dumps('Missing channel parameter')
            }
        
        if 'message' not in body:
            return {
                'statusCode': 400,
                'body': json.dumps('Missing message content')
            }
        
        if body['channel'] == 'both':
            # Invoke SMS Lambda function
            lambda_client.invoke(
                FunctionName='send-sns',
                InvocationType='Event',
                Payload=json.dumps(body)
            )
            
            # Invoke Email Lambda function
            lambda_client.invoke(
                FunctionName='send-email',
                InvocationType='Event',
                Payload=json.dumps(body)
            )
        else:
            # Validate channel value
            if body['channel'] not in ['sms', 'email']:
                return {
                    'statusCode': 400,
                    'body': json.dumps('Invalid channel specified')
                }
            
            # Invoke function based on specified channel
            function_name = 'send-sns' if body['channel'] == 'sms' else 'send-email'
            lambda_client.invoke(
                FunctionName=function_name,
                InvocationType='Event',
                Payload=json.dumps(body)
            )
        
        return {
            'statusCode': 200,
            'body': json.dumps('Messages sent successfully')
        }
        
    except json.JSONDecodeError:
        return {
            'statusCode': 400,
            'body': json.dumps('Invalid JSON in request body')
        }
    except Exception as e:
        return {
            'statusCode': 500,
            'body': json.dumps(f'Error: {str(e)}')
        }

Essa abordagem tem os seguintes problemas:

  • Tratamento complexo de erros: o orquestrador precisa gerenciar erros de várias invocações de funções.
  • Acoplamento forte: as funções dependem diretamente umas das outras.
  • Tempo de execução limitado: a função Lambda do orquestrador continua em execução enquanto as funções subLambda são executadas. Isso pode fazer com que a função Lambda do orquestrador atinja o tempo limite.
  • Recursos ociosos: como a função Lambda do orquestrador está ociosa aguardando retornos de outras funções do Lambda, nesse caso, o usuário agora está pagando por recursos ociosos.

Rearquitetura com Step Functions

Você pode reconstruir a lógica usando Step Functions e Amazon States Language para substituir a função do orquestrador Lambda. Você pode usar o estado Choice na Amazon States Language para definir condições lógicas para seguir um caminho específico. Essa abordagem reduz a complexidade da manutenção do código porque você define as condições usando a Amazon States Language. Você também pode usá-lo para estender a funcionalidade com alterações mínimas na base de código.

O diagrama de fluxo de trabalho do Step Functions a seguir mostra a versão rearquitetada da função anterior do Orchestrator Lambda:

A Amazon State Language a seguir representa o fluxo de trabalho:

{
  "Comment": "Multi-channel notification workflow",
  "StartAt": "ValidateInput",
  "States": {
    "ValidateInput": {
      "Type": "Choice",
      "Choices": [
        {
          "And": [
            {
              "Variable": "$.message",
              "IsPresent": true
            },
            {
              "Variable": "$.channel",
              "IsPresent": true
            }
          ],
          "Next": "DetermineChannel"
        }
      ],
      "Default": "ValidationError"
    },
    "ValidationError": {
      "Type": "Fail",
      "Error": "ValidationError",
      "Cause": "Required fields missing: message and/or channel"
    },
    "DetermineChannel": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.channel",
          "StringEquals": "both",
          "Next": "ParallelNotification"
        },
        {
          "Variable": "$.channel",
          "StringEquals": "sms",
          "Next": "SendSMSOnly"
        },
        {
          "Variable": "$.channel",
          "StringEquals": "email",
          "Next": "SendEmailOnly"
        }
      ],
      "Default": "FailState"
    },
    "ParallelNotification": {
      "Type": "Parallel",
      "Branches": [
        {
          "StartAt": "SendSMS",
          "States": {
            "SendSMS": {
              "Type": "Task",
              "Resource": "arn:aws:states:::sns:publish",
              "Parameters": {
                "Message.$": "$.message",
                "PhoneNumber.$": "$.phoneNumber"
              },
              "End": true
            }
          }
        },
        {
          "StartAt": "SendEmail",
          "States": {
            "SendEmail": {
              "Type": "Task",
              "Parameters": {
                "FromEmailAddress.$": "$.email.from",
                "Destination": {
                  "ToAddresses.$": "States.Array($.email.to)",
                  "CcAddresses.$": "States.ArrayGetItem(States.JsonToString($.email.cc), $)",
                  "BccAddresses.$": "States.ArrayGetItem(States.JsonToString($.email.bcc), $)"
                },
                "Content": {
                  "Simple": {
                    "Subject": {
                      "Data.$": "$.email.subject",
                      "Charset": "UTF-8"
                    },
                    "Body": {
                      "Text": {
                        "Data.$": "$.message",
                        "Charset": "UTF-8"
                      },
                      "Html": {
                        "Data.$": "$.email.htmlBody",
                        "Charset": "UTF-8"
                      }
                    }
                  }
                },
                "ReplyToAddresses.$": "States.Array($.email.replyTo)",
                "EmailTags": [
                  {
                    "Name": "channel",
                    "Value": "email"
                  },
                  {
                    "Name": "messageType",
                    "Value.$": "$.email.messageType"
                  }
                ],
                "ConfigurationSetName.$": "$.email.configurationSet",
                "ListManagementOptions": {
                  "ContactListName.$": "$.email.contactList",
                  "TopicName.$": "$.email.topic"
                }
              },
              "Resource": "arn:aws:states:::aws-sdk:sesv2:sendEmail",
              "End": true
            }
          }
        }
      ],
      "End": true
    },
    "SendSMSOnly": {
      "Type": "Task",
      "Resource": "arn:aws:states:::sns:publish",
      "Parameters": {
        "Message.$": "$.message",
        "PhoneNumber.$": "$.phoneNumber"
      },
      "End": true
    },
    "SendEmailOnly": {
      "Type": "Task",
      "Parameters": {
        "FromEmailAddress.$": "$.email.from",
        "Destination": {
          "ToAddresses.$": "States.Array($.email.to)"
        },
        "Content": {
          "Simple": {
            "Subject": {
              "Data.$": "$.email.subject",
              "Charset": "UTF-8"
            },
            "Body": {
              "Text": {
                "Data.$": "$.message",
                "Charset": "UTF-8"
              },
              "Html": {
                "Data.$": "$.email.htmlBody",
                "Charset": "UTF-8"
              }
            }
          }
        }
      },
      "Resource": "arn:aws:states:::aws-sdk:sesv2:sendEmail",
      "End": true
    },
    "FailState": {
      "Type": "Fail",
      "Cause": "Invalid channel specified"
    }
  }
}

Essa implementação do Step Functions oferece várias vantagens:

  • Integração nativa de serviços: a integração direta com o Amazon Simple Notification Service (Amazon SNS), o Amazon Simple Email Service (Amazon SES), o Amazon DynamoDB e o Amazon CloudWatch elimina a necessidade de funções wrapper Lambda
  • Fluxo de trabalho visual: o fluxo de execução é visível e pode ser mantido por meio do AWS Management Console
  • Tratamento de erros integrado: as políticas de repetição e os estados de erro podem ser definidos declarativamente
  • Execução paralela: o estado Parallel lida com a entrega de vários canais de forma eficiente
  • Lógica simplificada: o estado Choice substitui declarações if-else complexas
  • Fluxo de dados centralizado: a entrada e a saída são gerenciadas de forma consistente em todos os estados
  • Recursos aprimorados de duração do fluxo de trabalho: Step Functions Os fluxos de trabalho padrão suportam execuções que duram até um ano, em comparação com o tempo máximo de execução de 15 minutos para funções Lambda

Comparando a função Lambda como orquestradora com Step Functions

O resumo dos diferentes recursos implementados na função Lambda como orquestrador e Step Functions é refletido na tabela a seguir:

Característica Função Lambda como orquestrador Funções de etapa
Lógica de orquestração Implementado em Python com instruções if-else aninhadas. Definido declarativamente usando o estado Choice
Entrega multicanal Invocações de funções sequenciais. Execução paralela usando a lógica da função. Execução paralela usando o estado Paralelo
Integração de serviços Requer chamadas de SDK ou funções separadas do Lambda. Integração direta com os serviços da AWS (Amazon SNS, DynamoDB)
Tratamento de erros Blocos de teste e exceção personalizados em Python. Estados de erro incorporados e políticas de repetição
Persistência de dados Código personalizado para interagir com o DynamoDB. Integração nativa do DynamoDB com a tarefa PutItem
Registro de métricas Código personalizado para chamar o CloudWatch. Integração do SDK do CloudWatch Metrics

Considerações de implementação

Analise as seguintes considerações ao rearquitetar um orquestrador de funções Lambda para Step Functions:

  • Tipo de máquina de estado: escolha entre fluxos de trabalho Standard (tempo de execução de até 1 ano) e Express (até 5 minutos) com base em suas necessidades.
  • Gerenciamento de entrada/saída: a manipulação de parâmetros reduz o esforço de desenvolvimento e oferece alternativas flexíveis para implementar o fluxo de trabalho:
    • Parâmetros: seleciona campos de entrada específicos para passar para o próximo estado
    • resultSelector: filtra a resposta do estado para incluir somente campos relevantes
    • resultPath: armazena o resultado processado em um caminho específico da entrada de estado
    • OutputPath: determina quais dados passam para o próximo estado.
      Um trecho de código para esses recursos é:

      {
          "ProcessOrder": {
              "Type": "Task",
              "Resource": "arn:aws:states:::lambda:invoke",
              "Parameters": {
                  "FunctionName": "ProcessOrderFunction",
                  "Payload": {
                      "orderId.$": "$.orderId",
                      "customerId.$": "$.customerId"
                  }
              },
              "ResultSelector": {
                  "orderStatus.$": "$.Payload.status",
                  "processedDate.$": "$.Payload.timestamp"
              },
              "ResultPath": "$.orderProcessing",
              "OutputPath": "$.orderProcessing",
              "Next": "NotifyCustomer"
          }
      }
  • Tratamento de erros: implemente políticas de repetição e detecte erros nos níveis da tarefa e da máquina de estado.
  • Monitoramento: configure os registros e métricas do CloudWatch para sua máquina de estado para monitorar as execuções e o desempenho.

Benefícios do uso do Step Functions

O uso do Step Functions para rearquitetar cenários traz os seguintes benefícios:

  • Complexidade de código reduzida: a lógica de negócios agora está definida na Amazon States Language, em vez de distribuída em várias funções do Lambda.
  • Manutenção aprimorada: os desenvolvedores podem fazer alterações no fluxo de trabalho modificando a Amazon States Language, geralmente modificando várias funções do Lambda.
  • Integrações de serviços nativos da AWS: o Step Functions oferece integrações diretas com mais de 200 serviços da AWS, que você pode usar para conectar e coordenar recursos da AWS sem escrever um código de integração personalizado.
  • Otimização de custos: ao usar integrações diretas de serviços, há menos invocações do Lambda e custos reduzidos.
  • Processos de longa duração: o Step Functions pode gerenciar fluxos de trabalho que são executados por até um ano, além do limite de 15 minutos para funções Lambda.

Conclusão

A rearquitetura de aplicativos baseados em Lambda com o Step Functions pode melhorar significativamente a capacidade de manutenção, a escalabilidade e a eficiência operacional. Ao mover a lógica de orquestração para o Step Functions e usar suas integrações de serviços nativas, você pode criar aplicativos Serverless mais robustos e gerenciáveis.

Embora esta postagem tenha se concentrado em um fluxo de trabalho de distribuição de mensagens, os princípios se aplicam a muitas arquiteturas Serverless. Ao desenvolver seus aplicativos, considere como o Step Functions pode ajudá-lo a criar soluções mais resilientes e escaláveis.

Para saber mais sobre arquiteturas Serverless, visite Serverless Land.

Este conteúdo foi traduzido da postagem original do blog, que pode ser encontrada aqui.

Biografia dos Autores

Debasis Rath, Sr. Solutions Architect
Stefan Mationg, AWS WWPS Education SA

Biografia do Tradutor

Rodrigo Peres é Arquiteto de Soluções na AWS, com mais de 20 anos de experiência trabalhando com arquitetura de soluções, desenvolvimento de sistemas e modernização de sistemas legados.

Biografia do Revisor

Daniel Abib é arquiteto de soluções sênior na AWS, com mais de 25 anos trabalhando com gerenciamento de projetos, arquiteturas de soluções escaláveis, desenvolvimento de sistemas e CI/CD, microsserviços, arquitetura Serverless & Containers e segurança. Ele trabalha apoiando clientes corporativos, ajudando-os em sua jornada para a nuvem.

https://www.linkedin.com/in/danielabib/