.NET

27 jun, 2018

Aplicações serverless com .NET Core 2

Publicidade

Ou: “O que o StackOverFlow não te contou”

“No server is easier to manage than no server” – Werner Vogels (CTO, Amazon.com)

Bem-vindo, leitor! A frase acima representa uma ideia que se fortalece no contexto atual de desenvolvimento e disponibilização de aplicações para a web. Neste artigo, vamos falar sobre aplicações Serverless usando ASP.NET Core e Amazon Web Services (AWS).

Não vamos apresentar um passo a passo da implementação, já que você pode ver todo o código no GitHub. O foco será em conceitos importantes da abordagem, como escalabilidade e custos, assim como os elementos que compõem a implementação, usando as tecnologias mencionadas e suas relações entre si. Resumindo, vamos entender o que estamos colocando no ar e de onde vêm as cobranças ($); sem next, next, finish por aqui, ok?

1. Aplicações Serverless

Com a flecha do progresso passando diante de nossos olhos, vimos o surgimento e evolução da virtualização até os dias atuais da computação em nuvem, nos quais encontramos tanto elementos de software (filas, logs e envio de mensagens), como elementos de infra (storage e servidores), que são oferecidos em um modelo orientado a serviços.

Aplicações Serverless são aplicações disponibilizadas sem a necessidade de se preocupar com o provisionamento, escalabilidade ou gerenciamento de servidores.

A equipe responsável pela criação e manutenção da aplicação gera o pacote, integra com as ferramentas de um provedor (a AWS, no nosso caso), e observa o funcionamento do sistema sem outras preocupações além do desenvolvimento.

Contudo, nem toda aplicação está apta a existir nesse contexto. Um ponto importante que os desenvolvedores devem ter atenção, é que se antes a aplicação não estava em uma arquitetura distribuída, agora ela estará. Isso consequentemente demanda maior cuidado com o gerenciamento de estados na aplicação e utilização dos recursos do servidor, principalmente o file system.

Não significa, portanto, que a aplicação para ser Serverless tem que ser Stateless. Nada contra a utilização, por exemplo, de um banco chave-valor de alta performance (ex: Redis) para gerenciamento de estados.

Mas sabe aquele singleton que foi feito “rapidinho” para gerenciar um estado? Ou aquele dado que está persistindo em um arquivo para não precisar preencher um documento de três vias a ser julgado pelo conselho dos éforos caso se decida cometer a heresia de mexer no banco? (“It is a trap Bino!”). É importante se livrar dessas coisas antes de partir para uma arquitetura distribuída e escalável.

2. ASP.Net Core + AWS Lambda + API Gateway = ASP.Net Core Serverless

Quando fazemos uma busca sobre como implementar uma aplicação ASP.NET Core Serverless com AWS, a maioria dos resultados nos orienta a utilizar o plugin da AWS para o Visual Studio. Você cria sua aplicação ASP.NET, clica no projeto com o botão direito, seleciona publicar e “puf!” Como um passe de mágica, sua aplicação estará disponível.

Nós, no entanto, preferimos o desafio de estudar os elementos envolvidos nessa integração e suas relações entre si para atingir esse objetivo por meio de um código .NET Core que desenvolvemos. Nosso código consome a API da AWS com o auxílio do SDK para .NET Core disponibilizado pela própria AWS. Foi uma experiência muito produtiva, que rendeu conhecimentos valiosos. Vamos ver um pouco deles?

O código que realiza a publicação da aplicação está no projeto “AspNetServerPublish” da Solution “AspNetServerLessAWS”, no nosso repositório no GitHub. A aplicação que está sendo publicada é o projeto “AwsDeveloperTraining”, que está na mesma Solution. O código que realiza a publicação cria e configura os elementos da AWS de acordo com essa imagem:

Como podemos ver, nossa aplicação é carregada em uma função Lambda, que é acionada pelo API gateway. É o API gateway que fica exposto para o mundo e realiza o proxy das requisições para a Lambda.

2.1. Embarcando uma ASP.NET Core Web API em uma função Lambda

