Os 12 mandamentos das aplicações Cloud-Native
Tags: cloudnative, cloud, architecture, 12factor
Published: 2021-10-25 19:14:19

Fala pessoal!

Temos ouvido falar muito hoje em dia sobre aplicações em nuvem, software como serviço (SaaS), sistemas distribuídos, conteinerização, orquestração, além de muitos outros assuntos relacionados. 

Mas e ai? Precisamos saber de tudo? Por onde eu começo?

Há alguns anos, Adam Wiggins publicou uma metodologia, junto com seus colegas da Heroku, conhecida como "The Twelve-Factor App",  para auxiliar todo mundo que está desenvolvendo software como serviço. Todos esses pontos estão 100% alinhados com o desenvolvimento das famosas aplicações Cloud-Native.

Antes de entrar no "Twelve-Factor App", vale contextualizarmos a todos de que:

A arquitetura Cloud-Native é uma abordagem de design para que possamos construir e operar, facilmente, nossos sistemas em ambientes de nuvem, aproveitando todos os benefícios da mesma, sem utilizá-la como um Datacenter. Muitas pessoas hoje continuam utilizando a nuvem como um Datacenter, onde podem crescer seus ambientes de infraestrutura "Ao infinito e além!" (LIGHTYEAR, Buzz). Esse tópico sozinho merece um post separado (EM BREVE O LINK).

Voltando ao assunto do Twelve-Factor App, ele dita alguns pontos, observados pelo time do Adam, que vão nos auxiliar a entregar softwares melhores, reduzindo problemas conhecidos em cenários complexos. 

Senta que lá vem história:

1) Base de Código

O primeiro pilar se refere a sempre utilizarmos a mesma base de código, com isso sempre utilizaremos a mesma versão para publicação dos nossos sistemas, pelos nossos diversos ambientes  (Desenvolvimento, QA, Pre-Produção, Produção, etc).
 
Para cada repositório de código, devemos ter apenas um sistema, que respeitará o restante dos outros 11 pilares. Colocar mais de um sistema no mesmo repositório é uma violação do Twelve-Factor App.

2) Dependências

O segundo pilar fala sobre o empacotamento de nossas dependências. Nossas dependências devem ser isoladas e devidamente declaradas através de um manifesto de dependências. 

Ex.: quando utilizamos o Nuget com o .NET para declarar nossa árvore de dependências. Isso faz com que cada versão do nosso código não precise ocupar disco com todo pacote de dependências, facilitando a criação de novas ramificações e cópias locais do nosso código, dentre outros benefícios. 

3) Configurações

Na minha opinião esse é um dos pontos mais críticos do Twelve-Factor App. 
Com o advento dos contêineres e seus orquestradores, precisamos que TODOS os nossos sistemas não tenham configurações de AMBIENTE diretamente no código.

Image a seguinte situação: 
Estamos desenvolvendo um sistema com uma esteira de DevOps que transitará a mesma versão do código do ambiente de Desenvolvimento para o ambientes de testes. Após a aprovação no ambiente de testes nosso sistema precisa ir para Produção. Nesse caso, não podemos depender de intervenção manual para gerar um novo pacote com strings de conexão com banco, serviços de mensagens, endpoints de APIs, além de outros. 
A cultura de DevOps prega automação de processos.. não podemos automatizar algo de ponta a ponta se precisamos realizar intervenções manuais. 
Toda nossa configuração deve estar em variáveis dentro de cada um desses ambientes (ou serem empacotadas através de um pipeline de deploy junto com o compilado do sistema).
Com o Kubernetes conseguimos armazenar configurações em Segredos, podemos passar variáveis de ambiente para os nossos contêineres, isso deixa nosso processo limpo e automatizável. Utilizamos uma mesma imagem de contêiner para todos os ambientes. 

Acabou o "Works on my Machine"!

Quem nunca esqueceu de copiar novas configs do Config de Dev para Produção naquele deploy manual?

4) Serviços de Apoio

Quando falamos de serviços de apoio, estamos falando de serviços como Bancos de Dados, Armazenamento, APIs, serviços de E-mail, etc. 

Todos esses serviços devem ser utilizamos com uma camada que permita a troca, por outro compatível, sem mudanças no código da nossa aplicação, somente ajustando nossos arquivos de configuração.

5) Build, Release, Run

