Executando verificação de segurança...
1

Lambda Durable Functions: Como construir aplicações multi-etapas com um único serviço

Sempre quis rodar processos longos em uma função Lambda sem se preocupar com o limite de 15 minutos? E executar processos com múltiplos passos sem sair do lugar? Não diga mais nada! Conheça AWS Lambda Durable Functions...
Complexidade de workflows: um problema conhecido

Quem já trabalhou com sistemas baseados em workflows sabe que é uma tarefa que pode ficar complexa bem rapidamente.

Você começa definindo a sequência de instruções que precisa seguir. É fácil pensar no clássico caminho feliz quando tudo dá certo, mas a coisa começa a complicar quando lembramos que sistemas falham...

A cada passo, você precisa registrar o estado da sua aplicação para saber exatamente o que já foi executado. Se algo falha, você precisa garantir que é seguro dar um retry. Passos já completos devem ser pulados e se forem reexecutados, devem produzir o mesmo resultado sempre. Essa propriedade se chama idempotência. Ela que garante que uma re-execução não vai causar duplicações ou inconsistências no sistema.

Existem serviços de orquestração de workflows que simplificam esse processo de maneira visual, como o AWS Step Functions. Mas esses também trazem seus desafios porque o lugar onde os passos são declarados é diferente de onde são implementados. A definição do workflow fica em um arquivo JSON ou YAML e a implementação mora no código. Isso torna mais difícil testar o caminho inteiro e as várias possibilidades já que vemos a lógica toda distribuída.

Isso tudo sem contar a complexidade de administrar vários serviços separados. Quem nunca se perdeu ao tentar administrar e monitorar várias funções Lambda ao mesmo tempo que atire a primeira pedra...

Desenvolvedor estressado administrando várias funções Lambda

Lambda Durable Functions traz uma alternativa para criar e administrar workflows de maneira serverless de forma simplificada dentro do próprio código, centralizando tudo em uma mesma função.
O que é AWS Lambda Durable Functions?

Lambda Durable Functions é uma funcionalidade do AWS Lambda. Ela permite criar e executar workflows dentro de uma função lambda diretamente pelo código da função. No momento, a funcionalidade está disponível para Node.js a partir da versão 22.x e Python a partir da versão 3.13. A funcionalidade também está em fase de Developer Preview para Java, o que significa que tem suporte para a linguagem no SDK, mas só pode ser criada através de uma imagem de contêiner.

Sendo uma funcionalidade do AWS Lambda, Durable Functions naturalmente entra como parte do universo serverless. Isso significa que podemos implementar as funcionalidades de um workflow como checkpoints, retries, pausa e retorno sem administrar instâncias de servidores!
Como criar uma Durable Function

Para ter uma função Lambda com Durable Functions, é preciso habilitar essa funcionalidade desde sua criação. Não é possível alterar uma função já existente para ser uma função durável. Conseguimos criá-la pelo console, pela CLI ou usando infraestrutura como código.

No console, isso é feito habilitando a execução durável na página de criação como demonstrado abaixo.
Criação de Lambda Durable Function pelo console da AWS

Para criar através da CLI, rodamos o comando de criação com o atributo durable-config.

aws lambda create-function
--function-name minha-funcao-duravel
--runtime python3.14
--role arn:aws:iam::<numero_da_conta_aws>:role/minha-funcao-duravel-role
--handler lambda_function.lambda_handler
--zip-file fileb://function.zip
--durable-config '{"ExecutionTimeout": 3600, "RetentionPeriodInDays": 7}'

Esse atributo aceita dois parâmetros opcionais de configuração da Durable Function:

ExecutionTimeout - tempo máximo em segundos que a execução durável inteira pode durar, incluindo tempos de pausa e retry. O tempo máximo é de 1 ano
RetentionPeriodInDays - número de dias que o histórico das execuções duráveis são armazenados depois de serem finalizados

Observação: o comando acima de criação de função Lambda é um exemplo ilustrativo. Para executá-lo com sucesso, seria preciso criar a role do IAM chamada "minha-funcao-duravel-role" e ter um zip chamado "function.zip" contendo o código da função.