AWS Lambda é o serviço de computação Serverless da AWS. Você embarca seu código em uma função Lambda e a AWS provisiona de forma transparente e on-demand os recursos necessários para a execução do seu código. Você será cobrado apenas quando seu código for requisitado com base em uma fórmula que considera evento de requisição, tempo de processamento da requisição e memória utilizada para execução da função.

A criação da função Lambda em si é um processo simples, você só precisa utilizar a função “CreateFunctionAsync” da classe “AmazonLambdaClient”; não tem mistério. A parte interessante fica por conta da evolução apresentada pelo Lambda no suporte a aplicações .NET Core.

Antes do início da era multiplataforma do .NET, o Lambda suportava apenas linguagens que podiam ser executadas em ambiente Linux, como Python, Node, etc. Com a estabilização das primeiras versões do .NET Core, o Lambda começou a suportar funções escritas para .NET Core 1 e agora suporta também .NET Core 2. No entanto, o ponto de evolução mais interessante é o suporte de toda uma aplicação ASP.NET em uma única função Lambda.

A ideia inicial, como o próprio nome diz, era que uma função Lambda executasse um fluxo específico da sua aplicação, sendo que a totalidade da aplicação é composta por várias funções Lambda. Para o caso de uma Web API, cada função seria uma rota da sua API.

Dessa forma, já podemos entrever o desafio que é gerenciar a publicação e atualização de cada rota separadamente, cada uma demandando um pacote específico, além de ser do desenvolvedor a responsabilidade de realizar o roteamento da aplicação, fazendo com que a chamada a uma rota execute sua respectiva função lambda.

Entretanto, para garantir nosso sono tranquilo, a AWS criou o “Amazon.Lambda.AspNetCoreServer”.

Instalando essa dependência em nossa aplicação ASP.NET e criando uma classe para o que é chamado de “LambdaEntrypoint” (imagem abaixo), nossa aplicação pode ser embarcada por inteiro em uma função Lambda e o próprio Lambda se responsabiliza por carregar todos os elementos necessários para a completa execução do pipeline do ASP.NET: “HttpContext”, “RequestMessage”, entre outros. Assim, podemos usufruir de todos os recursos do pipeline: roteamento, “Model Binding”, “Filters” e por aí vai.

Exemplo Lambda Entry Point

2.2. Configurando o API Gateway como proxy da aplicação

Funções Lambda não ficam expostas publicamente. Toda função Lambda só pode ser invocada por um usuário ou serviço da AWS associado a sua conta e com respectiva permissão. Mas então, como faremos para que qualquer cliente na internet possa chamar as rotas de nossa API?

Esse papel cabe ao API Gateway, um serviço público por natureza. Toda API criada gera um domínio que fica público e pode ser acessado por https. A partir desse ponto, o API Gateway fará o proxy das requisições para a função Lambda carregada com nossa aplicação ASP.NET.

O API Gateway é um serviço da AWS que te permite criar uma API facilmente integrável com os demais serviços da AWS. Isso facilita o monitoramento, autorização, controle de acesso e demais medidas de segurança, como por exemplo a prevenção a ataques DDOS.

Nas próximas linhas vamos destacar alguns pontos importantes durante o processo de criação do Gateway e integração com a função Lambda; aquele bizú que poderá te poupar horas de pesquisa e debug. Lembrando sempre que você pode ter acesso a todo o código que realiza essa integração em nosso repositório de estudos no GitHub, projeto “AspNetServerPublish“.

Em sua totalidade, o processo consiste nas seguintes etapas:

  • Criação da API;
  • Criação de recursos para a API. No nosso caso, como queremos criar um proxy, nossa API terá somente um recurso com valor de path igual a “{proxy+}”;
  • Criação de métodos para o Recurso. Aqui, são definidas as ações que poderão ser executadas para o recurso: Get, Post, Put, Delete, entre outros. Como se trata de um proxy para toda a aplicação, definimos no processo de criação o valor coringa “ANY”, que automaticamente cria um abrangente conjunto de métodos para o recurso em questão;
  • Configuração da integração entre API Gateway e Lambda. Esse é o ponto central do processo e existem duas observações importantes a serem feitas. Veja o trecho de código que cria a integração na imagem abaixo:
