Structum Configuration Philosophy

Versione: 3.0
Ultima Revisione: 2025-01-11
Stato: Alpha


Indice

  1. Principio Fondamentale

  2. Il Modello a 5 Layer

  3. Separazione delle Responsabilità

  4. Flusso di Risoluzione

  5. Diagrammi e Visualizzazioni

  6. Scenari d’Uso

  7. Best Practices


1. Principio Fondamentale

Structum adotta un approccio layered configuration per separare le responsabilità tra:

  1. Developers → Definiscono la struttura e i default

  2. DevOps/SRE → Configurano l’infrastruttura e i secrets

  3. End Users → Personalizzano l’esperienza runtime

1.1 Core Philosophy: “Configuration as Code”

La configurazione in Structum segue gli stessi principi del codice:

  • Versionata (defaults in Git)

  • Validata (Pydantic schemas)

  • Testabile (mock providers per unit tests)

  • Overridable (layer di override senza modificare i sorgenti)

1.2 Separation of Concerns

Il codice applicativo non sa da dove arriva la configurazione.

# ✅ Codice business decoupled
config = get_config()
db_host = config.get("database.host")  
# Può venire da: TOML, ENV, Secrets, Runtime, ...

# ❌ Anti-pattern: accoppiamento
db_host = os.getenv("DB_HOST", "localhost")  
# Hard-coded al meccanismo di override

Questo permette di cambiare il backend (file → Redis → Vault) senza toccare una riga di business logic.


2. Il Modello a 5 Layer

Structum usa una gerarchia a priorità per risolvere ogni configurazione:

┌──────────────────────────────────────────────────┐
│ Layer 1: RUNTIME (In-Memory)                     │  ← MASSIMA PRIORITÀ
│ • Modifiche via config.set() non persistite      │
│ • Valido solo per il processo corrente           │
├──────────────────────────────────────────────────┤
│ Layer 2: USER PERSISTENT (Runtime Saved)         │
│ • File: ~/.structum/{namespace}_saved.json       │
│ • Modifiche persistite via config.save()         │
│ • Specifico per utente/istanza                   │
├──────────────────────────────────────────────────┤
│ Layer 3: ENVIRONMENT VARIABLES                   │
│ • Formato: STRUCTUM_{NAMESPACE}__{KEY}           │
│ • Configurazione infrastrutturale (Docker/K8s)   │
│ • Impostato da DevOps/deployment scripts         │
├──────────────────────────────────────────────────┤
│ Layer 4: SECRETS                                 │
│ • File: config/.secrets.toml (git-ignored)       │
│ • Contiene password, API keys, tokens            │
│ • Mergiato automaticamente con Layer 5           │
├──────────────────────────────────────────────────┤
│ Layer 5: APPLICATION DEFAULTS                    │  ← MINIMA PRIORITÀ
│ • File: config/app/{namespace}.toml              │
│ • Defaults dichiarati dallo sviluppatore         │
│ • Versionato in Git, baseline per tutti          │
└──────────────────────────────────────────────────┘

2.1 Risoluzione: “First Found Wins”

Quando richiedi config.get("database.host"):

# Pseudo-codice del resolver
def resolve_value(key):
    if key in runtime_layer:
        return runtime_layer[key]  # Layer 1
    
    if key in user_persistent_layer:
        return user_persistent_layer[key]  # Layer 2
    
    if key in environment_variables:
        return environment_variables[key]  # Layer 3
    
    if key in secrets_layer:
        return secrets_layer[key]  # Layer 4
    
    if key in application_defaults:
        return application_defaults[key]  # Layer 5
    
    return default_value  # Se nessun layer ha il valore

Il primo valore trovato vince. Layer superiori sovrascrivono quelli inferiori.


3. Separazione delle Responsabilità

3.1 Layer 5: Developer Layer (Application Defaults)

Chi: Software Engineer, Library Author
Formato: TOML
Location: config/app/{namespace}.toml (nel repository, versionato)
Scopo: Definire struttura, chiavi valide, valori di default sicuri

Esempio:

# config/app/database.toml
[default]
host = "localhost"
port = 5432
pool_size = 10
timeout = 30

[default.auth]
user = "app_user"
# ⚠️ NON mettere password qui

[production]
host = "db.prod.example.com"
pool_size = 50

Caratteristiche:

  • Versionato in Git (parte del codice)

  • Contiene solo valori sicuri (no secrets)

  • Supporta ambienti multipli ([default], [production], [development])

  • Validato da Pydantic schema (opzionale ma raccomandato)

Accesso:

config.get("database.host")       # "localhost" (da [default])
config.get("database.pool_size")  # 10

3.2 Layer 4: Secrets Layer

