O blog da AWS

CQRS na AWS: Qual Solução Escolher?

Por Roberto Perillo, arquiteto de soluções enterprise da AWS Brasil.

Esta é a última parte da série que aborda as diferentes formas de se implementar o padrão arquitetural CQRS na AWS! Durante a série, procurei abordar o que é o padrão e diferentes técnicas que demonstram como eventos podem ser publicados a partir do serviço de comandos para serem consumidos no serviço de consultas. Em cada blog post, o aspecto mais importante é na verdade o mecanismo que permite emitir e transmitir eventos a partir do serviço de comandos para ser consumido pelo serviço de consultas, então pode-se na verdade ter quaisquer bancos de dados em cada um dos serviços.

Na primeira parte, comecei com a opção mais simples, em que usamos uma fila do Amazon SQS para transmitir dados do serviço de comandos para o serviço de consulta. Em seguida, exploramos diferentes opções de uso do padrão Transactional Outbox. Na segunda parte, exploramos o padrão Transactional Outbox e a técnica Polling Publisher, em que temos uma tabela outbox que contem registros que representam eventos de domínio a serem publicados e consumidos pelo serviço de consultas, e que é lida de tempos em tempos para que os eventos sejam publicados. Na terceira parte, continuamos utilizando o padrão Transactional Outbox, mas com a técnica de Transactional Log Tailing. Para a leitura da tabela outbox, utilizei o Debezium Connector for PostgreSQL. O caso apresentado na quarta parte foi o mesmo da terceira parte, mas utilizei o Amazon Database Migration Service (Amazon DMS) para efetuar a leitura da tabela outbox. E por fim, demonstrei na quinta parte como é possível emitir e consumir eventos quando se tem uma tabela do Amazon DynamoDB no serviço de comandos, por meio do DynamoDB Streams.

Os posts também abordam formas serverless de se implementar o padrão, com APIs expostas pelo Amazon API Gateway, que delegam as requisições para funções AWS Lambda, mas pode-se utilizar qualquer forma de computação para a implementação do padrão.

Foram 5 blog posts que demonstraram diferentes mecanismos. Uma pergunta que os leitores podem se fazer é: diante de diferentes técnicas, diferentes possibilidades e diferentes serviços, qual opção se deve escolher? É essa pergunta que esse blog post pretende endereçar. A ideia é dar aos leitores um modelo mental para que se possa fazer uma escolha consciente diante do cenário que se tem.

Introdução

O padrão CQRS resolve o problema de diferentes necessidades computacionais para escrita e leitura. Por isso, o padrão propõe dois serviços para essas necessidades. Pode ser que o serviço de escrita seja implementado como um container no Amazon EKS e o serviço de leitura seja uma função AWS Lambda, acessando bancos de dados diferentes, e cada componente pode ter necessidades computacionais diferentes. Mas não necessariamente precisamos utilizar bancos de dados e/ou modelos de dados diferentes. Por exemplo, o banco de dados pode ser um Amazon Aurora que possui diferentes réplicas de leitura. O serviço de escrita pode lidar com o cluster endpoint, enquanto o serviço de leitura pode lidar com o reader endpoint, que pode abstrair até 15 instâncias de diferentes tamanhos.

Como vimos na primeira parte, a ideia do padrão é permitir que diferentes necessidades possam ser representadas de diferentes formas. No caso do Aurora mencionado acima, teremos o modelo relacional nos dois serviços. E como o Aurora tem características de um banco de HTAP (Hybrid Transactional/Analytical Processing), há diversos casos em que pode fazer sentido utilizá-lo tanto do lado do serviço de comandos quanto do serviço de consultas. Ou um determinado modelo de dados que seja complexo ou que, por algum motivo, não seja de fácil leitura, pode ser representado em outro formato, que possa ser lido com uma latência que seja aceitável pelo usuário. Dessa forma, um modelo de dados que esteja em um determinado formato ou banco de dados que seja apropriado para escrita mas não ofereça fácil recuperação pode ser representado de outra forma, de forma a facilitar a leitura.

