⚙️ Configuration and Environment Injection Patterns
Effective configuration management is crucial for building portable, maintainable, and secure applications. This pattern focuses on separating configuration from code and providing flexible injection mechanisms.
🎯 Core Principles
Separation of Concerns
Configuration should be externalized from application logic.
| # ❌ Hardcoded configuration
DATABASE_URL="localhost:5432/mydb"
API_KEY="secret123"
# ✅ Externalized configuration
DATABASE_URL=${DATABASE_URL:-"localhost:5432/default"}
API_KEY=${API_KEY:-""}
|
Environment-First Approach
Environments provide the most flexible configuration mechanism.
| # Environment variable precedence
export DB_HOST=production-db.example.com
export DB_PORT=5432
export LOG_LEVEL=info
# Application reads from environment
DB_HOST=${DB_HOST}
DB_PORT=${DB_PORT}
LOG_LEVEL=${LOG_LEVEL}
|
📋 Configuration Sources Hierarchy
Layered Configuration Loading
Establish clear precedence for configuration sources.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43 | #!/bin/bash
# Configuration loading order (highest priority first):
# 1. Command-line arguments
# 2. Environment variables
# 3. Local config file (.env, config.yaml)
# 4. User config (~/.app/config)
# 5. System config (/etc/app/config)
# 6. Built-in defaults
load_config() {
local config_file="${1:-}"
# Start with defaults
local db_host="localhost"
local db_port="5432"
local log_level="info"
# Load system config
if [ -f "/etc/myapp/config" ]; then
source "/etc/myapp/config"
fi
# Load user config
if [ -f "${HOME}/.myapp/config" ]; then
source "${HOME}/.myapp/config"
fi
# Load local config
if [ -f "${config_file}" ]; then
source "${config_file}"
fi
# Override with environment
db_host="${DB_HOST:-$db_host}"
db_port="${DB_PORT:-$db_port}"
log_level="${LOG_LEVEL:-$log_level}"
# Export for child processes
export DB_HOST="$db_host"
export DB_PORT="$db_port"
export LOG_LEVEL="$log_level"
}
|
🔧 Environment Variable Patterns
Prefix-Based Organization
Group related variables with consistent prefixes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | # Database configuration
export DB_HOST="localhost"
export DB_PORT="5432"
export DB_NAME="myapp"
export DB_USER="appuser"
export DB_PASSWORD="secret"
# Redis configuration
export REDIS_HOST="localhost"
export REDIS_PORT="6379"
export REDIS_DB="0"
# Application settings
export APP_ENV="development"
export APP_DEBUG="true"
export APP_LOG_LEVEL="debug"
|
Type-Safe Environment Parsing
Validate and convert environment values appropriately.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 | # Safe environment variable parsing
parse_env_vars() {
# String values (default to empty if not set)
APP_NAME="${APP_NAME:-"MyApplication"}"
# Boolean values
parse_boolean() {
case "${1,,}" in
"true"|"1"|"yes"|"on") echo "true" ;;
*) echo "false" ;;
esac
}
APP_DEBUG=$(parse_boolean "${APP_DEBUG:-"false"}")
ENABLE_CACHE=$(parse_boolean "${ENABLE_CACHE:-"true"}")
# Numeric values
parse_number() {
local value="${1:-0}"
if echo "$value" | grep -E '^[0-9]+$' >/dev/null; then
echo "$value"
else
echo "0"
fi
}
MAX_WORKERS=$(parse_number "${MAX_WORKERS:-"4"}")
TIMEOUT_SECONDS=$(parse_number "${TIMEOUT_SECONDS:-"30"}")
# Array values (comma-separated)
IFS=',' read -ra ALLOWED_ORIGINS <<< "${ALLOWED_ORIGINS:-"localhost,127.0.0.1"}"
}
|
📄 Configuration File Patterns
Dotenv File Support
Simple key-value configuration files.
1
2
3
4
5
6
7
8
9
10
11
12
13 | # .env file format
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp
DB_USER=appuser
DB_PASSWORD=secret
# Feature flags
FEATURE_NEW_UI=true
FEATURE_EXPERIMENTAL=false
# Lists (comma-separated)
ALLOWED_HOSTS=localhost,127.0.0.1,example.com
|
Loading dotenv files:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | # Load dotenv file
load_dotenv() {
local env_file="${1:-".env"}"
if [ -f "$env_file" ]; then
while IFS= read -r line || [[ -n "$line" ]]; do
# Skip comments and empty lines
if [[ ! "$line" =~ ^[[:space:]]*# ]] && [[ -n "$line" ]]; then
# Export key=value pairs
export "$line"
fi
done < "$env_file"
fi
}
# Usage
load_dotenv ".env.production"
|
YAML/JSON Configuration
Structured configuration with nesting support.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 | # config.yaml
database:
host: localhost
port: 5432
name: myapp
credentials:
username: appuser
password: secret
cache:
redis:
host: localhost
port: 6379
database: 0
features:
new_ui: true
experimental: false
logging:
level: info
format: json
|
Parsing YAML (with yq):
| # Extract configuration values
DB_HOST=$(yq '.database.host' config.yaml)
DB_PORT=$(yq '.database.port' config.yaml)
FEATURE_NEW_UI=$(yq '.features.new_ui' config.yaml)
|
🛡️ Security Considerations
Sensitive Data Handling
Never store secrets in plain text or version control.
1
2
3
4
5
6
7
8
9
10
11
12
13 | # ❌ Never do this
export DB_PASSWORD="plaintext_secret"
# ✅ Use secret managers or secure injection
export DB_PASSWORD=$(vault read -field=password secret/myapp/database)
# ✅ Or read from secure file
if [ -f "/run/secrets/db_password" ]; then
export DB_PASSWORD=$(cat /run/secrets/db_password)
fi
# ✅ Or use encrypted environment
export DB_PASSWORD=$(decrypt_env "DB_PASSWORD_ENCRYPTED")
|
Configuration Validation
Validate configuration at startup to prevent runtime errors.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 | validate_config() {
local errors=()
# Required fields
if [ -z "$DB_HOST" ]; then
errors+=("DB_HOST is required")
fi
if [ -z "$DB_PORT" ]; then
errors+=("DB_PORT is required")
fi
# Validations
if ! echo "$DB_PORT" | grep -E '^[0-9]+$' >/dev/null; then
errors+=("DB_PORT must be numeric")
fi
if [ "$DB_PORT" -lt 1 ] || [ "$DB_PORT" -gt 65535 ]; then
errors+=("DB_PORT must be between 1-65535")
fi
# Report validation errors
if [ ${#errors[@]} -gt 0 ]; then
echo "Configuration validation failed:" >&2
for error in "${errors[@]}"; do
echo " - $error" >&2
done
exit 1
fi
}
|
🎨 Advanced Patterns
Profile-Based Configuration
Support different configuration profiles for environments.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 | # Configuration profiles
load_profile() {
local profile="${1:-"default"}"
local base_config="/etc/myapp"
# Load base configuration
if [ -f "$base_config/config.yaml" ]; then
source_yaml_config "$base_config/config.yaml"
fi
# Load profile-specific overrides
local profile_config="$base_config/profiles/$profile.yaml"
if [ -f "$profile_config" ]; then
source_yaml_config "$profile_config"
fi
# Environment overrides still take precedence
override_with_env
}
# Usage
load_profile "production"
|
Dynamic Configuration Reloading
Support runtime configuration updates without restart.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 | # Watch for configuration changes
watch_config() {
local config_file="$1"
local last_modified=""
while true; do
local current_modified=$(stat -c %Y "$config_file" 2>/dev/null)
if [ "$current_modified" != "$last_modified" ]; then
echo "Configuration changed, reloading..." >&2
reload_config "$config_file"
last_modified="$current_modified"
fi
sleep 5
done
}
# Signal-based reloading
trap 'reload_config' SIGHUP
|
🧾 Summary Best Practices
Configuration Hierarchy
- Command-line arguments - Highest precedence
- Environment variables - Flexible and secure
- Local config files - Project-specific settings
- User config files - Personal preferences
- System config files - Global defaults
- Built-in defaults - Safe fallbacks
Security Guidelines
✅ Never commit secrets to version control
✅ Use secret managers for sensitive data
✅ Validate configuration at startup
✅ Encrypt sensitive configuration in transit
✅ Audit configuration access and changes
Usability Guidelines
✅ Provide clear documentation for all options
✅ Use sensible defaults wherever possible
✅ Support multiple configuration formats
✅ Implement configuration validation
✅ Enable easy environment switching
🧠 Implementation Template
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46 | #!/bin/bash
# config-manager.sh - Standard configuration pattern
# Default configuration
DEFAULT_CONFIG=(
["APP_NAME"]="MyApplication"
["APP_ENV"]="development"
["LOG_LEVEL"]="info"
["MAX_WORKERS"]="4"
)
# Load configuration with proper precedence
load_configuration() {
local config_sources=(
"/etc/myapp/config"
"${HOME}/.myapp/config"
"./.env"
)
# Start with defaults
declare -A config
for key in "${!DEFAULT_CONFIG[@]}"; do
config["$key"]="${DEFAULT_CONFIG[$key]}"
done
# Load from files
for source in "${config_sources[@]}"; do
if [ -f "$source" ]; then
source_config_file "$source" config
fi
done
# Override with environment
override_with_environment config
# Export final configuration
for key in "${!config[@]}"; do
export "$key=${config[$key]}"
done
# Validate configuration
validate_configuration
}
# Usage
load_configuration
|
🧾 See Also