Pular para o conteúdo

    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.

    AmbienteDe onde vêm os valoresObservação
    DesenvolvimentoUm arquivo .env localFora do Git (.gitignore)
    Testes e CIVariáveis definidas no jobValores descartáveis
    ProduçãoGerenciador de segredos ou variáveis do orquestradorNunca 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:

    Terminal
    uv add pydantic-settings
    
    backend/cap58_configuracao.pylinhas 10 a 23
    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:

    backend/cap58_configuracao.pylinhas 25 a 28
    try:
        Configuracao(_env_file=None)
    except ValidationError as erro:
        print([(e["loc"], e["type"]) for e in erro.errors()])
    
    Saída
    [(('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:

    backend/cap58_configuracao.pylinhas 30 a 35
    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])
    
    Saída
    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:

    backend/cap58_configuracao.pylinhas 37 a 42
    os.environ["LOJA_PORTA"] = "abc"
    try:
        Configuracao(_env_file=None)
    except ValidationError as erro:
        print(erro.errors()[0]["type"])
    os.environ["LOJA_PORTA"] = "9000"
    
    Saída
    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:

    backend/cap58_configuracao.pylinhas 47 a 51
    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)
    
    Saída
    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:

    backend/cap58_configuracao.pylinhas 56 a 69
    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"]
    
    Saída
    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áticaPor quê
    .env no .gitignore, e um .env.example versionado sem valores reaisDocumenta quais variáveis existem, sem vazar
    SecretStr para senhas e chavesNão aparecem em print, em repr nem em logs
    Segredos injetados na execução, nunca copiados para a imagem DockerUma imagem é distribuída, e as camadas guardam o que foi copiado
    Um arquivo por segredo (secrets_dir="/run/secrets"), no Docker e no KubernetesNão ficam visíveis na lista de variáveis do processo
    Rotação possível sem redeployUma 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.