De forma parecida, podemos criar Lambda Durable Functions usando infraestrutura como código. Fazemos isso através do CloudFormation, um serviço da AWS que permite criar e gerenciar sua infraestrutura na nuvem utilizando templates JSON ou YAML. Existem ainda ferramentas como AWS CDK e Terraform que auxiliam nessa criação permitindo usar linguagens de programação. Em um template de CloudFormation, a Durable Function também apresenta o atributo de DurableConfig:

Resources:
MyDurableFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: minha-funcao-duravel
Runtime: python3.14
Handler: lambda_function.lambda_handler
Code:
ZipFile: fileb://function.zip
DurableConfig:
ExecutionTimeout: 3600
RetentionPeriodInDays: 7

Como funciona?

Vamos imaginar como exemplo uma aplicação de criação de ordem de compra. Um funcionário cria a ordem de compra, então o sistema verifica no inventário se os itens solicitados estão disponíveis. Caso estejam, alguns gerentes responsáveis são notificados para revisar e aprovar a compra e, se aprovarem, a ordem é confirmada.

Quando uma função Lambda está habilitada como uma Durable Function, dizemos que ela permite uma durable execution (execução durável) e cada etapa do workflow é um durable step (passo durável). Isso significa que a função consegue "lembrar" o estado em que estava no caso de falhas ou pausas e retomar a execução a partir desse ponto.

Veja no diagrama abaixo como poderíamos representar os passos da aplicação de ordem de compra:

Diagrama mostrando passos do workflow de ordem de compra

Existem alguns conceitos fundamentais para administração de workflows que aparecem nas Durable Functions.
Checkpoint e replay

Veja abaixo o nosso exemplo como estrutura de um código de uma Lambda Durable Function em Python:

@durable_step
def create_order(step_context: StepContext, order_data: dict) -> dict:
# Implementação da criação de ordem

@durable_step
def check_inventory(step_context: StepContext, order_data: dict) -> dict:
# Implementação da checagem de inventário

@durable_step
def notify_approver(step_context: StepContext, callback_id: str, approver: dict) -> dict:
# Implementação da notificação de gestores

@durable_step
def confirm_order(step_context: StepContext, order_id: str) -> dict:
# Implementação da confirmação de ordem

@durable_execution
def lambda_handler(event: dict, context: DurableContext) -> dict:
# Chamada e orquestração dos steps

Note que o handler da função Lambda, ou seja, seu ponto de entrada, é anotado com @durable_execution. Cada um dos steps é anotado com @durable_step. Após a execução de cada um dos steps, é feito um checkpoint automático que registra o resultado de cada passo. Caso a função seja pausada (veremos isso numa próxima seção) e retome, ela será reexecutada do início, steps já completos serão pulados e a função usará os resultados já armazenados. Chamamos essa retomada de replay.
Retry

Diferentemente do replay que acontece quando o código é pausado, o retry acontece na falha. Quando uma exceção ocorre dentro de um step, ele será automaticamente retentado. É possível também declarar sua própria estratégia de retry como eu faço abaixo no step de criação de ordem:

@durable_execution
def lambda_handler(event: dict, context: DurableContext) -> dict:
# Recebe dados da ordem através do evento enviado para a função
order_data = event["order"]

retry_config = RetryStrategyConfig(max_attempts=5, backoff_rate=2.0) # Configuração personalizada de retry
order = context.step(
    create_order(order_data),
    config=StepConfig(retry_strategy=create_retry_strategy(retry_config)),
)

Wait

É possível fazer com que sua função espere um determinado tempo antes de executar uma próxima instrução. No nosso exemplo, vamos imaginar que queremos que a função espere um tempo depois da criação da ordem para checar o inventário.

@durable_execution
def lambda_handler(event: dict, context: DurableContext) -> dict:
order_data = event["order"]

order = context.step(create_order(order_data))

context.wait(Duration.from_minutes(5)) # Espera por 5 minutos

inventory_check = context.step(check_inventory(order_data))

# Restante da implementação

Durante o tempo de espera, a função está pausada, então não serão feitas cobranças por esse período. O timeout máximo de uma invocação individual de uma função Lambda continua sendo 15 minutos, mas a durable execution pode durar até 1 ano!
Callback

