No artigo anterior, foi apresentado o conceito do 12 Factor App. Vimos o conceito principal e os 3 primeiros fatores: Codebase, Dependencies e Configs.
De forma resumida, os 12 Factor App são 12 fatores definidos por um grupo de desenvolvedores da Heroku. Trata-se de 12 diretivas que norteiam o desenvolvimento saudável e sustentável de aplicações SaaS (Software as a Service).
Na segunda parte desse artigo, veremos mais três fatores: Backing Services, Build, Release, Run e Processes.
Backing Services
Backing Services, ou Serviços de Apoio, são serviços que são consumidos por uma aplicação principal para que esta funcione e desempenhe suas funções. Trata-se de serviços que tradicionalmente são consumidos via interface de rede.
Alguns exemplos de serviços de apoio são: bancos de dados (como MySQL, SQL Server, PostgreSQL, Oracle), serviços de cache (como Memcached, Redis, Cassandra), serviços de comunicação assíncrona como filas e tópicos (como Apache Kafka, RabbitMQ e Amazon SQS/SNS) e até mesmo serviços de monitoramento de aplicações (como Sentry ou New Relic).
Serviços de apoio não fazem parte do núcleo da aplicação de maneira direta, mas são imprescindíveis para que a aplicação funcione e desempenhe o papel esperado.
Por exemplo: uma aplicação que realiza a gestão de uma escola precisa de um banco de dados para armazenar os dados e informações a serem gerenciadas. Embora o núcleo da aplicação seja a gestão da escola em si, sem a presença de um banco de dados, a aplicação não funcionaria corretamente, pois ela não teria uma estrutura confiável para guardar todas as informações manipuladas. Isso mostra a força e importância dos Backing Services para a aplicação principal.
Backing Services podem estar expostos de várias maneiras dentro da infraestrutura de uma aplicação. Eles podem estar implantados dentro de uma infraestrutura local (como serviços On Premise) ou até mesmo serem gerenciados por terceiros (como o RDS, um serviço de banco de dados relacional gerenciado pela AWS).
O ponto importante é que a maneira como o serviço está disponível não pode impactar a maneira como a aplicação é executada: se o banco de dados da aplicação está instalado localmente ou se está em um serviço de nuvem, isso deve ser transparente para a aplicação.
Além disso, uma aplicação não deve ser acoplada com um de seus Backing Services: a aplicação deve ser capaz de trocar seu banco de dados entre uma instância local e uma instância gerenciada por terceiros de maneira transparente.
Build, release, run
Aplicações, quando são construídas, precisam ser entregues através de operações de deploy. Geralmente, para a geração de um deploy, são necessários três estágios:
• Estágio de construção, ou build: nesse momento, todo o código presente em uma versão do codebase é transformado em uma versão executável. Este processo de transformação envolve a compilação do código, a geração de binários e o empacotamento em conjunto com suas respectivas dependências necessárias;
• Estágio de lançamento, ou release: nesta etapa, o pacote gerado no estágio de build é aplicado dentro de uma configuração específica. Geralmente, estas configurações são específicas para diferentes ambientes de execução. Por exemplo, uma mesma aplicação pode ter configurações específicas para cada ambiente de execução, como um ambiente de desenvolvimento, um ambiente de homologação e um ambiente de produção. As configurações que serão utilizadas pela aplicação são definidas no estágio de lançamento;
• Estágio de execução, ou run: nesta etapa, a aplicação é executada dentro da versão gerada no estágio de construção e com as configurações definidas no estágio de lançamento.
Estas etapas precisam ser muito bem separadas umas das outras devido às suas naturezas: não deve ser possível alterar código em tempo de execução, pois este código fatalmente não poderá ser reintegrado ao codebase. Não deve ser possível definir configurações de execução na etapa de build, pois a aplicação ainda está sendo gerada, e não deve ser possível definir estas configurações durante a execução, pois a aplicação pode já ter sido iniciada com valores incorretos de configuração.
Gerenciar processos de build, release e run pode parecer algo simples em determinados ambientes, mas são processos que naturalmente se tornam complexos no decorrer do tempo. Aos poucos, é natural que vá ficando complicado gerenciar múltiplas versões geradas pelos processos de build de maneira alinhada com a coordenação de diferentes ambientes de execução com diferentes configurações.
Por isso, hoje existem ferramentas específicas para auxiliar nestes processos, ferramentas estas conhecidas como CI/CD (Continuous Integration/Continuous Delivery). Alguns exemplos destas aplicações são o Jenkins, o Ansible e o Deployer.
Processes
Quando uma aplicação entra no estágio de execução, esta passa a ser composta por múltiplos processos. Estes processos podem ser representados por várias estruturas, como diferentes threads ou até mesmo sub-rotinas, uma estrutura comum em linguagens modernas como Go (goroutines) e Kotlin (coroutines).
Ainda podemos ter a mesma aplicação com múltiplas instâncias de execução concorrentes, como em microsserviços que escalam em múltiplas instâncias para suportarem cargas maiores. Aplicações que seguem o 12 Factor App devem conseguir reagir naturalmente a estes ambientes de execução complexos.
Para que as aplicações consigam lidar com processos concorrentes de maneira natural, duas características são mandatórias: estas aplicações precisam ser stateless (ou seja, não devem guardar estados) e devem ser share-nothing (ou seja, seus diferentes processos não devem trocar informações e estados entre si).
Cada um dos processos precisa ser isolado e independente dos demais processos em execução, sejam estes processos sub-rotinas, threads ou múltiplas instâncias concorrentes. Caso seja necessário compartilhar algum estado, as aplicações devem utilizar os Backing Services, como um banco de dados ou um serviço de armazenamento externo. Em algumas situações, até pode ser plausível utilizar um processo de armazenamento temporário e transitório, como a própria memória, mas de maneira geral, isso deve ser evitado.
Isso é essencial para que estas aplicações possam reagir principalmente a situações como necessidade de escalabilidade (superior ou inferior), onde a gestão de estado compartilhado tende a se tornar um grande desafio.