Como falamos no item 3, precisamos automatizar nossos processos de publicação. Para isso, a transição do nosso código (pipeline) em desenvolvimento para um ambiente produtivo precisa passar por alguns pontos.

Precisamos de um passo onde nosso código será preparado para publicação. Por exemplo, se nosso projeto for .net, iremos rodar ferramentas de análise, compactação, compilação, montagem de uma imagem para contêiner, além de outros, dependendo dos processos adotados no nosso projeto.

Após a compilação, precisamos executar outro passo que fará a publicação do nosso sistema, juntamente com as configurações (se elas forem injetadas de alguma maneira através do pipeline de cada ambiente). 

Com o passo de publicação feito, temos o último processo que se encarregará de fazer uma verificação se os processos estão rodando de forma conforme no ambiente de destino.

6) Processos

Esse ponto prega que nossas aplicações devem ser publicadas em forma de um ou mais serviços Stateless, onde a persistência do estado deses serviços é feita através de Serviços de Apoio (nosso 4º Pilar). Com isso, garantimos, por exemplo, que um contêiner que está travado, por algum motivo, possa ser deletado, fazendo com que, quando uma nova instância desse contêiner fique disponível novamente, ela continue o processo com os dados que estão no serviço de apoio. 

7) Portas

O sétimo pilar nos guia a expor nossos serviços de forma autônoma, sem injeções em tempo de execução. Eles devem estar disponíveis através de portas, onde outros serviços farão uso dos mesmos. 

Como exemplo, podemos citar um contêiner que possui um sistema desenvolvido em ASP.NET exposto através da porta 5000. Esse container, dentro do nosso Kubernetes, será acessado através de um proxy reverso (um Nginx por exemplo), que tem seu endereço de rede mapeado em DNS, configurado na porta 80 desse mesmo proxy. 
Nosso Kubernetes fará então o meio de campo para redirecionar as requisições da porta 80 do nosso proxy para a porta 5000 do nosso contêiner. Se tivermos outro frontend rodando em outro contêiner na porta 2000, esse mesmo proxy pode redirecionar uma rota específica, na porta 80, para o segundo contêiner na porta 2000.

8) Concorrência

Esse pilar é de grande importância no que tange a resiliência e disponibilidade dos nossos serviços. Devemos projetar nossos serviços para escala de componentes individuais. 

Por exemplo, se temos um processo de leitura de uma fila de mensagem, que precisa ler uma mensagem quase que instantaneamente, devemos separar esse processo de forma que ele possa ser escalado de forma individual, garantindo a resposta de processamento através de realocação de recursos com processos como escalas horizontal e/ou vertical.  

9) Encerramento

Quando falamos de sistemas conteinerizados, uma das primeiras coisas que pensamos é na disponibilidade horizontal. Subir novas instâncias dos nossos serviços, matar instâncias sem uso para realocar recurso computacional para outro processo, etc. 

Para isso, precisamos garantir que nossos serviços não demorem uma eternidade para serem desligados (ou ligados). Com isso, garantimos que em uma necessidade de escala horizontal, nosso sistema contará com novas instâncias de algum processo de forma rápida.

10) Paridade entre os ambientes de Desenvolvimento e Produção

Nossos ambientes precisam ser o mais próximos possível. Claro que vamos ter algumas disparidades de recursos computacionais por conta de custos. 

O ponto aqui é: com menos variáveis, conseguimos identificar problemas de forma mais rápida.

11) Logs

Nossas aplicações precisam gerar logs centralizados, de forma que possamos identificar os eventos que estão acontecendo nas mesmas. Com isso, conseguimos, de forma eficiente, analisar problemas de execução dos nossos serviços, tendo em vista que na maioria das vezes podemos não ter mais acesso ao ambiente de execução de cada instância (se estivermos usando um contêiner, por exemplo).

12) Processos Administrativos

Todo processo administrativo, que precise ser executado em nosso ambiente, deve ser versionado na nossa Base de Código e empacotado junto com a aplicação.

Como exemplo, podemos citar a aplicação de novos scripts de banco de dados em um sistema que utiliza o Entity Framework Code-First. Um processo verifica a versão do banco de dados e atualiza o mesmo com os campos da nova versão.

---

Com todos esses "mandamentos" já podemos começar a desenvolver aplicações melhores, concordam?

Espero que este post dispare gatilhos sempre que alguém for "chumbar" uma string no código! :D

Grande abraço e até a próxima pessoal!

Yan.