Configuration & Secrets in Prod
Production Python services read configuration from the environment, validate with Pydantic 2 settings models, and load secrets from platform stores - never from files baked into container images or git.
Recipe
from pydantic import Field, SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=None)
database_url: SecretStr
log_level: str = "info"
environment: str = Field(default="dev")
settings = Settings()When to reach for this:
- Every container/Lambda deploy on AWS, K8s, or PaaS
- Separating config per environment with same image tag
- Rotating credentials without rebuilding images
- Audit who can read which secrets via IAM/RBAC
Working Example
Settings with optional local .env only in dev, prod from injected env + SSM fetch at startup.
import os
from functools import lru_cache
import boto3
from pydantic import Field, SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(extra="ignore")
environment: str = "dev"
database_url: SecretStr = Field(alias="DATABASE_URL")
ssm_parameter: str | None = Field(default=None, alias="API_KEY_PARAM")
@lru_cache
def get_settings() -> Settings:
return Settings()
def resolve_api_key(settings: Settings) -> str:
if settings.environment == "dev" and os.environ.get("API_KEY"):
return os.environ["API_KEY"]
if not settings.ssm_parameter:
raise RuntimeError("API_KEY_PARAM required in prod")
ssm = boto3.client("ssm")
value = ssm.get_parameter(Name=settings.ssm_parameter, WithDecryption=True)
return value["Parameter"]["Value"]
if __name__ == "__main__":
s = get_settings()
print(s.environment, resolve_api_key(s)[:3] + "***")What this demonstrates:
SecretStrprevents accidental logging of raw secret in repr- Prod uses SSM parameter name reference, not secret value in env
lru_cachesettings singleton per process matches FastAPI dependency pattern
Deep Dive
12-Factor Config Rules
- Store config in environment
- Strict separation of config from code
- Treat backing services as attached resources
Injection by Platform
| Platform | Mechanism |
|---|---|
| Kubernetes | Secret/ConfigMap envFrom |
| ECS | task definition secrets from SSM |
| Lambda | env + Secrets Manager extension |
| GitHub Actions | environment secrets |
Python Notes
# FastAPI dependency
from fastapi import Depends
def settings_dep() -> Settings:
return get_settings()
@app.get("/health")
def health(s: Settings = Depends(settings_dep)):
return {"env": s.environment}Gotchas
.envfile in production image - secrets in layers. Fix: dev-only dotenv; prod platform injection.- Logging
settings.model_dump()- leaks secrets. Fix:model_dump(exclude={"database_url"})or custom redacted repr. - Same image config for staging/prod - wrong DB wiped. Fix: environment-specific secret ARNs in deploy manifest only.
- World-readable env in
/proc- container escape risk mitigated by RBAC and minimal secrets per pod. - Hot-reloading config without restart - rare in Python web apps; document restart required after secret rotation.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Vault agent sidecar | Dynamic DB creds | Simple SSM static secrets |
| Sealed Secrets | GitOps encrypted secrets in git | SSM already standard |
| Config files on volume | Large non-secret YAML | Secrets in same file |
FAQs
pydantic-settings vs os.environ?
Settings gives validation, types, and aliases - worth it for every prod service.
How do Flask/Django load settings?
Django settings module from env; Flask app.config.from_prefixed_env() or pydantic wrapper.
How do I rotate DB password?
Dual-password window in RDS + rolling pod restart to pick up new SSM value.
ConfigMap for secrets in K8s?
Anti-pattern - use Secret resource or external secrets operator to SSM.
How do I test settings?
monkeypatch.setenv in pytest before get_settings.cache_clear().
Feature flags?
Non-secret flags in ConfigMap/env; dynamic flags via LaunchDarkly/etc. when needed.
How does this relate to infra templating?
Infra renders deploy manifests with secret ARNs; app fetches values at runtime - see Configuration Management in infra-automation.
Lambda env size limits?
4KB total env - use SSM/Secrets for large config blobs.
How do I document required env vars?
README table + Settings field descriptions + deploy manifest comments.
Default values safe?
Only for non-secret dev defaults; prod missing required field must fail fast at startup.
Related
- Secrets Manager & SSM - boto3 fetch
- Configuration Management - templating deploy artifacts
- Dockerizing Python - no secrets in image
- Kubernetes for Python Apps - secretRef
- Deployment Best Practices - config checklist
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.