Variable Templates and Substitution¶
Navigator supports flexible variable substitution in YAML configuration, enabling efficient multi-tenant and multi-environment setups with shared templates and tenant-specific variables.
How It Works¶
Variable substitution allows you to define templates in the env
section and populate them with tenant-specific values from the var
section:
applications:
# Template with variables
env:
DATABASE_URL: "postgresql://user:pass@localhost/${database}"
STORAGE_PATH: "/storage/${tenant_id}"
# Tenant-specific values
tenants:
- name: client-a
var:
database: "client_a_prod"
tenant_id: "client_a"
Result: Client A gets DATABASE_URL=postgresql://user:pass@localhost/client_a_prod
Variable Syntax¶
Basic Substitution¶
Default Values¶
# Use default if variable not defined
env:
DATABASE_URL: "postgresql://localhost/${database:-default_db}"
LOG_LEVEL: "${log_level:-info}"
PORT: "${port:-3000}"
Nested Variables¶
# Combine multiple variables
env:
FULL_DATABASE_URL: "postgresql://${db_user}:${db_pass}@${db_host}/${database}"
STORAGE_PATH: "/storage/${environment}/${tenant_id}"
LOG_FILE: "/var/log/${app_name}-${tenant_id}.log"
Configuration Structure¶
Complete Template Example¶
applications:
# Global environment (applied to all tenants)
global_env:
RAILS_ENV: production
SECRET_KEY_BASE: "${SECRET_KEY_BASE}"
# Template with variable placeholders
env:
# Database configuration
DATABASE_URL: "postgresql://${db_user}:${db_pass}@${db_host}/${database}"
DATABASE_POOL: "${db_pool:-25}"
# Application settings
RAILS_APP_NAME: "${app_name}"
RAILS_APP_OWNER: "${owner}"
RAILS_STORAGE: "${storage_path}"
# Tenant-specific settings
REDIS_NAMESPACE: "${tenant_prefix}"
LOG_FILE: "logs/${tenant_id}.log"
PIDFILE: "pids/${tenant_id}.pid"
# Feature flags
FEATURE_ANALYTICS: "${analytics_enabled:-false}"
FEATURE_UPLOADS: "${uploads_enabled:-true}"
# Individual tenant configurations
tenants:
- name: acme-corp
path: /acme/
working_dir: /var/www/app
var:
# Database variables
database: "acme_production"
db_user: "acme_user"
db_pass: "acme_secure_password"
db_host: "db-acme.internal.com"
db_pool: "50"
# Application variables
app_name: "Acme Portal"
owner: "Acme Corporation"
tenant_id: "acme"
tenant_prefix: "acme"
storage_path: "/storage/acme"
# Feature variables
analytics_enabled: "true"
uploads_enabled: "true"
- name: widget-inc
path: /widget/
working_dir: /var/www/app
var:
# Different database setup
database: "widget_prod"
db_user: "widget_user"
db_pass: "widget_password"
db_host: "localhost"
# db_pool uses default (25)
# Application variables
app_name: "Widget Manager"
owner: "Widget Inc"
tenant_id: "widget"
tenant_prefix: "widget"
storage_path: "/storage/widget"
# Different feature settings
analytics_enabled: "false"
# uploads_enabled uses default (true)
Use Cases¶
Multi-Tenant SaaS¶
Perfect for Software as a Service applications where each client needs their own database and configuration:
applications:
env:
# Each tenant gets their own database
DATABASE_URL: "postgresql://app_user:${DB_PASSWORD}@postgres.internal/${tenant_db}"
# Separate Redis namespaces
REDIS_URL: "redis://redis.internal:6379/0"
REDIS_NAMESPACE: "app:${tenant_id}"
# Tenant branding
COMPANY_NAME: "${company_name}"
COMPANY_LOGO: "${logo_filename}"
THEME_COLOR: "${theme_color:-#007bff}"
# Storage separation
UPLOAD_PATH: "/storage/uploads/${tenant_id}"
BACKUP_PATH: "/backup/${tenant_id}"
tenants:
- name: startup-alpha
var:
tenant_db: "startup_alpha_prod"
tenant_id: "startup_alpha"
company_name: "Startup Alpha Inc"
logo_filename: "startup-alpha-logo.png"
theme_color: "#ff6b6b"
- name: enterprise-beta
var:
tenant_db: "enterprise_beta_prod"
tenant_id: "enterprise_beta"
company_name: "Enterprise Beta Corp"
logo_filename: "enterprise-beta-logo.svg"
theme_color: "#4ecdc4"
Multi-Environment Deployment¶
Use the same configuration template across different environments:
applications:
env:
DATABASE_URL: "postgresql://${db_user}:${db_pass}@${db_host}/${database}"
REDIS_URL: "redis://${redis_host}:${redis_port}/0"
S3_BUCKET: "${s3_bucket}"
LOG_LEVEL: "${log_level:-info}"
WORKER_PROCESSES: "${worker_count:-2}"
tenants:
- name: development
path: /
var:
database: "myapp_development"
db_user: "dev_user"
db_pass: "dev_password"
db_host: "localhost"
redis_host: "localhost"
redis_port: "6379"
s3_bucket: "myapp-dev-uploads"
log_level: "debug"
worker_count: "1"
- name: staging
path: /
var:
database: "myapp_staging"
db_user: "staging_user"
db_pass: "${STAGING_DB_PASSWORD}"
db_host: "staging-db.internal"
redis_host: "staging-redis.internal"
redis_port: "6379"
s3_bucket: "myapp-staging-uploads"
log_level: "info"
worker_count: "3"
- name: production
path: /
var:
database: "myapp_production"
db_user: "prod_user"
db_pass: "${PRODUCTION_DB_PASSWORD}"
db_host: "prod-db.internal"
redis_host: "prod-redis.internal"
redis_port: "6379"
s3_bucket: "myapp-prod-uploads"
log_level: "warn"
worker_count: "10"
Feature Flag Management¶
Control features per tenant using variables:
applications:
env:
# Feature flags
FEATURE_ADVANCED_ANALYTICS: "${analytics:-false}"
FEATURE_API_ACCESS: "${api_access:-false}"
FEATURE_CUSTOM_BRANDING: "${custom_branding:-false}"
FEATURE_BULK_IMPORT: "${bulk_import:-false}"
# Limits based on plan
MAX_USERS: "${max_users:-10}"
MAX_STORAGE_MB: "${max_storage:-1000}"
API_RATE_LIMIT: "${api_rate_limit:-100}"
tenants:
- name: basic-plan-client
var:
analytics: "false"
api_access: "false"
custom_branding: "false"
bulk_import: "false"
max_users: "10"
max_storage: "1000"
api_rate_limit: "100"
- name: premium-plan-client
var:
analytics: "true"
api_access: "true"
custom_branding: "true"
bulk_import: "true"
max_users: "100"
max_storage: "10000"
api_rate_limit: "1000"
Advanced Patterns¶
Conditional Configuration¶
applications:
env:
# Use different services based on environment
CACHE_URL: "${cache_type:-redis}://localhost:6379"
SEARCH_URL: "${search_enabled:-false}"
# Conditional database settings
DATABASE_URL: "postgresql://user:pass@${db_host:-localhost}/${database}"
DATABASE_SSL: "${db_ssl:-false}"
tenants:
- name: dev-local
var:
database: "myapp_dev"
cache_type: "memory"
search_enabled: "false"
- name: production
var:
database: "myapp_prod"
db_host: "prod-db.amazonaws.com"
db_ssl: "true"
cache_type: "redis"
search_enabled: "elasticsearch://search.internal:9200"
Secrets Management Integration¶
applications:
env:
# Reference secrets from environment
DATABASE_PASSWORD: "${DB_PASSWORD}"
API_SECRET_KEY: "${API_SECRET_KEY}"
ENCRYPTION_KEY: "${ENCRYPTION_KEY}"
# Combine secrets with tenant info
S3_BUCKET: "myapp-${environment}-${tenant_id}"
tenants:
- name: client-a
var:
tenant_id: "client_a"
environment: "prod"
Set secrets via environment:
export DB_PASSWORD="secure-database-password"
export API_SECRET_KEY="api-secret-key-here"
export ENCRYPTION_KEY="encryption-key-here"
Dynamic Service Discovery¶
applications:
env:
# Service URLs based on environment
DATABASE_URL: "postgresql://user:pass@${db_service}.${region}.internal/${database}"
REDIS_URL: "redis://${redis_service}.${region}.internal:6379"
S3_ENDPOINT: "https://s3.${region}.amazonaws.com"
tenants:
- name: us-east-client
var:
database: "client_prod"
region: "us-east-1"
db_service: "postgres-primary"
redis_service: "redis-cluster"
- name: eu-west-client
var:
database: "client_prod"
region: "eu-west-1"
db_service: "postgres-primary"
redis_service: "redis-cluster"
Special Tenant Types¶
Special Tenants (No Variable Substitution)¶
Some tenants need fixed configuration without templates:
applications:
env:
# Template applies to regular tenants
DATABASE_URL: "postgresql://localhost/${database}"
TENANT_ID: "${tenant_id}"
tenants:
- name: main-app
path: /
var:
database: "main_prod"
tenant_id: "main"
# Special tenant - uses direct env, skips template
- name: monitoring
path: /monitoring/
special: true
env:
DATABASE_URL: "postgresql://localhost/monitoring_db"
MONITORING_MODE: "true"
LOG_LEVEL: "debug"
- name: health-check
path: /health/
special: true
env:
HEALTH_CHECK_ONLY: "true"
Variable Validation¶
Required Variables¶
Ensure critical variables are defined:
# Navigator will fail to start if required variables are missing
applications:
env:
DATABASE_URL: "postgresql://localhost/${database}" # Required
SECRET_KEY: "${secret_key}" # Required
OPTIONAL_FEATURE: "${optional_feature:-disabled}" # Has default
tenants:
- name: client-a
var:
database: "client_a_prod"
secret_key: "client-a-secret"
# optional_feature will use default "disabled"
Validation Script¶
#!/bin/bash
# validate-config.sh - Check if all required variables are defined
CONFIG_FILE="$1"
REQUIRED_VARS=("database" "secret_key" "tenant_id")
# Extract tenant configurations
for tenant in $(yq eval '.applications.tenants[].name' "$CONFIG_FILE"); do
echo "Validating tenant: $tenant"
for var in "${REQUIRED_VARS[@]}"; do
if ! yq eval ".applications.tenants[] | select(.name == \"$tenant\") | .var.$var" "$CONFIG_FILE" | grep -q .; then
echo "ERROR: Required variable '$var' missing for tenant '$tenant'"
exit 1
fi
done
done
echo "All required variables present"
Testing Templates¶
Template Resolution Testing¶
#!/bin/bash
# Test variable substitution
# Create test config
cat > test-config.yml << 'EOF'
applications:
env:
DATABASE_URL: "postgresql://localhost/${database}"
TENANT_NAME: "${name}"
LOG_LEVEL: "${log_level:-info}"
tenants:
- name: test-tenant
var:
database: "test_db"
name: "Test Tenant"
# log_level should use default
EOF
# Test Navigator configuration parsing
navigator --validate test-config.yml
# Test with debug output
LOG_LEVEL=debug navigator --validate test-config.yml 2>&1 | grep -E "(DATABASE_URL|TENANT_NAME|LOG_LEVEL)"
Environment Variable Testing¶
# Test template resolution in Rails
cd /var/www/app
RAILS_ENV=production bundle exec rails runner "
puts 'DATABASE_URL: ' + ENV['DATABASE_URL'].inspect
puts 'TENANT_NAME: ' + ENV['TENANT_NAME'].inspect
puts 'LOG_LEVEL: ' + ENV['LOG_LEVEL'].inspect
"
Best Practices¶
1. Template Organization¶
# Good - organized by function
env:
# Database settings
DATABASE_URL: "postgresql://${db_user}:${db_pass}@${db_host}/${database}"
DATABASE_POOL: "${db_pool:-25}"
# Storage settings
STORAGE_PATH: "/storage/${tenant_id}"
BACKUP_PATH: "/backup/${tenant_id}"
# Feature flags
FEATURE_X: "${feature_x:-false}"
FEATURE_Y: "${feature_y:-false}"
# Avoid - mixed concerns
env:
DATABASE_URL: "postgresql://localhost/${database}"
FEATURE_X: "${feature_x:-false}"
STORAGE_PATH: "/storage/${tenant_id}"
DATABASE_POOL: "${db_pool:-25}"
2. Default Values¶
# Good - provide sensible defaults
env:
LOG_LEVEL: "${log_level:-info}"
MAX_CONNECTIONS: "${max_connections:-100}"
TIMEOUT: "${timeout:-30}"
# Bad - no defaults for optional settings
env:
LOG_LEVEL: "${log_level}"
MAX_CONNECTIONS: "${max_connections}"
3. Variable Naming¶
# Good - clear, consistent naming
var:
tenant_id: "client_a"
database_name: "client_a_prod"
storage_path: "/storage/client_a"
feature_analytics: "true"
# Avoid - inconsistent naming
var:
id: "client_a"
db: "client_a_prod"
storage: "/storage/client_a"
analytics: "true"
4. Security Considerations¶
# Good - reference environment secrets
env:
DATABASE_PASSWORD: "${DB_PASSWORD}"
API_SECRET: "${API_SECRET}"
# Never hardcode secrets in templates
env:
DATABASE_PASSWORD: "hardcoded-password" # NEVER!
Troubleshooting¶
Variable Not Substituted¶
Issue: Variables appear as literal ${var}
in environment
Causes:
- Variable not defined in tenant's var
section
- Typo in variable name
- Missing tenant configuration
Solutions:
# Check Navigator debug logs
LOG_LEVEL=debug navigator config.yml | grep "variable substitution"
# Validate tenant configuration
yq eval '.applications.tenants[] | select(.name == "my-tenant") | .var' config.yml
# Test with simple config
Missing Required Variables¶
Issue: Navigator fails to start with variable errors
Solution:
# Add missing variables to tenant
tenants:
- name: my-tenant
var:
database: "required_db_name"
secret_key: "required_secret"
Template Resolution Order¶
Templates are resolved in this order:
1. global_env
(applied to all tenants)
2. env
template with variable substitution
3. Tenant-specific env
(overrides template)
applications:
global_env:
RAILS_ENV: production # Applied to all
env:
DATABASE_URL: "postgresql://localhost/${database}" # Template
tenants:
- name: special-tenant
env:
DATABASE_URL: "postgresql://special-host/special_db" # Override