Configuration Management Overview
Comprehensive guide to managing application configuration across environments, including environment variables, configuration files, secrets, feature flags, and following 12-factor app methodology.
When to Use Setting up configuration for different environments Managing secrets and credentials Implementing feature flags Creating configuration hierarchies Following 12-factor app principles Migrating configuration to cloud services Implementing dynamic configuration Managing multi-tenant configurations Instructions 1. Environment Variables Basic Setup (.env files)
.env.development
NODE_ENV=development PORT=3000 DATABASE_URL=postgresql://localhost:5432/myapp_dev REDIS_URL=redis://localhost:6379 LOG_LEVEL=debug API_KEY=dev-api-key-12345
.env.production
NODE_ENV=production PORT=8080 DATABASE_URL=${DATABASE_URL} # From environment REDIS_URL=${REDIS_URL} LOG_LEVEL=info API_KEY=${API_KEY} # From secret manager
.env.test
NODE_ENV=test DATABASE_URL=postgresql://localhost:5432/myapp_test LOG_LEVEL=error
Loading Environment Variables // config/env.ts import dotenv from 'dotenv'; import path from 'path';
// Load environment-specific .env file
const envFile = .env.${process.env.NODE_ENV || 'development'};
dotenv.config({ path: path.resolve(process.cwd(), envFile) });
// Validate required variables const required = ['DATABASE_URL', 'PORT', 'API_KEY']; const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
throw new Error(Missing required environment variables: ${missing.join(', ')});
}
// Export typed configuration export const config = { env: process.env.NODE_ENV || 'development', port: parseInt(process.env.PORT || '3000', 10), database: { url: process.env.DATABASE_URL!, poolSize: parseInt(process.env.DB_POOL_SIZE || '10', 10) }, redis: { url: process.env.REDIS_URL || 'redis://localhost:6379' }, logging: { level: process.env.LOG_LEVEL || 'info' }, api: { key: process.env.API_KEY!, timeout: parseInt(process.env.API_TIMEOUT || '5000', 10) } } as const;
Python Configuration
config/settings.py
import os from pathlib import Path from dotenv import load_dotenv
Load .env file
env_file = f'.env.{os.getenv("ENVIRONMENT", "development")}' load_dotenv(Path(file).parent.parent / env_file)
class Config: """Base configuration""" ENV = os.getenv('ENVIRONMENT', 'development') DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' SECRET_KEY = os.getenv('SECRET_KEY')
# Database
DATABASE_URL = os.getenv('DATABASE_URL')
DB_POOL_SIZE = int(os.getenv('DB_POOL_SIZE', 10))
# Redis
REDIS_URL = os.getenv('REDIS_URL', 'redis://localhost:6379')
# API
API_KEY = os.getenv('API_KEY')
API_TIMEOUT = int(os.getenv('API_TIMEOUT', 5000))
class DevelopmentConfig(Config): """Development configuration""" DEBUG = True LOG_LEVEL = 'DEBUG'
class ProductionConfig(Config): """Production configuration""" DEBUG = False LOG_LEVEL = 'INFO'
class TestConfig(Config): """Test configuration""" TESTING = True DATABASE_URL = 'sqlite:///:memory:'
Configuration dictionary
config_by_name = { 'development': DevelopmentConfig, 'production': ProductionConfig, 'test': TestConfig }
Get active config
config = config_by_nameConfig.ENV
- Configuration Hierarchies // config/config.ts import deepmerge from 'deepmerge';
// Base configuration (shared across all environments) const baseConfig = { app: { name: 'MyApp', version: '1.0.0' }, server: { timeout: 30000, bodyLimit: '100kb' }, database: { poolSize: 10, idleTimeout: 30000 }, logging: { format: 'json', destination: 'stdout' } };
// Environment-specific overrides const developmentConfig = { server: { port: 3000 }, database: { url: 'postgresql://localhost:5432/myapp_dev', logging: true }, logging: { level: 'debug', prettyPrint: true } };
const productionConfig = { server: { port: 8080, trustProxy: true }, database: { url: process.env.DATABASE_URL, ssl: true, logging: false }, logging: { level: 'info', prettyPrint: false } };
// Merge configurations const configs = { development: deepmerge(baseConfig, developmentConfig), production: deepmerge(baseConfig, productionConfig), test: deepmerge(baseConfig, { database: { url: 'postgresql://localhost:5432/myapp_test' } }) };
export const config = configs[process.env.NODE_ENV || 'development'];
YAML Configuration Files
config/default.yml
app: name: MyApp version: 1.0.0
server: timeout: 30000 bodyLimit: 100kb
database: poolSize: 10 idleTimeout: 30000
config/development.yml
server: port: 3000
database: url: postgresql://localhost:5432/myapp_dev logging: true
logging: level: debug prettyPrint: true
config/production.yml
server: port: 8080 trustProxy: true
database: url: ${DATABASE_URL} ssl: true logging: false
logging: level: info prettyPrint: false
// Load YAML config import yaml from 'js-yaml'; import fs from 'fs'; import path from 'path';
function loadYamlConfig(env: string) { const defaultConfig = yaml.load( fs.readFileSync(path.join(__dirname, 'config/default.yml'), 'utf8') );
const envConfig = yaml.load(
fs.readFileSync(path.join(__dirname, config/${env}.yml), 'utf8')
);
return deepmerge(defaultConfig, envConfig); }
export const config = loadYamlConfig(process.env.NODE_ENV || 'development');
- Secret Management AWS Secrets Manager // secrets/aws-secrets-manager.ts import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
export class SecretManager {
private client: SecretsManagerClient;
private cache = new Map
constructor() { this.client = new SecretsManagerClient({ region: process.env.AWS_REGION }); }
async getSecret(secretName: string): Promise
try {
const command = new GetSecretValueCommand({ SecretId: secretName });
const response = await this.client.send(command);
const secret = JSON.parse(response.SecretString || '{}');
// Cache the secret
this.cache.set(secretName, {
value: secret,
expiry: Date.now() + this.cacheTtl
});
return secret;
} catch (error) {
throw new Error(`Failed to retrieve secret ${secretName}: ${error.message}`);
}
}
async getDatabaseCredentials(): Promise
async getApiKey(service: string): Promise
// Usage const secretManager = new SecretManager();
async function connectDatabase() { const credentials = await secretManager.getDatabaseCredentials();
return createConnection({ host: credentials.host, port: credentials.port, username: credentials.username, password: credentials.password, database: credentials.database }); }
HashiCorp Vault // secrets/vault.ts import vault from 'node-vault';
export class VaultClient { private client: any;
constructor() { this.client = vault({ apiVersion: 'v1', endpoint: process.env.VAULT_ADDR || 'http://localhost:8200', token: process.env.VAULT_TOKEN }); }
async getSecret(path: string): PromiseFailed to read secret from ${path}: ${error.message});
}
}
async getDatabaseConfig(): Promise
async getApiKeys(): Promise
// Dynamic database credentials (rotated automatically)
async getDynamicDBCredentials(): Promise
Environment-Specific Secrets
// secrets/secret-provider.ts
export interface SecretProvider {
getSecret(key: string): Promise
// Development: Use .env file
export class EnvFileSecretProvider implements SecretProvider {
async getSecret(key: string): PromiseSecret ${key} not found in environment);
}
return value;
}
}
// Production: Use AWS Secrets Manager export class AWSSecretProvider implements SecretProvider { private secretManager: SecretManager;
constructor() { this.secretManager = new SecretManager(); }
async getSecret(key: string): Promise
// Factory export function createSecretProvider(): SecretProvider { if (process.env.NODE_ENV === 'production') { return new AWSSecretProvider(); } return new EnvFileSecretProvider(); }
- Feature Flags Simple Feature Flag Implementation // feature-flags/feature-flag.ts export interface FeatureFlag { enabled: boolean; rolloutPercentage?: number; allowedUsers?: string[]; allowedEnvironments?: string[]; }
export class FeatureFlagManager {
private flags: Map
constructor(flags: Record
isEnabled( flagName: string, context?: { userId?: string; environment?: string } ): boolean { const flag = this.flags.get(flagName); if (!flag) return false;
// Check if disabled globally
if (!flag.enabled) return false;
// Check environment restriction
if (flag.allowedEnvironments && context?.environment) {
if (!flag.allowedEnvironments.includes(context.environment)) {
return false;
}
}
// Check user whitelist
if (flag.allowedUsers && context?.userId) {
if (flag.allowedUsers.includes(context.userId)) {
return true;
}
}
// Check rollout percentage
if (flag.rolloutPercentage !== undefined && context?.userId) {
const hash = this.hashUserId(context.userId);
return (hash % 100) < flag.rolloutPercentage;
}
return true;
}
private hashUserId(userId: string): number { let hash = 0; for (let i = 0; i < userId.length; i++) { hash = ((hash << 5) - hash) + userId.charCodeAt(i); hash |= 0; } return Math.abs(hash); } }
// Configuration const featureFlags = { 'new-dashboard': { enabled: true, rolloutPercentage: 50 // 50% of users }, 'experimental-feature': { enabled: true, allowedUsers: ['user-123', 'user-456'], allowedEnvironments: ['development', 'staging'] }, 'beta-api': { enabled: true, rolloutPercentage: 10 } };
const flagManager = new FeatureFlagManager(featureFlags);
// Usage app.get('/api/dashboard', (req, res) => { if (flagManager.isEnabled('new-dashboard', { userId: req.user.id, environment: process.env.NODE_ENV })) { return res.json(getNewDashboard()); }
return res.json(getOldDashboard()); });
LaunchDarkly Integration // feature-flags/launchdarkly.ts import LaunchDarkly from 'launchdarkly-node-server-sdk';
export class LaunchDarklyClient { private client: LaunchDarkly.LDClient;
async initialize() { this.client = LaunchDarkly.init(process.env.LAUNCHDARKLY_SDK_KEY!); await this.client.waitForInitialization(); }
async isEnabled(flagKey: string, user: LaunchDarkly.LDUser): Promise
async getVariation
close() { this.client.close(); } }
// Usage const ldClient = new LaunchDarklyClient(); await ldClient.initialize();
app.get('/api/dashboard', async (req, res) => { const user = { key: req.user.id, email: req.user.email, custom: { groups: req.user.groups } };
const showNewDashboard = await ldClient.isEnabled('new-dashboard', user);
if (showNewDashboard) { return res.json(getNewDashboard()); }
return res.json(getOldDashboard()); });
- 12-Factor App Configuration // config/twelve-factor.ts
/* * 12-Factor App Configuration Principles * * III. Config - Store config in the environment * - Strict separation of config from code * - Config varies between deploys, code does not * - Store in environment variables /
// ✅ Good: Configuration from environment export const config = { database: { url: process.env.DATABASE_URL!, poolMin: parseInt(process.env.DB_POOL_MIN || '2', 10), poolMax: parseInt(process.env.DB_POOL_MAX || '10', 10) }, redis: { url: process.env.REDIS_URL! }, s3: { bucket: process.env.S3_BUCKET!, region: process.env.AWS_REGION! }, sendgrid: { apiKey: process.env.SENDGRID_API_KEY! } };
// ❌ Bad: Hardcoded configuration const badConfig = { database: { host: 'prod-db.example.com', // Hardcoded! password: 'secretpassword' // Secret in code! } };
/* * Backing Services - Treat backing services as attached resources * - Database, cache, message queue, etc. are accessed via URLs * - Should be swappable without code changes /
// ✅ Good: Backing service as URL const db = createConnection(process.env.DATABASE_URL); const cache = createClient(process.env.REDIS_URL);
// Can swap services by changing environment variable // DATABASE_URL=postgresql://localhost/dev (local dev) // DATABASE_URL=postgresql://prod-db/app (production)
/*
* Disposability - Fast startup and graceful shutdown
/
function startServer() {
const server = app.listen(config.port, () => {
console.log(Server started on port ${config.port});
});
// Graceful shutdown process.on('SIGTERM', async () => { console.log('SIGTERM received, shutting down gracefully');
server.close(() => {
console.log('HTTP server closed');
});
await db.close();
await cache.quit();
process.exit(0);
}); }
- Configuration Validation // config/validation.ts import Joi from 'joi';
const configSchema = Joi.object({ NODE_ENV: Joi.string() .valid('development', 'production', 'test') .default('development'),
PORT: Joi.number() .port() .default(3000),
DATABASE_URL: Joi.string() .uri() .required(),
REDIS_URL: Joi.string() .uri() .default('redis://localhost:6379'),
LOG_LEVEL: Joi.string() .valid('debug', 'info', 'warn', 'error') .default('info'),
API_KEY: Joi.string() .min(32) .required(),
API_TIMEOUT: Joi.number() .min(1000) .max(30000) .default(5000),
ENABLE_METRICS: Joi.boolean() .default(false) });
export function validateConfig() { const { error, value } = configSchema.validate(process.env, { allowUnknown: true, // Allow other env vars stripUnknown: true // Remove unknown vars });
if (error) {
throw new Error(Configuration validation error: ${error.message});
}
return value; }
// Usage const validatedConfig = validateConfig();
- Dynamic Configuration (Remote Config)
// config/remote-config.ts
export class RemoteConfigService {
private config: Map
= new Map(); private pollInterval: NodeJS.Timeout | null = null;
constructor(private configServiceUrl: string) {}
async initialize() { await this.fetchConfig(); this.startPolling(); }
private async fetchConfig() {
try {
const response = await fetch(${this.configServiceUrl}/config);
const config = await response.json();
for (const [key, value] of Object.entries(config)) {
const oldValue = this.config.get(key);
if (oldValue !== value) {
console.log(`Config changed: ${key} = ${value}`);
this.config.set(key, value);
}
}
} catch (error) {
console.error('Failed to fetch remote config:', error);
}
}
private startPolling() { // Poll every 60 seconds this.pollInterval = setInterval(() => { this.fetchConfig(); }, 60000); }
get(key: string, defaultValue?: any): any { return this.config.get(key) ?? defaultValue; }
stop() { if (this.pollInterval) { clearInterval(this.pollInterval); } } }
// Usage const remoteConfig = new RemoteConfigService('https://config-service.example.com'); await remoteConfig.initialize();
app.get('/api/users', (req, res) => { const pageSize = remoteConfig.get('api.users.pageSize', 20); const enableCache = remoteConfig.get('api.users.enableCache', false);
// Use dynamic config values });
Best Practices ✅ DO Store configuration in environment variables Use different config files per environment Validate configuration on startup Use secret managers for sensitive data Never commit secrets to version control Provide sensible defaults Document all configuration options Use type-safe configuration objects Implement configuration hierarchy (base + overrides) Use feature flags for gradual rollouts Follow 12-factor app principles Implement graceful degradation for missing config Cache secrets to reduce API calls ❌ DON'T Hardcode configuration in source code Commit .env files with real secrets Use different config formats across services Store secrets in plain text Expose configuration through APIs Use production credentials in development Ignore configuration validation errors Access process.env directly everywhere Store configuration in databases (circular dependency) Mix configuration with business logic Common Patterns Pattern 1: Config Service export class ConfigService { private static instance: ConfigService; private config: Config;
private constructor() { this.config = loadAndValidateConfig(); }
static getInstance(): ConfigService { if (!ConfigService.instance) { ConfigService.instance = new ConfigService(); } return ConfigService.instance; }
get
Pattern 2: Configuration Builder
export class ConfigBuilder {
private config: Partial
withDatabase(url: string): this { this.config.database = { url }; return this; }
withRedis(url: string): this { this.config.redis = { url }; return this; }
build(): Config { return this.config as Config; } }
Tools & Resources dotenv: Load environment variables from .env files convict: Configuration management with validation config: Hierarchical configurations for Node.js AWS Secrets Manager: Cloud-based secret storage HashiCorp Vault: Secret and encryption management LaunchDarkly: Feature flag management ConfigCat: Feature flag and configuration service Consul: Service configuration and discovery