O padrão permite representar aspectos de um modelo de dados de diferentes formas. No caso que abordei na série, temos um modelo de dados simples que remete a um ecommerce no lado do serviço de comandos. Na base de dados, teremos registros que representam clientes, produtos, pedidos e seus respectivos itens.

Imagem demonstrando o modelo de domínio utilizado para demonstrar as soluções apresentadas nos blog posts

Figura 1. O modelo de domínio utilizado para demonstrar as soluções apresentadas nos blog posts.

O aspecto que queremos representar aqui, de forma a facilitar as consultas, é o valor total em compras de cada cliente. Um outro exemplo seria uma série de registros em que precisássemos efetuar diferentes cálculos em um valor de uma determinada coluna para se chegar em algum resultado. Uma forma seria ler todos esses registros e efetuar computações em memória. Dependendo da quantidade de registros, isso poderia ser oneroso, além de demandar mais recursos computacionais. Nesse caso, poderíamos ter o modelo de dados como uma série de registros no lado do serviço de comandos e outro que fornecesse o resultado dos cálculos de forma pré-computada no lado do serviço de consultas. Assim, teríamos mais performance e menos demanda de recursos computacionais.

Duas observações que quero deixar registradas: a primeira é que, na AWS, segurança é um assunto que levamos muito a sério. Nos blog posts, em cada subsessão “Executando o Exemplo”, indiquei a criação de um usuário com acesso de admin. A ideia foi facilitar a vida do leitor e agilizar o processo de criação do ambiente. Mas, de fato, ter um usuário admin para criar o ambiente foge do princípio de least privilege. Assim, uma possibilidade é criar o usuário com acesso admin, criar o ambiente e excluir o usuário imediatamente após a criação do ambiente. Ou, como exercício, os leitores podem encontrar as policies do Amazon IAM mais apropriadas (para se encontrar as policies mais restritas para cada exemplo, pode-se utilizar o Amazon IAM Access Analyzer) e utilizá-las para a criação de usuários com acesso mais restrito, para a criação dos ambientes.

A segunda é que, depois que comecei a escrever a série e o Luiz e eu já havíamos começado a trabalhar no código CDK que está no repositório de códigos desta série de blog posts, a Redis Inc. anunciou que o Redis estava deixando de ser open source. Isso nos pegou de surpresa. Se eu soubesse antes de começar as implementações, teria escolhido outro banco de dados como exemplo para o banco de dados do serviço de consultas (provavelmente o DynamoDB), mas as técnicas que apresentei funcionam para qualquer banco de dados. Somente no primeiro blog post, mostro um esquema de idempotência com o Redis, que os leitores podem substituir por uma implementação que utilize, por exemplo, DynamoDB com escrita condicional, para atingir o mesmo efeito de lock do Redis, e TTL, para que o item que controla a idempotência expire depois de algum tempo na tabela do DynamoDB.

Opções Quando o Banco de Dados do Serviço de Comandos é Relacional

Ao se implementar o padrão CQRS na AWS utilizando um modelo de dados relacional, a forma mais simples é utilizar um banco de dados Aurora ou RDS e utilizar réplicas de leitura, tendo o serviço de escrita lidando com o cluster endpoint, e o serviço de leitura lidando com o reader endpoint. Nesse caso, o banco e o modelo de dados seriam os mesmos no serviço de comando e leitura, mas poderíamos ter o banco de escrita em uma máquina e as réplicas de leituras em máquinas diferentes. Nesse caso, estaríamos endereçando somente necessidades computacionais diferentes.

