Passei as últimas semanas trabalhando em uma prova de conceito que me ajudasse a validar a arquitetura da solução antes de iniciar o desenvolvimento do MVP. O objetivo era testar a stack escolhida, a integração dos serviços da AWS escolhidos e se eles se mantivessem dentro do nível gratuito do AWS.
Trata-se de uma etapa muito importante do projeto, validar a arquitetura agora evita problemas no futuro, mas não elimina 100% a chance de aparecerem. Até por porque um software desenvolvido de maneira ágil está em constante evolução, os requisitos funcionais e não funcionais podem mudar fazendo com que a solução inicial não atenda mais a nova realidade e precise de revisões.
Voltando ao meu caso, ter essa visão cedo me permite antecipar problemas, como incompatibilidade entre os serviços ou tecnologias escolhidas. Sem validar antes, eu só descobriria alguns obstáculos mais adiante no desenvolvimento, atrasando o projeto, gerando custos (no caso de projetos corporativos) e também retrabalho. Agora que conclui essa etapa, gostaria de detalhar as escolhas feitas e o motivo por trás delas.
Arquitetura Serverless
Por se tratar de um projeto com apenas uma pessoa desenvolvendo, uma arquitetura que necessite de pouca manutenção e baixa complexidade é exatamente o que eu preciso. Por isso estou utilizando apenas serviços serverless dentro do AWS. O foco desse artigo não é explicar sobre serverless mas para quem quiser uma definição rápida, o blog da IBM resume bem:
Serverless é um modelo de execução e desenvolvimento de aplicações de computação em nuvem para que os desenvolvedores criem e executem códigos de aplicação sem precisar provisionar nem gerenciar servidores e infraestruturas de back-end.
Se quiser saber mais, você pode ler o texto completo neste link
O diagrama acima mostra os serviços do AWS utilizados para compor a solução e a integração entre eles, para explicar cada componentes, vou separar a explicação da arquitetura em alguns grupos, front-end, back-end, armazenamento, observabilidade e CI/CD:
Front-end
Para esse projeto, uma aplicação Single Page Application (SPA) já era suficiente. Não há necessidade de Server-Side Rendering (SSR) o que simplifica bastante pra mim, especialmente em um ambiente serverless. Uma SPA não necessita de um servidor de aplicação em execução contínua, posso simplesmente gerar os arquivos estáticos e servi-los diretamente de um bucket do S3 com a função de static website hosting ativada.
A escolha da biblioteca / framework front-end ficou entre duas opções muito consolidadas: React e Angular. Aqui ambas se sairiam muito bem, mesmo com suas particularidades. Acabei optando pelo React por uma serie de motivos técnicos e pessoais, entre eles o desejo de aprofundar meu conhecimento em React já que minha experiencia mais sólida é com o Angular. Esses critérios e diferenças entre as duas opções inclusive, poderiam render uma postagem futura.
Back-end
Já trabalhei anteriormente com o .NET 8, o que tornou a escolha uma decisão segura. Ainda não experimentei alguns recursos como as Minimal APIs e o AOT (Ahead-of-Time Compilation). Esse projeto será uma oportunidade pra explorar esses recursos de forma prática, em vez de investir um tempo para aprofundar meu conhecimentos em outros frameworks de outras linguagens como o Spring Boot no Java (que tenho uma experiencia mais básica) ou algo mais distinto como Go usando Gin. Assim como os critérios de decisão entre Angular e React, aqui cabe também um texto futuro sobre a escolha entre .NET, Java ou Go. Que foram as tecnologias que eu cogitei utilizar.
Então meu projeto será composto de algumas APIs rodando em funções Lambdas independentes, todas expostas para o front-end através do API Gateway, atuando como uma camada de roteamento e controle de acesso, aproveitei os recursos de throttling para limitar o número de requisições por segundo e por cliente protegendo a API (não que eu espere um tráfego alto na aplicação, mas caso aconteça algum abuso, essas proteções configuradas podem me poupar de uma surpresa inesperada na fatura da AWS).
O API Gateway está integrado com o Cognito, fazendo um fluxo de autenticação desde o front-end SPA até as rotas da API, tudo com o mesmo userpool. Esse modelo permite uma arquitetura completamente serverless, segura e escalável.
Banco de Dados
Pensando em manter a proposta de uma arquitetura serveless, optei por utilizar o Amazon DynamoDB como banco de dados. Aqui não teve muita dúvida, além de ser um serviço totalmente gerenciado e escalável, o DynamoDB está incluso no nível gratuito da AWS, uma excelente escolha para projetos pessoais e MVPs.
Como estamos falando de um banco NoSQL, baseado em chave-valor e documentos, é necessário uma modelagem de dados diferente da relacional utilizada nos bancos SQL. Esse ponto acaba sendo uma oportunidade ótima para eu estudar e praticar uma modelagem diferente, baseada em padrões de acesso em vez de normalização e relacionamentos. O DynamoDB não suporta “joins” entre as tabelas como acontece em um banco relacional, então as tabelas tem que ser pensadas para retornar basicamente todos os dados de uma vez. É comum consolidar dados diferentes em uma única tabela para otimizar performance, podendo haver duplicação de dados. Por isso a modelagem das tabelas começa pelas consultas que serão feitas.
Observabilidade
Todos os serviços da AWS utilizados no projeto enviam logs e métricas de uso automaticamente para o AWS CloudWatch, permitindo um monitoramento centralizado dos erros e comportamentos, e da saúde das aplicações.
Através do CloudWatch é possível acompanhar várias métricas importantes desde erros de execução nas funções Lambda, taxas de requisições, códigos de status HTTP retornados pelo API Gateway e consumo de leitura/escrita no DynamoDB. Também e possível configurar alarmes com base nas métricas quando atingirem níveis anormais, enviando notificações por e-mail via SNS. Assim podemos agir rapidamente em casos de problemas ou abusos e restaurar a aplicação um estado saudável e evitar custos desnecessários.
Por fim, caso apareça a necessidade, seja para estudo ou para lidar com cenários mais complexos, existe a possibilidade de adicionar o AWS X-Ray para habilitar um rastreamento continuo entre os serviços, visualizando uma requisição de ponta-a-ponta, passando por todos os serviços e identificando pontos de gargalos, latência ou falhas ao longo da execução.
CI/CD
A etapa de CI/CD, até o momento, é a única que utiliza serviços de fora da AWS. O repositório com código-fonte está hospedado no GitHub por dois motivos: O primeiro pelo GitHub ser onde normalmente os desenvolvedores colocam sues projetos de portfólio. O segundo motivo é que a AWS está depreciando seu serviço de repositórios Git, o AWS CodeCommit.
Além disso, o GitHub inclui um serviço de CI/CD integrado, o GitHub Actions, que oferece 1000 minutos de execução por mês gratuitos para contas pessoais (o que é mais que o suficiente pra esse projeto). Com isso a cada a na branch alteração na branch de um ou mais repositórios (ainda não me decidi sobre utilizar monorepo ou não), um pipeline é disparado realizando as etapas de compilação, execução de testes e deploy de artefatos para os serviços da AWS. Toda a configuração das Actions ficam em arquivos .yaml que são versionados no próprio repositório. Esse fluxo já foi testado na prova de conceito desenvolvida anteriormente.
Outra funcionalidade importante do GitHub é o suporte a variáveis e segredos de repositório. Eles funcionam de forma análoga ao AWS System Manager Parameter Store (que armazena variáveis referente ao projeto como, como uma URL de retorno por exemplo) e ao AWS Secrets Manager (que armazena informações sensíveis como strings de conexão com bancos da dados ou chaves de APIs), permitindo um controle e armazenamento seguro dessas parâmetros que são necessários durante o processo de build e deploy.
Lições aprendidas
Como disse no inicio do texto, fazer uma prova de conceito era importante para identificar problemas cedo e adaptar a solução antes de avançar muito no desenvolvimento.
Durante essa etapa esbarrei em uma limitação importante, o Amazon S3 com static website hosting não oferece suporte a servir as páginas em HTTPS, aparentemente mesmo utilizando um domínio próprio via Route 53.
Isso virou um obstáculo porque o AWS Cognito, utilizado aqui como Identity Provider (para fazer autenticação e autorização) exige que as URLs de redirecionamento sejam HTTPS com exceção apenas do localhost para testes.
Pesquisando por uma solução, descobri que ao adicionar uma distribuição do CloudFront em frente ao bucket S3 seria possível servir a aplicação SPA via HTTPS e utilizando essas URLs nos callbacks do Cognito, viabilizando a integração. Não adicionei o CloudFront pensando em receber um grande volume de tráfego aproveitando as capacidade dele como CDN, mas sim para contornar essa limitação técnica de HTTPS.
De quebra o CloudFront ainda trás alguns recursos legais que podemos explorar no futuro como por exemplo restringir o tráfego por região geográfica, isso pode ser útil em alguns cenários como evitar tráfego indesejado de bots ou regiões com alto índices de ataque. Movendo o API Gateway para atrás do CoudFront ganhamos também a capacidade de fazer cache de requisições (quando aplicável) para dados públicos ou raramente mutáveis por exemplo.
Considerações Finais
Esse foi um texto longo, e olha que eu tentei resumir ao máximo sem comprometer o conteúdo. Mas isso é para mostrar a quantidade de possibilidades e decisões que fazem parte do processo de pensar e repensar a arquitetura de um sistema. Isso considerando que esse é um projeto pequeno e que está nas fases iniciais, começando a sair do papel. Espero ter conseguido demonstrar o valor desse processo tanto no ponto de vista técnico quando de aprendizado.
Para as próximas semanas, minha expectativa é implantar os ambientes reais na AWS. Vou também continuar documentando a jornada aqui, com postagens sobre os desafios, aprendizados que estão surgindo.