Chi: DevOps, Security Team
Formato: TOML (locale) o Secret Manager (produzione)
Location: config/.secrets.toml (git-ignored) o Vault/K8s Secrets
Scopo: Isolare credenziali sensibili dal codice

Esempio:

# config/.secrets.toml (NON versionare!)
[database.auth]
password = "SuperSecretPassword123"

[api_client]
api_key = "sk-proj-abc123xyz"
client_secret = "very-secret-value"

⚠️ CRITICO: Security Checklist

  • [ ] Aggiunto a .gitignore

  • [ ] Permessi file: chmod 600 config/.secrets.toml

  • [ ] In produzione: usa Vault/K8s Secrets invece di file

  • [ ] Rotazione periodica delle credenziali

Merge Automatico: Il plugin mergia automaticamente secrets con i namespace corrispondenti:

# Merged in-memory:
config.get("database.auth.user")      # "app_user" (da database.toml)
config.get("database.auth.password")  # "SuperSecret..." (da .secrets.toml)

3.3 Layer 3: Environment Variables (Infrastructure)

Chi: DevOps, SRE, CI/CD Pipeline
Formato: Environment Variables
Location: Shell, Docker Compose, K8s ConfigMaps
Scopo: Override per ambiente specifico senza modificare file

Convenzione:

{ENV_PREFIX}__{NAMESPACE}__{PATH__TO__KEY}

Esempio:

# Override singolo valore
export STRUCTUM__DATABASE__HOST=prod-db.example.com
export STRUCTUM__DATABASE__PORT=3306

# Override valore nested
export STRUCTUM__DATABASE__POOL__MAX_SIZE=100

# Liste (comma-separated)
export STRUCTUM__DATABASE__HOSTS=db1.com,db2.com,db3.com

Uso Tipico: Docker Compose

# docker-compose.yml
version: '3.8'
services:
  app:
    image: myapp:latest
    environment:
      - STRUCTUM__DATABASE__HOST=postgres
      - STRUCTUM__DATABASE__PORT=5432
      - STRUCTUM__LOG_LEVEL=INFO

Uso Tipico: Kubernetes

# k8s-deployment.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: myapp-config
data:
  STRUCTUM__DATABASE__HOST: "prod-db.svc.cluster.local"
  STRUCTUM__DATABASE__PORT: "5432"
---
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
      - name: myapp
        envFrom:
        - configMapRef:
            name: myapp-config

3.4 Layer 2: User Persistent (Runtime Saved)

Chi: Instance Operator, End User, Application Runtime
Formato: JSON
Location: ~/.structum/{namespace}_saved.json (user home)
Scopo: Salvare preferenze utente tra riavvii

Caratteristiche:

  • Creato automaticamente al primo config.save()

  • Contiene solo chiavi modificate (non duplica i default)

  • Specifico per utente/istanza (non condiviso)

Esempio:

# Modifica runtime
config = get_config()
config.set("ui.theme", "dark_mode")
config.set("ui.font_size", 14)

# Persiste su disco
config.save()

# File creato: ~/.structum/ui_saved.json
# {
#   "theme": "dark_mode",
#   "font_size": 14
# }

Al riavvio:

config = get_config()
theme = config.get("ui.theme")  # "dark_mode" (dal file saved)
# Anche se config/app/ui.toml dice theme = "light"

Casi d’Uso:

  • Preferenze UI (tema, font size, layout)

  • Ultimi file aperti

  • Dimensioni finestre

  • Cache persistente


3.5 Layer 1: Runtime (In-Memory)

Chi: Application Runtime
Formato: In-Memory Dict
Location: RAM (volatile)
Scopo: Modifiche temporanee senza persistenza

Caratteristiche:

  • Vive solo durante l’esecuzione del processo

  • Non persiste dopo exit() (a meno di chiamare save())

  • Utile per test, debugging, feature flags temporanei

Esempio:

config = get_config()

# Modifica temporanea (solo in-memory)
config.set("debug.verbose", True)
print(config.get("debug.verbose"))  # True

# NON chiamiamo config.save()

# Riavvio applicazione
config = get_config()
print(config.get("debug.verbose"))  # False (default dal TOML)

Casi d’Uso:

  • Feature flags temporanei

  • Debug mode attivato da CLI flag

  • Test A/B runtime

  • Override per sessione specifica


4. Flusso di Risoluzione

4.1 Esempio Completo: Risoluzione Multi-Layer

Setup:

# config/app/database.toml
[default]
host = "localhost"
port = 5432
pool_size = 10
# config/.secrets.toml
[database.auth]
password = "secret123"
# Environment
export STRUCTUM__DATABASE__HOST=staging-db.com
export STRUCTUM__DATABASE__PORT=3306
// ~/.structum/database_saved.json
{
  "host": "my-custom-db.local"
}
# Runtime
config.set("database.pool_size", 50)

