| name | configuration-management |
| description | Manage application configuration including environment variables, settings management, configuration hierarchies, secret management, feature flags, and 12-factor app principles. Use for config, environment setup, or settings management. |
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_name[Config.ENV]()
2. 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');
3. 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<string, { value: any; expiry: number }>();
private cacheTtl = 300000; // 5 minutes
constructor() {
this.client = new SecretsManagerClient({ region: process.env.AWS_REGION });
}
async getSecret(secretName: string): Promise<any> {
// Check cache
const cached = this.cache.get(secretName);
if (cached && cached.expiry > Date.now()) {
return cached.value;
}
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<DatabaseCredentials> {
return this.getSecret('prod/database/credentials');
}
async getApiKey(service: string): Promise<string> {
const secrets = await this.getSecret('prod/api-keys');
return secrets[service];
}
}
// 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): Promise<any> {
try {
const result = await this.client.read(path);
return result.data.data;
} catch (error) {
throw new Error(`Failed to read secret from ${path}: ${error.message}`);
}
}
async getDatabaseConfig(): Promise<DatabaseConfig> {
return this.getSecret('secret/data/database');
}
async getApiKeys(): Promise<Record<string, string>> {
return this.getSecret('secret/data/api-keys');
}
// Dynamic database credentials (rotated automatically)
async getDynamicDBCredentials(): Promise<Credentials> {
const result = await this.client.read('database/creds/readonly');
return {
username: result.data.username,
password: result.data.password,
leaseId: result.lease_id,
leaseDuration: result.lease_duration
};
}
}
Environment-Specific Secrets
// secrets/secret-provider.ts
export interface SecretProvider {
getSecret(key: string): Promise<string>;
}
// Development: Use .env file
export class EnvFileSecretProvider implements SecretProvider {
async getSecret(key: string): Promise<string> {
const value = process.env[key];
if (!value) {
throw new Error(`Secret ${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<string> {
const secrets = await this.secretManager.getSecret('prod/secrets');
return secrets[key];
}
}
// Factory
export function createSecretProvider(): SecretProvider {
if (process.env.NODE_ENV === 'production') {
return new AWSSecretProvider();
}
return new EnvFileSecretProvider();
}
4. 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<string, FeatureFlag>;
constructor(flags: Record<string, FeatureFlag>) {
this.flags = new Map(Object.entries(flags));
}
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<boolean> {
return this.client.variation(flagKey, user, false);
}
async getVariation<T>(
flagKey: string,
user: LaunchDarkly.LDUser,
defaultValue: T
): Promise<T> {
return this.client.variation(flagKey, user, defaultValue);
}
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());
});
5. 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);
});
}
6. 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();
7. Dynamic Configuration (Remote Config)
// config/remote-config.ts
export class RemoteConfigService {
private config: Map<string, any> = 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<K extends keyof Config>(key: K): Config[K] {
return this.config[key];
}
}
Pattern 2: Configuration Builder
export class ConfigBuilder {
private config: Partial<Config> = {};
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