Caso precisemos de modelos de dados diferentes, a primeira coisa a se fazer é verificar se a base sendo utilizada possui algum mecanismo nativo de stream, a facilidade de utilização desse mecanismo, se esse mecanismo requer algum componente periférico e o custo da solução. No caso do Aurora, como mencionei no primeiro post, a opção nativa é o Database Activity Streams, que implica na utilização mandatória do Amazon Kinesis Data Streams. Caso o utilizássemos, poderíamos ter um mecanismo similar ao apresentado no post que aborda a utilização do DMS em que um pipe do Amazon EventBridge tem como source um Amazon Kinesis Data Streams, e o restante da solução seria o mesmo. O detalhe é que essa funcionalidade do Aurora está mais para auditoria, e dessa forma, os registros enviados ao Amazon Kinesis Data Streams tem um formato não muito amigável para processamento. Então, precisaríamos tratar os registros, fosse na publicação do evento no serviço de escrita ou no processamento do evento no serviço de consultas.

A opção que tem o melhor custo-benefício dentre as soluções apresentadas é a que é abordada no primeiro post. Nela, um evento (representado por um documento JSON) é criado em tempo de execução para representar a situação ocorrida e publicado em uma fila SQS. Caso se esteja utilizando uma fila standard do SQS, o cuidado que se deve ter é tratar os eventos de forma idempotente na função Lambda que recupera mensagens. O primeiro post demonstra uma forma de tratamento de mensagens de forma idempotente utilizando o Amazon Elasticache for Redis. A razão pela qual filas standard têm semântica de entrega at-least once é abordada em Amazon SQS at-least once delivery.

Essa solução vai funcionar bem na grande maioria das situações. Entretanto, pode acontecer de um evento ser recuperado no lado do serviço de consultas e o componente que lê a fila principal e atualiza a base do serviço (nos exemplos abordados na série, uma função Lambda) não conseguir, por algum motivo, atualizá-la. Nesse caso, para evitar a perda do evento, ele terá que ser redirecionado para uma dead-letter queue, mas a base do serviço de consultas ficará desatualizada e sua atualização dependerá do tratamento à parte do evento a partir da dead-letter queue. Essa solução também não permite que eventos sejam reprocessados. Também não requer que a base do serviço de comandos seja limpa. Dessa forma, se o(a) leitor(a) estiver procurando uma solução simples, um eventual processamento tardio de eventos não for um problema e não houver necessidade de reprocessamento de eventos, essa é a solução que eu indicaria.

Em 2018, logo que foi lançado, li um livro incrível chamado Microservices Patterns, de Chris Richardson, que aborda diversos padrões fundamentais para se utilizar o estilo arquitetural de microservices da melhor forma possível. Especificamente no capítulo 3, Chris Richardson aborda a ideia de comunicação assíncrona entre os microservices, e nas páginas 97 e 98, na subsessão de Transactional Messaging, ele apresenta o padrão Transactional Outbox. Esse padrão pode ser utilizado em qualquer situação que envolva publicação de eventos, e como o CQRS precisa que eventos sejam publicados a partir do serviço de comandos para serem consumidos pelo serviço de consulta, pensei que faria sentido combinar os dois padrões e criar as soluções que apresentei nos blog posts. Então, a inspiração para a criação de boa parte das soluções que apresentei nesta série veio do livro Microservices Patterns.

Comecei a abordar o padrão Transactional Outbox a partir do segundo post. A ideia do padrão é que registros que representam eventos relacionados a acontecimentos em nossos aggregates sejam persistidos juntamente com os aggregates em si. Por exemplo, digamos que o status de pagamento de um aggregate Pedido tenha transitado de estado, de “PagamentoPendente” para “PagamentoConfirmado”. Na operação em que se persiste os dados do aggregate, persistiremos também um registro em uma tabela que representará o evento ocorrido. Depois, de alguma forma recuperaremos esse registro e o publicaremos em algum message broker. À tabela em que o registro que representa o evento é persistido se dá o nome de outbox table (em português, algo como “caixa de saída”).

No exemplo abaixo, persistimos nas tabelas referentes ao aggregate Pedido os detalhes de um pedido efetuado juntamente com os dados que representam o evento ocorrido em uma tabela “PedidoEvent”.

O padrão Transactional Outbox. Neste exemplo, um aggregate Pedido é composto pela classe Pedido, que por sua vez possui uma lista de ItemPedido. Ao efetuar um pedido, o aggregate é persistido juntamente com um registro que representa o evento de criação do pedido.