Risoluzione:

config = get_config()

config.get("database.host")
# Layer 1 (Runtime): ❌ Non impostato
# Layer 2 (User): ✅ "my-custom-db.local" ← VINCE

config.get("database.port")
# Layer 1 (Runtime): ❌ Non impostato
# Layer 2 (User): ❌ Non nel file saved
# Layer 3 (ENV): ✅ 3306 ← VINCE

config.get("database.pool_size")
# Layer 1 (Runtime): ✅ 50 ← VINCE

config.get("database.auth.password")
# Layer 1-3: ❌ Non impostato
# Layer 4 (Secrets): ✅ "secret123" ← VINCE

4.2 Diagramma di Flusso

                    ┌─────────────────────┐
                    │ config.get("key")   │
                    └──────────┬──────────┘
                               │
                               ▼
                    ┌──────────────────────┐
                    │ Layer 1: Runtime?    │
                    │ (in-memory via set()) │
                    └──────┬───────────────┘
                           │
                     NO   ┌┴┐   YES
                    ┌─────┤ ├─────┐
                    │     └─┘     │
                    ▼             ▼
         ┌──────────────────┐   [Return Value]
         │ Layer 2: User?   │
         │ (*_saved.json)   │
         └────┬─────────────┘
              │
        NO   ┌┴┐   YES
       ┌─────┤ ├─────┐
       │     └─┘     │
       ▼             ▼
┌──────────────┐   [Return Value]
│ Layer 3: ENV?│
│ (STRUCTUM__) │
└────┬─────────┘
     │
NO  ┌┴┐   YES
  ┌─┤ ├──┐
  │ └─┘  │
  ▼      ▼
┌─────────────┐   [Return Value]
│ Layer 4:    │
│ Secrets?    │
└──┬──────────┘
   │
NO ┌┴┐   YES
 ┌─┤ ├──┐
 │ └─┘  │
 ▼      ▼
┌──────────────┐   [Return Value]
│ Layer 5:     │
│ Defaults?    │
│ (.toml)      │
└──┬───────────┘
   │
NO ┌┴┐   YES
 ┌─┤ ├──┐
 │ └─┘  │
 ▼      ▼
[Return    [Return Value]
 Default
 Arg]

5. Diagrammi e Visualizzazioni

5.1 Architettura Completa

┌────────────────────────────────────────────────────────────────┐
│                     APPLICATION CODE                           │
│                   config.get("db.host")                        │
└────────────────────────┬───────────────────────────────────────┘
                         │
                         ▼
┌────────────────────────────────────────────────────────────────┐
│                  CONFIG FACADE (Singleton)                     │
│  • Gestisce layer priority                                     │
│  • Risolve dot-notation (a.b.c → nested dict)                  │
│  • Cache in-memory per performance                             │
└────────────────────────┬───────────────────────────────────────┘
                         │
                         ▼
