Enhance Configuration System with Environment Variable Priority Handling

- Updated the configuration loading mechanism to prioritize OS environment variables over .env file values, improving flexibility and security.
- Added a new function, GetEnvWithPriority, to explicitly check the source of configuration values.
- Revised documentation to reflect the new priority system, including detailed examples and usage instructions for mixed configuration approaches.
- Enhanced error handling for .env file loading, ensuring graceful handling of missing files without disrupting the application flow.
This commit is contained in:
n.nakhostin
2025-09-15 14:52:12 +03:30
parent c701053609
commit 05a307e345
2 changed files with 175 additions and 16 deletions
+141 -11
View File
@@ -1,6 +1,14 @@
# Environment-Based Configuration System
This package provides a configuration system for the Tender Management backend that loads configuration exclusively from `.env` files. It maintains the same structure as the previous YAML-based system while using environment variables for all configuration values.
This package provides a configuration system for the Tender Management backend that loads configuration from both OS environment variables and `.env` files, with **OS environment variables taking priority** over `.env` file values. It maintains the same structure as the previous YAML-based system while using environment variables for all configuration values.
## Priority System
The configuration loader follows this priority order:
1. **OS Environment Variables** (highest priority) - System-level environment variables
2. **`.env` File Values** (lower priority) - File-based configuration for development
This means that if you set `SERVER_PORT=8080` in your `.env` file but have `SERVER_PORT=9000` as an OS environment variable, the system will use `9000`.
## Architecture
@@ -63,7 +71,9 @@ func initConfig() Config {
}
```
### 3. Create .env Configuration File
### 3. Configuration Sources
#### Option A: Using .env File Only (Development)
Create a `.env` file in your command directory:
@@ -104,6 +114,82 @@ SOME_TIMEOUT=30s
SOME_NUMBER=42
```
#### Option B: Using OS Environment Variables (Production)
```bash
# Set environment variables directly
export SERVER_HOST=0.0.0.0
export SERVER_PORT=8080
export MONGODB_URI=mongodb://mongo:27017
export MONGODB_NAME=production_db
export LOG_LEVEL=error
# Run your application
./your-command
```
#### Option C: Mixed Approach (Recommended)
1. Create a `.env` file with development defaults
2. Override specific values with OS environment variables for production:
```bash
# .env file contains development defaults
# Override critical values with OS env vars
export MONGODB_URI=mongodb://production-server:27017
export MONGODB_NAME=production_db
export LOG_LEVEL=error
# Run application - will use .env defaults except for overridden values
./your-command
```
## Priority Example
Given this `.env` file:
```env
SERVER_HOST=localhost
SERVER_PORT=8080
LOG_LEVEL=debug
```
And these OS environment variables:
```bash
export SERVER_PORT=9000
export LOG_LEVEL=error
```
The final configuration will be:
- `SERVER_HOST=localhost` (from .env file, no OS override)
- `SERVER_PORT=9000` (OS environment variable overrides .env)
- `LOG_LEVEL=error` (OS environment variable overrides .env)
## Advanced Features
### Checking Configuration Source
You can check whether a configuration value came from OS environment or .env file:
```go
import "tm/pkg/config"
func main() {
value, isFromOS := config.GetEnvWithPriority("SERVER_PORT")
if isFromOS {
fmt.Printf("SERVER_PORT=%s (from OS environment)\n", value)
} else {
fmt.Printf("SERVER_PORT=%s (from .env file)\n", value)
}
}
```
### Graceful .env File Handling
The system gracefully handles missing `.env` files:
- If `.env` file exists: loads values as defaults
- If `.env` file is missing: continues with OS environment variables only
- No errors are thrown for missing `.env` files
## Examples
### Web Command Config
@@ -164,9 +250,11 @@ type ScraperConfig struct {
The configuration loader uses Go generics to provide type-safe configuration loading. The function signature ensures that you get back exactly the type you expect.
### Environment Variable Support
### Environment Variable Priority
The configuration system loads all values from `.env` files using the `github.com/subosito/gotenv` library.
The configuration system loads values with explicit priority:
1. OS environment variables (system-level, highest priority)
2. .env file values (file-based, development defaults)
### Reflection-Based Parsing
@@ -193,7 +281,11 @@ The configuration system supports the following Go types:
5. **Use appropriate types**: Use `time.Duration` for durations, proper numeric types for numbers, etc.
6. **Provide sensible defaults**: Consider providing sensible defaults in your code when environment variables are not set.
6. **Provide sensible defaults in .env**: Use `.env` files to provide development defaults and override with OS environment variables for production.
7. **Leverage the priority system**: Use `.env` for development defaults and OS environment variables for production overrides.
8. **Keep secrets in OS environment variables**: Never put sensitive values like API keys or passwords in `.env` files; use OS environment variables instead.
## Migration from YAML System
@@ -223,7 +315,7 @@ type Config struct {
// Old way (YAML loading)
conf, err := config.LoadConfig(".", &Config{})
// New way (.env loading)
// New way (.env loading) - same API!
conf, err := config.LoadConfig(".", &Config{})
```
@@ -240,13 +332,51 @@ The system follows these naming conventions:
## Error Handling
The configuration system provides detailed error messages when:
- The `.env` file cannot be loaded
- The `.env` file exists but cannot be loaded
- Environment variables cannot be parsed into the expected types
- Required fields are missing (handled by your application logic)
Note: Missing `.env` files do not cause errors - the system will continue with OS environment variables only.
## Security Considerations
1. **Never commit `.env` files**: Add `.env` to your `.gitignore` file
2. **Use environment-specific files**: Create different `.env` files for different environments (`.env.local`, `.env.production`, etc.)
3. **Validate sensitive values**: Always validate sensitive configuration values like API keys and database URIs
4. **Use secure defaults**: Provide secure default values for security-related configuration
1. **Never commit `.env` files with secrets**: Add `.env` to your `.gitignore` file and never include sensitive values
2. **Use OS environment variables for production secrets**: Set sensitive values like API keys and database passwords as OS environment variables
3. **Use environment-specific files**: Create different `.env` files for different environments (`.env.local`, `.env.development`, etc.)
4. **Validate sensitive values**: Always validate sensitive configuration values like API keys and database URIs
5. **Use secure defaults**: Provide secure default values for security-related configuration
6. **Leverage priority system for security**: Keep development defaults in `.env` and override with secure OS environment variables in production
## Deployment Strategies
### Development
```bash
# Use .env file for all configuration
./your-command
```
### Staging
```bash
# Use .env for defaults, override key values
export MONGODB_URI=mongodb://staging-db:27017
export LOG_LEVEL=warn
./your-command
```
### Production
```bash
# Override all critical values with OS environment variables
export MONGODB_URI=mongodb://prod-cluster:27017
export MONGODB_NAME=production
export LOG_LEVEL=error
export SERVER_HOST=0.0.0.0
./your-command
```
### Docker
```dockerfile
# Set production values in Dockerfile or docker-compose
ENV MONGODB_URI=mongodb://mongo:27017
ENV LOG_LEVEL=info
ENV SERVER_HOST=0.0.0.0
```