Figura 1. O padrão Transactional Outbox. Neste exemplo, um aggregate Pedido é composto pela classe Pedido, que por sua vez possui uma lista de ItemPedido. Ao efetuar um pedido, o aggregate é persistido juntamente com um registro que representa o evento de criação do pedido.

No exemplo apresentado acima, a tabela PedidoEvent representa somente eventos de criação de pedidos. Caso quiséssemos armazenar mais eventos nessa tabela, poderíamos ter uma coluna que representasse o tipo do evento. Nesse caso, seria ideal que os dados do evento em si fossem armazenados em uma coluna cujo tipo fosse JSON (para que não fosse preciso criar n colunas e ter valores nulos nos registros), ou também poderíamos criar uma tabela para cada tipo de evento que fosse acontecer na aplicação.

Embora a técnica remeta a bancos de dados relacionais, ela também funciona com bancos de dados NoSQL. Por exemplo, supondo que estivéssemos utilizando o Amazon DocumentDB, poderíamos persistir no documento que representa o aggregate uma entrada que representaria um evento a ser publicado. Caso o banco sendo utilizado não possua mecanismos de streaming, poderíamos aplicar o mesmo mecanismo do segundo blog post em que se utiliza a técnica Polling Publisher, ou caso possua, utilizar o mecanismo de streaming do próprio banco de dados para fazer a publicação dos eventos, como fiz no quinto blog post.

A vantagem desse padrão é que os eventos serão garantidamentes emitidos e processados em algum momento, então não há perda de eventos. O padrão lida somente com a persistência de eventos em tabelas outbox. Para a publicação dos eventos, é preciso utilizar algum mecanismo que recupere os eventos da tabela outbox de alguma forma e os publique.

Imagem demonstrando o mecanismo de publicação de eventos do padrão Transactional Outbox

Figura 3. No padrão Transactional Outbox, registros que representam eventos são persistidos de forma atômica juntamente com os dados que representam o aggregate sendo persistido. Para que os eventos sejam publicados, há um mecanismo que lê, de alguma forma, os eventos e os publica em um message broker.

O ponto de atenção ao se utilizar esse padrão deve ser o cuidado de sempre processar eventos de forma idempotente e ter algum mecanismo para manter a tabela outbox o mais limpa possível, para que ela não cresça inadvertidamente. Para publicar eventos a partir de tabelas outbox, há duas técnicas: Polling Publisher e Transaction Log Tailing. No segundo post, abordei a primeira técnica. Para exemplificar, utilizei o mecanismo de scheduler do Amazon EventBridge, invocando a função Lambda que recupera os eventos da tabela outbox de 1 em 1 minuto. Nos terceiro e quarto posts, abordei a segunda técnica. No terceiro post, para fazer a leitura do log de transações do banco de dados Aurora, utilizei o Debezium Connector for PostgreSQL instalado em um cluster do Amazon Managed Streaming for Kafka (Amazon MSK). No quarto post, utilizei o DMS com ongoing replication.

A vantagem da solução apresentada no segundo post e que utiliza a técnica Polling Publisher é que, embora seja um pouco mais complexa do que a apresentada no primeiro post, ainda é relativamente simples. Também não requer a instalação de componentes adicionais (como a solução apresentada no terceiro post), não requer mecanismos específicos de banco de dados (como parametrização para possibilitar a leitura do log de transações) e funciona com qualquer banco. A desvantagem é o tempo de atualização entre os bancos. Outra consideração é que, a princípio, o propósito primário dessa técnica não é o reprocessamento de eventos. E também não possibilita obter metadados das transações em si, pelo fato que recuperamos os eventos da tabela outbox para publicação, em vez do log de transações da tabela.