┌────────────────────────────────────────────────────────────────┐
│              DYNACONF CONFIG PROVIDER (Strategy)               │
│                                                                 │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐        │
│  │  Namespace   │  │  Namespace   │  │  Namespace   │        │
│  │  "database"  │  │ "api_client" │  │    "ui"      │        │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘        │
│         │                 │                  │                 │
│         ▼                 ▼                  ▼                 │
│  ┌─────────────────────────────────────────────────┐          │
│  │         LAYER RESOLVER (Priority Stack)         │          │
│  ├─────────────────────────────────────────────────┤          │
│  │ 1. Runtime (volatile)                           │          │
│  │ 2. User Persistent (~/.structum/*.json)         │          │
│  │ 3. Environment Variables (STRUCTUM__*)          │          │
│  │ 4. Secrets (.secrets.toml)                      │          │
│  │ 5. Application Defaults (.toml)                 │          │
│  └─────────────────────────────────────────────────┘          │
│                                                                 │
│  ┌─────────────────────────────────────────────────┐          │
│  │   PYDANTIC VALIDATOR (Optional)                 │          │
│  │   • Strong typing                                │          │
│  │   • Field validation                             │          │
│  │   • Custom rules                                 │          │
│  └─────────────────────────────────────────────────┘          │
└────────────────────────────────────────────────────────────────┘

5.2 Data Flow (Mermaid)

        graph TB
    subgraph "Configuration Layers (Priority ↓)"
        L1[Layer 1: Runtime In-Memory]
        L2[Layer 2: User Persistent JSON]
        L3[Layer 3: Environment Variables]
        L4[Layer 4: Secrets TOML]
        L5[Layer 5: Application Defaults]
    end
    
    subgraph "Validation & Merging"
        V[Pydantic Validator]
        M[Layer Merger]
    end
    
    subgraph "Application Access"
        APP[config.get - key -]
        FACADE[Config Facade]
    end
    
    L5 -->|Load| M
    L4 -->|Load & Merge| M
    L3 -->|Load & Override| M
    L2 -->|Load & Override| M
    L1 -->|Override| M
    
    M -->|Per-Layer| V
    V -->|Validated Data| FACADE
    APP -->|Request| FACADE
    FACADE -->|Resolve| L1
    FACADE -->|Fallback| L2
    FACADE -->|Fallback| L3
    FACADE -->|Fallback| L4
    FACADE -->|Fallback| L5
    

6. Scenari d’Uso

6.1 Scenario: Sviluppo Locale

Attori: Developer
Setup:

  • config/app/database.toml → defaults (versionato)

  • Nessuna env var

  • Nessun file saved

Comportamento:

config.get("database.host")  # "localhost" (da TOML)
config.get("database.port")  # 5432 (da TOML)

6.2 Scenario: Staging Environment

Attori: CI/CD Pipeline, DevOps
Setup:

  • config/app/database.toml → defaults

  • ENV vars impostate dal deployment:

    STRUCTUM__DATABASE__HOST=staging-db.internal
    STRUCTUM__DATABASE__PORT=5432
    

Comportamento:

config.get("database.host")  # "staging-db.internal" (ENV vince)
config.get("database.port")  # 5432 (ENV, anche se = default)

6.3 Scenario: Production con Secrets

Attori: SRE, Security Team
Setup:

  • config/app/database.toml → defaults

  • config/.secrets.toml → password (git-ignored)

  • ENV vars da K8s ConfigMap + Secrets

Comportamento:

config.get("database.host")      # "prod-db.cluster.local" (da ENV)
config.get("database.auth.user") # "prod_user" (da ENV)
config.get("database.auth.password")  # "..." (da .secrets.toml)

6.4 Scenario: End User Customization

Attori: End User
Setup:

  • App installata con defaults

  • User modifica tema UI

  • Preferenza salvata in ~/.structum/ui_saved.json

Comportamento:

# Prima modifica
config.set("ui.theme", "dark")
config.save()  # Scrive ~/.structum/ui_saved.json

# Dopo riavvio
config.get("ui.theme")  # "dark" (da saved.json, non da TOML)

7. Best Practices

7.1 Cosa Va in Ogni Layer

Layer

Contenuto

Esempi

Defaults (TOML)

Valori sicuri, struttura

log_level = "INFO", timeout = 30

Secrets

Credenziali sensibili

password, api_key, private_key

Environment

Config infrastrutturale

host, port, replicas

User Persistent

Preferenze utente

theme, language, last_opened

Runtime

Override temporanei

debug = True, feature_flag_x = True

7.2 Anti-Patterns da Evitare

❌ Secrets in TOML versionato

# config/app/database.toml
[default.auth]
password = "admin123"  # ❌ Esposto in Git!

❌ Hardcoded paths in codice

# ❌ Accoppiamento
with open("/etc/myapp/config.toml") as f:
    config = toml.load(f)

# ✅ Usa il framework
config = get_config()

❌ Magic values senza default

# ❌ Fallisce se chiave manca
db_host = config.get("database.host")  # KeyError!

# ✅ Sempre fornisci default
db_host = config.get("database.host", default="localhost")

7.3 Security Checklist

Production Deployment:

  • [ ] .secrets.toml in .gitignore

  • [ ] Permessi file secrets: chmod 600

  • [ ] Env vars per credenziali (no file in prod)

  • [ ] Vault/K8s Secrets per produzione

  • [ ] Rotazione periodica passwords

  • [ ] Audit logging delle modifiche config

  • [ ] Validazione Pydantic abilitata

7.4 Performance Tips

  • Cache aggressiva: Il facade cacheggia valori risolti

  • Lazy loading: I namespace vengono caricati solo se acceduti

  • Hot reload: Usa con cautela (overhead di watchdog)

  • Validazione: Valida all’init, non ad ogni get()


Changelog

v3.0.0 (2025-01-11)

  • 📝 Revisione completa della filosofia

  • ✨ Chiarito modello a 5 layer (prima erano 4)

  • 📊 Aggiunti diagrammi Mermaid e ASCII

  • 🎯 Separazione chiara: Runtime vs User Persistent

v2.0.0 (2024-09-01)

  • ✨ Introdotto layer Secrets separato

  • 🔒 Best practices di security

v0.1.0 (Alpha)

  • 🎉 Versione iniziale


Riferimenti


Maintainer: Structum Core Team
Feedback: https://github.com/structum-lab/structum/discussions