Além de querer pausar nossa execução por um tempo específico, podemos querer pausar até obter uma resposta de um serviço externo ou até mesmo de um usuário, como é o caso da aprovação dos gestores no nosso exemplo. Essa estratégia de resposta assíncrona é chamada de callback.

Quando chamamos a função create_callback do SDK, geramos um identificador único (o callback_id) que enviamos para o sistema ou pessoa que precisa responder. A função então pausa no callback.result() sem consumir recursos até que a resposta chegue. Portanto, assim como no caso do wait, não recebemos cobrança durante esse período de espera.

Vamos ver como ficaria a aprovação de um único gestor:

@durable_execution
def lambda_handler(event: dict, context: DurableContext) -> dict:
# Chamada de passos anteriores
approvers = event["approvers"] # [{"name": "Ana"}, {"name": "Júlia"}, {"name": "Bruno"}]

# Cria o callback com timeout de 24 horas
callback = context.create_callback(
    name="approval-manager",
    config=CallbackConfig(timeout=Duration.from_hours(24)),
)

context.step(notify_approver(callback.callback_id, approvers[0]))

approval_result = callback.result()

# Restante da implementação

Para responder, o sistema ou pessoa deve chamar a API SendDurableExecutionCallbackSuccess do Lambda. O gestor poderia usar um outro sistema com sua própria função Lambda que chama essa API do sistema de criação de ordem para notificar a aprovação. Em código teríamos:

def lambda_handler(event, context):
lambda_client = boto3.client("lambda")
callback_id = event["callback_id"]
approved = event.get("approved", True)

# Mandando callback de sucesso
if approved:
    lambda_client.send_durable_execution_callback_success(
        CallbackId=callback_id,
        Result=b'{"approved": true}',
    )
# Mandando callback de erro
else:
    lambda_client.send_durable_execution_callback_failure(
        CallbackId=callback_id,
        Error={"ErrorType": "Rejected", "ErrorMessage": "Approver rejected the order"},
    )

return {"statusCode": 200, "body": "Callback sent"}

Parallel

Vimos como configuraríamos para esperar a resposta de um gestor, mas no nosso exemplo, a ordem de compra precisa ser aprovada por vários gestores. Ao invés de enviar um callback de cada vez e esperar sequencialmente, podemos usar context.parallel para enviar todos ao mesmo tempo. Isso gera uma branch (ramificação) para cada chamada que roda de forma independente com seu próprio checkpoint.

No código abaixo, cada gestor recebe seu próprio callback e a função espera todas as respostas chegarem antes de continuar:

@durable_execution
def lambda_handler(event: dict, context: DurableContext) -> dict:
# Chamada de passos anteriores

approvers = event["approvers"]  # [{"name": "Ana"}, {"name": "Júlia"}, {"name": "Bruno"}]

def request_approval(approver: dict, ctx: DurableContext) -> dict:
    # Cria um callback para cada gestor
    callback = ctx.create_callback(
        name=f"approval-{approver['name']}",
        config=CallbackConfig(
            timeout=Duration.from_hours(24),
            heartbeat_timeout=Duration.from_hours(1),
        ),
    )

    # Notifica o gestor com o callback_id
    ctx.step(notify_approver(callback.callback_id, approver))

    # Pausa até o gestor responder
    approval = callback.result()
    return {"approver": approver["name"], "decision": approval}

# Executa todas as aprovações em paralelo
approval_results = context.parallel(
    [
        lambda ctx, a=approver: request_approval(a, ctx)
        for approver in approvers
    ],
    name="send-approvals",
)

# Restante da implementação

Se Ana responder em 5 minutos e Bruno demorar 2 horas, a função continua pausada sem custo até que todos respondam. Cada branch tem seus próprios checkpoints, então se uma falhar, as outras não são afetadas.
Cálculo de preço

A cobrança de uma Durable Function é composta por quatro componentes:

Cobranças padrão do Lambda — as mesmas cobranças de requests e duração de qualquer função Lambda: $0.20 a cada 1 milhão de requisições mais o valor por duração das execuções que varia de acordo com a arquitetura da função ($0.0000166667 por GB-segundo para arquitetura x86 e $0.0000133334 por GB-segundo para ARM/Graviton2) Isso inclui sub-invocações geradas por replays. Durante operações de wait ou callback, funções on-demand não geram cobrança de duração já que a execução está suspensa.

