|
2 | 2 |
|
3 | 3 | This directory contains configuration files for the PolicyEngine Household API. |
4 | 4 |
|
5 | | -## Current State |
| 5 | +## Current Implementation Status |
6 | 6 |
|
7 | | -The application currently uses environment variables for all configuration. These configuration files are placeholders that establish the structure for future migration to a file-based configuration system. |
| 7 | +The application now has a `ConfigLoader` class (`policyengine_household_api/utils/config_loader.py`) that supports hierarchical configuration loading. While the system is ready, the application code still uses environment variables directly. Configuration files in this directory establish the structure for gradual migration. |
8 | 8 |
|
9 | | -## Files |
| 9 | +## Configuration Priority |
10 | 10 |
|
11 | | -- `default.yaml` - Default configuration for local development (currently empty placeholders) |
12 | | -- `production.yaml.example` - Example production configuration (not used in deployment) |
13 | | -- `development.yaml.example` - Example development configuration |
| 11 | +The `ConfigLoader` loads configuration from multiple sources in the following priority order (highest to lowest): |
14 | 12 |
|
15 | | -## Migration Plan |
| 13 | +1. **Environment Variables** - Override everything |
| 14 | +2. **Mounted Config File** - External configuration file (via `CONFIG_FILE` env var) |
| 15 | +3. **Default Config** - Baked into the Docker image at `/app/config/default.yaml` |
16 | 16 |
|
17 | | -The configuration system will be migrated gradually: |
| 17 | +Environment variables always win, allowing you to override specific settings without changing config files. |
18 | 18 |
|
19 | | -1. **Phase 1 (Current)**: All configuration via environment variables |
20 | | -2. **Phase 2**: Configuration files with environment variable overrides |
21 | | -3. **Phase 3**: Configuration files as primary source, env vars for secrets only |
| 19 | +## Files in this Directory |
| 20 | + |
| 21 | +- `default.yaml` - Default configuration for local development (currently mostly empty/commented) |
| 22 | +- `custom.yaml.example` - Example template for mounting custom configuration |
| 23 | +- `production.yaml.example` - Example production configuration |
| 24 | +- `development.yaml.example` - Example development configuration |
| 25 | +- `local.yaml.example` - Example for fully local runs without external dependencies |
| 26 | + |
| 27 | +## Configuration Methods |
| 28 | + |
| 29 | +### Method 1: Environment Variables (Highest Priority) |
| 30 | + |
| 31 | +Environment variables override all other configuration sources. They work in two ways: |
| 32 | + |
| 33 | +#### Explicit Mapping |
| 34 | +Pre-defined environment variables that map to specific config paths: |
| 35 | + |
| 36 | +```bash |
| 37 | +# Database configuration |
| 38 | +USER_ANALYTICS_DB_CONNECTION_NAME=my-connection |
| 39 | +USER_ANALYTICS_DB_USERNAME=myuser |
| 40 | +USER_ANALYTICS_DB_PASSWORD=secret |
| 41 | + |
| 42 | +# Auth configuration |
| 43 | +AUTH0_ADDRESS_NO_DOMAIN=my-auth0-domain |
| 44 | +AUTH0_AUDIENCE_NO_DOMAIN=my-audience |
| 45 | + |
| 46 | +# AI configuration |
| 47 | +ANTHROPIC_API_KEY=sk-ant-... |
| 48 | + |
| 49 | +# Debug mode |
| 50 | +FLASK_DEBUG=1 |
| 51 | + |
| 52 | +# Server configuration |
| 53 | +PORT=8080 |
| 54 | +``` |
| 55 | + |
| 56 | +#### Double Underscore Notation |
| 57 | +Any environment variable with double underscores (`__`) is automatically mapped to nested config: |
| 58 | + |
| 59 | +```bash |
| 60 | +# DATABASE__HOST becomes database.host |
| 61 | +DATABASE__HOST=localhost |
| 62 | +DATABASE__PORT=3306 |
| 63 | + |
| 64 | +# AUTH__ENABLED becomes auth.enabled |
| 65 | +AUTH__ENABLED=true |
| 66 | + |
| 67 | +# SERVER__WORKERS becomes server.workers |
| 68 | +SERVER__WORKERS=4 |
| 69 | +``` |
| 70 | + |
| 71 | +Note: System environment variables starting with underscores are ignored. |
| 72 | + |
| 73 | +### Method 2: Mounted Config File (Medium Priority) |
| 74 | + |
| 75 | +You can mount an external configuration file to override the defaults: |
| 76 | + |
| 77 | +#### Docker Run |
| 78 | +```bash |
| 79 | +# Mount a custom config file |
| 80 | +docker run -v /path/to/your/config.yaml:/custom/config.yaml \ |
| 81 | + -e CONFIG_FILE=/custom/config.yaml \ |
| 82 | + policyengine/household-api |
| 83 | +``` |
| 84 | + |
| 85 | +#### Docker Compose |
| 86 | +```yaml |
| 87 | +version: '3.8' |
| 88 | +services: |
| 89 | + household-api: |
| 90 | + image: policyengine/household-api |
| 91 | + volumes: |
| 92 | + - ./my-config.yaml:/app/config/custom.yaml |
| 93 | + environment: |
| 94 | + - CONFIG_FILE=/app/config/custom.yaml |
| 95 | + # Still provide secrets via env vars |
| 96 | + - DATABASE__PASSWORD=${DB_PASSWORD} |
| 97 | + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} |
| 98 | +``` |
| 99 | +
|
| 100 | +#### Kubernetes ConfigMap |
| 101 | +```yaml |
| 102 | +apiVersion: v1 |
| 103 | +kind: ConfigMap |
| 104 | +metadata: |
| 105 | + name: household-api-config |
| 106 | +data: |
| 107 | + config.yaml: | |
| 108 | + database: |
| 109 | + provider: postgres |
| 110 | + host: postgres.default.svc.cluster.local |
| 111 | + auth: |
| 112 | + enabled: true |
| 113 | +--- |
| 114 | +apiVersion: apps/v1 |
| 115 | +kind: Deployment |
| 116 | +spec: |
| 117 | + template: |
| 118 | + spec: |
| 119 | + containers: |
| 120 | + - name: api |
| 121 | + image: policyengine/household-api |
| 122 | + env: |
| 123 | + - name: CONFIG_FILE |
| 124 | + value: /config/config.yaml |
| 125 | + - name: DATABASE__PASSWORD |
| 126 | + valueFrom: |
| 127 | + secretKeyRef: |
| 128 | + name: db-secret |
| 129 | + key: password |
| 130 | + volumeMounts: |
| 131 | + - name: config |
| 132 | + mountPath: /config |
| 133 | + volumes: |
| 134 | + - name: config |
| 135 | + configMap: |
| 136 | + name: household-api-config |
| 137 | +``` |
| 138 | +
|
| 139 | +### Method 3: Default Config (Lowest Priority) |
| 140 | +
|
| 141 | +The default configuration is baked into the Docker image at `/app/config/default.yaml`. This provides sensible defaults for local development and serves as a fallback. |
22 | 142 |
|
23 | 143 | ## Configuration Structure |
24 | 144 |
|
25 | 145 | ```yaml |
26 | 146 | app: |
27 | | - name: Application name |
| 147 | + name: Application name (default: policyengine-household-api) |
28 | 148 | environment: Environment (local/development/staging/production) |
| 149 | + debug: Debug mode (true/false) |
29 | 150 |
|
30 | 151 | database: |
31 | 152 | provider: Database type (sqlite/mysql/postgres) |
32 | | - # Connection details |
| 153 | + connection_name: Cloud SQL connection name (for GCP) |
| 154 | + username: Database username |
| 155 | + password: Database password |
| 156 | + host: Database host |
| 157 | + port: Database port |
| 158 | + path: SQLite file path (for sqlite provider) |
| 159 | + pool_size: Connection pool size |
33 | 160 |
|
34 | 161 | storage: |
35 | 162 | provider: Storage backend (local/gcs/s3) |
36 | | - # Provider-specific settings |
| 163 | + bucket: Storage bucket name (for cloud providers) |
| 164 | + path: Local storage path (for local provider) |
37 | 165 |
|
38 | 166 | auth: |
39 | | - enabled: Whether authentication is required |
| 167 | + enabled: Whether authentication is required (true/false) |
40 | 168 | provider: Auth provider (none/auth0/cognito) |
41 | | - # Provider-specific settings |
| 169 | + auth0: |
| 170 | + address: Auth0 domain |
| 171 | + audience: Auth0 audience |
42 | 172 |
|
43 | 173 | ai: |
44 | | - enabled: Whether AI features are enabled |
| 174 | + enabled: Whether AI features are enabled (true/false) |
45 | 175 | provider: AI service provider (none/anthropic/openai) |
46 | | - # Provider-specific settings |
| 176 | + anthropic: |
| 177 | + api_key: Anthropic API key |
| 178 | + model: Model name |
| 179 | + max_tokens: Maximum tokens |
| 180 | + temperature: Temperature setting |
47 | 181 |
|
48 | 182 | server: |
49 | | - port: Server port |
| 183 | + port: Server port (default: 8080) |
50 | 184 | workers: Number of worker processes |
| 185 | + threads: Number of threads per worker |
51 | 186 | timeout: Request timeout in seconds |
52 | 187 |
|
53 | 188 | logging: |
54 | 189 | level: Log level (DEBUG/INFO/WARNING/ERROR) |
55 | 190 | format: Log format (json/text) |
56 | 191 | ``` |
57 | 192 |
|
58 | | -## Environment Variable Mapping |
| 193 | +## Usage Examples |
59 | 194 |
|
60 | | -When migrated, environment variables will map to configuration as follows: |
| 195 | +### Production Deployment (Current) |
61 | 196 |
|
62 | | -- `FLASK_DEBUG` → `app.debug` |
63 | | -- `USER_ANALYTICS_DB_CONNECTION_NAME` → `database.connection_name` |
64 | | -- `USER_ANALYTICS_DB_USERNAME` → `database.username` |
65 | | -- `USER_ANALYTICS_DB_PASSWORD` → `database.password` |
66 | | -- `AUTH0_ADDRESS_NO_DOMAIN` → `auth.auth0.address` |
67 | | -- `AUTH0_AUDIENCE_NO_DOMAIN` → `auth.auth0.audience` |
68 | | -- `ANTHROPIC_API_KEY` → `ai.anthropic.api_key` |
| 197 | +Currently in production, all configuration comes from environment variables: |
69 | 198 |
|
70 | | -## Usage (Future) |
71 | | - |
72 | | -Once migration is complete: |
| 199 | +```bash |
| 200 | +# Via GitHub Actions secrets |
| 201 | +AUTH0_ADDRESS_NO_DOMAIN=${{ secrets.AUTH0_ADDRESS_NO_DOMAIN }} |
| 202 | +AUTH0_AUDIENCE_NO_DOMAIN=${{ secrets.AUTH0_AUDIENCE_NO_DOMAIN }} |
| 203 | +USER_ANALYTICS_DB_USERNAME=${{ secrets.USER_ANALYTICS_DB_USERNAME }} |
| 204 | +USER_ANALYTICS_DB_PASSWORD=${{ secrets.USER_ANALYTICS_DB_PASSWORD }} |
| 205 | +USER_ANALYTICS_DB_CONNECTION_NAME=${{ secrets.USER_ANALYTICS_DB_CONNECTION_NAME }} |
| 206 | +ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }} |
| 207 | +``` |
73 | 208 |
|
74 | 209 | ### Local Development |
75 | | -```bash |
76 | | -# Use default.yaml automatically |
77 | | -python -m policyengine_household_api.api |
78 | 210 |
|
79 | | -# Or specify a config file |
80 | | -CONFIG_FILE=config/development.yaml python -m policyengine_household_api.api |
| 211 | +Use environment variables to override specific settings: |
| 212 | + |
| 213 | +```bash |
| 214 | +docker run -e FLASK_DEBUG=1 \ |
| 215 | + -e AUTH__ENABLED=false \ |
| 216 | + -e AI__ENABLED=false \ |
| 217 | + -e DATABASE__PROVIDER=sqlite \ |
| 218 | + policyengine/household-api |
81 | 219 | ``` |
82 | 220 |
|
83 | | -### Docker |
| 221 | +### Custom Cloud Provider |
| 222 | + |
| 223 | +Mount a complete custom configuration: |
| 224 | + |
84 | 225 | ```bash |
85 | | -# Mount custom config |
86 | | -docker run -v $(pwd)/my-config.yaml:/app/config/active.yaml household-api |
| 226 | +# Create custom config |
| 227 | +cat > aws-config.yaml <<EOF |
| 228 | +database: |
| 229 | + provider: postgres |
| 230 | + host: my-rds-instance.amazonaws.com |
| 231 | +storage: |
| 232 | + provider: s3 |
| 233 | + bucket: my-household-data |
| 234 | +auth: |
| 235 | + provider: cognito |
| 236 | + pool_id: us-east-1_xxxxx |
| 237 | +EOF |
87 | 238 |
|
88 | | -# Or use environment variable to specify config location |
89 | | -docker run -e CONFIG_FILE=/app/config/production.yaml household-api |
| 239 | +# Run with custom config |
| 240 | +docker run -v $(pwd)/aws-config.yaml:/app/config/aws.yaml \ |
| 241 | + -e CONFIG_FILE=/app/config/aws.yaml \ |
| 242 | + -e DATABASE__PASSWORD="${RDS_PASSWORD}" \ |
| 243 | + policyengine/household-api |
90 | 244 | ``` |
91 | 245 |
|
92 | | -### Production |
93 | | -Environment variables will override config file values for sensitive data: |
| 246 | +## Migration Status |
| 247 | + |
| 248 | +### Phase 1 (Current) |
| 249 | +- ✅ `ConfigLoader` class implemented |
| 250 | +- ✅ Hierarchical configuration loading working |
| 251 | +- ✅ Environment variable mapping functional |
| 252 | +- ✅ Docker image includes default config |
| 253 | +- ⏳ Application code still uses `os.getenv()` directly |
| 254 | + |
| 255 | +### Phase 2 (Next Steps) |
| 256 | +- Gradually update application code to use `get_config_value()` instead of `os.getenv()` |
| 257 | +- Move non-sensitive defaults to config files |
| 258 | +- Keep sensitive values in environment variables |
| 259 | + |
| 260 | +### Phase 3 (Future) |
| 261 | +- Configuration files become primary source |
| 262 | +- Environment variables used only for secrets and overrides |
| 263 | +- Full configuration validation with Pydantic models |
| 264 | + |
| 265 | +## Using ConfigLoader in Code |
| 266 | + |
| 267 | +To use the configuration system in new code: |
| 268 | + |
| 269 | +```python |
| 270 | +from policyengine_household_api.utils import get_config_value |
| 271 | +
|
| 272 | +# Get a configuration value with a default |
| 273 | +db_provider = get_config_value("database.provider", "sqlite") |
| 274 | +
|
| 275 | +# Get nested configuration |
| 276 | +auth_enabled = get_config_value("auth.enabled", False) |
| 277 | +``` |
| 278 | + |
| 279 | +## Best Practices |
| 280 | + |
| 281 | +1. **Never put secrets in config files** - Use environment variables for sensitive data |
| 282 | +2. **Use mounted configs for structure** - Define your infrastructure layout in config files |
| 283 | +3. **Use env vars for secrets** - Override sensitive values at runtime |
| 284 | +4. **Version control your configs** - Keep config files in source control (without secrets) |
| 285 | +5. **Environment-specific configs** - Have separate config files for dev/staging/prod |
| 286 | + |
| 287 | +## Debugging Configuration |
| 288 | + |
| 289 | +To see what configuration is being loaded, set the log level to DEBUG: |
| 290 | + |
94 | 291 | ```bash |
95 | | -# Config file provides structure, env vars provide secrets |
96 | | -DATABASE__PASSWORD=secret CONFIG_FILE=production.yaml python -m app |
97 | | -``` |
| 292 | +docker run -e LOGGING__LEVEL=DEBUG policyengine/household-api |
| 293 | +``` |
| 294 | + |
| 295 | +The application will log: |
| 296 | +- Which config files were loaded |
| 297 | +- Which environment variables were applied |
| 298 | +- Any errors in loading configuration files |
0 commit comments