Pular para o conteúdo

    Capítulo 23, Intermediário

    Configuração e variáveis de ambiente

    O código é o mesmo em todos os ambientes. O que muda (o banco, a chave, as origens) vem de fora dele. Isso mantém os segredos fora do Git e permite rodar a mesma aplicação em desenvolvimento, teste e produção.

    Por que fora do código

    Um segredo escrito no código vai para o repositório, e um repositório vaza (um colaborador novo, um fork, um backup). E configurações como a URL do banco mudam de um ambiente para outro: em vez de editar o código para cada um, o ambiente fornece os valores. É a regra da aplicação de "12 fatores": configuração no ambiente.

    Os valores moram em variáveis de ambiente, e em desenvolvimento costumam ser lidos de um arquivo .env, que nunca vai para o Git:

    .env
    APP_CHAVE_SECRETA=troque-por-uma-chave-aleatoria-com-32-bytes-ou-mais
    APP_BANCO_URL=sqlite:///./app.db
    APP_EXPIRA_EM_MINUTOS=30
    APP_ORIGENS_PERMITIDAS=["http://localhost:5173"]
    
    .gitignore
    .env
    

    É boa prática versionar um .env.example com os nomes das variáveis e valores de mentira, para quem chega saber o que configurar.

    `pydantic-settings`: configuração tipada

    Ler os.getenv("X") devolve sempre texto (ou None), sem validação. O pydantic-settings lê as variáveis, converte e valida os tipos, e recusa iniciar se faltar algo obrigatório, o que é muito melhor do que descobrir em produção, no meio de uma requisição:

    Terminal
    uv add pydantic-settings
    
    intermediario/cap23_configuracao.pylinhas 10 a 31
    import os
    from functools import lru_cache
    from pathlib import Path
    
    from pydantic import Field, SecretStr, ValidationError
    from pydantic_settings import BaseSettings, SettingsConfigDict
    
    
    class Configuracoes(BaseSettings):
        model_config = SettingsConfigDict(env_file=".env", env_prefix="APP_", extra="ignore")
    
        chave_secreta: SecretStr
        algoritmo: str = "HS256"
        expira_em_minutos: int = Field(default=30, gt=0)
        banco_url: str = "sqlite:///./app.db"
        origens_permitidas: list[str] = ["http://localhost:5173"]
    
    
    try:
        Configuracoes(_env_file=None)
    except ValidationError as erro:
        print(erro.errors()[0]["type"], erro.errors()[0]["loc"])
    
    Saída
    missing ('chave_secreta',)
    

    Sem a variável APP_CHAVE_SECRETA (que não tem padrão), a aplicação nem constrói as configurações. O env_prefix evita colisões com outras variáveis do sistema.

    Os tipos são convertidos

    As variáveis de ambiente são sempre texto. O Pydantic converte: "45" vira int, e valores complexos (listas) são lidos como JSON. E o SecretStr esconde o valor ao imprimir, o que evita vazar a chave em um log:

    intermediario/cap23_configuracao.pylinhas 36 a 44
    os.environ["APP_CHAVE_SECRETA"] = "uma-chave-bem-comprida-com-mais-de-32-bytes!"
    os.environ["APP_EXPIRA_EM_MINUTOS"] = "45"
    os.environ["APP_ORIGENS_PERMITIDAS"] = '["https://app.exemplo.com", "http://localhost:5173"]'
    
    config = Configuracoes(_env_file=None)
    print(config.expira_em_minutos, type(config.expira_em_minutos).__name__)
    print(config.origens_permitidas)
    print(config.chave_secreta)
    print(len(config.chave_secreta.get_secret_value()))
    
    Saída
    45 int
    ['https://app.exemplo.com', 'http://localhost:5173']
    **********
    44
    

    Para obter o valor de verdade, você pede explicitamente (get_secret_value()), e isso deixa visível, no código, cada lugar onde o segredo é usado.

    Ordem de prioridade e o arquivo `.env`

    Quando o mesmo valor aparece em vários lugares, o vencedor é, do mais forte ao mais fraco: argumentos passados ao construtor, variáveis de ambiente, arquivo .env, padrão do modelo. Por isso um valor definido no ambiente de produção prevalece sobre o .env:

    intermediario/cap23_configuracao.pylinhas 49 a 57
    Path("exemplo.env").write_text(
        "APP_CHAVE_SECRETA=chave-do-arquivo-com-mais-de-32-bytes-0123456\n"
        "APP_EXPIRA_EM_MINUTOS=10\n"
        "APP_BANCO_URL=sqlite:///./do-arquivo.db\n",
        encoding="utf-8",
    )
    combinada = Configuracoes(_env_file="exemplo.env")
    print(combinada.expira_em_minutos, combinada.banco_url)
    print(Configuracoes(_env_file="exemplo.env", expira_em_minutos=5).expira_em_minutos)
    
    Saída
    45 sqlite:///./do-arquivo.db
    5
    

    O expira_em_minutos veio do ambiente (45), e não do arquivo (10), porque o ambiente vence. O banco_url, que não está no ambiente, veio do arquivo. E o argumento direto (5) vence tudo.

    Entregar à aplicação como uma dependência

    Criar as configurações é um trabalho que não precisa se repetir a cada requisição. O lru_cache guarda a instância, e uma função de dependência a entrega às rotas. A vantagem aparece nos testes: você pode trocar a dependência por outra configuração, sem mexer em variáveis de ambiente:

    intermediario/cap23_configuracao.pylinhas 62 a 88
    from typing import Annotated
    
    from fastapi import Depends, FastAPI
    from fastapi.testclient import TestClient
    
    
    @lru_cache
    def obter_configuracoes() -> Configuracoes:
        return Configuracoes(_env_file=None)
    
    
    app = FastAPI()
    
    
    @app.get("/info")
    def info(cfg: Annotated[Configuracoes, Depends(obter_configuracoes)]):
        return {"algoritmo": cfg.algoritmo, "expira_em_minutos": cfg.expira_em_minutos}
    
    
    cliente = TestClient(app)
    print(cliente.get("/info").json())
    
    app.dependency_overrides[obter_configuracoes] = lambda: Configuracoes(
        _env_file=None, chave_secreta="chave-de-teste-bem-comprida-0123456789", expira_em_minutos=1
    )
    print(cliente.get("/info").json())
    app.dependency_overrides.clear()
    
    Saída
    {'algoritmo': 'HS256', 'expira_em_minutos': 45}
    {'algoritmo': 'HS256', 'expira_em_minutos': 1}
    
    Onde ficaO que vai lá
    CódigoO que não muda entre ambientes (nomes de campos, regras)
    .env (fora do Git)Os valores de desenvolvimento
    Variáveis do ambiente de produçãoOs segredos reais, definidos na plataforma de deploy
    .env.example (no Git)Os nomes das variáveis, sem segredos

    Exercício 1

    Um ambiente restrito a valores conhecidos

    Crie ConfigAmbiente (com env_prefix="AMB_") com ambiente: Literal["dev", "prod"] e debug: bool = False. Confira que AMB_AMBIENTE=prod e AMB_DEBUG=true funcionam, e que AMB_AMBIENTE=staging é recusado.