Przejdź do treści

⚙️ 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.

1
2
3
4
5
6
7
# ❌ 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.

1
2
3
4
5
6
7
8
9
# 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):

1
2
3
4
# 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

  1. Command-line arguments - Highest precedence
  2. Environment variables - Flexible and secure
  3. Local config files - Project-specific settings
  4. User config files - Personal preferences
  5. System config files - Global defaults
  6. 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