Essa técnica implica na utilização de um mecanismo que consulte a tabela outbox de tempos em tempos, e pode ser que não haja dados em n consultas à tabela, desperdiçando assim processamento (caso se esteja utilizando Aurora, indico fortemente utilizar réplicas de leitura e recuperar os eventos a partir do reader endpoint ou criar um custom endpoint somente para a recuperação dos eventos). Antagonicamente, pode ser que muita coisa tenha acontecido na tabela outbox entre as consultas. Dessa forma, se não puder haver perda de eventos, não for necessário reprocessamento de eventos, não for necessário obter metadados das transações e não houver problema se as bases ficarem muito tempo dessincronizadas (e “muito tempo” é uma medida que o(a) leitor(a) terá que definir caso a caso), essa é a solução que indicaria.

A outra forma de se lidar com tabelas outbox é a que foi apresentada nos terceiro e quarto posts, que é a técnica chamada Transaction Log Tailing (em português, algo como “ler a cauda/fim do log de transações”). Com ela, teremos um componente que acompanha o fim do log de transações de uma determinada tabela e torna a publicação dos eventos mais fácil. E como estamos publicando os eventos em streams (no terceiro post utilizei o Amazon MSK e no quarto o Amazon Kinesis Data Streams), podemos reler os eventos e reprocessá-los.

No terceiro post, utilizamos o Debezium Connector for PostgreSQL. É um componente utilizado em conjunto com o Kafka e, dessa forma, ele lê o fim do log de transações de uma tabela e publica as alterações (inserções, atualizações deleções) em um tópico Kafka. A técnica consiste em persistir, na mesma transação, um registro em uma tabela outbox que é lida pelo Debezium. Assim, os registros enviados para o tópico Kafka representam um evento a ser publicado pelo serviço de comandos para o serviço de consultas que garantidamente já aconteceu, então não há chance de haver problema entre a publicação do evento e a transação do banco de dados.

No quarto post, apresentei exatamente a mesma técnica, porém utilizando o DMS, que além de efetuar migrações de bancos de dados, também permite fazer change data capture por meio de leitura de log de transações de bancos relacionais. A diferença entre o Debezium e o DMS é que, por um lado, o Debezium é mais abrangente do que o DMS. Assim, ele oferece suporte a mais bancos de dados e também é suportado pela comunidade. Diversos clientes da AWS reportaram que ele suporta alto throughput, em que dados de uma tabela que recebe milhares de inserções e atualizações por segundo aparecem em milisegundos no tópico Kafka. A desvantagem é a configuração. Diversos clientes também reportaram que a acertar a instalação e configuração, combinada com o modelo de segurança da AWS, pode ser um desafio. Mas, uma vez que tudo está configurado corretamente, funciona perfeitamente.

Por outro lado, a configuração do DMS é mais amigável. Em uma tarefa de migração de banco de dados, indica-se os source (ou de onde os dados serão lidos) e target endpoints (para onde o dado vai). Isso equivale no Debezium aos sync e source connectors. A desvantagem é que o DMS não tem o alcance do Debezium, então ele não suporta a recuperação de dados a partir de tantos bancos de dados quanto o Debezium. Além disso, caso uma tabela já possua registros antes da primeira leitura do DMS, haverá o penalty da leitura inicial. O DMS faz um full table scan na primeira leitura, e a partir disso começa a ler o log de transações.

É importante observar que, para que o DMS funcione corretamente, é preciso acertar na escolha do tamanho da máquina que realizará a tarefa de migração, que é a instância de replicação, para que a tarefa não apresente problemas e seja necessário reiniciá-la e efetuá-la mais de uma vez. Uma discussão sobre como efetuar a escolha pode ser encontrada em Escolhendo a Instância Correta para a Migração. Outra dica é ter uma tarefa de migração por tabela sempre que possível. Se não for possível (por questões de custo, por exemplo), o ideal é manter nas tarefas tabelas que sejam o mais similares possíveis (por exemplo, tamanho e padrões de acesso). No caso apresentado nos blog posts, temos somente a tabela outbox sendo lida pelo DMS e sendo replicada para um stream no Amazon Kinesis Data Streams, mas quando é necessário fazer a replicação de mais de uma tabela, o ideal é não manter todas as tabelas na mesma tarefa de migração, pois caso seja necessário reiniciar a tarefa, ela terá que ler todas as tabelas novamente desde o primeiro registro.

