Capítulo 58, Backend
Configuração profissional
Configuração é tudo o que muda entre o seu computador, o CI e a produção, sem mudar o código. Errar aqui é a causa mais comum de segredo vazado e de "funciona na minha máquina".
Código deste capítulo: backend/cap58_configuracao.py
A regra: configuração vem do ambiente
O princípio (dos twelve-factor apps) é simples: o mesmo código roda em qualquer lugar, e o que muda é lido de variáveis de ambiente. Nada de if ambiente == "prod" espalhado pelo código, e nenhuma senha em arquivo versionado.
| Ambiente | De onde vêm os valores | Observação |
|---|---|---|
| Desenvolvimento | Um arquivo .env local | Fora do Git (.gitignore) |
| Testes e CI | Variáveis definidas no job | Valores descartáveis |
| Produção | Gerenciador de segredos ou variáveis do orquestrador | Nunca em imagem nem em repositório |
Configuração tipada com pydantic-settings
Ler os.environ["PORTA"] devolve texto, e esquecer de converter é um bug esperando para acontecer. O pydantic-settings lê as variáveis, converte para o tipo declarado e valida, tudo ao iniciar. Se algo está errado, o programa nem sobe, o que é exatamente o que você quer:
uv add pydantic-settings
import os
from pathlib import Path
from typing import Literal
from pydantic import SecretStr, ValidationError, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class Configuracao(BaseSettings):
model_config = SettingsConfigDict(env_prefix="LOJA_", extra="ignore")
ambiente: Literal["dev", "test", "prod"] = "dev"
porta: int = 8000
chave_api: SecretStr
O campo chave_api não tem valor padrão, então é obrigatório. Sem a variável LOJA_CHAVE_API, a configuração recusa iniciar e diz exatamente o que falta:
try:
Configuracao(_env_file=None)
except ValidationError as erro:
print([(e["loc"], e["type"]) for e in erro.errors()])
[(('chave_api',), 'missing')]
Definindo as variáveis, a conversão acontece sozinha. A porta chegou como texto e virou int. E o SecretStr esconde o valor ao imprimir, o que protege a chave de vazar em logs e em mensagens de erro:
os.environ["LOJA_CHAVE_API"] = "segredo-123"
os.environ["LOJA_PORTA"] = "9000"
config = Configuracao(_env_file=None)
print(config)
print(config.porta + 1, type(config.porta).__name__)
print(config.chave_api.get_secret_value()[:7])
ambiente='dev' porta=9000 chave_api=SecretStr('**********')
9001 int
segredo
Um valor impossível de converter é recusado com um erro claro, em vez de explodir lá na frente com um ValueError sem contexto:
os.environ["LOJA_PORTA"] = "abc"
try:
Configuracao(_env_file=None)
except ValidationError as erro:
print(erro.errors()[0]["type"])
os.environ["LOJA_PORTA"] = "9000"
int_parsing
Quem vence: a ordem de precedência
Os valores podem vir de vários lugares, e a ordem decide. Da maior para a menor prioridade: argumentos passados ao construtor, depois variáveis de ambiente, depois o arquivo .env, depois os padrões do código. Isso permite que a produção sobrescreva o arquivo local sem editar nada:
Path(".env.demo").write_text("LOJA_PORTA=7000\nLOJA_AMBIENTE=test\n", encoding="utf-8")
print(Configuracao(_env_file=".env.demo").porta)
del os.environ["LOJA_PORTA"]
print(Configuracao(_env_file=".env.demo").porta)
print(Configuracao(_env_file=".env.demo", porta=1234).porta)
9000
7000
1234
Na primeira linha a variável de ambiente (9000) venceu o arquivo (7000). Depois de removê-la, valeu o arquivo. E o argumento explícito venceu os dois.
Regras de negócio sobre a configuração
Validações que cruzam campos entram em um model_validator. O exemplo clássico: produção exige uma chave forte, e o programa se recusa a subir com uma fraca:
class ConfiguracaoSegura(Configuracao):
@model_validator(mode="after")
def producao_exige_chave_forte(self):
if self.ambiente == "prod" and len(self.chave_api.get_secret_value()) < 16:
raise ValueError("em produção a chave precisa ter pelo menos 16 caracteres")
return self
os.environ["LOJA_AMBIENTE"] = "prod"
try:
ConfiguracaoSegura(_env_file=None)
except ValidationError as erro:
print(erro.errors()[0]["msg"])
del os.environ["LOJA_AMBIENTE"]
Value error, em produção a chave precisa ter pelo menos 16 caracteres
O projeto final (capítulo 62) usa a mesma ideia: API_AMBIENTE=prod com um banco SQLite é recusado na partida.
Segredos
| Prática | Por quê |
|---|---|
.env no .gitignore, e um .env.example versionado sem valores reais | Documenta quais variáveis existem, sem vazar |
SecretStr para senhas e chaves | Não aparecem em print, em repr nem em logs |
| Segredos injetados na execução, nunca copiados para a imagem Docker | Uma imagem é distribuída, e as camadas guardam o que foi copiado |
Um arquivo por segredo (secrets_dir="/run/secrets"), no Docker e no Kubernetes | Não ficam visíveis na lista de variáveis do processo |
| Rotação possível sem redeploy | Uma chave vazada precisa poder ser trocada rápido |
Um segredo que entrou no Git está vazado
Apagar o arquivo no commit seguinte não resolve: o histórico guarda tudo. A resposta correta a um segredo commitado é revogá-lo e gerar outro. Limpar o histórico é secundário.
Exercício 1
Um campo booleano de depuração
Acrescente debug: bool = False a uma subclasse de Configuracao. Mostre que LOJA_DEBUG=1 vira True e que LOJA_DEBUG=talvez é recusado.