Pular para conteúdo

12 Factor Apps (Aplicação de 12 Fatores)

O Que é "12-Factor App"? 🤔

O "12-Factor App" é um conjunto de 12 princípios que servem como um guia para construir aplicações web modernas, especialmente aquelas rodando na nuvem (como microserviços). Pensem nele como um "manual de boas práticas" para garantir que sua aplicação seja fácil de implantar, escalar e manter a longo prazo.

Por que usar o 12-Factor App?

  • Portabilidade: Sua aplicação funciona em qualquer ambiente (dev, staging, prod) sem precisar de grandes alterações.
  • Escalabilidade: A aplicação pode ser facilmente escalada horizontalmente (adicionando mais instâncias).
  • Manutenibilidade: Facilita a manutenção e atualização da aplicação ao longo do tempo.
  • Agilidade: Permite que o time de desenvolvimento trabalhe de forma mais eficiente e com menos dores de cabeça.

Como o 12-Factor App nos Ajuda nesse Projeto? 🛠️

Vamos analisar os fatores que podem nos ajudar a construir aplicações melhores:

  1. I. Codebase (Base de Código): Um código rastreado em um sistema de controle de versão

    • O Que é: Um codebase por aplicação, rastreado em um sistema de controle de versão como Git. Múltiplos deploy's compartilham o mesmo codebase.
    • Como Ajuda: Garante que todos os membros do time estejam trabalhando com a mesma versão do código e facilita o rastreamento de mudanças.
    • No Nosso Projeto: Já estamos usando Git, o que é ótimo! Certifiquem-se de que todos os commits sejam pequenos e descritivos, facilitando o entendimento do histórico.

      • Exemplos de Mensagens de Commit Descritivas:
        • feat: adicionar validação de email no formulário de cadastro
        • fix: corrigir bug que impedia o envio de mensagens
        • refactor: extrair componente de listagem de produtos
        • docs: adicionar documentação para a API de autenticação
        • test: adicionar testes unitários para a função de cálculo de frete
  2. III. Config (Configuração): Armazene a configuração no ambiente

    • O Que é: A configuração da aplicação (ex: URLs de banco de dados, chaves de API, segredos) deve ser armazenada no ambiente (variáveis de ambiente), não no código.
    • Como Ajuda: Permite que a mesma aplicação rode em diferentes ambientes (dev, staging, prod) sem precisar recompilar ou alterar o código. Melhora a segurança, evitando o armazenamento de informações sensíveis no código.
    • No Nosso Projeto: Evitem hardcoding de configurações! Usem variáveis de ambiente para armazenar informações sensíveis e específicas de cada ambiente.

      • Spring Boot: usem @Value("${variavel.ambiente}")

        @Value("${spring.datasource.url}")
        private String dbUrl;
        
      • FastAPI: usem os.environ.get("VARIAVEL_AMBIENTE")

        import os
        db_url = os.environ.get("DATABASE_URL")
        
      • Next.js:

        • Variáveis que começam com NEXT_PUBLIC_ são automaticamente expostas para o browser.
        • Use o arquivo .env.local para variáveis específicas do seu ambiente local (ele não deve ser committado!).
        • Exemplo:
          • No .env.local:
            NEXT_PUBLIC_API_URL=http://localhost:3000/api
            
          • No seu componente Next.js:
            function MeuComponente() {
              const apiUrl = process.env.NEXT_PUBLIC_API_URL;
              // ...
            }
            
  3. VIII. Logs (Registros): Trate os logs como fluxos de eventos

    • O Que é: A aplicação deve escrever seus logs para o stdout (saída padrão). O ambiente de execução se encarrega de coletar e armazenar esses logs.
    • Como Ajuda: Simplifica a coleta e análise de logs, permitindo o uso de ferramentas como ELK Stack (Elasticsearch, Logstash, Kibana) ou Grafana Loki.

      • Níveis de Log:
        • DEBUG: Informações detalhadas para debug durante o desenvolvimento. (ex: valores de variáveis, chamadas de função)
        • INFO: Informações gerais sobre o funcionamento da aplicação. (ex: "Servidor iniciado", "Requisição recebida")
        • WARN: Indica um evento inesperado, mas que não impede a aplicação de continuar funcionando. (ex: "Tentativa de acesso a recurso não autorizado")
        • ERROR: Indica um erro que impede a aplicação de realizar uma determinada ação. (ex: "Falha ao conectar ao banco de dados")
        • FATAL: Indica um erro crítico que impede a aplicação de continuar funcionando. (ex: "Memória insuficiente", geralmente causa o encerramento da aplicação)
    • No Nosso Projeto: Configurem suas aplicações para escrever logs estruturados para o stdout. Usem níveis de log de forma apropriada. Uma boa prática é usar um formato JSON para os logs, facilitando a análise.

      • Informações Essenciais nos Logs:

        • Timestamp: Quando o evento ocorreu.
        • Nível de Log: DEBUG, INFO, WARN, ERROR.
        • Mensagem: Descrição do evento.
        • Transaction ID: Um ID único para rastrear uma transação de ponta a ponta em vários microserviços.
        • IDs de Objetos/Dados Indexadores: IDs de entidades importantes (ex: ID do cliente, ID do pedido) para facilitar a filtragem e análise.
      • Logs de Eventos de Negócio:

        • É crucial registrar eventos que representem ações significativas do negócio (ex: "Novo pedido criado", "Pagamento recebido", "Produto enviado").
        • Esses logs podem ser usados para gerar métricas de negócio (KPIs) e monitorar o desempenho da aplicação (ex: número de pedidos por dia, taxa de conversão, tempo médio de entrega).
      • Exemplo Logging Estruturado (Spring Boot):

        import org.slf4j.Logger;
        import org.slf4j.LoggerFactory;
        import java.util.UUID;
        
        private static final Logger logger = LoggerFactory.getLogger(MeuServico.class);
        
        public void processarRequisicao(String requisicao, UUID transactionId) {
            logger.info("Requisição recebida", Map.of(
                "transactionId", transactionId,
                "requisicao", requisicao,
                "timestamp", System.currentTimeMillis()
            ));
        
            try {
               // ... lógica de processamento ...
               logger.info("Requisição processada com sucesso", Map.of("transactionId", transactionId));
            } catch (Exception e) {
               logger.error("Erro ao processar requisição", Map.of(
                   "transactionId", transactionId,
                   "erro", e.getMessage()
               ), e);
            }
        }
        
        public void validarDados(DadosCliente dados, UUID transactionId) {
            if (!dadosValidos(dados)) {
                logger.warn("Dados do cliente inválidos", Map.of(
                    "transactionId", transactionId,
                    "clienteId", dados.getId(),
                    "nome", dados.getNome()
                ));
                throw new IllegalArgumentException("Dados inválidos");
            }
            logger.info("Dados validados", Map.of("transactionId", transactionId, "clienteId", dados.getId()));
        }
        
      • Exemplo Logging Estruturado (FastAPI):
        import logging
        import json
        import uuid
        
        logger = logging.getLogger(__name__)
        logger.setLevel(logging.INFO)
        
        def processar_requisicao(requisicao: str, transaction_id: str):
            logger.info(json.dumps({
                "level": "INFO",
                "message": "Requisição recebida",
                "requisicao": requisicao,
                "transaction_id": transaction_id
            }))
        
            try:
                # ... lógica de processamento ...
                logger.info(json.dumps({"level": "INFO", "message": "Requisição processada com sucesso", "transaction_id": transaction_id}))
            except Exception as e:
                logger.error(json.dumps({
                    "level": "ERROR",
                    "message": "Erro ao processar requisição",
                    "transaction_id": transaction_id,
                    "erro": str(e)
                }), exc_info=True)
        
        def validar_dados(dados, transaction_id: str):
            if not dados_validos(dados):
                logger.warning(json.dumps({
                    "level": "WARN",
                    "message": "Dados do cliente inválidos",
                    "transaction_id": transaction_id,
                    "cliente_id": dados["id"],
                    "nome": dados["nome"]
                }))
                raise ValueError("Dados inválidos")
        
            logger.info(json.dumps({"level": "INFO", "message": "Dados validados", "transaction_id": transaction_id, "cliente_id": dados["id"]}))
        
  4. X. Dev/Prod Parity (Paridade Dev/Prod): Mantenha os ambientes o mais similares possível

    • O Que é: Mantenha os ambientes de desenvolvimento, staging e produção o mais similares possível. Isso inclui:
      • Código: Use o mesmo código em todos os ambientes.
      • Configuração: Use configurações diferentes, mas com a mesma estrutura.
      • Serviços: Use os mesmos serviços de apoio (ex: banco de dados, message queue).
    • Como Ajuda: Reduz o risco de bugs e problemas que só aparecem em produção.
    • No Nosso Projeto: Usem containers (Docker) para garantir que a aplicação rode da mesma forma em todos os ambientes. Usem ferramentas de provisionamento de infraestrutura (ex: Terraform) para automatizar a criação de ambientes similares.
  5. XI. Processes (Processos): Execute a aplicação como um ou mais processos stateless

    • O Que é: A aplicação deve ser executada como um ou mais processos stateless (sem estado). Isso significa que a aplicação não deve armazenar nenhum estado localmente. O estado deve ser armazenado em um serviço de apoio (ex: banco de dados).
    • Como Ajuda: Facilita a escalabilidade horizontal da aplicação.
    • No Nosso Projeto: Evitem armazenar dados de sessão localmente. Usem JWT (JSON Web Tokens) para armazenar as informações da sessão de forma segura e stateless.

      • Next.js e JWT com HttpOnly Cookies:
        • Em Next.js, ao receber o JWT após o login, armazene-o em um cookie HttpOnly.
        • Cookies HttpOnly não podem ser acessados via JavaScript no browser, tornando-os mais seguros contra ataques XSS.
        • No backend (API), verifique o JWT presente no cookie para autenticar o usuário.