Caso o banco de dados do serviço de comandos seja especificamente um SQL Server, alguns passos são necessários para que os tudo funcione corretamente e o DMS consiga ler o log de transações sem after a performance. Alguns clientes reportaram que, pelo fato de que a configuração do source endpoint não foi feita corretamente, o DMS onerou o processamento da máquina do SQL Server em aproximadamente 30%. Caso se esteja trabalhando com SQL Server, é importante verificar se todos os pré-requisitos estão contemplados em Change Data Capture para On-Going Replication no SQL Server.

Apresentei dois exemplos de como ler o log de transações da tabela outbox nos terceiro e quarto posts. Dessa forma, se o caso o banco de dados do serviço de comandos seja suportado somente pelo Debezium, ou a tabela outbox a ser replicada já possua registros na ordem de dezenas ou centenas de milhares, ou o time de desenvolvimento tenha conhecimento de como configurar o Debezium, ou até mesmo seja um requisito utilizar open source, a solução que eu indicaria é a que é apresentada no terceiro blog post. Caso o time de desenvolvimento saiba fazer a escolha correta da instância de replicação, ou a tabela outbox não possua registros, ou possua registros na ordem de centenas ou poucos milhares, ou caso possua mais de dezenas de milhares possa haver downtime para o full table scan da leitura inicial, e saiba também configurar corretamente o DMS, a solução que eu indicaria é a que é apresentada no quarto blog post.

Alguns leitores podem se perguntar se não seria válido utilizar a ideia do Two-Phase Commit. Assim, poderíamos ter em uma mesma transação a persistência do dado no banco de comandos e no banco de leituras. O detalhe é que, nesse caso, teríamos um ponto único de falha, que é o gerenciador de transações que coordena as transações. Além disso, estaríamos limitados a bancos de dados que implementam o XA Standard, e limitaríamos bastante nossas soluções. Por exemplo, nesse caso, não poderíamos utilizar nem o DynamoDB e nem o Amazon Neptune.

Opções Quando o Banco de Dados do Serviço de Comandos é NoSQL

Assim como com bancos relacionais, a solução do primeiro blog post também vai funcionar quando temos um banco NoSQL no serviço de comandos, com as mesmas considerações. Ao efetuar uma operação, publica-se um evento a ser consumido por um componente que atualiza o banco do serviço de consultas.

E, assim como com bancos relacionais, caso as desvantagens do primeiro blog post não sejam aceitáveis, deve-se verificar se o banco sendo utilizado possui mecanismos de disponibilização de alterações por meio de streaming. Por exemplo, o caso mostrado no quinto post, em que se utiliza uma tabela do Amazon DynamoDB no serviço de comandos, aborda a utilização do DynamoDB Streams. Para atingir o mesmo efeito que conseguimos com um tabela outbox, inserimos na tabela além dos dados que são persistidos normalmente um item que representa o evento que seria persistido na tabela outbox, e posteriormente o filtramos e o publicamos no nosso pipe do Amazon EventBridge. Além do blog post que publiquei, um outro blog post, que também aborda a utilização de DynamoDB Streams para CQRS, é o que foi publicado pelo meu amigo Roberto “Robertão” Silva, que mostra como foi feita a implementação pelo BTG Pactual.

Outros bancos de dados NoSQL na AWS também possuem mecanismos similares. Por exemplo, o Amazon DocumentDB e o Amazon Neptune também possuem formas de disponibilizar operações que já aconteceram por meio de streaming. O detalhe é que, no momento da escrita desse blog post, esses mecanismos não podem ser fontes de pipes no EventBridge, como fizemos com o DynamoDB Streams e, assim, o consumo desses streams devem ser realizados de outras formas.

Realizando a Escolha da Solução

Para facilitar a escolha da solução por parte dos leitores, criei a árvore de decisão abaixo. A ideia é guiar os leitores em cada cenário, para que seja possível escolher a solução mais adequada diante de um determinado cenário.

