The Twelve-Factor App de 2011
(12factor.net)- The Twelve-Factor App é uma metodologia para operar e escalar apps web·SaaS no longo prazo, abordando junto automação de configuração, portabilidade, deploy em nuvem e entrega contínua
- Não fica preso a uma combinação específica de linguagem de programação ou de serviços de apoio como banco de dados, fila ou cache em memória, podendo ser aplicado a vários tipos de aplicações como serviço
- Baseia-se na experiência de participação direta no desenvolvimento e deploy de centenas de apps na plataforma Heroku, além da observação indireta do desenvolvimento, operação e expansão de centenas de milhares de apps
- A principal preocupação é oferecer um vocabulário comum para reduzir o custo de colaboração e a erosão de software que surgem quando um app cresce organicamente
- Pode ser usado como referência prática não só por desenvolvedores que criam aplicações de serviço, mas também por engenheiros de operações responsáveis por implantá-las e gerenciá-las
12 princípios operacionais para apps SaaS
- O software moderno costuma ser oferecido na forma de app web ou SaaS, e o Twelve-Factor App é uma metodologia para criar esse tipo de aplicação
- O objetivo é tornar mais previsíveis os processos de desenvolvimento, deploy e operação do app
- Usa automação declarativa de configuração para reduzir o tempo e o custo de integrar novos desenvolvedores ao projeto
- Define um contrato claro com o sistema operacional subjacente para aumentar a portabilidade entre ambientes de execução
- É projetado para reduzir a carga de administração de servidores e sistemas e se adequar ao deploy em plataformas modernas de nuvem
- Reduz a diferença entre ambiente de desenvolvimento e produção para viabilizar entrega contínua
- Permite escalar sem mudar drasticamente ferramentas, arquitetura ou práticas de desenvolvimento
- O escopo de aplicação não se limita a uma stack tecnológica específica
- Pode ser aplicado a apps escritos em qualquer linguagem de programação
- Serviços de apoio incluem bancos de dados, filas, caches em memória etc.
Contexto organizado a partir da experiência na Heroku
- Os contribuidores participaram diretamente do desenvolvimento e deploy de centenas de apps na plataforma Heroku e observaram indiretamente o desenvolvimento, a operação e a expansão de centenas de milhares de apps
- Com base na experiência e nas observações obtidas em apps SaaS reais, organizaram práticas ideais para o desenvolvimento de aplicações
- Prestaram atenção ao fluxo em que o app cresce organicamente ao longo do tempo
- Tratam da forma como vários desenvolvedores colaboram na mesma codebase
- Consideram evitar o custo da erosão de software um objetivo importante
- O formato foi inspirado em Patterns of Enterprise Application Architecture e Refactoring, de Martin Fowler
Os 12 fatores
- I. Codebase: uma única codebase versionada e múltiplos deploys
- II. Dependencies: declarar e isolar dependências explicitamente
- III. Config: armazenar configuração no ambiente
- IV. Backing services: tratar serviços de apoio como recursos conectados
- V. Build, release, run: separar rigorosamente as etapas de build e execução
- VI. Processes: executar o app como um ou mais processos sem estado
- VII. Port binding: expor serviços externamente por meio de port binding
- VIII. Concurrency: escalar horizontalmente com o modelo de processos
- IX. Disposability: aumentar a robustez com inicialização rápida e encerramento elegante
- X. Dev/prod parity: manter desenvolvimento, staging e produção o mais parecidos possível
- XI. Logs: tratar logs como fluxo de eventos
- XII. Admin processes: executar tarefas administrativas como processos pontuais
1 comentários
Opiniões no Hacker News
O 12-Factor App é uma recomendação criada em 2011 mais apoiada no Heroku e nas limitações da infraestrutura conteinerizada da época, não um documento que pareça profundamente baseado em princípios de engenharia.
Por exemplo, a afirmação de que se deve colocar configuração em variáveis de ambiente existe porque os autores trabalhavam na Heroku, e a Heroku preenchia variáveis de ambiente por meio dos campos de entrada das aplicações web.
Se você quer rastrear o histórico de configuração com controle de versão, usar GitOps, usar ConfigMap do k8s ou colocar arquivos de configuração em um volume montado, todas essas são, em geral, escolhas aceitáveis. Porque separam o estado da configuração do estado de implantação da aplicação.
Vejo esse documento como uma diretriz prejudicial, porque confunde a floresta com as árvores e faz recomendações alinhadas aos recursos do produto da empresa que o escreveu, mais do que a princípios reais de engenharia.
Se você quer armazenamento seguro de configuração no Kubernetes, acaba usando Secrets, e isso assume a forma de chave-valor, como variáveis de ambiente. Para obter segurança semelhante no Git, é necessária uma camada de criptografia, o que quebra diffs e exige ferramentas adicionais.
No fim, voltamos ao motivo pelo qual ferramentas de implantação de alto nível como a Heroku foram criadas.
O princípio de tratar logs como streams continua correto. Basta escrever logs no STDOUT, não em arquivos, e deixar o orquestrador ler e armazená-los.
A configuração vem do ambiente. Dependendo da forma de implantação, aplicações tendem a ler configurações de fontes diferentes: localmente de um .env, em produção de um armazenamento de segredos.
O port binding também é igual: a aplicação abre uma porta, e algo como nginx fica na frente para compor um proxy reverso. Serviços e ingress do K8S cumprem esse papel.
Hoje, a maior crítica que se pode fazer ao 12-Factor é que o documento não foi realmente bem escrito e presume que o leitor já sabe exatamente do que ele está falando.
Ainda assim, reconheço o mérito da Heroku por popularizar esse conceito e ampliar seu uso.
Não é adicionar um settings.json à máquina antes de iniciar a aplicação; é o mesmo código-fonte usar os valores configurados naquele cluster quando implantado no cluster AKS do Azure EU north, e usar a configuração daquele cluster quando implantado no cluster Docker Swarm em um RPi Zero dentro de uma moldura da Ikea.
O nome desse cluster é Gibson.
Sinto que cada um desses itens pode ser refutado de forma bastante razoável.
Primeiro, o princípio de um repositório para uma aplicação não é fundamentalmente errado. Não há problema em desenvolver em um único repositório várias aplicações que são fortemente acopladas funcionalmente e compartilham o ciclo de release, mas que precisam ser implantadas separadamente para obter as vantagens de processos separados e escalabilidade independente. Penso na separação entre API pública e processos worker, por exemplo Sidekiq em Ruby, Celery em Python ou consumidores Kafka comuns.
Segundo, dizer que “uma aplicação 12-Factor não depende da existência implícita de pacotes globais do sistema” é muito difícil de alcançar na prática sem usar algo como Nix. Dependências da API de chamadas de sistema do kernel também vazam, e a maioria das aplicações Rust depende implicitamente da glibc, exceto quando usa musl. Ela existe nas principais distribuições Linux, salvo distribuições slim como Alpine. Vejo Docker como um apoio necessário para mitigar esse problema.
Terceiro, armazenar configuração em variáveis de ambiente parece mais frágil. Pode reduzir a segurança ao forçar segredos a ficarem no ambiente, e faz abrir mão de configuração estruturada em arquivos, que oferece segurança de tipos, autocompletar na IDE e parsing automático. Em variáveis de ambiente, é preciso implementar por conta própria parsers para valores de configuração complexos que não sejam strings. Na prática, muitas vezes isso acaba sendo salvo em arquivos .env e comitado no repositório, então o argumento de segurança de commit também perde sentido.
Não quer dizer que você não deva depender do sistema operacional, incluindo glibc; quer dizer que você não deve fazer com que a aplicação só funcione se um pacote de um gerenciador específico de linguagem estiver instalado na máquina.
Concordo com os demais pontos. Especialmente a parte sobre variáveis de ambiente parece menos um conselho com base real e mais algo que os autores escreveram assumindo como melhor prática uma abordagem comum no desenvolvimento Ruby da época.
Portanto, é preciso se preparar para essa situação, e acho que repositórios separados são melhores para testar facilmente combinações de versões diferentes.
A maioria das linguagens de alto nível não depende de uma glibc específica. Se o runtime da linguagem funciona corretamente, a aplicação também funciona sobre ele. Claro que, em alguns casos, você acaba usando algo como Docker. O fato de ser difícil não significa que não tenha valor.
Linguagens dinâmicas ou gerenciadas muitas vezes conseguem ignorar a maior parte das complicações relacionadas a pacotes do sistema.
No geral, gosto da ideia, mas houve tantas vezes em que pessoas não técnicas ou meio técnicas sacaram o “12 factor” como se fosse um cartão amarelo universal para atrasar releases que acabei passando a ignorá-lo quase completamente
Na verdade, com “agile” foi parecido. Entendo a intenção dessas diretrizes, mas o valor prático delas parece ser muito maior para pessoas que só conseguem oferecer liderança técnica de torre de marfim
Já passei por casos em que engenheiros juniores excessivamente empolgados ou aspirantes a arquitetos usavam o texto do 12-Factor como se fosse requisito obrigatório para todo lançamento
Essas coisas são bons objetivos a perseguir, mas é preciso explicar de forma firme e consistente que, no mundo real, é necessário fazer concessões para lançar e escolher o que será adiado ou postergado
Um desvio pequeno não deveria bloquear um release, mas, se no geral não estiver alinhado, no mínimo deve ser tratado como dívida técnica. Se um release representa um grande retrocesso em um dos itens, é justo bloqueá-lo ou, pelo menos, exigir uma revisão mais detalhada de por que aquela concessão foi considerada válida
Se a organização adotou o 12FA como boa prática, ele deve ser atendido, mas não deve impedir a implantação
12FA não é uma única caixa a marcar. À medida que o produto amadurece, cada item pode — e na maioria das vezes deve — ser implementado adicionando-o ao produto um por um, subdividindo quando necessário
Se a engenharia inicial foi bem feita, isto é, com abstrações e interfaces adequadas e sem deixar tudo hardcoded, isso não deveria ser um problema
YAGNI é tão abusado quanto 12FA. O que mais falta no 12FA são exemplos concretos que engenheiros juniores possam consultar
Aprendi a encarar esse tipo de problema de boa-fé. É preciso ver por que a questão está sendo levantada, se o processo existente é pouco claro, defeituoso ou pouco confiável
Se uma preocupação razoável for encontrada, basta mudar o processo e atualizar a documentação
O Twelve-Factor App diz para usar o ambiente para configuração, e o Docker diz para não usar o ambiente para configuração, porque não é seguro
Gosto e uso muitos dos padrões do 12-Factor, mas alguns foram pensados no contexto de VPS. Naquela época, o ambiente era estável, seguro e mais fixo; em contêineres, porém, o ambiente pode acabar sendo carregado em alguma camada
Na era dos contêineres, esse item específico foi uma preocupação considerável. Docker secrets nem sempre se encaixa bem, então é preciso fazer várias acrobacias para funcionar
Injetar segredos na aplicação como variáveis de ambiente não significa que todo o tratamento externo a isso seja inseguro. Por exemplo, contêineres do AWS ECS têm suporte integrado para buscar segredos no Secret Manager na inicialização e passá-los como variáveis de ambiente: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/...
O segredo é obtido no momento em que o contêiner inicia, usando as credenciais IAM da aplicação em execução para buscá-lo no Secret Manager. Portanto, é necessário ter permissão para esse segredo
A vantagem de usar variáveis de ambiente parece depender totalmente de como elas acabam sendo configuradas. Com um mecanismo assim, não vejo grandes desvantagens
A principal desvantagem que vejo é que um malware genérico tentando despejar variáveis de ambiente poderia capturar os valores, mas, se os segredos não deixam de ficar armazenados permanentemente na memória, bloquear essa ameaça em geral se aproxima mais de ofuscação
É algo que você realmente quer evitar quando se trata de uma chave privada
Não vejo grandes problemas em montar segredos no sistema de arquivos com k8s. Claro que tudo isso depende do ambiente de implantação
Às vezes, variáveis de ambiente são a opção menos ruim. Segredos são sempre difíceis por natureza
Se você confiar cegamente na entrada só porque é single-tenant, quando as condições mudarem depois por alguma guinada arbitrária de negócios, acabará com bugs difíceis de rastrear e longas noites
Nos últimos anos, discuti bastante sobre apps 12-Factor e também vi muita confusão. O site do 12-Factor é excelente, mas é útil principalmente para quem já entende por que aqueles itens são importantes.
Para quem não sabe o motivo por trás das regras, era necessária uma explicação mais aprofundada. Por isso fiz o vídeo “What are 12 Factor Apps and Why Should You Care?”[1], e ouvi que ele ajudou bastante em algumas empresas que o usam no treinamento de novos engenheiros/DevOps
Independentemente de onde você aprenda, vale dedicar uma ou duas horas para estudar apps 12-Factor. A maioria das “regras” são coisas que exigem consciência, e não ficam imediatamente claras até você errar por conta própria e sofrer com isso
[1] https://youtu.be/REbM4BDeua0
O conceito de “teste unitário” começou a ganhar tração por volta do fim dos anos 90, e foi um alívio ver alguém com autoridade defendê-lo.
O motivo de eu não fazer isso de fato até Kent Beck lançar o JUnit era que o código em que eu trabalhava não tinha uma estrutura boa para ser controlado por outro código. Variáveis globais, estado espalhado, dependência de sistemas externos e de um layout específico de sistema de arquivos, além de ignorar modularidade, faziam com que nada pudesse ser executado fora do contexto para o qual tinha sido projetado.
Tudo isso era “mau design”, mas os prazos eram cumpridos, então todo mundo fazia assim. Eu esperava que, quando os testes unitários ganhassem tração, os programadores se afastassem dos designs monolíticos.
Depois de 25 anos vendo testes unitários para getters/setters e um único teste unitário gigantesco que cria um banco de dados em memória porque todas as funções do app precisam de um banco de dados real só para tentar executar, para no fim falhar e ser comentado, perdi a fé de que testes unitários se tornariam algo além de uma caixa de seleção sem sentido. Todo mundo marca porque é uma “boa prática”, mas não para para pensar no porquê.
O conselho sobre configuração sempre foi o ponto de que eu mais discordei. Configuração acaba sendo definida por várias partes, muitas vezes também por desenvolvedores, então frequentemente o melhor é empacotar valores padrão razoáveis junto com a aplicação e sobrescrevê-los com arquivos por ambiente e variáveis de ambiente.
Para a maioria das aplicações server-side, esse é o método mais flexível. Muitas vezes você já sabe qual deve ser a configuração, e é melhor que isso fique sob controle de código-fonte, mas valores secretos devem ser injetados em tempo de execução.
Para não gastar um tempo enorme de configuração em desenvolvimento, teste e produção, a configuração precisa de sobrescrita hierárquica.
Se forem de desenvolvimento, acabarão quebrando produção; e padrões de produção podem não fazer sentido nenhum em desenvolvimento.
Uma forma de pensar na seção de configuração é: “essa estratégia de configuração combina bem com contêineres?”. Ao construir uma imagem, você cria um estado de disco estático no qual mudanças não persistem a menos que uma nova imagem seja criada.
Se a configuração for apenas baseada em arquivos, é preciso construir uma imagem completamente nova para alternar entre o comportamento de teste e o de produção.
Permitir que a configuração seja alterada independentemente do disco base ajuda a isolar mudanças. É preciso distinguir se o app quebrou porque o deploy, isto é, a criação da imagem, quebrou, ou porque a configuração estava errada.
Ao separar a criação da imagem da mudança de configuração, essa própria pergunta desaparece.
Por exemplo, não se deve permitir um erro humano bastante previsível como enviar, a partir de QA, 100 mil novas tentativas de negação de autenticação para uma fila de submissão de jobs de produção.
Ainda assim, muitas configurações não dizem respeito à infraestrutura, mas a coisas como qual bean ResolverStrategy conectar em cada ambiente.
A configuração deve ser versionada, mas gerenciada separadamente do código-fonte. Isso porque a configuração descreve o deploy em si, não a imagem usada no deploy.
Certamente foi uma norma de engenharia influente. Hoje há muitas abstrações fáceis de hospedagem, como Render ou Vercel, então é um pouco estranho pensar que este documento foi escrito em 2012, quando apps web eram muito mais como um Velho Oeste em termos de práticas comuns aceitas.
O que falta muito neste documento é a justificativa para as regras. Quase tudo é apenas regra.
É difícil avaliar se as regras são boas, e o documento não ajuda a descobrir isso.
Só pelo título, achei que fosse um comentário sobre autenticação em duas etapas. Tipo aqueles apps que exigem foto do passaporte, escaneamento facial, carteira de motorista, SMS, Google Authenticator, link por e-mail, senha e impressão digital só para fazer login uma vez.
Nos primórdios do Docker, fiz bastante trabalho para fazer o WordPress se comportar como um Twelve-Factor App.
Tradicionalmente, o WordPress não se comportava assim, e isso até fazia sentido. O WordPress cresceu em um mundo em que servidores longevos com disco local gravável e persistente eram comuns.
As coisas devem ter mudado bastante desde então. Estou falando de algo por volta de 2016, mas foi um desafio muito interessante.
Fiquei surpreso com o quanto as coisas ficavam mais simples, e passou a ser possível balancear carga entre vários nós sem se preocupar com afinidade de sessão.
Claro, isso ficou mais difícil de outras formas, como a necessidade de um banco de dados separado para sessões.