Padrão Saga #3: Implementação Serverless

Bianca Cristina
9 min readFeb 27, 2023

If you’d like, you can read this article in English here

Nos artigos anteriores, discutimos sobre os principais conceitos relacionados ao padrão Saga (primeiro artigo) e também sobre formas de lidar com a falta de isolamento entre transações (segundo artigo).

Até agora, focamos apenas na teoria do padrão Saga porque é, essencialmente, isso que ele é: um conceito. Isso significa que podemos implementá-lo utilizando qualquer tecnologia e a melhor escolha sempre será aquela que se adequa ao seu negócio. Como, infelizmente, não fomos nós que simplificamos o trânsito urbano com um aplicativo, não tem nenhum negócio envolvido nesse artigo (triste, eu sei 😥), então todas as escolhas de linguagens e ferramentas utilizadas são para fins exploratórios.

Chega de papo e mão na massa!

Vários exemplos de sagas foram apresentados nos outros artigos e seria impraticável implementar todos. Logo, apenas a saga “Cancelar Viagem com Tempo Reorganizada” será implementada aqui:

Saga “Cancelar Viagem com Tempo Reorganizada”

Vale ressaltar que a escolha dessa saga também simplifica algumas decisões de implementação, sobretudo, em relação a como minimizar os riscos de transações ACD por meio da técnica visão pessimista.

Padrão Saga via Coreografia

Sou fã de spoilers e já vou te mostrar como é o desenho da implementação do padrão Saga via coregrafia:

Padrão Saga via Coreografia

Destrinchando o desenho, temos:

  • Gatilho (A): o gatilho define como cada transação local da saga será acionada. Para a maioria das transações, o gatilho é um evento publicado no SNS, sendo a única exceção a primeira transação que é acionada pela solicitação do usuário
  • Transação local como Lambda (B): cada transação local da saga representa uma função Lambda que utiliza Python para implementar o código
  • Banco de dados (C): pelo contexto do padrão Saga, cada serviço possui um banco de dados distinto e durante a execução da transação local esse banco pode ser acessado
  • Tomada de decisão após execução da transação local (D): na coordenação de sagas via coreografia, cada serviço é responsável por informar quando uma transação local é finalizada. Nesse caso, para cada transação local, em caso de sucesso, um evento é publicado no tópico daquele serviço. Em caso de falha que excede a quantidade máxima de retentativas da função Lambda, o evento problemático é enviado para a DLQ para processamento futuro.

Beleza… Mas como transformar esse desenho em uma aplicação Serverless real?

Esse é nosso próximo passo!

⚠️ Assim como é inviável implementar todas as sagas discutidas, também é inviável mostrar como cada recurso do desenho é criado em um único artigo. Para simplificar, os próximos passos demonstram como criar todos os recursos necessários para a transação local “Verificar Tempo (T1)”.

1: Configuração AWS CLI

Começando pelo começo, usaremos a AWS CLI para criar os recursos, logo, é necessário configurar as credenciais do usuário utilizado:

Note que existem dois comandos extras nessa parte que exportam as variáveis AWS_ACCOUNT_ID e AWS_REGION, as quais serão úteis para evitar repetição dessas informações nos próximos comandos.

2: Configuração IAM

Para que a função Lambda possa interagir com o tópico e DLQ, é necessário definir as seguintes policy e trust policy:

Uma vez definidas as policies, basta criar a role:

3: Criação do banco de dados

Já comentamos que cada serviço possui um banco de dados distinto, logo, também precisamos criar esse recurso na AWS:

Para os fins desse artigo, a escolha do PostgreSQL não tem nenhuma justificativa particular. Seja livre para escolher o que melhor atende suas necessidades!

4: Criação do Tópico

Todas as transações locais são acionadas por meio de eventos e por isso o SNS foi utilizado como gatilho. No caso da transação T1, o gatilho inicial é o usuário, mas ainda é necessário publicar no tópico quando o processamento é concluído. Sendo assim, é necessário criar o tópico no qual a transação deve publicar (nesse caso, é o tópico responsável por acionar a transação T2):

Assim como a escolha do PostgreSQL, não há nada particular que exija o SNS como gatilho para as transações locais. Já deu pra entender o ponto, certo? Use o que melhor atende as suas necessidades, sempre!

5: Criação da DLQ

Ainda que as funções Lambda sejam preparadas para retentar automaticamente um evento em caso de falha e isso seja suficiente para falhas temporárias (indisponibilidade de algum serviço, por exemplo), há casos em que mesmo com as retentativas não é possível processar o evento. É nesse ponto que as dead letter queues (DLQ) entram: todo evento que excede o número máximo de retentativas é enviado para a DLQ, a qual pode ser usada para uma lógica futura de reprocessamento de eventos. Dito isso, é mais um recurso que precisa ser criado na nossa estrutura:

6: Criação da camada comum (Lambda Layer)

A fim de definir uma maneira uniforme de acessar o banco de dados e evitar duplicação de código, uma Lambda Layer foi criada para definir uma camada compartilhada entre todas as funções Lambdas. Essa camada pode ser criada da seguinte forma:

7: Criação da função Lambda

Finalmente, podemos subir nosso código! 🥳

Uma função Lambda precisa definir um método de entrada padrão para quando for acionada. Aqui, utilizaremos o método padrão da AWS def lambda_handler(event, context):

Bom, você sabe, não há…

razão específica para a escolha do Python, eu sei, já ficou claro 😑

Certo… Entendido o código, basta criar a função Lambda:

Ao final dessa etapa, a função Lambda criada será semelhante a essa:

Note que, para essa transação local, não há um trigger definido: isso acontece porque, no nosso desenho, o gatilho inicial é o usuário e cada aplicação pode definir a melhor interface para que esse gatilho inicial ocorra utilizando recursos como API Gateway, Load Balancer, SQS, SNS, entre outros. Para fins de teste, nesse artigo, o gatilho inicial é executar a função Lambda via AWS Console.

Além disso, também vale conferir se a DLQ foi configurada corretamente. Essa visualização está disponível na aba Configuration -> Asynchronous invocation do console.

Voilà! 🥳

Você já está com tudo preparado pra testar o padrão Saga via coreografia!

Padrão Saga via Orquestração

Usando coreografia, além de criar cada recurso, também é necessário definir como interagem entre si (por exemplo, qual tópico é responsável por acionar um Lambda). Já na orquestração, o gerenciamento dessa interação é de responsabilidade de um terceiro serviço e, como estamos usando a AWS, podemos nos beneficiar do AWS Step Functions para definir os steps necessários para acionar a execução de cada Lambda. Por conta disso, há diferenças relevantes entre o diagrama da coreografia e orquestração:

Padrão Saga via Orquestração

Nesse desenho, a explicação anterior sobre a transação local como Lambda (B) e o banco de dados (C) ainda é válida. Entretanto, o gatilho (A) e a tomada de decisões após execução da transação local (D) são afetados quando utilizamos Step Functions:

  • Gatilho (A): o gatilho ainda é responsável por definir como cada transação local (Lambda) será acionada, porém agora é responsabilidade da AWS definir qual recurso será utilizado para esse fim e não precisamos controlar tais recursos explicitamente. Dessa forma, um tópico SNS já não é mais necessário.
  • Tomada de decisão após execução da transação local (D): uma vez que o Step Functions é o orquestrador das sagas, nossa única responsabilidade é definir os steps do fluxo e a AWS fica encarregada de controlar a execução (tanto em caso de sucesso quanto de falha).

Bacana, mas como podemos configurar essa mágica?

1: Configuração AWS Console

Diferente dos passos anteriores, para a orquestração, utilizaremos o console ao invés da CLI para usufruir do editor visual do Step Functions. Como cada conta AWS possui suas especificidade, é inviável tentar mapear aqui como configurar para todos os casos, mas tenha em mente que o usuário utilizado nessa etapa deve possuir, no mínimo, permissões para acessar o console e editar fluxos no Step Functions.

2: Reaproveitar recursos já criados para a coreografia

Os passos para a criação do banco de dados, DLQ, camada comum e função Lambda definidos anteriormente ainda são válidos aqui e podem ser reaproveitados.

Opcionalmente, também vale a pena criar uma nova função Lambda para evidenciar as diferenças da configuração anterior e a do Step Functions, fica a seu critério.

3: Criar cada step do fluxo no Step Functions

No console da AWS, procure pelo serviço Step Functions e clique em State Machines para visualizar os fluxos existentes na sua conta:

Você deve definir o tipo mais adequado para suas necessidades, mas se só estiver aqui para fins de teste, a opção Standard é suficiente:

A próxima página já é o editor visual do Step Functions:

No canto esquerdo, escolha AWS Lambda: Invoke e arraste para o editor, isso abrirá uma janela de configuração no canto direito, na qual apenas os nomes da função (nesse caso route-check-time:$LATEST) e do próprio step (nesse caso T1: Check Time) precisam ser configurados nesse momento:

Repita o processo anterior para todos os Lambdas necessários até obter o seguinte fluxo no editor:

Também precisamos indicar qual ação deve ser tomada em casos de erro. Para isso, selecione algum dos steps e selecione a aba Error Handling no canto direito:

Nessa aba, selecione Add new catcher e especifique o erro States.ALL, isso indica para o Step Functions que esse step deve ser acionado em caso de qualquer erro.

A ação que deve ser executada é enviar uma mensagem para a DLQ. Para que isso seja configurado, na opção Fallback state, selecione Add new state. Um novo step (ainda vazio) será adicionado no fluxo:

No canto esquerdo, procure por SQS, selecione a opção Amazon SQS: SendMessage e arraste para o editor. Ao arrastar o novo step, uma aba de configuração abrirá no canto direito, na qual apenas a URL do SQS e nome do step precisam ser informados:

Repita o processo anterior para todas as DLQs até obter o seguinte fluxo:

Todos os steps já foram definidos, basta clicar em Next . Na página seguinte, revise o JSON gerado pelo editor e caso tudo esteja de acordo clique em Next . A última etapa consiste em especificar as configurações básicas do fluxo criado tais como o nome, permissões e nível de log. Realize essas configurações e finalize a criação do fluxo clicando em Create state machine .

Caso você tenha criado uma nova função Lambda, é possível notar as diferenças na configuração ao finalizar a criação do fluxo no Step Functions. Considere a função route-check-time:

Note que diferente de quando não utilizamos o Step Functions, não há configuração para o destination: o Step Functions se torna responsável por definir toda a execução do fluxo dos Lambdas sem a necessidade de tornar essa configuração explícita na função Lambda.

O mesmo ocorre para a configuração de DLQ:

Voilà! 🥳

Agora você também está pronto para testar sagas via orquestração!

Com isso finalizamos o assunto padrão Saga, espero que tenha sido útil. Se precisar, os exemplos discutidos nesse artigo estão disponíveis no GitHub.

Até mais! 🙂

--

--