Imagem demonstrando a árvore de decisão para a escolha de implementação do padrão CQRS com serviços AWS

Figura 4. Árvore de decisão para a escolha de implementação do padrão CQRS na AWS.

Conclusão

Este é o último blog post da série que escrevi sobre as diferentes formas de se implementar o padrão CQRS na AWS. Nesse post, procurei fazer considerações sobre o padrão e sobre cada uma das soluções, e também um guia de como os leitores podem fazer a escolha mais adequada de implementação para cada cenário.

O padrão resolve a questão de diferentes necessidades computacionais para a escrita e leitura de dados. Dessa forma, o padrão propõe ter um serviço que lida somente com escrita e outro que lida somente com leitura de dados, e cada serviço pode ter os recursos computacionais mais adequados. Além disso, cada serviço pode também ter o banco de dados que mais facilite as operações de escrita e leitura. O desafio é como replicar os dados do lado do serviço de comandos para o serviço de consultas.

Durante a série, apresentei diferentes formas de fazer a replicação utilizando diferentes técnicas, cada técnica mais apropriada para cada cenário. Apresentei também uma árvore de decisão, que permitirá aos leitores fazerem a escolha mais adequada.

É isso para esta série! Espero que os blog posts ajudem usuários e clientes AWS a fazerem a melhor escolha de implementação do CQRS em cada cenário e, em caso de dúvidas ou considerações, os leitores podem me encontrar no LinkedIn! E meus mais sinceros agradecimentos aos amigos que contribuíram com o material que apresentei nessa série: Luiz Santos, Érika “Erikinha” Nagamine, Karine “Kah” Ferrari, Maria “Mavi” Mendes, Ciro Santos, Daniel ABIB, Gerson Itiro, Luiz Yanai, Peterson Larentis e Ricardo Tasso. Obrigado demais a todos vocês! Sem vocês, essa série não seria possível!

Sobre o Autor

Roberto Perillo Roberto Perillo é arquiteto de soluções enterprise da AWS Brasil especialista em serverless, atendendo a clientes da indústria financeira, e atua na indústria de software desde 2001. Atuou por quase 20 anos como arquiteto de software e desenvolvedor Java antes de ingressar na AWS, em 2022. Possui graduação em Ciência da Computação, especialização em Engenharia de Software e mestrado também em Ciência da Computação. Um eterno aprendiz. Nas horas vagas, gosta de estudar, tocar guitarra, e também jogar boliche e futebol de botão com seu filho, Lorenzo!

Sobre o Colaborador

Luiz Santos Luiz Santos trabalha atualmente como Technical Account Manager (TAM) na AWS e é um entusiasta de tecnologia, sempre busca novos conhecimentos e possui maior profundidade sobre desenvolvimento de Software, Data Analytics, Segurança, Serverless e DevOps. Anteriormente, teve experiência como Arquiteto de Soluções AWS e SDE.

Sobre os Revisores

Erika Nagamine Erika Nagamine é arquiteta de soluções especialista em Dados na AWS. Tem formação acadêmica sólida, graduada em Sistemas de Informação, com pós graduação em Administração de Banco de Dados, Engenharia de Dados e especialização em mineração de Dados Complexos pela Unicamp. Atua com clientes de diversos segmentos e já participou de mais de 200 projetos no Brasil e no mundo. Atualmente múltiplas certificações em dados, computação em nuvem, todas as certificações AWS, e ama compartilhar conhecimento em comunidades e é palestrante em eventos técnicos de destaque em todo o Brasil e no mundo.
Luiz Yanai Luiz Yanai é arquiteto especialista sênior em analytics na AWS atuando com clientes nativos na nuvem e empresas do ramo financeiro en suas jornadas para se tornarem data-driven. Possui quase 20 anos de experiência em arquitetura e desenvolvimento de soluções envolvendo sistemas empresariais e de missão crítica sendo que os últimos 5 anos estão focados na nuvem AWS.