Outros Fatores Importantes

Embora os fatores acima sejam os mais diretamente relacionados aos gaps identificados, os outros fatores do 12-Factor App também são importantes para construir aplicações robustas e escaláveis:

  • II. Dependencies (Dependências): Declare e isole explicitamente as dependências
    • Já está implementado no projeto base, com o uso de ferramentas de gestão de dependencias como Maven, npm, poetry e além é claro do docker compose
  • IV. Backing Services (Serviços de Apoio): Trate serviços de apoio como recursos anexados
    • Com o uso de containeres e orquestração (Docker + Docker Compose), podemos gerenciar a base de dados com facilidade, além disso, reforçamos a necessidade de uso de testes integrados, seja usando bancos em memória ou orquestrando containeres dentro da suite de testes. Além disso o uso de ORM's como o JPA/Hibernate e Peewee, nos deixam desacoplados da técnologia de banco de dados SQL dando mais flexibilidade para tratá-los como recursos anexados e flexiveis
  • V. Build, Release, Run (Construir, Lançar, Executar): Separe estritamente as fases de construir, lançar e executar
    • Temos uma pipeline bitbucket que trata esses paços, também temo essa gestão de cada projeto com suas respectivas ferramentas que isolam esses passos, como o Maven, Npm e Poetry. Fora isso temos um docker-compose-dev.yaml que fornece uma forma de trabalhar o ambiente de desenvolvimento com docker compose
  • VI. Port Binding (Vinculação de Porta): Exporte serviços via port binding
    • Nesse quesito optamos por usar um api gateway, porém os serviços são expostos por porta para esse gateway
  • VII. Concurrency (Concorrência): Escale via processos
    • É possível configurar essa concorrencia com o docker compose que permite ajuste fino de quantas instancias do container vamos rodar por serviço, além disso as ferramentas como spring, fast api e express fazem essa gestão automáticamente a nível do processo da linguagem
  • IX. Disposability (Descartabilidade): Maximize a robustez com rápido startup e shutdown
    • O uso de containeres juntamente com a orquestração com docker compose, possibilita
  • XII. Admin Processes (Processos Administrativos): Execute tarefas administrativas como processos one-off
    • Pedimos para que a criação de usuários admin por exemplo seja feita via uma linha de comando cuja apenas a TI pode executar, esse é um exemplo clássico desse fator

Conclusão 🎉

O 12-Factor App é um guia valioso para construir aplicações modernas e escaláveis. Ao aplicar esses princípios, vocês estarão no caminho certo para criar sistemas mais robustos, fáceis de manter e com menos dores de cabeça! Lembrem-se de que o objetivo é aprender e aplicar esses conceitos de forma gradual.

Se tiverem dúvidas, não hesitem em perguntar! Estamos aqui para ajudar. 💪