Configurando integração entre API Gateway e Lambda

Observe as propriedades “HttpMethod” e “IntegrationHttpMethod”. A primeira diz respeito à nossa aplicação e recebe o valor do método que sofre a ação de proxy, no nosso caso o valor é “ANY”. Já a segunda, corresponde ao método para integração entre os serviços API Gateway e Lambda, e o valor será sempre “POST”, pois aos olhos da AWS, o que está sendo feito é a criação de um novo recurso habilitável, no caso uma “Invocation”. Você será cobrado por cada invocação feita ao Lambda, então a AWS registra isso em seus sistemas por meio de um serviço “POST Invocation”.

A Segunda observação diz respeito à propriedade “Uri”. Está vendo aquela data ali, logo depois do termo “path”? Então, essa é a versão do endpoint do Lambda para registro da invocação e execução da função. Esse formato de URI, com versão, eu não consegui encontrar em nenhuma documentação, fórum ou artigo da internet, e sem isso, eu não consegui fazer a integração funcionar. Para encontrar referências a esse formato, precisei ficar analisando os logs do API Gateway no CloudWatch e isso inspirou o título desse artigo.

  • Criação do Deployment da API. Aqui vamos publicar o nosso proxy para o mundo;
  • Conceder permissão ao Proxy para invocar a função Lambda. Essa é a última etapa, pois precisaremos estar com os dois elementos (Proxy e Lambda) já criados. A lógica aqui consiste em declarar na função Lambda as APIs que poderão invocá-la.

O ponto de destaque é o formato do ARN de permissão, definido na propriedade SourceArn:

Concedendo permissão de invocação ao API Gateway

Após a conclusão dessa etapa, você deverá ter sua aplicação ASP.NET Core pública e totalmente Serverless com a AWS.

3. Analisando os custos

De um ponto de vista prático, a abordagem Serverless demonstra vantagens consideráveis que já mencionamos. Contudo, vamos analisar a solução por outro prisma, o financeiro. Quanto será que custa essa brincadeira? Como sabemos bem, ninguém trabalha de graça. Se a AWS vai te deixar despreocupado com relação a disponibilidade e escalabilidade de sua aplicação, é fato e justo que ela cobrará o preço.

Uma coisa evidente é que o padrão de cobrança do Lambda e API Gateway que cobra apenas pelas requisições processadas é vantajoso para aplicações que recebem pouca carga ou recebem carga de forma intermitente, com períodos consideráveis sem receber carga significativa.

E quanto ao caso de aplicações que recebem cargas massivas? Já que é nesses casos que podemos desfrutar das facilidades relacionadas à escalabilidade dos recursos. Em uma palestra sobre performance de APIS de um arquiteto do principal portal de conteúdo esportivo do País, ele relatou que seu back-end recebia em média 6.000 requisições por minuto. Vamos considerar essa carga válida como cenário para o nosso estudo de caso.

Vamos, então, analisar os custos mensais para o seguinte cenário: uma API que recebe 6.000 requisições por minuto, utiliza 128 MB de memória, tem suas requisições processadas em 200 milissegundos, e transporta 3 KB de dados por requisição.

Seguem abaixo imagens da própria AWS demonstrando o cálculo do custo para o Lambda e API Gateway sobre requisições com as mesmas características que as descritas em nosso cenário. Depois, vamos aplicar um cálculo de regra de três para proporcionalmente projetarmos o custo de nosso cenário (6.000 requisições por minuto).

Para o cálculo do Lambda, temos como base o seguinte:

Fonte: https://aws.amazon.com/pt/lambda/pricing/
Fonte: https://aws.amazon.com/pt/lambda/pricing/

E para o API Gateway, teremos a seguinte base:

Fonte: https://aws.amazon.com/pt/api-gateway/pricing/

Dessa forma, ficamos com os seguintes custos:

  • Total de requisições em um mês: 259.200.000
  • Lambda | Cobrança calculada mensalmente:

(5,83 / 30) = (X / 259,2)
X = (5,83 * 259,2) / 30
X = U$ 50,37

  • Lambda | Cobrança de solicitações mensais

