Padrão Saga #3: Implementação Serverless
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:
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:
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:
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! 🙂