Operações duráveis — cada operação durável (iniciar uma execução, completar um step, criar um wait, etc.) é cobrada individualmente. O preço na região US East (N. Virginia) é de $8.00 por milhão de operações.

Dados escritos — os dados persistidos pelos checkpoints (payloads de steps, waits, callbacks) são cobrados por GB escrito. O preço é de $0.25 por GB.

Retenção de dados — os dados dos checkpoints são armazenados durante a execução e após sua finalização. A cobrança é de $0.15 por GB-mês, proporcional ao tempo de armazenamento. O período de retenção após a conclusão é configurável de 1 a 90 dias (padrão: 14 dias).

Para mais informações sobre preço, consulte a página de preços do AWS Lambda e a calculadora de preços da AWS.
Diferença entre Lambda Durable Functions e Step Functions

Se você já conhece a AWS, provavelmente está se perguntando: "mas isso não é o que o Step Functions faz?". Boa pergunta! Os dois serviços resolvem o mesmo problema, mas de formas bem diferentes.

A principal diferença está em onde o workflow é definido. No Step Functions, você declara o fluxo usando um arquivo JSON chamado ASL (Amazon States Language). Cada estado e transição é definido nesse arquivo, separado do código que executa a lógica. Cada passo do Step Functions pode chamar uma função Lambda, outro serviço de compute, ou até mesmo fazer uma chamada de serviço diretamente, como chamar o Dynamo ou uma fila, por exemplo.

Já com Durable Functions, o workflow inteiro mora no código da função Lambda. Você escreve a sequência de passos como código normal e o SDK cuida do resto.

Ambos são modelos de orquestração, ou seja, existe um coordenador central que controla o fluxo e sabe qual é o próximo passo. A diferença é que no Step Functions o orquestrador é a máquina de estados que é demonstrada de forma visual no console da AWS e no Durable Functions o orquestrador é o próprio código do handler.

Veja na tabela abaixo um comparativo entre os dois serviços:
Step Functions Durable Functions
Definição do workflow Declarativa (ASL JSON/YAML), separada do código Imperativa, dentro do código da função
Gerenciamento de estado Gerenciado pelo serviço através de transições de estado Gerenciado pelo SDK através de checkpoints
Visibilidade da execução Console visual com diagrama do fluxo e histórico de execução Aba "Durable executions" no console do Lambda + Logs
Integrações nativas Mais de 200 integrações diretas com serviços AWS sem precisar de código Integrações feitas via SDK dentro de steps no código
Melhor para Fluxos complexos com muitas integrações AWS e equipes que preferem workflows visuais, como equipes voltadas a negócios Fluxos com foco no programa, equipes técnicas que preferem manter tudo no código

Uma vantagem prática do Durable Functions é que como tudo está no código, fica mais fácil testar o fluxo inteiro localmente. Com Step Functions, testar o workflow completo geralmente exige deployar a máquina de estados e as funções Lambda separadamente, o que adiciona complexidade no ciclo de desenvolvimento.

Por outro lado, Step Functions brilha quando você precisa de integrações diretas com serviços AWS sem escrever código para cada chamada, ou quando a visibilidade visual do fluxo é importante para a equipe.
Conclusão

Nesse artigo, vimos como Lambda Durable Functions permite construir workflows multi-etapas diretamente no código de uma função Lambda. Passamos pelos conceitos de durable execution, steps, checkpoints e replay, e vimos na prática como funcionalidades como wait, callbacks e execução paralela se traduzem para código em um exemplo de criação de ordem de compra.

Também entendemos como Durable Functions se compara ao Step Functions: ambos orquestram processos, mas com modelos mentais diferentes.

Como próximo passo, que tal experimentar reproduzir o exemplo desse artigo na sua própria conta? Você pode testar usando uma conta Free Tier da AWS! Ela permite que você explore serviços AWS com até 200 dólares de créditos por um período de 6 meses. Você não será cobrado a não ser que explicitamente escolha trocar de plano. Se quiser começar a pensar em problemas de fim a fim e criar sistemas serverless, deixo aqui ainda esse artigo para criar seu primeiro sistema serverless com AWS Lambda.

Carregando publicação patrocinada...