(5,8 / 29) = (X / 258,2)
X = (5,8 * 258,2) / 29
X = U$ 51,64

  • – Custos Totais Lambda:

50,37 + 51,64 = U$ 102,01

  • Custos API Gateway

(18,79 / 5) = (X / 259,2)
X = (18,79 * 259,2) / 5
X = U$ 974,07

  • Custos Totais | Lambda + API Gateway

102,01 + 974,07 = U$ 1076,08

Assim, podemos concluir que os custos de uma abordagem Serverless com a AWS podem ser altos para uma aplicação que receba uma considerável carga de requisições, principalmente por conta do API Gateway, que no nosso caso representa 90,52% dos custos totais. Ele é 954,88% mais caro que o Lambda.

Então, da próxima vez que ouvirmos a máxima de que Serverless é barato, vamos nos lembrar de que de fato o Lambda é bem barato, mas o API Gateway é caro e pode pesar bastante nos nossos custos totais.

3.1. Reduzindo os Custos: Uma proposta de arquitetura híbrida

Entendemos que os elevados custos do API Gateway refletem todos os recursos que ele disponibiliza. Contudo, também consideramos que muitas aplicações podem ter interesse apenas na realização do proxy para o Lambda, deixando a cargo da própria aplicação processos como autorização e controle de acesso.

Tendo isso em vista, achamos que pode ser útil analisar os custos e implantação de uma abordagem híbrida com uma aplicação hospedada em servidores EC2 responsável apenas por fazer o proxy para o Lambda.

Como a execução do proxy não é um processo oneroso, pode ser executado mesmo para altas cargas de requisições e por servidores de propósito geral, o que reflete em custos menores que os do API Gateway. Em nosso estudo, desenvolvemos uma aplicação ASP.NET Core que realiza esse proxy das requisições para o Lambda. Essa aplicação também está no nosso repositório de estudos com o nome “ASPNetLambdaProxy”.

A aplicação é simples, seu pipeline é composto por apenas um middleware que realiza a integração com o AWS Lambda (ver imagens abaixo):

Construção do Pipeline
Fragmento do Middleware

Hospedamos essa aplicação em uma única máquina t2.medium, e com ela fomos capazes de processar 6.000 requisições em cerca de 33 segundos, consumindo no máximo cerca de 3,5% de CPU.

3 testes em sequência ,sendo que o último é de 6.000 requisições
Consumo de CPU

Conforme podemos observar, o maior gargalo dos testes foi o tráfego de rede entre nós e a AWS, o que já era esperado, pois estamos usando a região de Virgínia na AWS. Vale ressaltar também que o código executado pelo Lambda possui esforço computacional relevante: a computação de um hash SHA256 das credenciais de uma conta a ser criada.

Dessa forma, considerando que o preço informado pela AWS para t2.mediun em Virgínia e SO Linux no momento em que escrevemos esse artigo é de U$ 0.0464 por hora (imagem abaixo) teremos um custo mensal de U$ 33,41 por máquina utilizada (30 * 24 * 0,0464).

fonte: https://aws.amazon.com/pt/ec2/pricing/on-demand/

Ainda que adicionemos custos de tráfego de rede e Load Balancer (para os casos de se usar mais de uma máquina), projetamos com essa estratégia um custo bem inferior aos U$ 974,07 que teríamos ao utilizar o API Gateway para realizar o proxy.

4. Conclusão

Gostamos bastante da abordagem Serverless e principalmente do suporte prestado pela AWS para aplicações .NET Core. Consideremos que a abordagem é vantajosa para aplicações de pequeno e médio porte, mas para
aplicações de grande porte, que recebam 20 milhões de requisições ou mais por mês, achamos interessante a utilização de uma abordagem híbrida, com o objetivo de evitar os altos custos do API Gateway.

E você? O que tem a nos dizer sobre aplicações Serverless com o .NET Core? Está pensando em utilizar essa abordagem em seus projetos? Deixe seu comentário e até a próxima!

***

Este artigo foi publicado originalmente em: https://www.concrete.com.br/2018/05/21/aplicacoes-serverless-com-net-core-2/